feat(evm): add EVM grants screen with create UI and list
This commit is contained in:
11
.claude/memory/feedback_widget_decomposition.md
Normal file
11
.claude/memory/feedback_widget_decomposition.md
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
name: Widget decomposition and provider subscriptions
|
||||
description: Prefer splitting screens into multiple focused files/widgets; each widget subscribes to its own relevant providers
|
||||
type: feedback
|
||||
---
|
||||
|
||||
Split screens into multiple smaller widgets across multiple files. Each widget should subscribe only to the providers it needs (`ref.watch` at lowest possible level), rather than having one large screen widget that watches everything and passes data down as parameters.
|
||||
|
||||
**Why:** Reduces unnecessary rebuilds; improves readability; each file has one clear responsibility.
|
||||
|
||||
**How to apply:** When building a new screen, identify which sub-widgets need their own provider subscriptions and extract them into separate files (e.g., `widgets/grant_card.dart` watches enrichment providers itself, rather than the screen doing it and passing resolved strings down).
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@ scripts/__pycache__/
|
||||
.DS_Store
|
||||
.cargo/config.toml
|
||||
.vscode/
|
||||
docs/
|
||||
|
||||
@@ -29,17 +29,27 @@ Future<List<GrantEntry>> listEvmGrants(Connection connection) async {
|
||||
|
||||
Future<int> createEvmGrant(
|
||||
Connection connection, {
|
||||
required int clientId,
|
||||
required int walletId,
|
||||
required Int64 chainId,
|
||||
DateTime? validFrom,
|
||||
DateTime? validUntil,
|
||||
List<int>? maxGasFeePerGas,
|
||||
List<int>? maxPriorityFeePerGas,
|
||||
TransactionRateLimit? rateLimit,
|
||||
required SharedSettings sharedSettings,
|
||||
required SpecificGrant specific,
|
||||
}) async {
|
||||
throw UnimplementedError('EVM grant creation is not yet implemented.');
|
||||
final request = UserAgentRequest(
|
||||
evmGrantCreate: EvmGrantCreateRequest(
|
||||
shared: sharedSettings,
|
||||
specific: specific,
|
||||
),
|
||||
);
|
||||
|
||||
final resp = await connection.ask(request);
|
||||
|
||||
if (!resp.hasEvmGrantCreate()) {
|
||||
throw Exception(
|
||||
'Expected EVM grant create response, got ${resp.whichPayload()}',
|
||||
);
|
||||
}
|
||||
|
||||
final result = resp.evmGrantCreate;
|
||||
|
||||
return result.grantId;
|
||||
}
|
||||
|
||||
Future<void> deleteEvmGrant(Connection connection, int grantId) async {
|
||||
|
||||
@@ -16,10 +16,24 @@ Future<Set<int>> readClientWalletAccess(
|
||||
}
|
||||
return {
|
||||
for (final entry in response.listWalletAccessResponse.accesses)
|
||||
if (entry.access != null && entry.access.sdkClientId == clientId) entry.access.walletId,
|
||||
if (entry.access.sdkClientId == clientId) entry.access.walletId,
|
||||
};
|
||||
}
|
||||
|
||||
Future<List<SdkClientWalletAccess>> listAllWalletAccesses(
|
||||
Connection connection,
|
||||
) async {
|
||||
final response = await connection.ask(
|
||||
UserAgentRequest(listWalletAccess: Empty()),
|
||||
);
|
||||
if (!response.hasListWalletAccessResponse()) {
|
||||
throw Exception(
|
||||
'Expected list wallet access response, got ${response.whichPayload()}',
|
||||
);
|
||||
}
|
||||
return response.listWalletAccessResponse.accesses.toList(growable: false);
|
||||
}
|
||||
|
||||
Future<void> writeClientWalletAccess(
|
||||
Connection connection, {
|
||||
required int clientId,
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:fixnum/fixnum.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:hooks_riverpod/experimental/mutation.dart';
|
||||
import 'package:mtcore/markettakers.dart';
|
||||
import 'package:protobuf/well_known_types/google/protobuf/timestamp.pb.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'evm_grants.freezed.dart';
|
||||
@@ -73,14 +74,7 @@ class EvmGrants extends _$EvmGrants {
|
||||
|
||||
Future<int> executeCreateEvmGrant(
|
||||
MutationTarget ref, {
|
||||
required int clientId,
|
||||
required int walletId,
|
||||
required Int64 chainId,
|
||||
DateTime? validFrom,
|
||||
DateTime? validUntil,
|
||||
List<int>? maxGasFeePerGas,
|
||||
List<int>? maxPriorityFeePerGas,
|
||||
TransactionRateLimit? rateLimit,
|
||||
required SharedSettings sharedSettings,
|
||||
required SpecificGrant specific,
|
||||
}) {
|
||||
return createEvmGrantMutation.run(ref, (tsx) async {
|
||||
@@ -91,14 +85,7 @@ Future<int> executeCreateEvmGrant(
|
||||
|
||||
final grantId = await createEvmGrant(
|
||||
connection,
|
||||
clientId: clientId,
|
||||
walletId: walletId,
|
||||
chainId: chainId,
|
||||
validFrom: validFrom,
|
||||
validUntil: validUntil,
|
||||
maxGasFeePerGas: maxGasFeePerGas,
|
||||
maxPriorityFeePerGas: maxPriorityFeePerGas,
|
||||
rateLimit: rateLimit,
|
||||
sharedSettings: sharedSettings,
|
||||
specific: specific,
|
||||
);
|
||||
|
||||
|
||||
22
useragent/lib/providers/sdk_clients/wallet_access_list.dart
Normal file
22
useragent/lib/providers/sdk_clients/wallet_access_list.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
import 'package:arbiter/features/connection/evm/wallet_access.dart';
|
||||
import 'package:arbiter/proto/user_agent.pb.dart';
|
||||
import 'package:arbiter/providers/connection/connection_manager.dart';
|
||||
import 'package:mtcore/markettakers.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'wallet_access_list.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<List<SdkClientWalletAccess>?> walletAccessList(Ref ref) async {
|
||||
final connection = await ref.watch(connectionManagerProvider.future);
|
||||
if (connection == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return await listAllWalletAccesses(connection);
|
||||
} catch (e, st) {
|
||||
talker.handle(e, st);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'wallet_access_list.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(walletAccessList)
|
||||
final walletAccessListProvider = WalletAccessListProvider._();
|
||||
|
||||
final class WalletAccessListProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<SdkClientWalletAccess>?>,
|
||||
List<SdkClientWalletAccess>?,
|
||||
FutureOr<List<SdkClientWalletAccess>?>
|
||||
>
|
||||
with
|
||||
$FutureModifier<List<SdkClientWalletAccess>?>,
|
||||
$FutureProvider<List<SdkClientWalletAccess>?> {
|
||||
WalletAccessListProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'walletAccessListProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$walletAccessListHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<List<SdkClientWalletAccess>?> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<List<SdkClientWalletAccess>?> create(Ref ref) {
|
||||
return walletAccessList(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$walletAccessListHash() => r'c06006d6792ae463105a539723e9bb396192f96b';
|
||||
@@ -19,6 +19,7 @@ class Router extends RootStackRouter {
|
||||
children: [
|
||||
AutoRoute(page: EvmRoute.page, path: 'evm'),
|
||||
AutoRoute(page: ClientsRoute.page, path: 'clients'),
|
||||
AutoRoute(page: EvmGrantsRoute.page, path: 'grants'),
|
||||
AutoRoute(page: AboutRoute.page, path: 'about'),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
// coverage:ignore-file
|
||||
|
||||
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
||||
import 'package:arbiter/proto/user_agent.pb.dart' as _i14;
|
||||
import 'package:arbiter/proto/user_agent.pb.dart' as _i15;
|
||||
import 'package:arbiter/screens/bootstrap.dart' as _i2;
|
||||
import 'package:arbiter/screens/dashboard.dart' as _i7;
|
||||
import 'package:arbiter/screens/dashboard/about.dart' as _i1;
|
||||
@@ -17,23 +17,24 @@ import 'package:arbiter/screens/dashboard/clients/details.dart' as _i3;
|
||||
import 'package:arbiter/screens/dashboard/clients/details/client_details.dart'
|
||||
as _i4;
|
||||
import 'package:arbiter/screens/dashboard/clients/table.dart' as _i5;
|
||||
import 'package:arbiter/screens/dashboard/evm/evm.dart' as _i8;
|
||||
import 'package:arbiter/screens/dashboard/evm/evm.dart' as _i9;
|
||||
import 'package:arbiter/screens/dashboard/evm/grants/grant_create.dart' as _i6;
|
||||
import 'package:arbiter/screens/server_connection.dart' as _i9;
|
||||
import 'package:arbiter/screens/server_info_setup.dart' as _i10;
|
||||
import 'package:arbiter/screens/vault_setup.dart' as _i11;
|
||||
import 'package:auto_route/auto_route.dart' as _i12;
|
||||
import 'package:flutter/material.dart' as _i13;
|
||||
import 'package:arbiter/screens/dashboard/evm/grants/grants.dart' as _i8;
|
||||
import 'package:arbiter/screens/server_connection.dart' as _i10;
|
||||
import 'package:arbiter/screens/server_info_setup.dart' as _i11;
|
||||
import 'package:arbiter/screens/vault_setup.dart' as _i12;
|
||||
import 'package:auto_route/auto_route.dart' as _i13;
|
||||
import 'package:flutter/material.dart' as _i14;
|
||||
|
||||
/// generated route for
|
||||
/// [_i1.AboutScreen]
|
||||
class AboutRoute extends _i12.PageRouteInfo<void> {
|
||||
const AboutRoute({List<_i12.PageRouteInfo>? children})
|
||||
class AboutRoute extends _i13.PageRouteInfo<void> {
|
||||
const AboutRoute({List<_i13.PageRouteInfo>? children})
|
||||
: super(AboutRoute.name, initialChildren: children);
|
||||
|
||||
static const String name = 'AboutRoute';
|
||||
|
||||
static _i12.PageInfo page = _i12.PageInfo(
|
||||
static _i13.PageInfo page = _i13.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i1.AboutScreen();
|
||||
@@ -43,13 +44,13 @@ class AboutRoute extends _i12.PageRouteInfo<void> {
|
||||
|
||||
/// generated route for
|
||||
/// [_i2.Bootstrap]
|
||||
class Bootstrap extends _i12.PageRouteInfo<void> {
|
||||
const Bootstrap({List<_i12.PageRouteInfo>? children})
|
||||
class Bootstrap extends _i13.PageRouteInfo<void> {
|
||||
const Bootstrap({List<_i13.PageRouteInfo>? children})
|
||||
: super(Bootstrap.name, initialChildren: children);
|
||||
|
||||
static const String name = 'Bootstrap';
|
||||
|
||||
static _i12.PageInfo page = _i12.PageInfo(
|
||||
static _i13.PageInfo page = _i13.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i2.Bootstrap();
|
||||
@@ -59,11 +60,11 @@ class Bootstrap extends _i12.PageRouteInfo<void> {
|
||||
|
||||
/// generated route for
|
||||
/// [_i3.ClientDetails]
|
||||
class ClientDetails extends _i12.PageRouteInfo<ClientDetailsArgs> {
|
||||
class ClientDetails extends _i13.PageRouteInfo<ClientDetailsArgs> {
|
||||
ClientDetails({
|
||||
_i13.Key? key,
|
||||
required _i14.SdkClientEntry client,
|
||||
List<_i12.PageRouteInfo>? children,
|
||||
_i14.Key? key,
|
||||
required _i15.SdkClientEntry client,
|
||||
List<_i13.PageRouteInfo>? children,
|
||||
}) : super(
|
||||
ClientDetails.name,
|
||||
args: ClientDetailsArgs(key: key, client: client),
|
||||
@@ -72,7 +73,7 @@ class ClientDetails extends _i12.PageRouteInfo<ClientDetailsArgs> {
|
||||
|
||||
static const String name = 'ClientDetails';
|
||||
|
||||
static _i12.PageInfo page = _i12.PageInfo(
|
||||
static _i13.PageInfo page = _i13.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
final args = data.argsAs<ClientDetailsArgs>();
|
||||
@@ -84,9 +85,9 @@ class ClientDetails extends _i12.PageRouteInfo<ClientDetailsArgs> {
|
||||
class ClientDetailsArgs {
|
||||
const ClientDetailsArgs({this.key, required this.client});
|
||||
|
||||
final _i13.Key? key;
|
||||
final _i14.Key? key;
|
||||
|
||||
final _i14.SdkClientEntry client;
|
||||
final _i15.SdkClientEntry client;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
@@ -106,11 +107,11 @@ class ClientDetailsArgs {
|
||||
|
||||
/// generated route for
|
||||
/// [_i4.ClientDetailsScreen]
|
||||
class ClientDetailsRoute extends _i12.PageRouteInfo<ClientDetailsRouteArgs> {
|
||||
class ClientDetailsRoute extends _i13.PageRouteInfo<ClientDetailsRouteArgs> {
|
||||
ClientDetailsRoute({
|
||||
_i13.Key? key,
|
||||
_i14.Key? key,
|
||||
required int clientId,
|
||||
List<_i12.PageRouteInfo>? children,
|
||||
List<_i13.PageRouteInfo>? children,
|
||||
}) : super(
|
||||
ClientDetailsRoute.name,
|
||||
args: ClientDetailsRouteArgs(key: key, clientId: clientId),
|
||||
@@ -120,7 +121,7 @@ class ClientDetailsRoute extends _i12.PageRouteInfo<ClientDetailsRouteArgs> {
|
||||
|
||||
static const String name = 'ClientDetailsRoute';
|
||||
|
||||
static _i12.PageInfo page = _i12.PageInfo(
|
||||
static _i13.PageInfo page = _i13.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
final pathParams = data.inheritedPathParams;
|
||||
@@ -136,7 +137,7 @@ class ClientDetailsRoute extends _i12.PageRouteInfo<ClientDetailsRouteArgs> {
|
||||
class ClientDetailsRouteArgs {
|
||||
const ClientDetailsRouteArgs({this.key, required this.clientId});
|
||||
|
||||
final _i13.Key? key;
|
||||
final _i14.Key? key;
|
||||
|
||||
final int clientId;
|
||||
|
||||
@@ -158,13 +159,13 @@ class ClientDetailsRouteArgs {
|
||||
|
||||
/// generated route for
|
||||
/// [_i5.ClientsScreen]
|
||||
class ClientsRoute extends _i12.PageRouteInfo<void> {
|
||||
const ClientsRoute({List<_i12.PageRouteInfo>? children})
|
||||
class ClientsRoute extends _i13.PageRouteInfo<void> {
|
||||
const ClientsRoute({List<_i13.PageRouteInfo>? children})
|
||||
: super(ClientsRoute.name, initialChildren: children);
|
||||
|
||||
static const String name = 'ClientsRoute';
|
||||
|
||||
static _i12.PageInfo page = _i12.PageInfo(
|
||||
static _i13.PageInfo page = _i13.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i5.ClientsScreen();
|
||||
@@ -174,13 +175,13 @@ class ClientsRoute extends _i12.PageRouteInfo<void> {
|
||||
|
||||
/// generated route for
|
||||
/// [_i6.CreateEvmGrantScreen]
|
||||
class CreateEvmGrantRoute extends _i12.PageRouteInfo<void> {
|
||||
const CreateEvmGrantRoute({List<_i12.PageRouteInfo>? children})
|
||||
class CreateEvmGrantRoute extends _i13.PageRouteInfo<void> {
|
||||
const CreateEvmGrantRoute({List<_i13.PageRouteInfo>? children})
|
||||
: super(CreateEvmGrantRoute.name, initialChildren: children);
|
||||
|
||||
static const String name = 'CreateEvmGrantRoute';
|
||||
|
||||
static _i12.PageInfo page = _i12.PageInfo(
|
||||
static _i13.PageInfo page = _i13.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i6.CreateEvmGrantScreen();
|
||||
@@ -190,13 +191,13 @@ class CreateEvmGrantRoute extends _i12.PageRouteInfo<void> {
|
||||
|
||||
/// generated route for
|
||||
/// [_i7.DashboardRouter]
|
||||
class DashboardRouter extends _i12.PageRouteInfo<void> {
|
||||
const DashboardRouter({List<_i12.PageRouteInfo>? children})
|
||||
class DashboardRouter extends _i13.PageRouteInfo<void> {
|
||||
const DashboardRouter({List<_i13.PageRouteInfo>? children})
|
||||
: super(DashboardRouter.name, initialChildren: children);
|
||||
|
||||
static const String name = 'DashboardRouter';
|
||||
|
||||
static _i12.PageInfo page = _i12.PageInfo(
|
||||
static _i13.PageInfo page = _i13.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i7.DashboardRouter();
|
||||
@@ -205,29 +206,45 @@ class DashboardRouter extends _i12.PageRouteInfo<void> {
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i8.EvmScreen]
|
||||
class EvmRoute extends _i12.PageRouteInfo<void> {
|
||||
const EvmRoute({List<_i12.PageRouteInfo>? children})
|
||||
: super(EvmRoute.name, initialChildren: children);
|
||||
/// [_i8.EvmGrantsScreen]
|
||||
class EvmGrantsRoute extends _i13.PageRouteInfo<void> {
|
||||
const EvmGrantsRoute({List<_i13.PageRouteInfo>? children})
|
||||
: super(EvmGrantsRoute.name, initialChildren: children);
|
||||
|
||||
static const String name = 'EvmRoute';
|
||||
static const String name = 'EvmGrantsRoute';
|
||||
|
||||
static _i12.PageInfo page = _i12.PageInfo(
|
||||
static _i13.PageInfo page = _i13.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i8.EvmScreen();
|
||||
return const _i8.EvmGrantsScreen();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i9.ServerConnectionScreen]
|
||||
/// [_i9.EvmScreen]
|
||||
class EvmRoute extends _i13.PageRouteInfo<void> {
|
||||
const EvmRoute({List<_i13.PageRouteInfo>? children})
|
||||
: super(EvmRoute.name, initialChildren: children);
|
||||
|
||||
static const String name = 'EvmRoute';
|
||||
|
||||
static _i13.PageInfo page = _i13.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i9.EvmScreen();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i10.ServerConnectionScreen]
|
||||
class ServerConnectionRoute
|
||||
extends _i12.PageRouteInfo<ServerConnectionRouteArgs> {
|
||||
extends _i13.PageRouteInfo<ServerConnectionRouteArgs> {
|
||||
ServerConnectionRoute({
|
||||
_i13.Key? key,
|
||||
_i14.Key? key,
|
||||
String? arbiterUrl,
|
||||
List<_i12.PageRouteInfo>? children,
|
||||
List<_i13.PageRouteInfo>? children,
|
||||
}) : super(
|
||||
ServerConnectionRoute.name,
|
||||
args: ServerConnectionRouteArgs(key: key, arbiterUrl: arbiterUrl),
|
||||
@@ -236,13 +253,13 @@ class ServerConnectionRoute
|
||||
|
||||
static const String name = 'ServerConnectionRoute';
|
||||
|
||||
static _i12.PageInfo page = _i12.PageInfo(
|
||||
static _i13.PageInfo page = _i13.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
final args = data.argsAs<ServerConnectionRouteArgs>(
|
||||
orElse: () => const ServerConnectionRouteArgs(),
|
||||
);
|
||||
return _i9.ServerConnectionScreen(
|
||||
return _i10.ServerConnectionScreen(
|
||||
key: args.key,
|
||||
arbiterUrl: args.arbiterUrl,
|
||||
);
|
||||
@@ -253,7 +270,7 @@ class ServerConnectionRoute
|
||||
class ServerConnectionRouteArgs {
|
||||
const ServerConnectionRouteArgs({this.key, this.arbiterUrl});
|
||||
|
||||
final _i13.Key? key;
|
||||
final _i14.Key? key;
|
||||
|
||||
final String? arbiterUrl;
|
||||
|
||||
@@ -274,33 +291,33 @@ class ServerConnectionRouteArgs {
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i10.ServerInfoSetupScreen]
|
||||
class ServerInfoSetupRoute extends _i12.PageRouteInfo<void> {
|
||||
const ServerInfoSetupRoute({List<_i12.PageRouteInfo>? children})
|
||||
/// [_i11.ServerInfoSetupScreen]
|
||||
class ServerInfoSetupRoute extends _i13.PageRouteInfo<void> {
|
||||
const ServerInfoSetupRoute({List<_i13.PageRouteInfo>? children})
|
||||
: super(ServerInfoSetupRoute.name, initialChildren: children);
|
||||
|
||||
static const String name = 'ServerInfoSetupRoute';
|
||||
|
||||
static _i12.PageInfo page = _i12.PageInfo(
|
||||
static _i13.PageInfo page = _i13.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i10.ServerInfoSetupScreen();
|
||||
return const _i11.ServerInfoSetupScreen();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [_i11.VaultSetupScreen]
|
||||
class VaultSetupRoute extends _i12.PageRouteInfo<void> {
|
||||
const VaultSetupRoute({List<_i12.PageRouteInfo>? children})
|
||||
/// [_i12.VaultSetupScreen]
|
||||
class VaultSetupRoute extends _i13.PageRouteInfo<void> {
|
||||
const VaultSetupRoute({List<_i13.PageRouteInfo>? children})
|
||||
: super(VaultSetupRoute.name, initialChildren: children);
|
||||
|
||||
static const String name = 'VaultSetupRoute';
|
||||
|
||||
static _i12.PageInfo page = _i12.PageInfo(
|
||||
static _i13.PageInfo page = _i13.PageInfo(
|
||||
name,
|
||||
builder: (data) {
|
||||
return const _i11.VaultSetupScreen();
|
||||
return const _i12.VaultSetupScreen();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,12 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
const breakpoints = MaterialAdaptiveBreakpoints();
|
||||
|
||||
final routes = [const EvmRoute(), const ClientsRoute(), const AboutRoute()];
|
||||
final routes = [
|
||||
const EvmRoute(),
|
||||
const ClientsRoute(),
|
||||
const EvmGrantsRoute(),
|
||||
const AboutRoute(),
|
||||
];
|
||||
|
||||
@RoutePage()
|
||||
class DashboardRouter extends StatelessWidget {
|
||||
@@ -38,6 +43,11 @@ class DashboardRouter extends StatelessWidget {
|
||||
selectedIcon: Icon(Icons.devices_other),
|
||||
label: "Clients",
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.policy_outlined),
|
||||
selectedIcon: Icon(Icons.policy),
|
||||
label: "Grants",
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.info_outline),
|
||||
selectedIcon: Icon(Icons.info),
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import 'package:arbiter/proto/evm.pb.dart';
|
||||
import 'package:arbiter/proto/user_agent.pb.dart';
|
||||
import 'package:arbiter/providers/evm/evm.dart';
|
||||
import 'package:arbiter/providers/evm/evm_grants.dart';
|
||||
import 'package:arbiter/providers/sdk_clients/list.dart';
|
||||
import 'package:arbiter/providers/sdk_clients/wallet_access_list.dart';
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:hooks_riverpod/experimental/mutation.dart';
|
||||
import 'package:protobuf/well_known_types/google/protobuf/timestamp.pb.dart';
|
||||
import 'package:sizer/sizer.dart';
|
||||
|
||||
@RoutePage()
|
||||
@@ -15,11 +19,10 @@ class CreateEvmGrantScreen extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final wallets = ref.watch(evmProvider).asData?.value ?? const <WalletEntry>[];
|
||||
final createMutation = ref.watch(createEvmGrantMutation);
|
||||
|
||||
final selectedWalletIndex = useState<int?>(wallets.isEmpty ? null : 0);
|
||||
final clientIdController = useTextEditingController();
|
||||
final selectedClientId = useState<int?>(null);
|
||||
final selectedWalletAccessId = useState<int?>(null);
|
||||
final chainIdController = useTextEditingController(text: '1');
|
||||
final gasFeeController = useTextEditingController();
|
||||
final priorityFeeController = useTextEditingController();
|
||||
@@ -40,14 +43,13 @@ class CreateEvmGrantScreen extends HookConsumerWidget {
|
||||
]);
|
||||
|
||||
Future<void> submit() async {
|
||||
final selectedWallet = selectedWalletIndex.value;
|
||||
if (selectedWallet == null) {
|
||||
_showCreateMessage(context, 'At least one wallet is required.');
|
||||
final accessId = selectedWalletAccessId.value;
|
||||
if (accessId == null) {
|
||||
_showCreateMessage(context, 'Select a client and wallet access.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
final clientId = int.parse(clientIdController.text.trim());
|
||||
final chainId = Int64.parseInt(chainIdController.text.trim());
|
||||
final rateLimit = _buildRateLimit(
|
||||
txCountController.text,
|
||||
@@ -83,16 +85,25 @@ class CreateEvmGrantScreen extends HookConsumerWidget {
|
||||
_ => throw Exception('Unsupported grant type.'),
|
||||
};
|
||||
|
||||
final sharedSettings = SharedSettings(
|
||||
walletAccessId: accessId,
|
||||
chainId: chainId,
|
||||
);
|
||||
if (validFrom.value != null) {
|
||||
sharedSettings.validFrom = _toTimestamp(validFrom.value!);
|
||||
}
|
||||
if (validUntil.value != null) {
|
||||
sharedSettings.validUntil = _toTimestamp(validUntil.value!);
|
||||
}
|
||||
final gasBytes = _optionalBigIntBytes(gasFeeController.text);
|
||||
if (gasBytes != null) sharedSettings.maxGasFeePerGas = gasBytes;
|
||||
final priorityBytes = _optionalBigIntBytes(priorityFeeController.text);
|
||||
if (priorityBytes != null) sharedSettings.maxPriorityFeePerGas = priorityBytes;
|
||||
if (rateLimit != null) sharedSettings.rateLimit = rateLimit;
|
||||
|
||||
await executeCreateEvmGrant(
|
||||
ref,
|
||||
clientId: clientId,
|
||||
walletId: selectedWallet + 1,
|
||||
chainId: chainId,
|
||||
validFrom: validFrom.value,
|
||||
validUntil: validUntil.value,
|
||||
maxGasFeePerGas: _optionalBigIntBytes(gasFeeController.text),
|
||||
maxPriorityFeePerGas: _optionalBigIntBytes(priorityFeeController.text),
|
||||
rateLimit: rateLimit,
|
||||
sharedSettings: sharedSettings,
|
||||
specific: specific,
|
||||
);
|
||||
if (!context.mounted) {
|
||||
@@ -113,22 +124,23 @@ class CreateEvmGrantScreen extends HookConsumerWidget {
|
||||
child: ListView(
|
||||
padding: EdgeInsets.fromLTRB(2.4.w, 2.h, 2.4.w, 3.2.h),
|
||||
children: [
|
||||
_CreateIntroCard(walletCount: wallets.length),
|
||||
const _CreateIntroCard(),
|
||||
SizedBox(height: 1.8.h),
|
||||
_CreateSection(
|
||||
title: 'Shared grant options',
|
||||
children: [
|
||||
_WalletPickerField(
|
||||
wallets: wallets,
|
||||
selectedIndex: selectedWalletIndex.value,
|
||||
onChanged: (value) => selectedWalletIndex.value = value,
|
||||
_ClientPickerField(
|
||||
selectedClientId: selectedClientId.value,
|
||||
onChanged: (clientId) {
|
||||
selectedClientId.value = clientId;
|
||||
selectedWalletAccessId.value = null;
|
||||
},
|
||||
),
|
||||
_NumberInputField(
|
||||
controller: clientIdController,
|
||||
label: 'Client ID',
|
||||
hint: '42',
|
||||
helper:
|
||||
'Manual for now. The app does not yet expose a client picker.',
|
||||
_WalletAccessPickerField(
|
||||
selectedClientId: selectedClientId.value,
|
||||
selectedAccessId: selectedWalletAccessId.value,
|
||||
onChanged: (accessId) =>
|
||||
selectedWalletAccessId.value = accessId,
|
||||
),
|
||||
_NumberInputField(
|
||||
controller: chainIdController,
|
||||
@@ -204,9 +216,7 @@ class CreateEvmGrantScreen extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
class _CreateIntroCard extends StatelessWidget {
|
||||
const _CreateIntroCard({required this.walletCount});
|
||||
|
||||
final int walletCount;
|
||||
const _CreateIntroCard();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -222,7 +232,7 @@ class _CreateIntroCard extends StatelessWidget {
|
||||
border: Border.all(color: const Color(0x1A17324A)),
|
||||
),
|
||||
child: Text(
|
||||
'Compose shared constraints once, then switch between Ether and token transfer rules. $walletCount wallet${walletCount == 1 ? '' : 's'} currently available.',
|
||||
'Pick a client, then select one of the wallet accesses already granted to it. Compose shared constraints once, then switch between Ether and token transfer rules.',
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(height: 1.5),
|
||||
),
|
||||
);
|
||||
@@ -266,37 +276,98 @@ class _CreateSection extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _WalletPickerField extends StatelessWidget {
|
||||
const _WalletPickerField({
|
||||
required this.wallets,
|
||||
required this.selectedIndex,
|
||||
class _ClientPickerField extends ConsumerWidget {
|
||||
const _ClientPickerField({
|
||||
required this.selectedClientId,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
final List<WalletEntry> wallets;
|
||||
final int? selectedIndex;
|
||||
final int? selectedClientId;
|
||||
final ValueChanged<int?> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final clients =
|
||||
ref.watch(sdkClientsProvider).asData?.value ?? const <SdkClientEntry>[];
|
||||
|
||||
return DropdownButtonFormField<int>(
|
||||
initialValue: selectedIndex,
|
||||
value: clients.any((c) => c.id == selectedClientId)
|
||||
? selectedClientId
|
||||
: null,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Wallet',
|
||||
helperText:
|
||||
'Uses the current wallet order. The API still does not expose stable wallet IDs directly.',
|
||||
labelText: 'Client',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: [
|
||||
for (var i = 0; i < wallets.length; i++)
|
||||
for (final c in clients)
|
||||
DropdownMenuItem(
|
||||
value: i,
|
||||
value: c.id,
|
||||
child: Text(
|
||||
'Wallet ${(i + 1).toString().padLeft(2, '0')} · ${_shortAddress(wallets[i].address)}',
|
||||
c.info.name.isEmpty ? 'Client #${c.id}' : c.info.name,
|
||||
),
|
||||
),
|
||||
],
|
||||
onChanged: wallets.isEmpty ? null : onChanged,
|
||||
onChanged: clients.isEmpty ? null : onChanged,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WalletAccessPickerField extends ConsumerWidget {
|
||||
const _WalletAccessPickerField({
|
||||
required this.selectedClientId,
|
||||
required this.selectedAccessId,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
final int? selectedClientId;
|
||||
final int? selectedAccessId;
|
||||
final ValueChanged<int?> onChanged;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final allAccesses =
|
||||
ref.watch(walletAccessListProvider).asData?.value ??
|
||||
const <SdkClientWalletAccess>[];
|
||||
final wallets =
|
||||
ref.watch(evmProvider).asData?.value ?? const <WalletEntry>[];
|
||||
|
||||
final walletById = <int, WalletEntry>{
|
||||
for (final w in wallets) w.id: w,
|
||||
};
|
||||
|
||||
final accesses = selectedClientId == null
|
||||
? const <SdkClientWalletAccess>[]
|
||||
: allAccesses
|
||||
.where((a) => a.access.sdkClientId == selectedClientId)
|
||||
.toList();
|
||||
|
||||
final effectiveValue =
|
||||
accesses.any((a) => a.id == selectedAccessId) ? selectedAccessId : null;
|
||||
|
||||
return DropdownButtonFormField<int>(
|
||||
value: effectiveValue,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Wallet access',
|
||||
helperText: selectedClientId == null
|
||||
? 'Select a client first'
|
||||
: accesses.isEmpty
|
||||
? 'No wallet accesses for this client'
|
||||
: null,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
items: [
|
||||
for (final a in accesses)
|
||||
DropdownMenuItem(
|
||||
value: a.id,
|
||||
child: Text(() {
|
||||
final wallet = walletById[a.access.walletId];
|
||||
return wallet != null
|
||||
? _shortAddress(wallet.address)
|
||||
: 'Wallet #${a.access.walletId}';
|
||||
}()),
|
||||
),
|
||||
],
|
||||
onChanged: accesses.isEmpty ? null : onChanged,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -735,6 +806,13 @@ class _VolumeLimitValue {
|
||||
}
|
||||
}
|
||||
|
||||
Timestamp _toTimestamp(DateTime value) {
|
||||
final utc = value.toUtc();
|
||||
return Timestamp()
|
||||
..seconds = Int64(utc.millisecondsSinceEpoch ~/ 1000)
|
||||
..nanos = (utc.microsecondsSinceEpoch % 1000000) * 1000;
|
||||
}
|
||||
|
||||
TransactionRateLimit? _buildRateLimit(String countText, String windowText) {
|
||||
if (countText.trim().isEmpty || windowText.trim().isEmpty) {
|
||||
return null;
|
||||
|
||||
231
useragent/lib/screens/dashboard/evm/grants/grants.dart
Normal file
231
useragent/lib/screens/dashboard/evm/grants/grants.dart
Normal file
@@ -0,0 +1,231 @@
|
||||
import 'package:arbiter/proto/evm.pb.dart';
|
||||
import 'package:arbiter/providers/evm/evm_grants.dart';
|
||||
import 'package:arbiter/providers/sdk_clients/wallet_access_list.dart';
|
||||
import 'package:arbiter/router.gr.dart';
|
||||
import 'package:arbiter/screens/dashboard/evm/grants/widgets/grant_card.dart';
|
||||
import 'package:arbiter/theme/palette.dart';
|
||||
import 'package:arbiter/widgets/page_header.dart';
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:sizer/sizer.dart';
|
||||
|
||||
String _formatError(Object error) {
|
||||
final message = error.toString();
|
||||
if (message.startsWith('Exception: ')) {
|
||||
return message.substring('Exception: '.length);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
// ─── State panel ──────────────────────────────────────────────────────────────
|
||||
|
||||
class _StatePanel extends StatelessWidget {
|
||||
const _StatePanel({
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.body,
|
||||
this.actionLabel,
|
||||
this.onAction,
|
||||
this.busy = false,
|
||||
});
|
||||
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final String body;
|
||||
final String? actionLabel;
|
||||
final Future<void> Function()? onAction;
|
||||
final bool busy;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
color: Palette.cream.withValues(alpha: 0.92),
|
||||
border: Border.all(color: Palette.line),
|
||||
),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(2.8.h),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (busy)
|
||||
SizedBox(
|
||||
width: 2.8.h,
|
||||
height: 2.8.h,
|
||||
child: const CircularProgressIndicator(strokeWidth: 2.5),
|
||||
)
|
||||
else
|
||||
Icon(icon, size: 34, color: Palette.coral),
|
||||
SizedBox(height: 1.8.h),
|
||||
Text(
|
||||
title,
|
||||
style: theme.textTheme.headlineSmall?.copyWith(
|
||||
color: Palette.ink,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 1.h),
|
||||
Text(
|
||||
body,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: Palette.ink.withValues(alpha: 0.72),
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
if (actionLabel != null && onAction != null) ...[
|
||||
SizedBox(height: 2.h),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => onAction!(),
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: Text(actionLabel!),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Grant list ───────────────────────────────────────────────────────────────
|
||||
|
||||
class _GrantList extends StatelessWidget {
|
||||
const _GrantList({required this.grants});
|
||||
|
||||
final List<GrantEntry> grants;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
for (var i = 0; i < grants.length; i++)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: i == grants.length - 1 ? 0 : 1.8.h,
|
||||
),
|
||||
child: GrantCard(grant: grants[i]),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Screen ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@RoutePage()
|
||||
class EvmGrantsScreen extends ConsumerWidget {
|
||||
const EvmGrantsScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// Screen watches only the grant list for top-level state decisions
|
||||
final grantsAsync = ref.watch(evmGrantsProvider);
|
||||
|
||||
Future<void> refresh() async {
|
||||
ref.invalidate(walletAccessListProvider);
|
||||
ref.invalidate(evmGrantsProvider);
|
||||
}
|
||||
|
||||
void showMessage(String message) {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(message), behavior: SnackBarBehavior.floating),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> safeRefresh() async {
|
||||
try {
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
showMessage(_formatError(e));
|
||||
}
|
||||
}
|
||||
|
||||
final grantsState = grantsAsync.asData?.value;
|
||||
final grants = grantsState?.grants;
|
||||
|
||||
final content = switch (grantsAsync) {
|
||||
AsyncLoading() when grantsState == null => const _StatePanel(
|
||||
icon: Icons.hourglass_top,
|
||||
title: 'Loading grants',
|
||||
body: 'Pulling grant registry from Arbiter.',
|
||||
busy: true,
|
||||
),
|
||||
AsyncError(:final error) => _StatePanel(
|
||||
icon: Icons.sync_problem,
|
||||
title: 'Grant registry unavailable',
|
||||
body: _formatError(error),
|
||||
actionLabel: 'Retry',
|
||||
onAction: safeRefresh,
|
||||
),
|
||||
AsyncData(:final value) when value == null => _StatePanel(
|
||||
icon: Icons.portable_wifi_off,
|
||||
title: 'No active server connection',
|
||||
body: 'Reconnect to Arbiter to list EVM grants.',
|
||||
actionLabel: 'Refresh',
|
||||
onAction: safeRefresh,
|
||||
),
|
||||
_ when grants != null && grants.isEmpty => _StatePanel(
|
||||
icon: Icons.policy_outlined,
|
||||
title: 'No grants yet',
|
||||
body: 'Create a grant to allow SDK clients to sign transactions.',
|
||||
actionLabel: 'Create grant',
|
||||
onAction: () async => context.router.push(const CreateEvmGrantRoute()),
|
||||
),
|
||||
_ => _GrantList(grants: grants ?? const []),
|
||||
};
|
||||
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: RefreshIndicator.adaptive(
|
||||
color: Palette.ink,
|
||||
backgroundColor: Colors.white,
|
||||
onRefresh: safeRefresh,
|
||||
child: ListView(
|
||||
physics: const BouncingScrollPhysics(
|
||||
parent: AlwaysScrollableScrollPhysics(),
|
||||
),
|
||||
padding: EdgeInsets.fromLTRB(2.4.w, 2.4.h, 2.4.w, 3.2.h),
|
||||
children: [
|
||||
PageHeader(
|
||||
title: 'EVM Grants',
|
||||
isBusy: grantsAsync.isLoading,
|
||||
actions: [
|
||||
FilledButton.icon(
|
||||
onPressed: () =>
|
||||
context.router.push(const CreateEvmGrantRoute()),
|
||||
icon: const Icon(Icons.add_rounded),
|
||||
label: const Text('Create grant'),
|
||||
),
|
||||
SizedBox(width: 1.w),
|
||||
OutlinedButton.icon(
|
||||
onPressed: safeRefresh,
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Palette.ink,
|
||||
side: BorderSide(color: Palette.line),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 1.4.w,
|
||||
vertical: 1.2.h,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
),
|
||||
icon: const Icon(Icons.refresh, size: 18),
|
||||
label: const Text('Refresh'),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 1.8.h),
|
||||
content,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
import 'package:arbiter/proto/evm.pb.dart';
|
||||
import 'package:arbiter/proto/user_agent.pb.dart';
|
||||
import 'package:arbiter/providers/evm/evm.dart';
|
||||
import 'package:arbiter/providers/evm/evm_grants.dart';
|
||||
import 'package:arbiter/providers/sdk_clients/list.dart';
|
||||
import 'package:arbiter/providers/sdk_clients/wallet_access_list.dart';
|
||||
import 'package:arbiter/theme/palette.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/experimental/mutation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:sizer/sizer.dart';
|
||||
|
||||
String _shortAddress(List<int> bytes) {
|
||||
final hex = bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join();
|
||||
return '0x${hex.substring(0, 6)}...${hex.substring(hex.length - 4)}';
|
||||
}
|
||||
|
||||
String _formatError(Object error) {
|
||||
final message = error.toString();
|
||||
if (message.startsWith('Exception: ')) {
|
||||
return message.substring('Exception: '.length);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
class GrantCard extends ConsumerWidget {
|
||||
const GrantCard({super.key, required this.grant});
|
||||
|
||||
final GrantEntry grant;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// Enrichment lookups — each watch scopes rebuilds to this card only
|
||||
final walletAccesses =
|
||||
ref.watch(walletAccessListProvider).asData?.value ?? const [];
|
||||
final wallets = ref.watch(evmProvider).asData?.value ?? const [];
|
||||
final clients = ref.watch(sdkClientsProvider).asData?.value ?? const [];
|
||||
final revoking = ref.watch(revokeEvmGrantMutation) is MutationPending;
|
||||
|
||||
final isEther =
|
||||
grant.specific.whichGrant() == SpecificGrant_Grant.etherTransfer;
|
||||
final accent = isEther ? Palette.coral : Palette.token;
|
||||
final typeLabel = isEther ? 'Ether' : 'Token';
|
||||
final theme = Theme.of(context);
|
||||
final muted = Palette.ink.withValues(alpha: 0.62);
|
||||
|
||||
// Resolve wallet_access_id → wallet address + client name
|
||||
final accessById = <int, SdkClientWalletAccess>{
|
||||
for (final a in walletAccesses) a.id: a,
|
||||
};
|
||||
final walletById = <int, WalletEntry>{
|
||||
for (final w in wallets) w.id: w,
|
||||
};
|
||||
final clientNameById = <int, String>{
|
||||
for (final c in clients) c.id: c.info.name,
|
||||
};
|
||||
|
||||
final accessId = grant.shared.walletAccessId;
|
||||
final access = accessById[accessId];
|
||||
final wallet = access != null ? walletById[access.access.walletId] : null;
|
||||
|
||||
final walletLabel = wallet != null
|
||||
? _shortAddress(wallet.address)
|
||||
: 'Access #$accessId';
|
||||
|
||||
final clientLabel = () {
|
||||
if (access == null) return '';
|
||||
final name = clientNameById[access.access.sdkClientId] ?? '';
|
||||
return name.isEmpty ? 'Client #${access.access.sdkClientId}' : name;
|
||||
}();
|
||||
|
||||
void showError(String message) {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(message), behavior: SnackBarBehavior.floating),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> revoke() async {
|
||||
try {
|
||||
await executeRevokeEvmGrant(ref, grantId: grant.id);
|
||||
} catch (e) {
|
||||
showError(_formatError(e));
|
||||
}
|
||||
}
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
color: Palette.cream.withValues(alpha: 0.92),
|
||||
border: Border.all(color: Palette.line),
|
||||
),
|
||||
child: IntrinsicHeight(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Accent strip
|
||||
Container(
|
||||
width: 0.8.w,
|
||||
decoration: BoxDecoration(
|
||||
color: accent,
|
||||
borderRadius: const BorderRadius.horizontal(
|
||||
left: Radius.circular(24),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Card body
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 1.6.w,
|
||||
vertical: 1.4.h,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Row 1: type badge · chain · spacer · revoke button
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 1.w,
|
||||
vertical: 0.4.h,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: accent.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
typeLabel,
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: accent,
|
||||
fontWeight: FontWeight.w800,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 1.w),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 1.w,
|
||||
vertical: 0.4.h,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Palette.ink.withValues(alpha: 0.06),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'Chain ${grant.shared.chainId}',
|
||||
style: theme.textTheme.labelSmall?.copyWith(
|
||||
color: muted,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (revoking)
|
||||
SizedBox(
|
||||
width: 1.8.h,
|
||||
height: 1.8.h,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Palette.coral,
|
||||
),
|
||||
)
|
||||
else
|
||||
OutlinedButton.icon(
|
||||
onPressed: revoke,
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Palette.coral,
|
||||
side: BorderSide(
|
||||
color: Palette.coral.withValues(alpha: 0.4),
|
||||
),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 1.w,
|
||||
vertical: 0.6.h,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
icon: const Icon(Icons.block_rounded, size: 16),
|
||||
label: const Text('Revoke'),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 0.8.h),
|
||||
// Row 2: wallet address · client name
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
walletLabel,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: Palette.ink,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 0.8.w),
|
||||
child: Text(
|
||||
'·',
|
||||
style: theme.textTheme.bodySmall
|
||||
?.copyWith(color: muted),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
clientLabel,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodySmall
|
||||
?.copyWith(color: muted),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,4 +5,5 @@ class Palette {
|
||||
static const coral = Color(0xFFE26254);
|
||||
static const cream = Color(0xFFFFFAF4);
|
||||
static const line = Color(0x1A15263C);
|
||||
static const token = Color(0xFF5C6BC0);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user