fix(clippy): apply auto-fixable linting suggestions
This commit is contained in:
@@ -1,16 +1,16 @@
|
||||
import 'package:arbiter/features/callouts/callout_event.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'active_callout.freezed.dart';
|
||||
|
||||
@freezed
|
||||
abstract class ActiveCallout with _$ActiveCallout {
|
||||
const factory ActiveCallout({
|
||||
required String id,
|
||||
required String title,
|
||||
required String description,
|
||||
String? iconUrl,
|
||||
required DateTime addedAt,
|
||||
required CalloutData data,
|
||||
}) = _ActiveCallout;
|
||||
}
|
||||
import 'package:arbiter/features/callouts/callout_event.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'active_callout.freezed.dart';
|
||||
|
||||
@freezed
|
||||
abstract class ActiveCallout with _$ActiveCallout {
|
||||
const factory ActiveCallout({
|
||||
required String id,
|
||||
required String title,
|
||||
required String description,
|
||||
String? iconUrl,
|
||||
required DateTime addedAt,
|
||||
required CalloutData data,
|
||||
}) = _ActiveCallout;
|
||||
}
|
||||
|
||||
@@ -1,304 +1,304 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'active_callout.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$ActiveCallout {
|
||||
|
||||
String get id; String get title; String get description; String? get iconUrl; DateTime get addedAt; CalloutData get data;
|
||||
/// Create a copy of ActiveCallout
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$ActiveCalloutCopyWith<ActiveCallout> get copyWith => _$ActiveCalloutCopyWithImpl<ActiveCallout>(this as ActiveCallout, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ActiveCallout&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.iconUrl, iconUrl) || other.iconUrl == iconUrl)&&(identical(other.addedAt, addedAt) || other.addedAt == addedAt)&&(identical(other.data, data) || other.data == data));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,title,description,iconUrl,addedAt,data);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ActiveCallout(id: $id, title: $title, description: $description, iconUrl: $iconUrl, addedAt: $addedAt, data: $data)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $ActiveCalloutCopyWith<$Res> {
|
||||
factory $ActiveCalloutCopyWith(ActiveCallout value, $Res Function(ActiveCallout) _then) = _$ActiveCalloutCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id, String title, String description, String? iconUrl, DateTime addedAt, CalloutData data
|
||||
});
|
||||
|
||||
|
||||
$CalloutDataCopyWith<$Res> get data;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$ActiveCalloutCopyWithImpl<$Res>
|
||||
implements $ActiveCalloutCopyWith<$Res> {
|
||||
_$ActiveCalloutCopyWithImpl(this._self, this._then);
|
||||
|
||||
final ActiveCallout _self;
|
||||
final $Res Function(ActiveCallout) _then;
|
||||
|
||||
/// Create a copy of ActiveCallout
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? title = null,Object? description = null,Object? iconUrl = freezed,Object? addedAt = null,Object? data = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
|
||||
as String,description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||
as String,iconUrl: freezed == iconUrl ? _self.iconUrl : iconUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String?,addedAt: null == addedAt ? _self.addedAt : addedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,data: null == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
|
||||
as CalloutData,
|
||||
));
|
||||
}
|
||||
/// Create a copy of ActiveCallout
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$CalloutDataCopyWith<$Res> get data {
|
||||
|
||||
return $CalloutDataCopyWith<$Res>(_self.data, (value) {
|
||||
return _then(_self.copyWith(data: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [ActiveCallout].
|
||||
extension ActiveCalloutPatterns on ActiveCallout {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ActiveCallout value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ActiveCallout() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ActiveCallout value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ActiveCallout():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ActiveCallout value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ActiveCallout() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String title, String description, String? iconUrl, DateTime addedAt, CalloutData data)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ActiveCallout() when $default != null:
|
||||
return $default(_that.id,_that.title,_that.description,_that.iconUrl,_that.addedAt,_that.data);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String title, String description, String? iconUrl, DateTime addedAt, CalloutData data) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ActiveCallout():
|
||||
return $default(_that.id,_that.title,_that.description,_that.iconUrl,_that.addedAt,_that.data);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String title, String description, String? iconUrl, DateTime addedAt, CalloutData data)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ActiveCallout() when $default != null:
|
||||
return $default(_that.id,_that.title,_that.description,_that.iconUrl,_that.addedAt,_that.data);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _ActiveCallout implements ActiveCallout {
|
||||
const _ActiveCallout({required this.id, required this.title, required this.description, this.iconUrl, required this.addedAt, required this.data});
|
||||
|
||||
|
||||
@override final String id;
|
||||
@override final String title;
|
||||
@override final String description;
|
||||
@override final String? iconUrl;
|
||||
@override final DateTime addedAt;
|
||||
@override final CalloutData data;
|
||||
|
||||
/// Create a copy of ActiveCallout
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$ActiveCalloutCopyWith<_ActiveCallout> get copyWith => __$ActiveCalloutCopyWithImpl<_ActiveCallout>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ActiveCallout&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.iconUrl, iconUrl) || other.iconUrl == iconUrl)&&(identical(other.addedAt, addedAt) || other.addedAt == addedAt)&&(identical(other.data, data) || other.data == data));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,title,description,iconUrl,addedAt,data);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ActiveCallout(id: $id, title: $title, description: $description, iconUrl: $iconUrl, addedAt: $addedAt, data: $data)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$ActiveCalloutCopyWith<$Res> implements $ActiveCalloutCopyWith<$Res> {
|
||||
factory _$ActiveCalloutCopyWith(_ActiveCallout value, $Res Function(_ActiveCallout) _then) = __$ActiveCalloutCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, String title, String description, String? iconUrl, DateTime addedAt, CalloutData data
|
||||
});
|
||||
|
||||
|
||||
@override $CalloutDataCopyWith<$Res> get data;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$ActiveCalloutCopyWithImpl<$Res>
|
||||
implements _$ActiveCalloutCopyWith<$Res> {
|
||||
__$ActiveCalloutCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _ActiveCallout _self;
|
||||
final $Res Function(_ActiveCallout) _then;
|
||||
|
||||
/// Create a copy of ActiveCallout
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? title = null,Object? description = null,Object? iconUrl = freezed,Object? addedAt = null,Object? data = null,}) {
|
||||
return _then(_ActiveCallout(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
|
||||
as String,description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||
as String,iconUrl: freezed == iconUrl ? _self.iconUrl : iconUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String?,addedAt: null == addedAt ? _self.addedAt : addedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,data: null == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
|
||||
as CalloutData,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of ActiveCallout
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$CalloutDataCopyWith<$Res> get data {
|
||||
|
||||
return $CalloutDataCopyWith<$Res>(_self.data, (value) {
|
||||
return _then(_self.copyWith(data: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// dart format on
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'active_callout.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$ActiveCallout {
|
||||
|
||||
String get id; String get title; String get description; String? get iconUrl; DateTime get addedAt; CalloutData get data;
|
||||
/// Create a copy of ActiveCallout
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$ActiveCalloutCopyWith<ActiveCallout> get copyWith => _$ActiveCalloutCopyWithImpl<ActiveCallout>(this as ActiveCallout, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ActiveCallout&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.iconUrl, iconUrl) || other.iconUrl == iconUrl)&&(identical(other.addedAt, addedAt) || other.addedAt == addedAt)&&(identical(other.data, data) || other.data == data));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,title,description,iconUrl,addedAt,data);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ActiveCallout(id: $id, title: $title, description: $description, iconUrl: $iconUrl, addedAt: $addedAt, data: $data)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $ActiveCalloutCopyWith<$Res> {
|
||||
factory $ActiveCalloutCopyWith(ActiveCallout value, $Res Function(ActiveCallout) _then) = _$ActiveCalloutCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id, String title, String description, String? iconUrl, DateTime addedAt, CalloutData data
|
||||
});
|
||||
|
||||
|
||||
$CalloutDataCopyWith<$Res> get data;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$ActiveCalloutCopyWithImpl<$Res>
|
||||
implements $ActiveCalloutCopyWith<$Res> {
|
||||
_$ActiveCalloutCopyWithImpl(this._self, this._then);
|
||||
|
||||
final ActiveCallout _self;
|
||||
final $Res Function(ActiveCallout) _then;
|
||||
|
||||
/// Create a copy of ActiveCallout
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? title = null,Object? description = null,Object? iconUrl = freezed,Object? addedAt = null,Object? data = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
|
||||
as String,description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||
as String,iconUrl: freezed == iconUrl ? _self.iconUrl : iconUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String?,addedAt: null == addedAt ? _self.addedAt : addedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,data: null == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
|
||||
as CalloutData,
|
||||
));
|
||||
}
|
||||
/// Create a copy of ActiveCallout
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$CalloutDataCopyWith<$Res> get data {
|
||||
|
||||
return $CalloutDataCopyWith<$Res>(_self.data, (value) {
|
||||
return _then(_self.copyWith(data: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [ActiveCallout].
|
||||
extension ActiveCalloutPatterns on ActiveCallout {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ActiveCallout value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ActiveCallout() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ActiveCallout value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ActiveCallout():
|
||||
return $default(_that);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ActiveCallout value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ActiveCallout() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String title, String description, String? iconUrl, DateTime addedAt, CalloutData data)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ActiveCallout() when $default != null:
|
||||
return $default(_that.id,_that.title,_that.description,_that.iconUrl,_that.addedAt,_that.data);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String title, String description, String? iconUrl, DateTime addedAt, CalloutData data) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ActiveCallout():
|
||||
return $default(_that.id,_that.title,_that.description,_that.iconUrl,_that.addedAt,_that.data);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String title, String description, String? iconUrl, DateTime addedAt, CalloutData data)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ActiveCallout() when $default != null:
|
||||
return $default(_that.id,_that.title,_that.description,_that.iconUrl,_that.addedAt,_that.data);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _ActiveCallout implements ActiveCallout {
|
||||
const _ActiveCallout({required this.id, required this.title, required this.description, this.iconUrl, required this.addedAt, required this.data});
|
||||
|
||||
|
||||
@override final String id;
|
||||
@override final String title;
|
||||
@override final String description;
|
||||
@override final String? iconUrl;
|
||||
@override final DateTime addedAt;
|
||||
@override final CalloutData data;
|
||||
|
||||
/// Create a copy of ActiveCallout
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$ActiveCalloutCopyWith<_ActiveCallout> get copyWith => __$ActiveCalloutCopyWithImpl<_ActiveCallout>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ActiveCallout&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.iconUrl, iconUrl) || other.iconUrl == iconUrl)&&(identical(other.addedAt, addedAt) || other.addedAt == addedAt)&&(identical(other.data, data) || other.data == data));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,title,description,iconUrl,addedAt,data);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ActiveCallout(id: $id, title: $title, description: $description, iconUrl: $iconUrl, addedAt: $addedAt, data: $data)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$ActiveCalloutCopyWith<$Res> implements $ActiveCalloutCopyWith<$Res> {
|
||||
factory _$ActiveCalloutCopyWith(_ActiveCallout value, $Res Function(_ActiveCallout) _then) = __$ActiveCalloutCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, String title, String description, String? iconUrl, DateTime addedAt, CalloutData data
|
||||
});
|
||||
|
||||
|
||||
@override $CalloutDataCopyWith<$Res> get data;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$ActiveCalloutCopyWithImpl<$Res>
|
||||
implements _$ActiveCalloutCopyWith<$Res> {
|
||||
__$ActiveCalloutCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _ActiveCallout _self;
|
||||
final $Res Function(_ActiveCallout) _then;
|
||||
|
||||
/// Create a copy of ActiveCallout
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? title = null,Object? description = null,Object? iconUrl = freezed,Object? addedAt = null,Object? data = null,}) {
|
||||
return _then(_ActiveCallout(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
|
||||
as String,description: null == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||
as String,iconUrl: freezed == iconUrl ? _self.iconUrl : iconUrl // ignore: cast_nullable_to_non_nullable
|
||||
as String?,addedAt: null == addedAt ? _self.addedAt : addedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,data: null == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
|
||||
as CalloutData,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of ActiveCallout
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$CalloutDataCopyWith<$Res> get data {
|
||||
|
||||
return $CalloutDataCopyWith<$Res>(_self.data, (value) {
|
||||
return _then(_self.copyWith(data: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// dart format on
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import 'package:arbiter/proto/shared/client.pb.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'callout_event.freezed.dart';
|
||||
|
||||
@freezed
|
||||
sealed class CalloutData with _$CalloutData {
|
||||
const factory CalloutData.connectApproval({
|
||||
required String pubkey,
|
||||
required ClientInfo clientInfo,
|
||||
}) = ConnectApprovalData;
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class CalloutEvent with _$CalloutEvent {
|
||||
const factory CalloutEvent.added({
|
||||
required String id,
|
||||
required CalloutData data,
|
||||
}) = CalloutEventAdded;
|
||||
|
||||
const factory CalloutEvent.cancelled({required String id}) =
|
||||
CalloutEventCancelled;
|
||||
}
|
||||
import 'package:arbiter/proto/shared/client.pb.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'callout_event.freezed.dart';
|
||||
|
||||
@freezed
|
||||
sealed class CalloutData with _$CalloutData {
|
||||
const factory CalloutData.connectApproval({
|
||||
required String pubkey,
|
||||
required ClientInfo clientInfo,
|
||||
}) = ConnectApprovalData;
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class CalloutEvent with _$CalloutEvent {
|
||||
const factory CalloutEvent.added({
|
||||
required String id,
|
||||
required CalloutData data,
|
||||
}) = CalloutEventAdded;
|
||||
|
||||
const factory CalloutEvent.cancelled({required String id}) =
|
||||
CalloutEventCancelled;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,57 +1,57 @@
|
||||
import 'package:arbiter/features/callouts/active_callout.dart';
|
||||
import 'package:arbiter/features/callouts/callout_event.dart';
|
||||
import 'package:arbiter/features/callouts/types/sdk_connect_approve.dart'
|
||||
as connect_approve;
|
||||
import 'package:arbiter/proto/shared/client.pb.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'callout_manager.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class CalloutManager extends _$CalloutManager {
|
||||
@override
|
||||
Map<String, ActiveCallout> build() {
|
||||
ref.listen(connect_approve.connectApproveEventsProvider, (_, next) {
|
||||
next.whenData(_processEvent);
|
||||
});
|
||||
return {};
|
||||
}
|
||||
|
||||
void _processEvent(CalloutEvent event) {
|
||||
switch (event) {
|
||||
case CalloutEventAdded(:final id, :final data):
|
||||
state = {...state, id: _toActiveCallout(id, data)};
|
||||
case CalloutEventCancelled(:final id):
|
||||
state = {...state}..remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sendDecision(String id, bool approved) async {
|
||||
final callout = state[id];
|
||||
if (callout == null) return;
|
||||
switch (callout.data) {
|
||||
case ConnectApprovalData(:final pubkey):
|
||||
await connect_approve.sendDecision(ref, pubkey, approved);
|
||||
}
|
||||
dismiss(id);
|
||||
}
|
||||
|
||||
void dismiss(String id) {
|
||||
state = {...state}..remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
ActiveCallout _toActiveCallout(String id, CalloutData data) => switch (data) {
|
||||
ConnectApprovalData(:final clientInfo) => ActiveCallout(
|
||||
id: id,
|
||||
title: 'Connection Request',
|
||||
description: _clientDisplayName(clientInfo) != null
|
||||
? '${_clientDisplayName(clientInfo)} is requesting a connection.'
|
||||
: 'An SDK client is requesting a connection.',
|
||||
addedAt: DateTime.now(),
|
||||
data: data,
|
||||
),
|
||||
};
|
||||
|
||||
String? _clientDisplayName(ClientInfo info) =>
|
||||
info.hasName() && info.name.isNotEmpty ? info.name : null;
|
||||
import 'package:arbiter/features/callouts/active_callout.dart';
|
||||
import 'package:arbiter/features/callouts/callout_event.dart';
|
||||
import 'package:arbiter/features/callouts/types/sdk_connect_approve.dart'
|
||||
as connect_approve;
|
||||
import 'package:arbiter/proto/shared/client.pb.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'callout_manager.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class CalloutManager extends _$CalloutManager {
|
||||
@override
|
||||
Map<String, ActiveCallout> build() {
|
||||
ref.listen(connect_approve.connectApproveEventsProvider, (_, next) {
|
||||
next.whenData(_processEvent);
|
||||
});
|
||||
return {};
|
||||
}
|
||||
|
||||
void _processEvent(CalloutEvent event) {
|
||||
switch (event) {
|
||||
case CalloutEventAdded(:final id, :final data):
|
||||
state = {...state, id: _toActiveCallout(id, data)};
|
||||
case CalloutEventCancelled(:final id):
|
||||
state = {...state}..remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sendDecision(String id, bool approved) async {
|
||||
final callout = state[id];
|
||||
if (callout == null) return;
|
||||
switch (callout.data) {
|
||||
case ConnectApprovalData(:final pubkey):
|
||||
await connect_approve.sendDecision(ref, pubkey, approved);
|
||||
}
|
||||
dismiss(id);
|
||||
}
|
||||
|
||||
void dismiss(String id) {
|
||||
state = {...state}..remove(id);
|
||||
}
|
||||
}
|
||||
|
||||
ActiveCallout _toActiveCallout(String id, CalloutData data) => switch (data) {
|
||||
ConnectApprovalData(:final clientInfo) => ActiveCallout(
|
||||
id: id,
|
||||
title: 'Connection Request',
|
||||
description: _clientDisplayName(clientInfo) != null
|
||||
? '${_clientDisplayName(clientInfo)} is requesting a connection.'
|
||||
: 'An SDK client is requesting a connection.',
|
||||
addedAt: DateTime.now(),
|
||||
data: data,
|
||||
),
|
||||
};
|
||||
|
||||
String? _clientDisplayName(ClientInfo info) =>
|
||||
info.hasName() && info.name.isNotEmpty ? info.name : null;
|
||||
|
||||
@@ -1,67 +1,67 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'callout_manager.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(CalloutManager)
|
||||
final calloutManagerProvider = CalloutManagerProvider._();
|
||||
|
||||
final class CalloutManagerProvider
|
||||
extends $NotifierProvider<CalloutManager, Map<String, ActiveCallout>> {
|
||||
CalloutManagerProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'calloutManagerProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$calloutManagerHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
CalloutManager create() => CalloutManager();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(Map<String, ActiveCallout> value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<Map<String, ActiveCallout>>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$calloutManagerHash() => r'ff8c9a03a6bbbca822242eb497c503b18240a289';
|
||||
|
||||
abstract class _$CalloutManager extends $Notifier<Map<String, ActiveCallout>> {
|
||||
Map<String, ActiveCallout> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref =
|
||||
this.ref
|
||||
as $Ref<Map<String, ActiveCallout>, Map<String, ActiveCallout>>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<
|
||||
Map<String, ActiveCallout>,
|
||||
Map<String, ActiveCallout>
|
||||
>,
|
||||
Map<String, ActiveCallout>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'callout_manager.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(CalloutManager)
|
||||
final calloutManagerProvider = CalloutManagerProvider._();
|
||||
|
||||
final class CalloutManagerProvider
|
||||
extends $NotifierProvider<CalloutManager, Map<String, ActiveCallout>> {
|
||||
CalloutManagerProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'calloutManagerProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$calloutManagerHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
CalloutManager create() => CalloutManager();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(Map<String, ActiveCallout> value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<Map<String, ActiveCallout>>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$calloutManagerHash() => r'ff8c9a03a6bbbca822242eb497c503b18240a289';
|
||||
|
||||
abstract class _$CalloutManager extends $Notifier<Map<String, ActiveCallout>> {
|
||||
Map<String, ActiveCallout> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref =
|
||||
this.ref
|
||||
as $Ref<Map<String, ActiveCallout>, Map<String, ActiveCallout>>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<
|
||||
Map<String, ActiveCallout>,
|
||||
Map<String, ActiveCallout>
|
||||
>,
|
||||
Map<String, ActiveCallout>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,99 +1,99 @@
|
||||
import 'package:arbiter/features/callouts/callout_event.dart';
|
||||
import 'package:arbiter/features/callouts/callout_manager.dart';
|
||||
import 'package:arbiter/screens/callouts/sdk_connect.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
Future<void> showCallout(BuildContext context, WidgetRef ref, String id) async {
|
||||
final data = ref.read(calloutManagerProvider)[id]?.data;
|
||||
if (data == null) return;
|
||||
|
||||
await showGeneralDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
|
||||
barrierColor: Colors.transparent,
|
||||
transitionDuration: const Duration(milliseconds: 320),
|
||||
pageBuilder: (_, animation, _) =>
|
||||
_CalloutOverlay(id: id, data: data, animation: animation),
|
||||
);
|
||||
}
|
||||
|
||||
class _CalloutOverlay extends ConsumerWidget {
|
||||
const _CalloutOverlay({
|
||||
required this.id,
|
||||
required this.data,
|
||||
required this.animation,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final CalloutData data;
|
||||
final Animation<double> animation;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
ref.listen(calloutManagerProvider.select((map) => map.containsKey(id)), (
|
||||
wasPresent,
|
||||
isPresent,
|
||||
) {
|
||||
if (wasPresent == true && !isPresent && context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
});
|
||||
|
||||
final content = switch (data) {
|
||||
ConnectApprovalData(:final pubkey, :final clientInfo) =>
|
||||
SdkConnectCallout(
|
||||
pubkey: pubkey,
|
||||
clientInfo: clientInfo,
|
||||
onAccept: () =>
|
||||
ref.read(calloutManagerProvider.notifier).sendDecision(id, true),
|
||||
onDecline: () =>
|
||||
ref.read(calloutManagerProvider.notifier).sendDecision(id, false),
|
||||
),
|
||||
};
|
||||
|
||||
final barrierAnim = CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: const Interval(0, 0.3125, curve: Curves.easeOut),
|
||||
);
|
||||
final popupAnim = CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: const Interval(0.3125, 1, curve: Curves.easeOutCubic),
|
||||
);
|
||||
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: AnimatedBuilder(
|
||||
animation: barrierAnim,
|
||||
builder: (_, __) => ColoredBox(
|
||||
color: Colors.black.withValues(alpha: 0.35 * barrierAnim.value),
|
||||
),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: FadeTransition(
|
||||
opacity: popupAnim,
|
||||
child: SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0, 0.08),
|
||||
end: Offset.zero,
|
||||
).animate(popupAnim),
|
||||
child: content,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
import 'package:arbiter/features/callouts/callout_event.dart';
|
||||
import 'package:arbiter/features/callouts/callout_manager.dart';
|
||||
import 'package:arbiter/screens/callouts/sdk_connect.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
Future<void> showCallout(BuildContext context, WidgetRef ref, String id) async {
|
||||
final data = ref.read(calloutManagerProvider)[id]?.data;
|
||||
if (data == null) return;
|
||||
|
||||
await showGeneralDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
|
||||
barrierColor: Colors.transparent,
|
||||
transitionDuration: const Duration(milliseconds: 320),
|
||||
pageBuilder: (_, animation, _) =>
|
||||
_CalloutOverlay(id: id, data: data, animation: animation),
|
||||
);
|
||||
}
|
||||
|
||||
class _CalloutOverlay extends ConsumerWidget {
|
||||
const _CalloutOverlay({
|
||||
required this.id,
|
||||
required this.data,
|
||||
required this.animation,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final CalloutData data;
|
||||
final Animation<double> animation;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
ref.listen(calloutManagerProvider.select((map) => map.containsKey(id)), (
|
||||
wasPresent,
|
||||
isPresent,
|
||||
) {
|
||||
if (wasPresent == true && !isPresent && context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
});
|
||||
|
||||
final content = switch (data) {
|
||||
ConnectApprovalData(:final pubkey, :final clientInfo) =>
|
||||
SdkConnectCallout(
|
||||
pubkey: pubkey,
|
||||
clientInfo: clientInfo,
|
||||
onAccept: () =>
|
||||
ref.read(calloutManagerProvider.notifier).sendDecision(id, true),
|
||||
onDecline: () =>
|
||||
ref.read(calloutManagerProvider.notifier).sendDecision(id, false),
|
||||
),
|
||||
};
|
||||
|
||||
final barrierAnim = CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: const Interval(0, 0.3125, curve: Curves.easeOut),
|
||||
);
|
||||
final popupAnim = CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: const Interval(0.3125, 1, curve: Curves.easeOutCubic),
|
||||
);
|
||||
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: AnimatedBuilder(
|
||||
animation: barrierAnim,
|
||||
builder: (_, __) => ColoredBox(
|
||||
color: Colors.black.withValues(alpha: 0.35 * barrierAnim.value),
|
||||
),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: FadeTransition(
|
||||
opacity: popupAnim,
|
||||
child: SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0, 0.08),
|
||||
end: Offset.zero,
|
||||
).animate(popupAnim),
|
||||
child: content,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,221 +1,221 @@
|
||||
import 'package:arbiter/features/callouts/active_callout.dart';
|
||||
import 'package:arbiter/features/callouts/callout_manager.dart';
|
||||
import 'package:arbiter/features/callouts/show_callout.dart';
|
||||
import 'package:arbiter/theme/palette.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:sizer/sizer.dart';
|
||||
import 'package:timeago/timeago.dart' as timeago;
|
||||
|
||||
Future<void> showCalloutList(BuildContext context, WidgetRef ref) async {
|
||||
final selectedId = await showGeneralDialog<String>(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
|
||||
barrierColor: Colors.transparent,
|
||||
transitionDuration: const Duration(milliseconds: 280),
|
||||
pageBuilder: (_, animation, __) =>
|
||||
_CalloutListOverlay(animation: animation),
|
||||
);
|
||||
|
||||
if (selectedId != null && context.mounted) {
|
||||
await showCallout(context, ref, selectedId);
|
||||
}
|
||||
}
|
||||
|
||||
class _CalloutListOverlay extends ConsumerWidget {
|
||||
const _CalloutListOverlay({required this.animation});
|
||||
|
||||
final Animation<double> animation;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final callouts = ref.watch(calloutManagerProvider);
|
||||
|
||||
final barrierAnim = CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: const Interval(0, 0.3, curve: Curves.easeOut),
|
||||
);
|
||||
final panelAnim = CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: const Interval(0.3, 1, curve: Curves.easeOutCubic),
|
||||
);
|
||||
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
child: AnimatedBuilder(
|
||||
animation: barrierAnim,
|
||||
builder: (_, __) => ColoredBox(
|
||||
color: Colors.black.withValues(
|
||||
alpha: 0.35 * barrierAnim.value,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(1.6.h),
|
||||
child: FadeTransition(
|
||||
opacity: panelAnim,
|
||||
child: SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0, 0.08),
|
||||
end: Offset.zero,
|
||||
).animate(panelAnim),
|
||||
child: GestureDetector(
|
||||
onTap: () {},
|
||||
child: _CalloutListPanel(callouts: callouts),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CalloutListPanel extends StatelessWidget {
|
||||
const _CalloutListPanel({required this.callouts});
|
||||
|
||||
final Map<String, ActiveCallout> callouts;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
constraints: BoxConstraints(maxHeight: 48.h),
|
||||
decoration: BoxDecoration(
|
||||
color: Palette.cream,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(color: Palette.line),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(2.h, 2.h, 2.h, 1.2.h),
|
||||
child: Text(
|
||||
'Notifications',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color: Palette.ink,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (callouts.isEmpty)
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(2.h, 0, 2.h, 2.h),
|
||||
child: Text(
|
||||
'No pending notifications.',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: Palette.ink.withValues(alpha: 0.50),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.fromLTRB(1.2.h, 0, 1.2.h, 1.2.h),
|
||||
child: Column(
|
||||
spacing: 0.5.h,
|
||||
children: [
|
||||
for (final entry in callouts.values)
|
||||
_CalloutListEntry(
|
||||
callout: entry,
|
||||
onTap: () => Navigator.of(context).pop(entry.id),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CalloutListEntry extends StatelessWidget {
|
||||
const _CalloutListEntry({required this.callout, required this.onTap});
|
||||
|
||||
final ActiveCallout callout;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 1.2.h, vertical: 1.2.h),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Palette.line),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 1.2.h,
|
||||
children: [
|
||||
if (callout.iconUrl != null)
|
||||
CircleAvatar(
|
||||
radius: 2.2.h,
|
||||
backgroundColor: Palette.line,
|
||||
backgroundImage: NetworkImage(callout.iconUrl!),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 0.3.h,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
callout.title,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: Palette.ink,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
timeago.format(callout.addedAt),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: Palette.ink.withValues(alpha: 0.45),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
callout.description,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: Palette.ink.withValues(alpha: 0.65),
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
import 'package:arbiter/features/callouts/active_callout.dart';
|
||||
import 'package:arbiter/features/callouts/callout_manager.dart';
|
||||
import 'package:arbiter/features/callouts/show_callout.dart';
|
||||
import 'package:arbiter/theme/palette.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:sizer/sizer.dart';
|
||||
import 'package:timeago/timeago.dart' as timeago;
|
||||
|
||||
Future<void> showCalloutList(BuildContext context, WidgetRef ref) async {
|
||||
final selectedId = await showGeneralDialog<String>(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
|
||||
barrierColor: Colors.transparent,
|
||||
transitionDuration: const Duration(milliseconds: 280),
|
||||
pageBuilder: (_, animation, __) =>
|
||||
_CalloutListOverlay(animation: animation),
|
||||
);
|
||||
|
||||
if (selectedId != null && context.mounted) {
|
||||
await showCallout(context, ref, selectedId);
|
||||
}
|
||||
}
|
||||
|
||||
class _CalloutListOverlay extends ConsumerWidget {
|
||||
const _CalloutListOverlay({required this.animation});
|
||||
|
||||
final Animation<double> animation;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final callouts = ref.watch(calloutManagerProvider);
|
||||
|
||||
final barrierAnim = CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: const Interval(0, 0.3, curve: Curves.easeOut),
|
||||
);
|
||||
final panelAnim = CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: const Interval(0.3, 1, curve: Curves.easeOutCubic),
|
||||
);
|
||||
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
child: AnimatedBuilder(
|
||||
animation: barrierAnim,
|
||||
builder: (_, __) => ColoredBox(
|
||||
color: Colors.black.withValues(
|
||||
alpha: 0.35 * barrierAnim.value,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
child: Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(1.6.h),
|
||||
child: FadeTransition(
|
||||
opacity: panelAnim,
|
||||
child: SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0, 0.08),
|
||||
end: Offset.zero,
|
||||
).animate(panelAnim),
|
||||
child: GestureDetector(
|
||||
onTap: () {},
|
||||
child: _CalloutListPanel(callouts: callouts),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CalloutListPanel extends StatelessWidget {
|
||||
const _CalloutListPanel({required this.callouts});
|
||||
|
||||
final Map<String, ActiveCallout> callouts;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
constraints: BoxConstraints(maxHeight: 48.h),
|
||||
decoration: BoxDecoration(
|
||||
color: Palette.cream,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(color: Palette.line),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(2.h, 2.h, 2.h, 1.2.h),
|
||||
child: Text(
|
||||
'Notifications',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color: Palette.ink,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (callouts.isEmpty)
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(2.h, 0, 2.h, 2.h),
|
||||
child: Text(
|
||||
'No pending notifications.',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: Palette.ink.withValues(alpha: 0.50),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.fromLTRB(1.2.h, 0, 1.2.h, 1.2.h),
|
||||
child: Column(
|
||||
spacing: 0.5.h,
|
||||
children: [
|
||||
for (final entry in callouts.values)
|
||||
_CalloutListEntry(
|
||||
callout: entry,
|
||||
onTap: () => Navigator.of(context).pop(entry.id),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _CalloutListEntry extends StatelessWidget {
|
||||
const _CalloutListEntry({required this.callout, required this.onTap});
|
||||
|
||||
final ActiveCallout callout;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 1.2.h, vertical: 1.2.h),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Palette.line),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 1.2.h,
|
||||
children: [
|
||||
if (callout.iconUrl != null)
|
||||
CircleAvatar(
|
||||
radius: 2.2.h,
|
||||
backgroundColor: Palette.line,
|
||||
backgroundImage: NetworkImage(callout.iconUrl!),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 0.3.h,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
callout.title,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: Palette.ink,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
timeago.format(callout.addedAt),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: Palette.ink.withValues(alpha: 0.45),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
callout.description,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: Palette.ink.withValues(alpha: 0.65),
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +1,62 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:arbiter/features/callouts/callout_event.dart';
|
||||
import 'package:arbiter/proto/user_agent/sdk_client.pb.dart' as ua_sdk;
|
||||
import 'package:arbiter/proto/user_agent.pb.dart';
|
||||
import 'package:arbiter/providers/connection/connection_manager.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'sdk_connect_approve.g.dart';
|
||||
|
||||
@riverpod
|
||||
Stream<CalloutEvent> connectApproveEvents(Ref ref) async* {
|
||||
final connection = await ref.watch(connectionManagerProvider.future);
|
||||
if (connection == null) return;
|
||||
|
||||
await for (final message in connection.outOfBandMessages) {
|
||||
switch (message.whichPayload()) {
|
||||
case UserAgentResponse_Payload.sdkClient:
|
||||
final sdkClientMessage = message.sdkClient;
|
||||
switch (sdkClientMessage.whichPayload()) {
|
||||
case ua_sdk.Response_Payload.connectionRequest:
|
||||
final body = sdkClientMessage.connectionRequest;
|
||||
final id = base64Encode(body.pubkey);
|
||||
yield CalloutEvent.added(
|
||||
id: 'connect_approve:$id',
|
||||
data: CalloutData.connectApproval(
|
||||
pubkey: id,
|
||||
clientInfo: body.info,
|
||||
),
|
||||
);
|
||||
|
||||
case ua_sdk.Response_Payload.connectionCancel:
|
||||
final id = base64Encode(sdkClientMessage.connectionCancel.pubkey);
|
||||
yield CalloutEvent.cancelled(id: 'connect_approve:$id');
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sendDecision(Ref ref, String pubkey, bool approved) async {
|
||||
final connection = await ref.watch(connectionManagerProvider.future);
|
||||
if (connection == null) return;
|
||||
|
||||
final bytes = base64Decode(pubkey);
|
||||
|
||||
final req = UserAgentRequest(
|
||||
sdkClient: ua_sdk.Request(
|
||||
connectionResponse: ua_sdk.ConnectionResponse(
|
||||
approved: approved,
|
||||
pubkey: bytes,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await connection.tell(req);
|
||||
}
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:arbiter/features/callouts/callout_event.dart';
|
||||
import 'package:arbiter/proto/user_agent/sdk_client.pb.dart' as ua_sdk;
|
||||
import 'package:arbiter/proto/user_agent.pb.dart';
|
||||
import 'package:arbiter/providers/connection/connection_manager.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'sdk_connect_approve.g.dart';
|
||||
|
||||
@riverpod
|
||||
Stream<CalloutEvent> connectApproveEvents(Ref ref) async* {
|
||||
final connection = await ref.watch(connectionManagerProvider.future);
|
||||
if (connection == null) return;
|
||||
|
||||
await for (final message in connection.outOfBandMessages) {
|
||||
switch (message.whichPayload()) {
|
||||
case UserAgentResponse_Payload.sdkClient:
|
||||
final sdkClientMessage = message.sdkClient;
|
||||
switch (sdkClientMessage.whichPayload()) {
|
||||
case ua_sdk.Response_Payload.connectionRequest:
|
||||
final body = sdkClientMessage.connectionRequest;
|
||||
final id = base64Encode(body.pubkey);
|
||||
yield CalloutEvent.added(
|
||||
id: 'connect_approve:$id',
|
||||
data: CalloutData.connectApproval(
|
||||
pubkey: id,
|
||||
clientInfo: body.info,
|
||||
),
|
||||
);
|
||||
|
||||
case ua_sdk.Response_Payload.connectionCancel:
|
||||
final id = base64Encode(sdkClientMessage.connectionCancel.pubkey);
|
||||
yield CalloutEvent.cancelled(id: 'connect_approve:$id');
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> sendDecision(Ref ref, String pubkey, bool approved) async {
|
||||
final connection = await ref.watch(connectionManagerProvider.future);
|
||||
if (connection == null) return;
|
||||
|
||||
final bytes = base64Decode(pubkey);
|
||||
|
||||
final req = UserAgentRequest(
|
||||
sdkClient: ua_sdk.Request(
|
||||
connectionResponse: ua_sdk.ConnectionResponse(
|
||||
approved: approved,
|
||||
pubkey: bytes,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await connection.tell(req);
|
||||
}
|
||||
|
||||
@@ -1,50 +1,50 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'sdk_connect_approve.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(connectApproveEvents)
|
||||
final connectApproveEventsProvider = ConnectApproveEventsProvider._();
|
||||
|
||||
final class ConnectApproveEventsProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<CalloutEvent>,
|
||||
CalloutEvent,
|
||||
Stream<CalloutEvent>
|
||||
>
|
||||
with $FutureModifier<CalloutEvent>, $StreamProvider<CalloutEvent> {
|
||||
ConnectApproveEventsProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'connectApproveEventsProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$connectApproveEventsHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$StreamProviderElement<CalloutEvent> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $StreamProviderElement(pointer);
|
||||
|
||||
@override
|
||||
Stream<CalloutEvent> create(Ref ref) {
|
||||
return connectApproveEvents(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$connectApproveEventsHash() =>
|
||||
r'abab87cc875a9a4834f836c2c0eba4aa7671d82e';
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'sdk_connect_approve.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(connectApproveEvents)
|
||||
final connectApproveEventsProvider = ConnectApproveEventsProvider._();
|
||||
|
||||
final class ConnectApproveEventsProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<CalloutEvent>,
|
||||
CalloutEvent,
|
||||
Stream<CalloutEvent>
|
||||
>
|
||||
with $FutureModifier<CalloutEvent>, $StreamProvider<CalloutEvent> {
|
||||
ConnectApproveEventsProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'connectApproveEventsProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$connectApproveEventsHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$StreamProviderElement<CalloutEvent> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $StreamProviderElement(pointer);
|
||||
|
||||
@override
|
||||
Stream<CalloutEvent> create(Ref ref) {
|
||||
return connectApproveEvents(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$connectApproveEventsHash() =>
|
||||
r'abab87cc875a9a4834f836c2c0eba4aa7671d82e';
|
||||
|
||||
@@ -1,58 +1,58 @@
|
||||
import 'dart:convert';
|
||||
|
||||
class ArbiterUrl {
|
||||
const ArbiterUrl({
|
||||
required this.host,
|
||||
required this.port,
|
||||
required this.caCert,
|
||||
this.bootstrapToken,
|
||||
});
|
||||
|
||||
final String host;
|
||||
final int port;
|
||||
final List<int> caCert;
|
||||
final String? bootstrapToken;
|
||||
|
||||
static const _scheme = 'arbiter';
|
||||
static const _certQueryKey = 'cert';
|
||||
static const _bootstrapTokenQueryKey = 'bootstrap_token';
|
||||
|
||||
static ArbiterUrl parse(String value) {
|
||||
final uri = Uri.tryParse(value);
|
||||
if (uri == null || uri.scheme != _scheme) {
|
||||
throw const FormatException("Invalid URL scheme, expected 'arbiter://'");
|
||||
}
|
||||
|
||||
if (uri.host.isEmpty) {
|
||||
throw const FormatException('Missing host in URL');
|
||||
}
|
||||
|
||||
if (!uri.hasPort) {
|
||||
throw const FormatException('Missing port in URL');
|
||||
}
|
||||
|
||||
final cert = uri.queryParameters[_certQueryKey];
|
||||
if (cert == null || cert.isEmpty) {
|
||||
throw const FormatException("Missing 'cert' query parameter in URL");
|
||||
}
|
||||
|
||||
final decodedCert = _decodeCert(cert);
|
||||
|
||||
return ArbiterUrl(
|
||||
host: uri.host,
|
||||
port: uri.port,
|
||||
caCert: decodedCert,
|
||||
bootstrapToken: uri.queryParameters[_bootstrapTokenQueryKey],
|
||||
);
|
||||
}
|
||||
|
||||
static List<int> _decodeCert(String cert) {
|
||||
try {
|
||||
return base64Url.decode(base64Url.normalize(cert));
|
||||
} on FormatException catch (error) {
|
||||
throw FormatException(
|
||||
"Invalid base64 in 'cert' query parameter: ${error.message}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
import 'dart:convert';
|
||||
|
||||
class ArbiterUrl {
|
||||
const ArbiterUrl({
|
||||
required this.host,
|
||||
required this.port,
|
||||
required this.caCert,
|
||||
this.bootstrapToken,
|
||||
});
|
||||
|
||||
final String host;
|
||||
final int port;
|
||||
final List<int> caCert;
|
||||
final String? bootstrapToken;
|
||||
|
||||
static const _scheme = 'arbiter';
|
||||
static const _certQueryKey = 'cert';
|
||||
static const _bootstrapTokenQueryKey = 'bootstrap_token';
|
||||
|
||||
static ArbiterUrl parse(String value) {
|
||||
final uri = Uri.tryParse(value);
|
||||
if (uri == null || uri.scheme != _scheme) {
|
||||
throw const FormatException("Invalid URL scheme, expected 'arbiter://'");
|
||||
}
|
||||
|
||||
if (uri.host.isEmpty) {
|
||||
throw const FormatException('Missing host in URL');
|
||||
}
|
||||
|
||||
if (!uri.hasPort) {
|
||||
throw const FormatException('Missing port in URL');
|
||||
}
|
||||
|
||||
final cert = uri.queryParameters[_certQueryKey];
|
||||
if (cert == null || cert.isEmpty) {
|
||||
throw const FormatException("Missing 'cert' query parameter in URL");
|
||||
}
|
||||
|
||||
final decodedCert = _decodeCert(cert);
|
||||
|
||||
return ArbiterUrl(
|
||||
host: uri.host,
|
||||
port: uri.port,
|
||||
caCert: decodedCert,
|
||||
bootstrapToken: uri.queryParameters[_bootstrapTokenQueryKey],
|
||||
);
|
||||
}
|
||||
|
||||
static List<int> _decodeCert(String cert) {
|
||||
try {
|
||||
return base64Url.decode(base64Url.normalize(cert));
|
||||
} on FormatException catch (error) {
|
||||
throw FormatException(
|
||||
"Invalid base64 in 'cert' query parameter: ${error.message}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,170 +1,170 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:arbiter/features/connection/connection.dart';
|
||||
import 'package:arbiter/features/connection/server_info_storage.dart';
|
||||
import 'package:arbiter/features/identity/pk_manager.dart';
|
||||
import 'package:arbiter/proto/arbiter.pbgrpc.dart';
|
||||
import 'package:arbiter/proto/user_agent/auth.pb.dart' as ua_auth;
|
||||
import 'package:arbiter/proto/user_agent.pb.dart';
|
||||
import 'package:arbiter/src/rust/api.dart';
|
||||
import 'package:grpc/grpc.dart';
|
||||
import 'package:mtcore/markettakers.dart';
|
||||
|
||||
class AuthorizationException implements Exception {
|
||||
const AuthorizationException(this.result);
|
||||
|
||||
final ua_auth.AuthResult result;
|
||||
|
||||
String get message => switch (result) {
|
||||
ua_auth.AuthResult.AUTH_RESULT_INVALID_KEY =>
|
||||
'Authentication failed: this device key is not registered on the server.',
|
||||
ua_auth.AuthResult.AUTH_RESULT_INVALID_SIGNATURE =>
|
||||
'Authentication failed: the server rejected the signature for this device key.',
|
||||
ua_auth.AuthResult.AUTH_RESULT_BOOTSTRAP_REQUIRED =>
|
||||
'Authentication failed: the server requires bootstrap before this device can connect.',
|
||||
ua_auth.AuthResult.AUTH_RESULT_TOKEN_INVALID =>
|
||||
'Authentication failed: the bootstrap token is invalid.',
|
||||
ua_auth.AuthResult.AUTH_RESULT_INTERNAL =>
|
||||
'Authentication failed: the server hit an internal error.',
|
||||
ua_auth.AuthResult.AUTH_RESULT_UNSPECIFIED =>
|
||||
'Authentication failed: the server returned an unspecified auth error.',
|
||||
ua_auth.AuthResult.AUTH_RESULT_SUCCESS => 'Authentication succeeded.',
|
||||
_ => 'Authentication failed: ${result.name}.',
|
||||
};
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
|
||||
class ConnectionException implements Exception {
|
||||
const ConnectionException(this.message);
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
|
||||
Future<Connection> connectAndAuthorize(
|
||||
StoredServerInfo serverInfo,
|
||||
KeyHandle key, {
|
||||
String? bootstrapToken,
|
||||
}) async {
|
||||
Connection? connection;
|
||||
try {
|
||||
connection = await _connect(serverInfo);
|
||||
talker.info(
|
||||
'Connected to server at ${serverInfo.address}:${serverInfo.port}',
|
||||
);
|
||||
final pubkey = await key.getPublicKey();
|
||||
|
||||
final req = ua_auth.AuthChallengeRequest(
|
||||
pubkey: pubkey,
|
||||
bootstrapToken: bootstrapToken,
|
||||
);
|
||||
final response = await connection.ask(
|
||||
UserAgentRequest(auth: ua_auth.Request(challengeRequest: req)),
|
||||
);
|
||||
talker.info(
|
||||
"Sent auth challenge request with pubkey ${base64Encode(pubkey)}",
|
||||
);
|
||||
talker.info('Received response from server, checking auth flow...');
|
||||
|
||||
if (!response.hasAuth()) {
|
||||
throw ConnectionException(
|
||||
'Expected auth response, got ${response.whichPayload()}',
|
||||
);
|
||||
}
|
||||
|
||||
final authResponse = response.auth;
|
||||
|
||||
if (authResponse.hasResult()) {
|
||||
if (authResponse.result != ua_auth.AuthResult.AUTH_RESULT_SUCCESS) {
|
||||
throw AuthorizationException(authResponse.result);
|
||||
}
|
||||
talker.info('Authentication successful, connection established');
|
||||
return connection;
|
||||
}
|
||||
|
||||
if (!authResponse.hasChallenge()) {
|
||||
throw ConnectionException(
|
||||
'Expected auth challenge response, got ${authResponse.whichPayload()}',
|
||||
);
|
||||
}
|
||||
|
||||
final challenge = await formatChallenge(
|
||||
random: authResponse.challenge.random,
|
||||
timestamp: authResponse.challenge.timestampNanos.toInt(),
|
||||
);
|
||||
talker.info(
|
||||
'Received auth challenge, signing with key ${base64Encode(pubkey)}',
|
||||
);
|
||||
|
||||
final signature = await key.sign(challenge);
|
||||
final solutionResponse = await connection.ask(
|
||||
UserAgentRequest(
|
||||
auth: ua_auth.Request(
|
||||
challengeSolution: ua_auth.AuthChallengeSolution(
|
||||
signature: signature,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
talker.info('Sent auth challenge solution, waiting for server response...');
|
||||
|
||||
if (!solutionResponse.hasAuth()) {
|
||||
throw ConnectionException(
|
||||
'Expected auth solution response, got ${solutionResponse.whichPayload()}',
|
||||
);
|
||||
}
|
||||
|
||||
final authSolutionResponse = solutionResponse.auth;
|
||||
|
||||
if (!authSolutionResponse.hasResult()) {
|
||||
throw ConnectionException(
|
||||
'Expected auth solution result, got ${authSolutionResponse.whichPayload()}',
|
||||
);
|
||||
}
|
||||
if (authSolutionResponse.result != ua_auth.AuthResult.AUTH_RESULT_SUCCESS) {
|
||||
throw AuthorizationException(authSolutionResponse.result);
|
||||
}
|
||||
|
||||
talker.info('Authentication successful, connection established');
|
||||
return connection;
|
||||
} on AuthorizationException {
|
||||
await connection?.close();
|
||||
rethrow;
|
||||
} on GrpcError catch (error) {
|
||||
await connection?.close();
|
||||
throw ConnectionException('Failed to connect to server: ${error.message}');
|
||||
} catch (e) {
|
||||
await connection?.close();
|
||||
if (e is ConnectionException) {
|
||||
rethrow;
|
||||
}
|
||||
throw ConnectionException('Failed to connect to server: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<Connection> _connect(StoredServerInfo serverInfo) async {
|
||||
final channel = ClientChannel(
|
||||
serverInfo.address,
|
||||
port: serverInfo.port,
|
||||
options: ChannelOptions(
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
credentials: ChannelCredentials.secure(
|
||||
onBadCertificate: (cert, host) {
|
||||
return true;
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final client = ArbiterServiceClient(channel);
|
||||
final tx = StreamController<UserAgentRequest>();
|
||||
final rx = client.userAgent(tx.stream);
|
||||
|
||||
return Connection(channel: channel, tx: tx, rx: rx);
|
||||
}
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:arbiter/features/connection/connection.dart';
|
||||
import 'package:arbiter/features/connection/server_info_storage.dart';
|
||||
import 'package:arbiter/features/identity/pk_manager.dart';
|
||||
import 'package:arbiter/proto/arbiter.pbgrpc.dart';
|
||||
import 'package:arbiter/proto/user_agent/auth.pb.dart' as ua_auth;
|
||||
import 'package:arbiter/proto/user_agent.pb.dart';
|
||||
import 'package:arbiter/src/rust/api.dart';
|
||||
import 'package:grpc/grpc.dart';
|
||||
import 'package:mtcore/markettakers.dart';
|
||||
|
||||
class AuthorizationException implements Exception {
|
||||
const AuthorizationException(this.result);
|
||||
|
||||
final ua_auth.AuthResult result;
|
||||
|
||||
String get message => switch (result) {
|
||||
ua_auth.AuthResult.AUTH_RESULT_INVALID_KEY =>
|
||||
'Authentication failed: this device key is not registered on the server.',
|
||||
ua_auth.AuthResult.AUTH_RESULT_INVALID_SIGNATURE =>
|
||||
'Authentication failed: the server rejected the signature for this device key.',
|
||||
ua_auth.AuthResult.AUTH_RESULT_BOOTSTRAP_REQUIRED =>
|
||||
'Authentication failed: the server requires bootstrap before this device can connect.',
|
||||
ua_auth.AuthResult.AUTH_RESULT_TOKEN_INVALID =>
|
||||
'Authentication failed: the bootstrap token is invalid.',
|
||||
ua_auth.AuthResult.AUTH_RESULT_INTERNAL =>
|
||||
'Authentication failed: the server hit an internal error.',
|
||||
ua_auth.AuthResult.AUTH_RESULT_UNSPECIFIED =>
|
||||
'Authentication failed: the server returned an unspecified auth error.',
|
||||
ua_auth.AuthResult.AUTH_RESULT_SUCCESS => 'Authentication succeeded.',
|
||||
_ => 'Authentication failed: ${result.name}.',
|
||||
};
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
|
||||
class ConnectionException implements Exception {
|
||||
const ConnectionException(this.message);
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() => message;
|
||||
}
|
||||
|
||||
Future<Connection> connectAndAuthorize(
|
||||
StoredServerInfo serverInfo,
|
||||
KeyHandle key, {
|
||||
String? bootstrapToken,
|
||||
}) async {
|
||||
Connection? connection;
|
||||
try {
|
||||
connection = await _connect(serverInfo);
|
||||
talker.info(
|
||||
'Connected to server at ${serverInfo.address}:${serverInfo.port}',
|
||||
);
|
||||
final pubkey = await key.getPublicKey();
|
||||
|
||||
final req = ua_auth.AuthChallengeRequest(
|
||||
pubkey: pubkey,
|
||||
bootstrapToken: bootstrapToken,
|
||||
);
|
||||
final response = await connection.ask(
|
||||
UserAgentRequest(auth: ua_auth.Request(challengeRequest: req)),
|
||||
);
|
||||
talker.info(
|
||||
"Sent auth challenge request with pubkey ${base64Encode(pubkey)}",
|
||||
);
|
||||
talker.info('Received response from server, checking auth flow...');
|
||||
|
||||
if (!response.hasAuth()) {
|
||||
throw ConnectionException(
|
||||
'Expected auth response, got ${response.whichPayload()}',
|
||||
);
|
||||
}
|
||||
|
||||
final authResponse = response.auth;
|
||||
|
||||
if (authResponse.hasResult()) {
|
||||
if (authResponse.result != ua_auth.AuthResult.AUTH_RESULT_SUCCESS) {
|
||||
throw AuthorizationException(authResponse.result);
|
||||
}
|
||||
talker.info('Authentication successful, connection established');
|
||||
return connection;
|
||||
}
|
||||
|
||||
if (!authResponse.hasChallenge()) {
|
||||
throw ConnectionException(
|
||||
'Expected auth challenge response, got ${authResponse.whichPayload()}',
|
||||
);
|
||||
}
|
||||
|
||||
final challenge = await formatChallenge(
|
||||
random: authResponse.challenge.random,
|
||||
timestamp: authResponse.challenge.timestampNanos.toInt(),
|
||||
);
|
||||
talker.info(
|
||||
'Received auth challenge, signing with key ${base64Encode(pubkey)}',
|
||||
);
|
||||
|
||||
final signature = await key.sign(challenge);
|
||||
final solutionResponse = await connection.ask(
|
||||
UserAgentRequest(
|
||||
auth: ua_auth.Request(
|
||||
challengeSolution: ua_auth.AuthChallengeSolution(
|
||||
signature: signature,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
talker.info('Sent auth challenge solution, waiting for server response...');
|
||||
|
||||
if (!solutionResponse.hasAuth()) {
|
||||
throw ConnectionException(
|
||||
'Expected auth solution response, got ${solutionResponse.whichPayload()}',
|
||||
);
|
||||
}
|
||||
|
||||
final authSolutionResponse = solutionResponse.auth;
|
||||
|
||||
if (!authSolutionResponse.hasResult()) {
|
||||
throw ConnectionException(
|
||||
'Expected auth solution result, got ${authSolutionResponse.whichPayload()}',
|
||||
);
|
||||
}
|
||||
if (authSolutionResponse.result != ua_auth.AuthResult.AUTH_RESULT_SUCCESS) {
|
||||
throw AuthorizationException(authSolutionResponse.result);
|
||||
}
|
||||
|
||||
talker.info('Authentication successful, connection established');
|
||||
return connection;
|
||||
} on AuthorizationException {
|
||||
await connection?.close();
|
||||
rethrow;
|
||||
} on GrpcError catch (error) {
|
||||
await connection?.close();
|
||||
throw ConnectionException('Failed to connect to server: ${error.message}');
|
||||
} catch (e) {
|
||||
await connection?.close();
|
||||
if (e is ConnectionException) {
|
||||
rethrow;
|
||||
}
|
||||
throw ConnectionException('Failed to connect to server: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<Connection> _connect(StoredServerInfo serverInfo) async {
|
||||
final channel = ClientChannel(
|
||||
serverInfo.address,
|
||||
port: serverInfo.port,
|
||||
options: ChannelOptions(
|
||||
connectTimeout: const Duration(seconds: 10),
|
||||
credentials: ChannelCredentials.secure(
|
||||
onBadCertificate: (cert, host) {
|
||||
return true;
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final client = ArbiterServiceClient(channel);
|
||||
final tx = StreamController<UserAgentRequest>();
|
||||
final rx = client.userAgent(tx.stream);
|
||||
|
||||
return Connection(channel: channel, tx: tx, rx: rx);
|
||||
}
|
||||
|
||||
@@ -1,136 +1,136 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:arbiter/proto/user_agent.pb.dart';
|
||||
import 'package:grpc/grpc.dart';
|
||||
import 'package:mtcore/markettakers.dart';
|
||||
|
||||
class Connection {
|
||||
Connection({
|
||||
required this.channel,
|
||||
required StreamController<UserAgentRequest> tx,
|
||||
required ResponseStream<UserAgentResponse> rx,
|
||||
}) : _tx = tx {
|
||||
_rxSubscription = rx.listen(
|
||||
_handleResponse,
|
||||
onError: _handleError,
|
||||
onDone: _handleDone,
|
||||
cancelOnError: true,
|
||||
);
|
||||
}
|
||||
|
||||
final ClientChannel channel;
|
||||
final StreamController<UserAgentRequest> _tx;
|
||||
final Map<int, Completer<UserAgentResponse>> _pendingRequests = {};
|
||||
final StreamController<UserAgentResponse> _outOfBandMessages =
|
||||
StreamController<UserAgentResponse>.broadcast();
|
||||
|
||||
StreamSubscription<UserAgentResponse>? _rxSubscription;
|
||||
int _nextRequestId = 0;
|
||||
|
||||
Stream<UserAgentResponse> get outOfBandMessages => _outOfBandMessages.stream;
|
||||
|
||||
Future<UserAgentResponse> ask(UserAgentRequest message) async {
|
||||
_ensureOpen();
|
||||
|
||||
final requestId = _nextRequestId++;
|
||||
final completer = Completer<UserAgentResponse>();
|
||||
_pendingRequests[requestId] = completer;
|
||||
|
||||
message.id = requestId;
|
||||
talker.debug('Sending request: ${message.toDebugString()}');
|
||||
|
||||
try {
|
||||
_tx.add(message);
|
||||
} catch (error, stackTrace) {
|
||||
_pendingRequests.remove(requestId);
|
||||
completer.completeError(error, stackTrace);
|
||||
}
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future<void> tell(UserAgentRequest message) async {
|
||||
_ensureOpen();
|
||||
|
||||
final requestId = _nextRequestId++;
|
||||
message.id = requestId;
|
||||
|
||||
talker.debug('Sending message: ${message.toDebugString()}');
|
||||
|
||||
try {
|
||||
_tx.add(message);
|
||||
} catch (error, stackTrace) {
|
||||
talker.error('Failed to send message: $error', error, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> close() async {
|
||||
talker.debug('Closing connection...');
|
||||
final rxSubscription = _rxSubscription;
|
||||
if (rxSubscription == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
_rxSubscription = null;
|
||||
await rxSubscription.cancel();
|
||||
_failPendingRequests(Exception('Connection closed.'));
|
||||
await _outOfBandMessages.close();
|
||||
await _tx.close();
|
||||
await channel.shutdown();
|
||||
}
|
||||
|
||||
void _handleResponse(UserAgentResponse response) {
|
||||
talker.debug('Received response: ${response.toDebugString()}');
|
||||
|
||||
if (response.hasId()) {
|
||||
final completer = _pendingRequests.remove(response.id);
|
||||
if (completer == null) {
|
||||
talker.warning(
|
||||
'Received response for unknown request id ${response.id}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
completer.complete(response);
|
||||
return;
|
||||
}
|
||||
|
||||
_outOfBandMessages.add(response);
|
||||
}
|
||||
|
||||
void _handleError(Object error, StackTrace stackTrace) {
|
||||
_rxSubscription = null;
|
||||
_failPendingRequests(error, stackTrace);
|
||||
_outOfBandMessages.addError(error, stackTrace);
|
||||
}
|
||||
|
||||
void _handleDone() {
|
||||
talker.debug('Connection closed by server.');
|
||||
if (_rxSubscription == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
_rxSubscription = null;
|
||||
final error = Exception(
|
||||
'Connection closed while waiting for server response.',
|
||||
);
|
||||
_failPendingRequests(error);
|
||||
_outOfBandMessages.close();
|
||||
}
|
||||
|
||||
void _failPendingRequests(Object error, [StackTrace? stackTrace]) {
|
||||
final pendingRequests = _pendingRequests.values.toList(growable: false);
|
||||
_pendingRequests.clear();
|
||||
|
||||
for (final completer in pendingRequests) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.completeError(error, stackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _ensureOpen() {
|
||||
if (_rxSubscription == null) {
|
||||
throw StateError('Connection is closed.');
|
||||
}
|
||||
}
|
||||
}
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:arbiter/proto/user_agent.pb.dart';
|
||||
import 'package:grpc/grpc.dart';
|
||||
import 'package:mtcore/markettakers.dart';
|
||||
|
||||
class Connection {
|
||||
Connection({
|
||||
required this.channel,
|
||||
required StreamController<UserAgentRequest> tx,
|
||||
required ResponseStream<UserAgentResponse> rx,
|
||||
}) : _tx = tx {
|
||||
_rxSubscription = rx.listen(
|
||||
_handleResponse,
|
||||
onError: _handleError,
|
||||
onDone: _handleDone,
|
||||
cancelOnError: true,
|
||||
);
|
||||
}
|
||||
|
||||
final ClientChannel channel;
|
||||
final StreamController<UserAgentRequest> _tx;
|
||||
final Map<int, Completer<UserAgentResponse>> _pendingRequests = {};
|
||||
final StreamController<UserAgentResponse> _outOfBandMessages =
|
||||
StreamController<UserAgentResponse>.broadcast();
|
||||
|
||||
StreamSubscription<UserAgentResponse>? _rxSubscription;
|
||||
int _nextRequestId = 0;
|
||||
|
||||
Stream<UserAgentResponse> get outOfBandMessages => _outOfBandMessages.stream;
|
||||
|
||||
Future<UserAgentResponse> ask(UserAgentRequest message) async {
|
||||
_ensureOpen();
|
||||
|
||||
final requestId = _nextRequestId++;
|
||||
final completer = Completer<UserAgentResponse>();
|
||||
_pendingRequests[requestId] = completer;
|
||||
|
||||
message.id = requestId;
|
||||
talker.debug('Sending request: ${message.toDebugString()}');
|
||||
|
||||
try {
|
||||
_tx.add(message);
|
||||
} catch (error, stackTrace) {
|
||||
_pendingRequests.remove(requestId);
|
||||
completer.completeError(error, stackTrace);
|
||||
}
|
||||
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
Future<void> tell(UserAgentRequest message) async {
|
||||
_ensureOpen();
|
||||
|
||||
final requestId = _nextRequestId++;
|
||||
message.id = requestId;
|
||||
|
||||
talker.debug('Sending message: ${message.toDebugString()}');
|
||||
|
||||
try {
|
||||
_tx.add(message);
|
||||
} catch (error, stackTrace) {
|
||||
talker.error('Failed to send message: $error', error, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> close() async {
|
||||
talker.debug('Closing connection...');
|
||||
final rxSubscription = _rxSubscription;
|
||||
if (rxSubscription == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
_rxSubscription = null;
|
||||
await rxSubscription.cancel();
|
||||
_failPendingRequests(Exception('Connection closed.'));
|
||||
await _outOfBandMessages.close();
|
||||
await _tx.close();
|
||||
await channel.shutdown();
|
||||
}
|
||||
|
||||
void _handleResponse(UserAgentResponse response) {
|
||||
talker.debug('Received response: ${response.toDebugString()}');
|
||||
|
||||
if (response.hasId()) {
|
||||
final completer = _pendingRequests.remove(response.id);
|
||||
if (completer == null) {
|
||||
talker.warning(
|
||||
'Received response for unknown request id ${response.id}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
completer.complete(response);
|
||||
return;
|
||||
}
|
||||
|
||||
_outOfBandMessages.add(response);
|
||||
}
|
||||
|
||||
void _handleError(Object error, StackTrace stackTrace) {
|
||||
_rxSubscription = null;
|
||||
_failPendingRequests(error, stackTrace);
|
||||
_outOfBandMessages.addError(error, stackTrace);
|
||||
}
|
||||
|
||||
void _handleDone() {
|
||||
talker.debug('Connection closed by server.');
|
||||
if (_rxSubscription == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
_rxSubscription = null;
|
||||
final error = Exception(
|
||||
'Connection closed while waiting for server response.',
|
||||
);
|
||||
_failPendingRequests(error);
|
||||
_outOfBandMessages.close();
|
||||
}
|
||||
|
||||
void _failPendingRequests(Object error, [StackTrace? stackTrace]) {
|
||||
final pendingRequests = _pendingRequests.values.toList(growable: false);
|
||||
_pendingRequests.clear();
|
||||
|
||||
for (final completer in pendingRequests) {
|
||||
if (!completer.isCompleted) {
|
||||
completer.completeError(error, stackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _ensureOpen() {
|
||||
if (_rxSubscription == null) {
|
||||
throw StateError('Connection is closed.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,67 +1,67 @@
|
||||
import 'package:arbiter/features/connection/connection.dart';
|
||||
import 'package:arbiter/proto/evm.pb.dart';
|
||||
import 'package:arbiter/proto/user_agent/evm.pb.dart' as ua_evm;
|
||||
import 'package:arbiter/proto/user_agent.pb.dart';
|
||||
import 'package:protobuf/well_known_types/google/protobuf/empty.pb.dart';
|
||||
|
||||
Future<List<WalletEntry>> listEvmWallets(Connection connection) async {
|
||||
final response = await connection.ask(
|
||||
UserAgentRequest(evm: ua_evm.Request(walletList: Empty())),
|
||||
);
|
||||
if (!response.hasEvm()) {
|
||||
throw Exception('Expected EVM response, got ${response.whichPayload()}');
|
||||
}
|
||||
|
||||
final evmResponse = response.evm;
|
||||
if (!evmResponse.hasWalletList()) {
|
||||
throw Exception(
|
||||
'Expected EVM wallet list response, got ${evmResponse.whichPayload()}',
|
||||
);
|
||||
}
|
||||
|
||||
final result = evmResponse.walletList;
|
||||
switch (result.whichResult()) {
|
||||
case WalletListResponse_Result.wallets:
|
||||
return result.wallets.wallets.toList(growable: false);
|
||||
case WalletListResponse_Result.error:
|
||||
throw Exception(_describeEvmError(result.error));
|
||||
case WalletListResponse_Result.notSet:
|
||||
throw Exception('EVM wallet list response was empty.');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> createEvmWallet(Connection connection) async {
|
||||
final response = await connection.ask(
|
||||
UserAgentRequest(evm: ua_evm.Request(walletCreate: Empty())),
|
||||
);
|
||||
if (!response.hasEvm()) {
|
||||
throw Exception('Expected EVM response, got ${response.whichPayload()}');
|
||||
}
|
||||
|
||||
final evmResponse = response.evm;
|
||||
if (!evmResponse.hasWalletCreate()) {
|
||||
throw Exception(
|
||||
'Expected EVM wallet create response, got ${evmResponse.whichPayload()}',
|
||||
);
|
||||
}
|
||||
|
||||
final result = evmResponse.walletCreate;
|
||||
switch (result.whichResult()) {
|
||||
case WalletCreateResponse_Result.wallet:
|
||||
return;
|
||||
case WalletCreateResponse_Result.error:
|
||||
throw Exception(_describeEvmError(result.error));
|
||||
case WalletCreateResponse_Result.notSet:
|
||||
throw Exception('Wallet creation returned no result.');
|
||||
}
|
||||
}
|
||||
|
||||
String _describeEvmError(EvmError error) {
|
||||
return switch (error) {
|
||||
EvmError.EVM_ERROR_VAULT_SEALED =>
|
||||
'The vault is sealed. Unseal it before using EVM wallets.',
|
||||
EvmError.EVM_ERROR_INTERNAL || EvmError.EVM_ERROR_UNSPECIFIED =>
|
||||
'The server failed to process the EVM request.',
|
||||
_ => 'The server failed to process the EVM request.',
|
||||
};
|
||||
}
|
||||
import 'package:arbiter/features/connection/connection.dart';
|
||||
import 'package:arbiter/proto/evm.pb.dart';
|
||||
import 'package:arbiter/proto/user_agent/evm.pb.dart' as ua_evm;
|
||||
import 'package:arbiter/proto/user_agent.pb.dart';
|
||||
import 'package:protobuf/well_known_types/google/protobuf/empty.pb.dart';
|
||||
|
||||
Future<List<WalletEntry>> listEvmWallets(Connection connection) async {
|
||||
final response = await connection.ask(
|
||||
UserAgentRequest(evm: ua_evm.Request(walletList: Empty())),
|
||||
);
|
||||
if (!response.hasEvm()) {
|
||||
throw Exception('Expected EVM response, got ${response.whichPayload()}');
|
||||
}
|
||||
|
||||
final evmResponse = response.evm;
|
||||
if (!evmResponse.hasWalletList()) {
|
||||
throw Exception(
|
||||
'Expected EVM wallet list response, got ${evmResponse.whichPayload()}',
|
||||
);
|
||||
}
|
||||
|
||||
final result = evmResponse.walletList;
|
||||
switch (result.whichResult()) {
|
||||
case WalletListResponse_Result.wallets:
|
||||
return result.wallets.wallets.toList(growable: false);
|
||||
case WalletListResponse_Result.error:
|
||||
throw Exception(_describeEvmError(result.error));
|
||||
case WalletListResponse_Result.notSet:
|
||||
throw Exception('EVM wallet list response was empty.');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> createEvmWallet(Connection connection) async {
|
||||
final response = await connection.ask(
|
||||
UserAgentRequest(evm: ua_evm.Request(walletCreate: Empty())),
|
||||
);
|
||||
if (!response.hasEvm()) {
|
||||
throw Exception('Expected EVM response, got ${response.whichPayload()}');
|
||||
}
|
||||
|
||||
final evmResponse = response.evm;
|
||||
if (!evmResponse.hasWalletCreate()) {
|
||||
throw Exception(
|
||||
'Expected EVM wallet create response, got ${evmResponse.whichPayload()}',
|
||||
);
|
||||
}
|
||||
|
||||
final result = evmResponse.walletCreate;
|
||||
switch (result.whichResult()) {
|
||||
case WalletCreateResponse_Result.wallet:
|
||||
return;
|
||||
case WalletCreateResponse_Result.error:
|
||||
throw Exception(_describeEvmError(result.error));
|
||||
case WalletCreateResponse_Result.notSet:
|
||||
throw Exception('Wallet creation returned no result.');
|
||||
}
|
||||
}
|
||||
|
||||
String _describeEvmError(EvmError error) {
|
||||
return switch (error) {
|
||||
EvmError.EVM_ERROR_VAULT_SEALED =>
|
||||
'The vault is sealed. Unseal it before using EVM wallets.',
|
||||
EvmError.EVM_ERROR_INTERNAL || EvmError.EVM_ERROR_UNSPECIFIED =>
|
||||
'The server failed to process the EVM request.',
|
||||
_ => 'The server failed to process the EVM request.',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,102 +1,102 @@
|
||||
import 'package:arbiter/features/connection/connection.dart';
|
||||
import 'package:arbiter/proto/evm.pb.dart';
|
||||
import 'package:arbiter/proto/user_agent/evm.pb.dart' as ua_evm;
|
||||
import 'package:arbiter/proto/user_agent.pb.dart';
|
||||
|
||||
Future<List<GrantEntry>> listEvmGrants(Connection connection) async {
|
||||
final request = EvmGrantListRequest();
|
||||
|
||||
final response = await connection.ask(
|
||||
UserAgentRequest(evm: ua_evm.Request(grantList: request)),
|
||||
);
|
||||
if (!response.hasEvm()) {
|
||||
throw Exception('Expected EVM response, got ${response.whichPayload()}');
|
||||
}
|
||||
|
||||
final evmResponse = response.evm;
|
||||
if (!evmResponse.hasGrantList()) {
|
||||
throw Exception(
|
||||
'Expected EVM grant list response, got ${evmResponse.whichPayload()}',
|
||||
);
|
||||
}
|
||||
|
||||
final result = evmResponse.grantList;
|
||||
switch (result.whichResult()) {
|
||||
case EvmGrantListResponse_Result.grants:
|
||||
return result.grants.grants.toList(growable: false);
|
||||
case EvmGrantListResponse_Result.error:
|
||||
throw Exception(_describeGrantError(result.error));
|
||||
case EvmGrantListResponse_Result.notSet:
|
||||
throw Exception('EVM grant list response was empty.');
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> createEvmGrant(
|
||||
Connection connection, {
|
||||
required SharedSettings sharedSettings,
|
||||
required SpecificGrant specific,
|
||||
}) async {
|
||||
final request = UserAgentRequest(
|
||||
evm: ua_evm.Request(
|
||||
grantCreate: EvmGrantCreateRequest(
|
||||
shared: sharedSettings,
|
||||
specific: specific,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final resp = await connection.ask(request);
|
||||
|
||||
if (!resp.hasEvm()) {
|
||||
throw Exception('Expected EVM response, got ${resp.whichPayload()}');
|
||||
}
|
||||
|
||||
final evmResponse = resp.evm;
|
||||
if (!evmResponse.hasGrantCreate()) {
|
||||
throw Exception(
|
||||
'Expected EVM grant create response, got ${evmResponse.whichPayload()}',
|
||||
);
|
||||
}
|
||||
|
||||
final result = evmResponse.grantCreate;
|
||||
|
||||
return result.grantId;
|
||||
}
|
||||
|
||||
Future<void> deleteEvmGrant(Connection connection, int grantId) async {
|
||||
final response = await connection.ask(
|
||||
UserAgentRequest(
|
||||
evm: ua_evm.Request(grantDelete: EvmGrantDeleteRequest(grantId: grantId)),
|
||||
),
|
||||
);
|
||||
if (!response.hasEvm()) {
|
||||
throw Exception('Expected EVM response, got ${response.whichPayload()}');
|
||||
}
|
||||
|
||||
final evmResponse = response.evm;
|
||||
if (!evmResponse.hasGrantDelete()) {
|
||||
throw Exception(
|
||||
'Expected EVM grant delete response, got ${evmResponse.whichPayload()}',
|
||||
);
|
||||
}
|
||||
|
||||
final result = evmResponse.grantDelete;
|
||||
switch (result.whichResult()) {
|
||||
case EvmGrantDeleteResponse_Result.ok:
|
||||
return;
|
||||
case EvmGrantDeleteResponse_Result.error:
|
||||
throw Exception(_describeGrantError(result.error));
|
||||
case EvmGrantDeleteResponse_Result.notSet:
|
||||
throw Exception('Grant revoke returned no result.');
|
||||
}
|
||||
}
|
||||
|
||||
String _describeGrantError(EvmError error) {
|
||||
return switch (error) {
|
||||
EvmError.EVM_ERROR_VAULT_SEALED =>
|
||||
'The vault is sealed. Unseal it before using EVM grants.',
|
||||
EvmError.EVM_ERROR_INTERNAL || EvmError.EVM_ERROR_UNSPECIFIED =>
|
||||
'The server failed to process the EVM grant request.',
|
||||
_ => 'The server failed to process the EVM grant request.',
|
||||
};
|
||||
}
|
||||
import 'package:arbiter/features/connection/connection.dart';
|
||||
import 'package:arbiter/proto/evm.pb.dart';
|
||||
import 'package:arbiter/proto/user_agent/evm.pb.dart' as ua_evm;
|
||||
import 'package:arbiter/proto/user_agent.pb.dart';
|
||||
|
||||
Future<List<GrantEntry>> listEvmGrants(Connection connection) async {
|
||||
final request = EvmGrantListRequest();
|
||||
|
||||
final response = await connection.ask(
|
||||
UserAgentRequest(evm: ua_evm.Request(grantList: request)),
|
||||
);
|
||||
if (!response.hasEvm()) {
|
||||
throw Exception('Expected EVM response, got ${response.whichPayload()}');
|
||||
}
|
||||
|
||||
final evmResponse = response.evm;
|
||||
if (!evmResponse.hasGrantList()) {
|
||||
throw Exception(
|
||||
'Expected EVM grant list response, got ${evmResponse.whichPayload()}',
|
||||
);
|
||||
}
|
||||
|
||||
final result = evmResponse.grantList;
|
||||
switch (result.whichResult()) {
|
||||
case EvmGrantListResponse_Result.grants:
|
||||
return result.grants.grants.toList(growable: false);
|
||||
case EvmGrantListResponse_Result.error:
|
||||
throw Exception(_describeGrantError(result.error));
|
||||
case EvmGrantListResponse_Result.notSet:
|
||||
throw Exception('EVM grant list response was empty.');
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> createEvmGrant(
|
||||
Connection connection, {
|
||||
required SharedSettings sharedSettings,
|
||||
required SpecificGrant specific,
|
||||
}) async {
|
||||
final request = UserAgentRequest(
|
||||
evm: ua_evm.Request(
|
||||
grantCreate: EvmGrantCreateRequest(
|
||||
shared: sharedSettings,
|
||||
specific: specific,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final resp = await connection.ask(request);
|
||||
|
||||
if (!resp.hasEvm()) {
|
||||
throw Exception('Expected EVM response, got ${resp.whichPayload()}');
|
||||
}
|
||||
|
||||
final evmResponse = resp.evm;
|
||||
if (!evmResponse.hasGrantCreate()) {
|
||||
throw Exception(
|
||||
'Expected EVM grant create response, got ${evmResponse.whichPayload()}',
|
||||
);
|
||||
}
|
||||
|
||||
final result = evmResponse.grantCreate;
|
||||
|
||||
return result.grantId;
|
||||
}
|
||||
|
||||
Future<void> deleteEvmGrant(Connection connection, int grantId) async {
|
||||
final response = await connection.ask(
|
||||
UserAgentRequest(
|
||||
evm: ua_evm.Request(grantDelete: EvmGrantDeleteRequest(grantId: grantId)),
|
||||
),
|
||||
);
|
||||
if (!response.hasEvm()) {
|
||||
throw Exception('Expected EVM response, got ${response.whichPayload()}');
|
||||
}
|
||||
|
||||
final evmResponse = response.evm;
|
||||
if (!evmResponse.hasGrantDelete()) {
|
||||
throw Exception(
|
||||
'Expected EVM grant delete response, got ${evmResponse.whichPayload()}',
|
||||
);
|
||||
}
|
||||
|
||||
final result = evmResponse.grantDelete;
|
||||
switch (result.whichResult()) {
|
||||
case EvmGrantDeleteResponse_Result.ok:
|
||||
return;
|
||||
case EvmGrantDeleteResponse_Result.error:
|
||||
throw Exception(_describeGrantError(result.error));
|
||||
case EvmGrantDeleteResponse_Result.notSet:
|
||||
throw Exception('Grant revoke returned no result.');
|
||||
}
|
||||
}
|
||||
|
||||
String _describeGrantError(EvmError error) {
|
||||
return switch (error) {
|
||||
EvmError.EVM_ERROR_VAULT_SEALED =>
|
||||
'The vault is sealed. Unseal it before using EVM grants.',
|
||||
EvmError.EVM_ERROR_INTERNAL || EvmError.EVM_ERROR_UNSPECIFIED =>
|
||||
'The server failed to process the EVM grant request.',
|
||||
_ => 'The server failed to process the EVM grant request.',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,86 +1,86 @@
|
||||
import 'package:arbiter/features/connection/connection.dart';
|
||||
import 'package:arbiter/proto/user_agent/sdk_client.pb.dart' as ua_sdk;
|
||||
import 'package:arbiter/proto/user_agent.pb.dart';
|
||||
import 'package:protobuf/well_known_types/google/protobuf/empty.pb.dart';
|
||||
|
||||
Future<Set<int>> readClientWalletAccess(
|
||||
Connection connection, {
|
||||
required int clientId,
|
||||
}) async {
|
||||
final response = await connection.ask(
|
||||
UserAgentRequest(sdkClient: ua_sdk.Request(listWalletAccess: Empty())),
|
||||
);
|
||||
if (!response.hasSdkClient()) {
|
||||
throw Exception(
|
||||
'Expected SDK client response, got ${response.whichPayload()}',
|
||||
);
|
||||
}
|
||||
final sdkClientResponse = response.sdkClient;
|
||||
if (!sdkClientResponse.hasListWalletAccess()) {
|
||||
throw Exception(
|
||||
'Expected list wallet access response, got ${sdkClientResponse.whichPayload()}',
|
||||
);
|
||||
}
|
||||
return {
|
||||
for (final entry in sdkClientResponse.listWalletAccess.accesses)
|
||||
if (entry.access.sdkClientId == clientId) entry.access.walletId,
|
||||
};
|
||||
}
|
||||
|
||||
Future<List<ua_sdk.WalletAccessEntry>> listAllWalletAccesses(
|
||||
Connection connection,
|
||||
) async {
|
||||
final response = await connection.ask(
|
||||
UserAgentRequest(sdkClient: ua_sdk.Request(listWalletAccess: Empty())),
|
||||
);
|
||||
if (!response.hasSdkClient()) {
|
||||
throw Exception(
|
||||
'Expected SDK client response, got ${response.whichPayload()}',
|
||||
);
|
||||
}
|
||||
final sdkClientResponse = response.sdkClient;
|
||||
if (!sdkClientResponse.hasListWalletAccess()) {
|
||||
throw Exception(
|
||||
'Expected list wallet access response, got ${sdkClientResponse.whichPayload()}',
|
||||
);
|
||||
}
|
||||
return sdkClientResponse.listWalletAccess.accesses.toList(growable: false);
|
||||
}
|
||||
|
||||
Future<void> writeClientWalletAccess(
|
||||
Connection connection, {
|
||||
required int clientId,
|
||||
required Set<int> walletIds,
|
||||
}) async {
|
||||
final current = await readClientWalletAccess(connection, clientId: clientId);
|
||||
|
||||
final toGrant = walletIds.difference(current);
|
||||
final toRevoke = current.difference(walletIds);
|
||||
|
||||
if (toGrant.isNotEmpty) {
|
||||
await connection.tell(
|
||||
UserAgentRequest(
|
||||
sdkClient: ua_sdk.Request(
|
||||
grantWalletAccess: ua_sdk.GrantWalletAccess(
|
||||
accesses: [
|
||||
for (final walletId in toGrant)
|
||||
ua_sdk.WalletAccess(sdkClientId: clientId, walletId: walletId),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (toRevoke.isNotEmpty) {
|
||||
await connection.tell(
|
||||
UserAgentRequest(
|
||||
sdkClient: ua_sdk.Request(
|
||||
revokeWalletAccess: ua_sdk.RevokeWalletAccess(
|
||||
accesses: [for (final walletId in toRevoke) walletId],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
import 'package:arbiter/features/connection/connection.dart';
|
||||
import 'package:arbiter/proto/user_agent/sdk_client.pb.dart' as ua_sdk;
|
||||
import 'package:arbiter/proto/user_agent.pb.dart';
|
||||
import 'package:protobuf/well_known_types/google/protobuf/empty.pb.dart';
|
||||
|
||||
Future<Set<int>> readClientWalletAccess(
|
||||
Connection connection, {
|
||||
required int clientId,
|
||||
}) async {
|
||||
final response = await connection.ask(
|
||||
UserAgentRequest(sdkClient: ua_sdk.Request(listWalletAccess: Empty())),
|
||||
);
|
||||
if (!response.hasSdkClient()) {
|
||||
throw Exception(
|
||||
'Expected SDK client response, got ${response.whichPayload()}',
|
||||
);
|
||||
}
|
||||
final sdkClientResponse = response.sdkClient;
|
||||
if (!sdkClientResponse.hasListWalletAccess()) {
|
||||
throw Exception(
|
||||
'Expected list wallet access response, got ${sdkClientResponse.whichPayload()}',
|
||||
);
|
||||
}
|
||||
return {
|
||||
for (final entry in sdkClientResponse.listWalletAccess.accesses)
|
||||
if (entry.access.sdkClientId == clientId) entry.access.walletId,
|
||||
};
|
||||
}
|
||||
|
||||
Future<List<ua_sdk.WalletAccessEntry>> listAllWalletAccesses(
|
||||
Connection connection,
|
||||
) async {
|
||||
final response = await connection.ask(
|
||||
UserAgentRequest(sdkClient: ua_sdk.Request(listWalletAccess: Empty())),
|
||||
);
|
||||
if (!response.hasSdkClient()) {
|
||||
throw Exception(
|
||||
'Expected SDK client response, got ${response.whichPayload()}',
|
||||
);
|
||||
}
|
||||
final sdkClientResponse = response.sdkClient;
|
||||
if (!sdkClientResponse.hasListWalletAccess()) {
|
||||
throw Exception(
|
||||
'Expected list wallet access response, got ${sdkClientResponse.whichPayload()}',
|
||||
);
|
||||
}
|
||||
return sdkClientResponse.listWalletAccess.accesses.toList(growable: false);
|
||||
}
|
||||
|
||||
Future<void> writeClientWalletAccess(
|
||||
Connection connection, {
|
||||
required int clientId,
|
||||
required Set<int> walletIds,
|
||||
}) async {
|
||||
final current = await readClientWalletAccess(connection, clientId: clientId);
|
||||
|
||||
final toGrant = walletIds.difference(current);
|
||||
final toRevoke = current.difference(walletIds);
|
||||
|
||||
if (toGrant.isNotEmpty) {
|
||||
await connection.tell(
|
||||
UserAgentRequest(
|
||||
sdkClient: ua_sdk.Request(
|
||||
grantWalletAccess: ua_sdk.GrantWalletAccess(
|
||||
accesses: [
|
||||
for (final walletId in toGrant)
|
||||
ua_sdk.WalletAccess(sdkClientId: clientId, walletId: walletId),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (toRevoke.isNotEmpty) {
|
||||
await connection.tell(
|
||||
UserAgentRequest(
|
||||
sdkClient: ua_sdk.Request(
|
||||
revokeWalletAccess: ua_sdk.RevokeWalletAccess(
|
||||
accesses: [for (final walletId in toRevoke) walletId],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,62 +1,62 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'server_info_storage.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class StoredServerInfo {
|
||||
const StoredServerInfo({
|
||||
required this.address,
|
||||
required this.port,
|
||||
required this.caCertFingerprint,
|
||||
});
|
||||
|
||||
final String address;
|
||||
final int port;
|
||||
final String caCertFingerprint;
|
||||
|
||||
factory StoredServerInfo.fromJson(Map<String, dynamic> json) =>
|
||||
_$StoredServerInfoFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$StoredServerInfoToJson(this);
|
||||
}
|
||||
|
||||
abstract class ServerInfoStorage {
|
||||
Future<StoredServerInfo?> load();
|
||||
Future<void> save(StoredServerInfo serverInfo);
|
||||
Future<void> clear();
|
||||
}
|
||||
|
||||
class SecureServerInfoStorage implements ServerInfoStorage {
|
||||
static const _storageKey = 'server_info';
|
||||
|
||||
const SecureServerInfoStorage();
|
||||
|
||||
static const _storage = FlutterSecureStorage();
|
||||
|
||||
@override
|
||||
Future<StoredServerInfo?> load() async {
|
||||
return null;
|
||||
final rawValue = await _storage.read(key: _storageKey);
|
||||
if (rawValue == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final decoded = jsonDecode(rawValue) as Map<String, dynamic>;
|
||||
return StoredServerInfo.fromJson(decoded);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> save(StoredServerInfo serverInfo) {
|
||||
return _storage.write(
|
||||
key: _storageKey,
|
||||
value: jsonEncode(serverInfo.toJson()),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clear() {
|
||||
return _storage.delete(key: _storageKey);
|
||||
}
|
||||
}
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'server_info_storage.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
class StoredServerInfo {
|
||||
const StoredServerInfo({
|
||||
required this.address,
|
||||
required this.port,
|
||||
required this.caCertFingerprint,
|
||||
});
|
||||
|
||||
final String address;
|
||||
final int port;
|
||||
final String caCertFingerprint;
|
||||
|
||||
factory StoredServerInfo.fromJson(Map<String, dynamic> json) =>
|
||||
_$StoredServerInfoFromJson(json);
|
||||
Map<String, dynamic> toJson() => _$StoredServerInfoToJson(this);
|
||||
}
|
||||
|
||||
abstract class ServerInfoStorage {
|
||||
Future<StoredServerInfo?> load();
|
||||
Future<void> save(StoredServerInfo serverInfo);
|
||||
Future<void> clear();
|
||||
}
|
||||
|
||||
class SecureServerInfoStorage implements ServerInfoStorage {
|
||||
static const _storageKey = 'server_info';
|
||||
|
||||
const SecureServerInfoStorage();
|
||||
|
||||
static const _storage = FlutterSecureStorage();
|
||||
|
||||
@override
|
||||
Future<StoredServerInfo?> load() async {
|
||||
return null;
|
||||
final rawValue = await _storage.read(key: _storageKey);
|
||||
if (rawValue == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final decoded = jsonDecode(rawValue) as Map<String, dynamic>;
|
||||
return StoredServerInfo.fromJson(decoded);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> save(StoredServerInfo serverInfo) {
|
||||
return _storage.write(
|
||||
key: _storageKey,
|
||||
value: jsonEncode(serverInfo.toJson()),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clear() {
|
||||
return _storage.delete(key: _storageKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'server_info_storage.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
StoredServerInfo _$StoredServerInfoFromJson(Map<String, dynamic> json) =>
|
||||
StoredServerInfo(
|
||||
address: json['address'] as String,
|
||||
port: (json['port'] as num).toInt(),
|
||||
caCertFingerprint: json['caCertFingerprint'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$StoredServerInfoToJson(StoredServerInfo instance) =>
|
||||
<String, dynamic>{
|
||||
'address': instance.address,
|
||||
'port': instance.port,
|
||||
'caCertFingerprint': instance.caCertFingerprint,
|
||||
};
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'server_info_storage.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
StoredServerInfo _$StoredServerInfoFromJson(Map<String, dynamic> json) =>
|
||||
StoredServerInfo(
|
||||
address: json['address'] as String,
|
||||
port: (json['port'] as num).toInt(),
|
||||
caCertFingerprint: json['caCertFingerprint'] as String,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$StoredServerInfoToJson(StoredServerInfo instance) =>
|
||||
<String, dynamic>{
|
||||
'address': instance.address,
|
||||
'port': instance.port,
|
||||
'caCertFingerprint': instance.caCertFingerprint,
|
||||
};
|
||||
|
||||
@@ -1,160 +1,160 @@
|
||||
import 'package:arbiter/features/connection/connection.dart';
|
||||
import 'package:arbiter/proto/user_agent/vault/bootstrap.pb.dart'
|
||||
as ua_bootstrap;
|
||||
import 'package:arbiter/proto/user_agent/vault/unseal.pb.dart' as ua_unseal;
|
||||
import 'package:arbiter/proto/user_agent/vault/vault.pb.dart' as ua_vault;
|
||||
import 'package:arbiter/proto/user_agent.pb.dart';
|
||||
import 'package:cryptography/cryptography.dart';
|
||||
|
||||
const _vaultKeyAssociatedData = 'arbiter.vault.password';
|
||||
|
||||
Future<ua_bootstrap.BootstrapResult> bootstrapVault(
|
||||
Connection connection,
|
||||
String password,
|
||||
) async {
|
||||
final encryptedKey = await _encryptVaultKeyMaterial(connection, password);
|
||||
|
||||
final response = await connection.ask(
|
||||
UserAgentRequest(
|
||||
vault: ua_vault.Request(
|
||||
bootstrap: ua_bootstrap.Request(
|
||||
encryptedKey: ua_bootstrap.BootstrapEncryptedKey(
|
||||
nonce: encryptedKey.nonce,
|
||||
ciphertext: encryptedKey.ciphertext,
|
||||
associatedData: encryptedKey.associatedData,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (!response.hasVault()) {
|
||||
throw Exception('Expected vault response, got ${response.whichPayload()}');
|
||||
}
|
||||
|
||||
final vaultResponse = response.vault;
|
||||
if (!vaultResponse.hasBootstrap()) {
|
||||
throw Exception(
|
||||
'Expected bootstrap result, got ${vaultResponse.whichPayload()}',
|
||||
);
|
||||
}
|
||||
|
||||
final bootstrapResponse = vaultResponse.bootstrap;
|
||||
if (!bootstrapResponse.hasResult()) {
|
||||
throw Exception('Expected bootstrap result payload.');
|
||||
}
|
||||
|
||||
return bootstrapResponse.result;
|
||||
}
|
||||
|
||||
Future<ua_unseal.UnsealResult> unsealVault(
|
||||
Connection connection,
|
||||
String password,
|
||||
) async {
|
||||
final encryptedKey = await _encryptVaultKeyMaterial(connection, password);
|
||||
|
||||
final response = await connection.ask(
|
||||
UserAgentRequest(
|
||||
vault: ua_vault.Request(
|
||||
unseal: ua_unseal.Request(
|
||||
encryptedKey: ua_unseal.UnsealEncryptedKey(
|
||||
nonce: encryptedKey.nonce,
|
||||
ciphertext: encryptedKey.ciphertext,
|
||||
associatedData: encryptedKey.associatedData,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (!response.hasVault()) {
|
||||
throw Exception('Expected vault response, got ${response.whichPayload()}');
|
||||
}
|
||||
|
||||
final vaultResponse = response.vault;
|
||||
if (!vaultResponse.hasUnseal()) {
|
||||
throw Exception(
|
||||
'Expected unseal result, got ${vaultResponse.whichPayload()}',
|
||||
);
|
||||
}
|
||||
|
||||
final unsealResponse = vaultResponse.unseal;
|
||||
if (!unsealResponse.hasResult()) {
|
||||
throw Exception(
|
||||
'Expected unseal result payload, got ${unsealResponse.whichPayload()}',
|
||||
);
|
||||
}
|
||||
|
||||
return unsealResponse.result;
|
||||
}
|
||||
|
||||
Future<_EncryptedVaultKey> _encryptVaultKeyMaterial(
|
||||
Connection connection,
|
||||
String password,
|
||||
) async {
|
||||
final keyExchange = X25519();
|
||||
final cipher = Xchacha20.poly1305Aead();
|
||||
final clientKeyPair = await keyExchange.newKeyPair();
|
||||
final clientPublicKey = await clientKeyPair.extractPublicKey();
|
||||
|
||||
final handshakeResponse = await connection.ask(
|
||||
UserAgentRequest(
|
||||
vault: ua_vault.Request(
|
||||
unseal: ua_unseal.Request(
|
||||
start: ua_unseal.UnsealStart(clientPubkey: clientPublicKey.bytes),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (!handshakeResponse.hasVault()) {
|
||||
throw Exception(
|
||||
'Expected vault response, got ${handshakeResponse.whichPayload()}',
|
||||
);
|
||||
}
|
||||
|
||||
final vaultResponse = handshakeResponse.vault;
|
||||
if (!vaultResponse.hasUnseal()) {
|
||||
throw Exception(
|
||||
'Expected unseal handshake response, got ${vaultResponse.whichPayload()}',
|
||||
);
|
||||
}
|
||||
|
||||
final unsealResponse = vaultResponse.unseal;
|
||||
if (!unsealResponse.hasStart()) {
|
||||
throw Exception(
|
||||
'Expected unseal handshake payload, got ${unsealResponse.whichPayload()}',
|
||||
);
|
||||
}
|
||||
|
||||
final serverPublicKey = SimplePublicKey(
|
||||
unsealResponse.start.serverPubkey,
|
||||
type: KeyPairType.x25519,
|
||||
);
|
||||
final sharedSecret = await keyExchange.sharedSecretKey(
|
||||
keyPair: clientKeyPair,
|
||||
remotePublicKey: serverPublicKey,
|
||||
);
|
||||
|
||||
final secretBox = await cipher.encrypt(
|
||||
password.codeUnits,
|
||||
secretKey: sharedSecret,
|
||||
nonce: cipher.newNonce(),
|
||||
aad: _vaultKeyAssociatedData.codeUnits,
|
||||
);
|
||||
|
||||
return _EncryptedVaultKey(
|
||||
nonce: secretBox.nonce,
|
||||
ciphertext: [...secretBox.cipherText, ...secretBox.mac.bytes],
|
||||
associatedData: _vaultKeyAssociatedData.codeUnits,
|
||||
);
|
||||
}
|
||||
|
||||
class _EncryptedVaultKey {
|
||||
const _EncryptedVaultKey({
|
||||
required this.nonce,
|
||||
required this.ciphertext,
|
||||
required this.associatedData,
|
||||
});
|
||||
|
||||
final List<int> nonce;
|
||||
final List<int> ciphertext;
|
||||
final List<int> associatedData;
|
||||
}
|
||||
import 'package:arbiter/features/connection/connection.dart';
|
||||
import 'package:arbiter/proto/user_agent/vault/bootstrap.pb.dart'
|
||||
as ua_bootstrap;
|
||||
import 'package:arbiter/proto/user_agent/vault/unseal.pb.dart' as ua_unseal;
|
||||
import 'package:arbiter/proto/user_agent/vault/vault.pb.dart' as ua_vault;
|
||||
import 'package:arbiter/proto/user_agent.pb.dart';
|
||||
import 'package:cryptography/cryptography.dart';
|
||||
|
||||
const _vaultKeyAssociatedData = 'arbiter.vault.password';
|
||||
|
||||
Future<ua_bootstrap.BootstrapResult> bootstrapVault(
|
||||
Connection connection,
|
||||
String password,
|
||||
) async {
|
||||
final encryptedKey = await _encryptVaultKeyMaterial(connection, password);
|
||||
|
||||
final response = await connection.ask(
|
||||
UserAgentRequest(
|
||||
vault: ua_vault.Request(
|
||||
bootstrap: ua_bootstrap.Request(
|
||||
encryptedKey: ua_bootstrap.BootstrapEncryptedKey(
|
||||
nonce: encryptedKey.nonce,
|
||||
ciphertext: encryptedKey.ciphertext,
|
||||
associatedData: encryptedKey.associatedData,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (!response.hasVault()) {
|
||||
throw Exception('Expected vault response, got ${response.whichPayload()}');
|
||||
}
|
||||
|
||||
final vaultResponse = response.vault;
|
||||
if (!vaultResponse.hasBootstrap()) {
|
||||
throw Exception(
|
||||
'Expected bootstrap result, got ${vaultResponse.whichPayload()}',
|
||||
);
|
||||
}
|
||||
|
||||
final bootstrapResponse = vaultResponse.bootstrap;
|
||||
if (!bootstrapResponse.hasResult()) {
|
||||
throw Exception('Expected bootstrap result payload.');
|
||||
}
|
||||
|
||||
return bootstrapResponse.result;
|
||||
}
|
||||
|
||||
Future<ua_unseal.UnsealResult> unsealVault(
|
||||
Connection connection,
|
||||
String password,
|
||||
) async {
|
||||
final encryptedKey = await _encryptVaultKeyMaterial(connection, password);
|
||||
|
||||
final response = await connection.ask(
|
||||
UserAgentRequest(
|
||||
vault: ua_vault.Request(
|
||||
unseal: ua_unseal.Request(
|
||||
encryptedKey: ua_unseal.UnsealEncryptedKey(
|
||||
nonce: encryptedKey.nonce,
|
||||
ciphertext: encryptedKey.ciphertext,
|
||||
associatedData: encryptedKey.associatedData,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (!response.hasVault()) {
|
||||
throw Exception('Expected vault response, got ${response.whichPayload()}');
|
||||
}
|
||||
|
||||
final vaultResponse = response.vault;
|
||||
if (!vaultResponse.hasUnseal()) {
|
||||
throw Exception(
|
||||
'Expected unseal result, got ${vaultResponse.whichPayload()}',
|
||||
);
|
||||
}
|
||||
|
||||
final unsealResponse = vaultResponse.unseal;
|
||||
if (!unsealResponse.hasResult()) {
|
||||
throw Exception(
|
||||
'Expected unseal result payload, got ${unsealResponse.whichPayload()}',
|
||||
);
|
||||
}
|
||||
|
||||
return unsealResponse.result;
|
||||
}
|
||||
|
||||
Future<_EncryptedVaultKey> _encryptVaultKeyMaterial(
|
||||
Connection connection,
|
||||
String password,
|
||||
) async {
|
||||
final keyExchange = X25519();
|
||||
final cipher = Xchacha20.poly1305Aead();
|
||||
final clientKeyPair = await keyExchange.newKeyPair();
|
||||
final clientPublicKey = await clientKeyPair.extractPublicKey();
|
||||
|
||||
final handshakeResponse = await connection.ask(
|
||||
UserAgentRequest(
|
||||
vault: ua_vault.Request(
|
||||
unseal: ua_unseal.Request(
|
||||
start: ua_unseal.UnsealStart(clientPubkey: clientPublicKey.bytes),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (!handshakeResponse.hasVault()) {
|
||||
throw Exception(
|
||||
'Expected vault response, got ${handshakeResponse.whichPayload()}',
|
||||
);
|
||||
}
|
||||
|
||||
final vaultResponse = handshakeResponse.vault;
|
||||
if (!vaultResponse.hasUnseal()) {
|
||||
throw Exception(
|
||||
'Expected unseal handshake response, got ${vaultResponse.whichPayload()}',
|
||||
);
|
||||
}
|
||||
|
||||
final unsealResponse = vaultResponse.unseal;
|
||||
if (!unsealResponse.hasStart()) {
|
||||
throw Exception(
|
||||
'Expected unseal handshake payload, got ${unsealResponse.whichPayload()}',
|
||||
);
|
||||
}
|
||||
|
||||
final serverPublicKey = SimplePublicKey(
|
||||
unsealResponse.start.serverPubkey,
|
||||
type: KeyPairType.x25519,
|
||||
);
|
||||
final sharedSecret = await keyExchange.sharedSecretKey(
|
||||
keyPair: clientKeyPair,
|
||||
remotePublicKey: serverPublicKey,
|
||||
);
|
||||
|
||||
final secretBox = await cipher.encrypt(
|
||||
password.codeUnits,
|
||||
secretKey: sharedSecret,
|
||||
nonce: cipher.newNonce(),
|
||||
aad: _vaultKeyAssociatedData.codeUnits,
|
||||
);
|
||||
|
||||
return _EncryptedVaultKey(
|
||||
nonce: secretBox.nonce,
|
||||
ciphertext: [...secretBox.cipherText, ...secretBox.mac.bytes],
|
||||
associatedData: _vaultKeyAssociatedData.codeUnits,
|
||||
);
|
||||
}
|
||||
|
||||
class _EncryptedVaultKey {
|
||||
const _EncryptedVaultKey({
|
||||
required this.nonce,
|
||||
required this.ciphertext,
|
||||
required this.associatedData,
|
||||
});
|
||||
|
||||
final List<int> nonce;
|
||||
final List<int> ciphertext;
|
||||
final List<int> associatedData;
|
||||
}
|
||||
|
||||
@@ -1,71 +1,71 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:arbiter/src/rust/api.dart';
|
||||
import 'package:cryptography/cryptography.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:arbiter/features/identity/pk_manager.dart';
|
||||
|
||||
final storage = FlutterSecureStorage(
|
||||
aOptions: AndroidOptions.biometric(
|
||||
enforceBiometrics: true,
|
||||
biometricPromptTitle: 'Authentication Required',
|
||||
),
|
||||
mOptions: MacOsOptions(
|
||||
accessibility: KeychainAccessibility.unlocked_this_device,
|
||||
label: "Arbiter",
|
||||
description: "Confirm your identity to access vault",
|
||||
synchronizable: false,
|
||||
accessControlFlags: [AccessControlFlag.userPresence],
|
||||
usesDataProtectionKeychain: true,
|
||||
),
|
||||
);
|
||||
|
||||
class HazmatMldsa extends KeyHandle {
|
||||
final MldsaKey _key;
|
||||
|
||||
HazmatMldsa({required MldsaKey key}) : _key = key;
|
||||
|
||||
@override
|
||||
Future<List<int>> getPublicKey() async {
|
||||
final publicKey = await _key.getPublicKey();
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<int>> sign(List<int> data) async {
|
||||
final signature = await _key.sign(message: data);
|
||||
return signature;
|
||||
}
|
||||
}
|
||||
|
||||
class HazmatMLDSAManager extends KeyManager {
|
||||
static const _storageKey = "ed25519_identity";
|
||||
|
||||
@override
|
||||
Future<KeyHandle> create() async {
|
||||
final storedKey = await get();
|
||||
if (storedKey != null) {
|
||||
return storedKey;
|
||||
}
|
||||
|
||||
final newKeypair = await MldsaKey.generate();
|
||||
final keyBytes = await newKeypair.toBytes();
|
||||
|
||||
await storage.write(key: _storageKey, value: base64Encode(keyBytes));
|
||||
|
||||
return HazmatMldsa(key: newKeypair);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<KeyHandle?> get() async {
|
||||
final storedKeyPair = await storage.read(key: _storageKey);
|
||||
if (storedKeyPair == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final keyBytes = base64Decode(storedKeyPair);
|
||||
final key = await MldsaKey.fromBytes(bytes: keyBytes);
|
||||
|
||||
return HazmatMldsa(key: key);
|
||||
}
|
||||
}
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:arbiter/src/rust/api.dart';
|
||||
import 'package:cryptography/cryptography.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:arbiter/features/identity/pk_manager.dart';
|
||||
|
||||
final storage = FlutterSecureStorage(
|
||||
aOptions: AndroidOptions.biometric(
|
||||
enforceBiometrics: true,
|
||||
biometricPromptTitle: 'Authentication Required',
|
||||
),
|
||||
mOptions: MacOsOptions(
|
||||
accessibility: KeychainAccessibility.unlocked_this_device,
|
||||
label: "Arbiter",
|
||||
description: "Confirm your identity to access vault",
|
||||
synchronizable: false,
|
||||
accessControlFlags: [AccessControlFlag.userPresence],
|
||||
usesDataProtectionKeychain: true,
|
||||
),
|
||||
);
|
||||
|
||||
class HazmatMldsa extends KeyHandle {
|
||||
final MldsaKey _key;
|
||||
|
||||
HazmatMldsa({required MldsaKey key}) : _key = key;
|
||||
|
||||
@override
|
||||
Future<List<int>> getPublicKey() async {
|
||||
final publicKey = await _key.getPublicKey();
|
||||
return publicKey;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<int>> sign(List<int> data) async {
|
||||
final signature = await _key.sign(message: data);
|
||||
return signature;
|
||||
}
|
||||
}
|
||||
|
||||
class HazmatMLDSAManager extends KeyManager {
|
||||
static const _storageKey = "ed25519_identity";
|
||||
|
||||
@override
|
||||
Future<KeyHandle> create() async {
|
||||
final storedKey = await get();
|
||||
if (storedKey != null) {
|
||||
return storedKey;
|
||||
}
|
||||
|
||||
final newKeypair = await MldsaKey.generate();
|
||||
final keyBytes = await newKeypair.toBytes();
|
||||
|
||||
await storage.write(key: _storageKey, value: base64Encode(keyBytes));
|
||||
|
||||
return HazmatMldsa(key: newKeypair);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<KeyHandle?> get() async {
|
||||
final storedKeyPair = await storage.read(key: _storageKey);
|
||||
if (storedKeyPair == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final keyBytes = base64Decode(storedKeyPair);
|
||||
final key = await MldsaKey.fromBytes(bytes: keyBytes);
|
||||
|
||||
return HazmatMldsa(key: key);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// The API to handle without storing the private key in memory.
|
||||
//The implementation will use platform-specific secure storage and signing capabilities.
|
||||
abstract class KeyHandle {
|
||||
Future<List<int>> sign(List<int> data);
|
||||
Future<List<int>> getPublicKey();
|
||||
}
|
||||
|
||||
abstract class KeyManager {
|
||||
Future<KeyHandle?> get();
|
||||
Future<KeyHandle> create();
|
||||
}
|
||||
// The API to handle without storing the private key in memory.
|
||||
//The implementation will use platform-specific secure storage and signing capabilities.
|
||||
abstract class KeyHandle {
|
||||
Future<List<int>> sign(List<int> data);
|
||||
Future<List<int>> getPublicKey();
|
||||
}
|
||||
|
||||
abstract class KeyManager {
|
||||
Future<KeyHandle?> get();
|
||||
Future<KeyHandle> create();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user