fix(clippy): apply auto-fixable linting suggestions

This commit is contained in:
Clippy Bot
2026-06-23 18:47:23 +00:00
parent 4fd75701c7
commit cc21036448
425 changed files with 79190 additions and 79190 deletions

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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,
),
),
),
),
),
],
),
);
}
}

View File

@@ -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,
),
),
],
),
),
],
),
),
);
}
}

View File

@@ -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);
}

View File

@@ -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';

View File

@@ -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}",
);
}
}
}

View File

@@ -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);
}

View File

@@ -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.');
}
}
}

View File

@@ -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.',
};
}

View File

@@ -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.',
};
}

View File

@@ -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],
),
),
),
);
}
}

View File

@@ -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);
}
}

View File

@@ -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,
};

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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();
}