fix(useragent): unsafe, but working implementation of ml-dsa

This commit is contained in:
hdbg
2026-04-07 15:41:50 +02:00
parent 6b8da567dd
commit a4070e7df7
104 changed files with 11133 additions and 461 deletions

View File

@@ -28,8 +28,7 @@ class SdkConnectCallout extends StatelessWidget {
final hasDescription =
clientInfo.hasDescription() && clientInfo.description.isNotEmpty;
final hasVersion =
clientInfo.hasVersion() && clientInfo.version.isNotEmpty;
final hasVersion = clientInfo.hasVersion() && clientInfo.version.isNotEmpty;
final showInfoCard = hasDescription || hasVersion;
return CreamFrame(
@@ -74,10 +73,7 @@ class SdkConnectCallout extends StatelessWidget {
borderRadius: BorderRadius.circular(14),
border: Border.all(color: Palette.line),
),
padding: EdgeInsets.symmetric(
horizontal: 1.6.w,
vertical: 1.2.h,
),
padding: EdgeInsets.symmetric(horizontal: 1.6.w, vertical: 1.2.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 0.6.h,

View File

@@ -78,7 +78,7 @@ class DashboardRouter extends StatelessWidget {
}
class _CalloutBell extends ConsumerWidget {
const _CalloutBell({super.key});
const _CalloutBell();
@override
Widget build(BuildContext context, WidgetRef ref) {

View File

@@ -1,4 +1,3 @@
import 'package:arbiter/proto/user_agent/sdk_client.pb.dart' as ua_sdk;
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
@@ -13,5 +12,4 @@ class ClientDetails extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
throw UnimplementedError();
}
}

View File

@@ -12,30 +12,24 @@ class ClientSummaryCard extends StatelessWidget {
return CreamFrame(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
client.info.name,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text(client.info.description),
const SizedBox(height: 16),
Wrap(
runSpacing: 8,
spacing: 16,
children: [
_Fact(label: 'Client ID', value: '${client.id}'),
_Fact(label: 'Version', value: client.info.version),
_Fact(
label: 'Registered',
value: _formatDate(client.createdAt),
),
_Fact(label: 'Pubkey', value: _shortPubkey(client.pubkey)),
],
),
],
),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(client.info.name, style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 8),
Text(client.info.description),
const SizedBox(height: 16),
Wrap(
runSpacing: 8,
spacing: 16,
children: [
_Fact(label: 'Client ID', value: '${client.id}'),
_Fact(label: 'Version', value: client.info.version),
_Fact(label: 'Registered', value: _formatDate(client.createdAt)),
_Fact(label: 'Pubkey', value: _shortPubkey(client.pubkey)),
],
),
],
),
);
}
}

View File

@@ -28,27 +28,27 @@ class WalletAccessSaveBar extends StatelessWidget {
return CreamFrame(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (errorText != null) ...[
Text(errorText, style: TextStyle(color: Palette.coral)),
const SizedBox(height: 12),
],
Row(
children: [
TextButton(
onPressed: state.hasChanges && !isPending ? onDiscard : null,
child: const Text('Reset'),
),
const Spacer(),
FilledButton(
onPressed: state.hasChanges && !isPending ? onSave : null,
child: Text(isPending ? 'Saving...' : 'Save changes'),
),
],
),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (errorText != null) ...[
Text(errorText, style: TextStyle(color: Palette.coral)),
const SizedBox(height: 12),
],
),
Row(
children: [
TextButton(
onPressed: state.hasChanges && !isPending ? onDiscard : null,
child: const Text('Reset'),
),
const Spacer(),
FilledButton(
onPressed: state.hasChanges && !isPending ? onSave : null,
child: Text(isPending ? 'Saving...' : 'Save changes'),
),
],
),
],
),
);
}
}

View File

@@ -30,26 +30,23 @@ class WalletAccessSection extends ConsumerWidget {
return CreamFrame(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Wallet access',
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 8),
Text('Choose which managed wallets this client can see.'),
const SizedBox(height: 16),
_WalletAccessBody(
clientId: clientId,
state: state,
accessSelectionAsync: accessSelectionAsync,
isSavePending: isSavePending,
optionsAsync: optionsAsync,
onSearchChanged: onSearchChanged,
onToggleWallet: onToggleWallet,
),
],
),
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Wallet access', style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 8),
Text('Choose which managed wallets this client can see.'),
const SizedBox(height: 16),
_WalletAccessBody(
clientId: clientId,
state: state,
accessSelectionAsync: accessSelectionAsync,
isSavePending: isSavePending,
optionsAsync: optionsAsync,
onSearchChanged: onSearchChanged,
onToggleWallet: onToggleWallet,
),
],
),
);
}
}

View File

@@ -378,48 +378,48 @@ class _ClientTable extends StatelessWidget {
builder: (context, constraints) {
final tableWidth = math.max(_tableMinWidth, constraints.maxWidth);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Registered clients',
style: theme.textTheme.titleLarge?.copyWith(
color: Palette.ink,
fontWeight: FontWeight.w800,
),
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Registered clients',
style: theme.textTheme.titleLarge?.copyWith(
color: Palette.ink,
fontWeight: FontWeight.w800,
),
SizedBox(height: 0.6.h),
Text(
'Every entry here has authenticated with Arbiter at least once.',
style: theme.textTheme.bodyMedium?.copyWith(
color: Palette.ink.withValues(alpha: 0.70),
height: 1.4,
),
),
SizedBox(height: 0.6.h),
Text(
'Every entry here has authenticated with Arbiter at least once.',
style: theme.textTheme.bodyMedium?.copyWith(
color: Palette.ink.withValues(alpha: 0.70),
height: 1.4,
),
SizedBox(height: 1.6.h),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SizedBox(
width: tableWidth,
child: Column(
children: [
const _ClientTableHeader(),
SizedBox(height: 1.h),
for (var i = 0; i < clients.length; i++)
Padding(
padding: EdgeInsets.only(
bottom: i == clients.length - 1 ? 0 : 1.h,
),
child: _ClientTableRow(client: clients[i]),
),
SizedBox(height: 1.6.h),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SizedBox(
width: tableWidth,
child: Column(
children: [
const _ClientTableHeader(),
SizedBox(height: 1.h),
for (var i = 0; i < clients.length; i++)
Padding(
padding: EdgeInsets.only(
bottom: i == clients.length - 1 ? 0 : 1.h,
),
],
),
child: _ClientTableRow(client: clients[i]),
),
],
),
),
],
);
},
),
),
],
);
},
),
);
}
}

View File

@@ -31,7 +31,9 @@ class ClientPickerField extends ConsumerWidget {
? null
: (value) {
ref.read(grantCreationProvider.notifier).setClientId(value);
FormBuilder.of(context)?.fields['walletAccessId']?.didChange(null);
FormBuilder.of(
context,
)?.fields['walletAccessId']?.didChange(null);
},
);
}

View File

@@ -16,46 +16,48 @@ class FormBuilderDateTimeField extends FormBuilderField<DateTime?> {
super.onChanged,
super.validator,
}) : super(
builder: (FormFieldState<DateTime?> field) {
final value = field.value;
return OutlinedButton(
onPressed: () async {
final ctx = field.context;
final now = DateTime.now();
final date = await showDatePicker(
context: ctx,
firstDate: DateTime(now.year - 5),
lastDate: DateTime(now.year + 10),
initialDate: value ?? now,
);
if (date == null) return;
if (!ctx.mounted) return;
final time = await showTimePicker(
context: ctx,
initialTime: TimeOfDay.fromDateTime(value ?? now),
);
if (time == null) return;
field.didChange(DateTime(
date.year,
date.month,
date.day,
time.hour,
time.minute,
));
},
onLongPress: value == null ? null : () => field.didChange(null),
child: Padding(
padding: EdgeInsets.symmetric(vertical: 1.8.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label),
SizedBox(height: 0.6.h),
Text(value?.toLocal().toString() ?? 'Not set'),
],
),
),
);
},
);
builder: (FormFieldState<DateTime?> field) {
final value = field.value;
return OutlinedButton(
onPressed: () async {
final ctx = field.context;
final now = DateTime.now();
final date = await showDatePicker(
context: ctx,
firstDate: DateTime(now.year - 5),
lastDate: DateTime(now.year + 10),
initialDate: value ?? now,
);
if (date == null) return;
if (!ctx.mounted) return;
final time = await showTimePicker(
context: ctx,
initialTime: TimeOfDay.fromDateTime(value ?? now),
);
if (time == null) return;
field.didChange(
DateTime(
date.year,
date.month,
date.day,
time.hour,
time.minute,
),
);
},
onLongPress: value == null ? null : () => field.didChange(null),
child: Padding(
padding: EdgeInsets.symmetric(vertical: 1.8.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label),
SizedBox(height: 0.6.h),
Text(value?.toLocal().toString() ?? 'Not set'),
],
),
),
);
},
);
}

View File

@@ -36,8 +36,8 @@ class WalletAccessPickerField extends ConsumerWidget {
helperText: state.selectedClientId == null
? 'Select a client first'
: accesses.isEmpty
? 'No wallet accesses for this client'
: null,
? 'No wallet accesses for this client'
: null,
border: const OutlineInputBorder(),
),
items: [

View File

@@ -93,9 +93,9 @@ class _EtherTransferForm extends ConsumerWidget {
SizedBox(height: 1.6.h),
Text(
'Ether volume limit',
style: Theme.of(context).textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w800,
),
style: Theme.of(
context,
).textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w800),
),
SizedBox(height: 0.8.h),
Row(
@@ -157,9 +157,9 @@ class _EtherTargetsField extends StatelessWidget {
Expanded(
child: Text(
'Ether targets',
style: Theme.of(context).textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w800,
),
style: Theme.of(
context,
).textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w800),
),
),
TextButton.icon(

View File

@@ -13,7 +13,11 @@ import 'package:sizer/sizer.dart';
part 'token_transfer_grant.g.dart';
class VolumeLimitEntry {
VolumeLimitEntry({required this.id, this.amount = '', this.windowSeconds = ''});
VolumeLimitEntry({
required this.id,
this.amount = '',
this.windowSeconds = '',
});
final int id;
final String amount;
@@ -27,7 +31,6 @@ class VolumeLimitEntry {
);
}
@riverpod
class TokenGrantLimits extends _$TokenGrantLimits {
int _nextId = 0;
@@ -47,7 +50,6 @@ class TokenGrantLimits extends _$TokenGrantLimits {
void remove(int index) => state = [...state]..removeAt(index);
}
class TokenTransferGrantHandler implements GrantFormHandler {
const TokenTransferGrantHandler();
@@ -65,11 +67,16 @@ class TokenTransferGrantHandler implements GrantFormHandler {
return SpecificGrant(
tokenTransfer: TokenTransferSettings(
tokenContract:
parseHexAddress(formValues['tokenContract'] as String? ?? ''),
tokenContract: parseHexAddress(
formValues['tokenContract'] as String? ?? '',
),
target: targetText.trim().isEmpty ? null : parseHexAddress(targetText),
volumeLimits: limits
.where((e) => e.amount.trim().isNotEmpty && e.windowSeconds.trim().isNotEmpty)
.where(
(e) =>
e.amount.trim().isNotEmpty &&
e.windowSeconds.trim().isNotEmpty,
)
.map(
(e) => VolumeRateLimit(
maxVolume: parseBigIntBytes(e.amount),
@@ -153,9 +160,9 @@ class _TokenVolumeLimitsField extends StatelessWidget {
Expanded(
child: Text(
'Token volume limits',
style: Theme.of(context).textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w800,
),
style: Theme.of(
context,
).textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w800),
),
),
TextButton.icon(
@@ -196,7 +203,9 @@ class _TokenVolumeLimitRow extends HookWidget {
@override
Widget build(BuildContext context) {
final amountController = useTextEditingController(text: value.amount);
final windowController = useTextEditingController(text: value.windowSeconds);
final windowController = useTextEditingController(
text: value.windowSeconds,
);
return Row(
children: [
@@ -214,8 +223,7 @@ class _TokenVolumeLimitRow extends HookWidget {
Expanded(
child: TextField(
controller: windowController,
onChanged: (next) =>
onChanged(value.copyWith(windowSeconds: next)),
onChanged: (next) => onChanged(value.copyWith(windowSeconds: next)),
decoration: const InputDecoration(
labelText: 'Window (seconds)',
border: OutlineInputBorder(),

View File

@@ -25,10 +25,10 @@ const _etherHandler = EtherTransferGrantHandler();
const _tokenHandler = TokenTransferGrantHandler();
GrantFormHandler _handlerFor(SpecificGrant_Grant type) => switch (type) {
SpecificGrant_Grant.etherTransfer => _etherHandler,
SpecificGrant_Grant.tokenTransfer => _tokenHandler,
_ => throw ArgumentError('Unsupported grant type: $type'),
};
SpecificGrant_Grant.etherTransfer => _etherHandler,
SpecificGrant_Grant.tokenTransfer => _tokenHandler,
_ => throw ArgumentError('Unsupported grant type: $type'),
};
@RoutePage()
class CreateEvmGrantScreen extends HookConsumerWidget {
@@ -62,12 +62,14 @@ class CreateEvmGrantScreen extends HookConsumerWidget {
);
final validFrom = formValues['validFrom'] as DateTime?;
final validUntil = formValues['validUntil'] as DateTime?;
if (validFrom != null) sharedSettings.validFrom = toTimestamp(validFrom);
if (validFrom != null)
sharedSettings.validFrom = toTimestamp(validFrom);
if (validUntil != null) {
sharedSettings.validUntil = toTimestamp(validUntil);
}
final gasBytes =
optionalBigIntBytes(formValues['maxGasFeePerGas'] as String? ?? '');
final gasBytes = optionalBigIntBytes(
formValues['maxGasFeePerGas'] as String? ?? '',
);
if (gasBytes != null) sharedSettings.maxGasFeePerGas = gasBytes;
final priorityBytes = optionalBigIntBytes(
formValues['maxPriorityFeePerGas'] as String? ?? '',
@@ -106,7 +108,8 @@ class CreateEvmGrantScreen extends HookConsumerWidget {
SizedBox(height: 1.8.h),
const _Section(
title: 'Authorization',
tooltip: 'Select which SDK client receives this grant and '
tooltip:
'Select which SDK client receives this grant and '
'which of its wallet accesses it applies to.',
child: AuthorizationFields(),
),
@@ -118,7 +121,8 @@ class CreateEvmGrantScreen extends HookConsumerWidget {
const Expanded(
child: _Section(
title: 'Chain',
tooltip: 'Restrict this grant to a specific EVM chain ID. '
tooltip:
'Restrict this grant to a specific EVM chain ID. '
'Leave empty to allow any chain.',
optional: true,
child: ChainIdField(),
@@ -128,7 +132,8 @@ class CreateEvmGrantScreen extends HookConsumerWidget {
const Expanded(
child: _Section(
title: 'Timing',
tooltip: 'Set an optional validity window. '
tooltip:
'Set an optional validity window. '
'Signing requests outside this period will be rejected.',
optional: true,
child: ValidityWindowField(),
@@ -145,7 +150,8 @@ class CreateEvmGrantScreen extends HookConsumerWidget {
const Expanded(
child: _Section(
title: 'Gas limits',
tooltip: 'Cap the gas fees this grant may authorize. '
tooltip:
'Cap the gas fees this grant may authorize. '
'Transactions exceeding these values will be rejected.',
optional: true,
child: GasFeeOptionsField(),
@@ -155,7 +161,8 @@ class CreateEvmGrantScreen extends HookConsumerWidget {
const Expanded(
child: _Section(
title: 'Transaction limits',
tooltip: 'Limit how many transactions can be signed '
tooltip:
'Limit how many transactions can be signed '
'within a rolling time window.',
optional: true,
child: TransactionRateLimitField(),
@@ -172,7 +179,8 @@ class CreateEvmGrantScreen extends HookConsumerWidget {
SizedBox(height: 1.8.h),
_Section(
title: 'Grant-specific options',
tooltip: 'Rules specific to the selected transfer type. '
tooltip:
'Rules specific to the selected transfer type. '
'Switch between Ether and token above to change these fields.',
child: handler.buildForm(context, ref),
),
@@ -180,8 +188,7 @@ class CreateEvmGrantScreen extends HookConsumerWidget {
Align(
alignment: Alignment.centerRight,
child: FilledButton.icon(
onPressed:
createMutation is MutationPending ? null : submit,
onPressed: createMutation is MutationPending ? null : submit,
icon: createMutation is MutationPending
? SizedBox(
width: 1.8.h,
@@ -266,9 +273,9 @@ class _Section extends StatelessWidget {
children: [
Text(
title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w800,
),
style: Theme.of(
context,
).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w800),
),
SizedBox(width: 0.4.w),
Tooltip(
@@ -283,9 +290,9 @@ class _Section extends StatelessWidget {
SizedBox(width: 0.6.w),
Text(
'(optional)',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: subtleColor,
),
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(color: subtleColor),
),
],
],

View File

@@ -19,4 +19,3 @@ class AuthorizationFields extends StatelessWidget {
);
}
}

View File

@@ -46,9 +46,7 @@ class GrantCard extends ConsumerWidget {
final accessById = <int, ua_sdk.WalletAccessEntry>{
for (final a in walletAccesses) a.id: a,
};
final walletById = <int, WalletEntry>{
for (final w in wallets) w.id: w,
};
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,
};
@@ -192,8 +190,9 @@ class GrantCard extends ConsumerWidget {
padding: EdgeInsets.symmetric(horizontal: 0.8.w),
child: Text(
'·',
style: theme.textTheme.bodySmall
?.copyWith(color: muted),
style: theme.textTheme.bodySmall?.copyWith(
color: muted,
),
),
),
Expanded(
@@ -201,8 +200,9 @@ class GrantCard extends ConsumerWidget {
clientLabel,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall
?.copyWith(color: muted),
style: theme.textTheme.bodySmall?.copyWith(
color: muted,
),
),
),
],

View File

@@ -5,7 +5,6 @@ import 'package:hooks_riverpod/experimental/mutation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:sizer/sizer.dart';
class CreateWalletButton extends ConsumerWidget {
const CreateWalletButton({super.key});
@@ -88,7 +87,6 @@ class RefreshWalletButton extends ConsumerWidget {
}
}
String _formatError(Object error) {
final message = error.toString();
if (message.startsWith('Exception: ')) {

View File

@@ -36,54 +36,51 @@ class WalletTable extends StatelessWidget {
return CreamFrame(
padding: EdgeInsets.all(2.h),
child: LayoutBuilder(
builder: (context, constraints) {
final tableWidth = math.max(_tableMinWidth, constraints.maxWidth);
builder: (context, constraints) {
final tableWidth = math.max(_tableMinWidth, constraints.maxWidth);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Managed wallets',
style: theme.textTheme.titleLarge?.copyWith(
color: Palette.ink,
fontWeight: FontWeight.w800,
),
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Managed wallets',
style: theme.textTheme.titleLarge?.copyWith(
color: Palette.ink,
fontWeight: FontWeight.w800,
),
SizedBox(height: 0.6.h),
Text(
'Every address here is generated and held by Arbiter.',
style: theme.textTheme.bodyMedium?.copyWith(
color: Palette.ink.withValues(alpha: 0.70),
height: 1.4,
),
),
SizedBox(height: 0.6.h),
Text(
'Every address here is generated and held by Arbiter.',
style: theme.textTheme.bodyMedium?.copyWith(
color: Palette.ink.withValues(alpha: 0.70),
height: 1.4,
),
SizedBox(height: 1.6.h),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SizedBox(
width: tableWidth,
child: Column(
children: [
const _WalletTableHeader(),
SizedBox(height: 1.h),
for (var i = 0; i < wallets.length; i++)
Padding(
padding: EdgeInsets.only(
bottom: i == wallets.length - 1 ? 0 : 1.h,
),
child: _WalletTableRow(
wallet: wallets[i],
index: i,
),
),
SizedBox(height: 1.6.h),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: SizedBox(
width: tableWidth,
child: Column(
children: [
const _WalletTableHeader(),
SizedBox(height: 1.h),
for (var i = 0; i < wallets.length; i++)
Padding(
padding: EdgeInsets.only(
bottom: i == wallets.length - 1 ? 0 : 1.h,
),
],
),
child: _WalletTableRow(wallet: wallets[i], index: i),
),
],
),
),
],
);
},
),
),
],
);
},
),
);
}
}