refactor(useragent::evm::grants): split into more files & flutter_form_builder usage
This commit is contained in:
@@ -18,7 +18,7 @@ import 'package:arbiter/screens/dashboard/clients/details/client_details.dart'
|
|||||||
as _i4;
|
as _i4;
|
||||||
import 'package:arbiter/screens/dashboard/clients/table.dart' as _i5;
|
import 'package:arbiter/screens/dashboard/clients/table.dart' as _i5;
|
||||||
import 'package:arbiter/screens/dashboard/evm/evm.dart' as _i9;
|
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/dashboard/evm/grants/create/screen.dart' as _i6;
|
||||||
import 'package:arbiter/screens/dashboard/evm/grants/grants.dart' as _i8;
|
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_connection.dart' as _i10;
|
||||||
import 'package:arbiter/screens/server_info_setup.dart' as _i11;
|
import 'package:arbiter/screens/server_info_setup.dart' as _i11;
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// lib/screens/dashboard/evm/grants/create/fields/chain_id_field.dart
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
|
|
||||||
|
class ChainIdField extends StatelessWidget {
|
||||||
|
const ChainIdField({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return FormBuilderTextField(
|
||||||
|
name: 'chainId',
|
||||||
|
initialValue: '1',
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Chain ID',
|
||||||
|
hintText: '1',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
// lib/screens/dashboard/evm/grants/create/fields/client_picker_field.dart
|
||||||
|
import 'package:arbiter/proto/user_agent.pb.dart';
|
||||||
|
import 'package:arbiter/providers/sdk_clients/list.dart';
|
||||||
|
import 'package:arbiter/screens/dashboard/evm/grants/create/provider.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
|
class ClientPickerField extends ConsumerWidget {
|
||||||
|
const ClientPickerField({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final clients =
|
||||||
|
ref.watch(sdkClientsProvider).asData?.value ?? const <SdkClientEntry>[];
|
||||||
|
|
||||||
|
return FormBuilderDropdown<int>(
|
||||||
|
name: 'clientId',
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Client',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
items: [
|
||||||
|
for (final c in clients)
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: c.id,
|
||||||
|
child: Text(c.info.name.isEmpty ? 'Client #${c.id}' : c.info.name),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: clients.isEmpty
|
||||||
|
? null
|
||||||
|
: (value) {
|
||||||
|
ref.read(grantCreationProvider.notifier).setClientId(value);
|
||||||
|
FormBuilder.of(context)?.fields['walletAccessId']?.didChange(null);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// lib/screens/dashboard/evm/grants/create/fields/date_time_field.dart
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
|
import 'package:sizer/sizer.dart';
|
||||||
|
|
||||||
|
/// A [FormBuilderField] that opens a date picker followed by a time picker.
|
||||||
|
/// Long-press clears the value.
|
||||||
|
class FormBuilderDateTimeField extends FormBuilderField<DateTime?> {
|
||||||
|
final String label;
|
||||||
|
|
||||||
|
FormBuilderDateTimeField({
|
||||||
|
super.key,
|
||||||
|
required super.name,
|
||||||
|
required this.label,
|
||||||
|
super.initialValue,
|
||||||
|
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'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// lib/screens/dashboard/evm/grants/create/fields/gas_fee_options_field.dart
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
|
import 'package:sizer/sizer.dart';
|
||||||
|
|
||||||
|
class GasFeeOptionsField extends StatelessWidget {
|
||||||
|
const GasFeeOptionsField({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: FormBuilderTextField(
|
||||||
|
name: 'maxGasFeePerGas',
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Max gas fee / gas',
|
||||||
|
hintText: '1000000000',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 1.w),
|
||||||
|
Expanded(
|
||||||
|
child: FormBuilderTextField(
|
||||||
|
name: 'maxPriorityFeePerGas',
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Max priority fee / gas',
|
||||||
|
hintText: '100000000',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// lib/screens/dashboard/evm/grants/create/fields/transaction_rate_limit_field.dart
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
|
import 'package:sizer/sizer.dart';
|
||||||
|
|
||||||
|
class TransactionRateLimitField extends StatelessWidget {
|
||||||
|
const TransactionRateLimitField({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: FormBuilderTextField(
|
||||||
|
name: 'txCount',
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Tx count limit',
|
||||||
|
hintText: '10',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 1.w),
|
||||||
|
Expanded(
|
||||||
|
child: FormBuilderTextField(
|
||||||
|
name: 'txWindow',
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Window (seconds)',
|
||||||
|
hintText: '3600',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
// lib/screens/dashboard/evm/grants/create/fields/validity_window_field.dart
|
||||||
|
import 'package:arbiter/screens/dashboard/evm/grants/create/fields/date_time_field.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:sizer/sizer.dart';
|
||||||
|
|
||||||
|
class ValidityWindowField extends StatelessWidget {
|
||||||
|
const ValidityWindowField({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: FormBuilderDateTimeField(
|
||||||
|
name: 'validFrom',
|
||||||
|
label: 'Valid from',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 1.w),
|
||||||
|
Expanded(
|
||||||
|
child: FormBuilderDateTimeField(
|
||||||
|
name: 'validUntil',
|
||||||
|
label: 'Valid until',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
// lib/screens/dashboard/evm/grants/create/fields/wallet_access_picker_field.dart
|
||||||
|
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/sdk_clients/wallet_access_list.dart';
|
||||||
|
import 'package:arbiter/screens/dashboard/evm/grants/create/provider.dart';
|
||||||
|
import 'package:arbiter/screens/dashboard/evm/grants/create/utils.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
|
class WalletAccessPickerField extends ConsumerWidget {
|
||||||
|
const WalletAccessPickerField({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final state = ref.watch(grantCreationProvider);
|
||||||
|
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 = state.selectedClientId == null
|
||||||
|
? const <SdkClientWalletAccess>[]
|
||||||
|
: allAccesses
|
||||||
|
.where((a) => a.access.sdkClientId == state.selectedClientId)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return FormBuilderDropdown<int>(
|
||||||
|
name: 'walletAccessId',
|
||||||
|
enabled: accesses.isNotEmpty,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Wallet access',
|
||||||
|
helperText: state.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}';
|
||||||
|
}()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
// lib/screens/dashboard/evm/grants/create/grants/ether_transfer_grant.dart
|
||||||
|
import 'package:arbiter/proto/evm.pb.dart';
|
||||||
|
import 'package:arbiter/screens/dashboard/evm/grants/create/grants/grant_form_handler.dart';
|
||||||
|
import 'package:arbiter/screens/dashboard/evm/grants/create/utils.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:sizer/sizer.dart';
|
||||||
|
|
||||||
|
part 'ether_transfer_grant.g.dart';
|
||||||
|
|
||||||
|
class EtherTargetEntry {
|
||||||
|
EtherTargetEntry({required this.id, this.address = ''});
|
||||||
|
|
||||||
|
final int id;
|
||||||
|
final String address;
|
||||||
|
|
||||||
|
EtherTargetEntry copyWith({String? address}) =>
|
||||||
|
EtherTargetEntry(id: id, address: address ?? this.address);
|
||||||
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
class EtherGrantTargets extends _$EtherGrantTargets {
|
||||||
|
int _nextId = 0;
|
||||||
|
int _newId() => _nextId++;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<EtherTargetEntry> build() => [EtherTargetEntry(id: _newId())];
|
||||||
|
|
||||||
|
void add() => state = [...state, EtherTargetEntry(id: _newId())];
|
||||||
|
|
||||||
|
void update(int index, EtherTargetEntry entry) {
|
||||||
|
final updated = [...state];
|
||||||
|
updated[index] = entry;
|
||||||
|
state = updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
void remove(int index) => state = [...state]..removeAt(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
class EtherTransferGrantHandler implements GrantFormHandler {
|
||||||
|
const EtherTransferGrantHandler();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildForm(BuildContext context, WidgetRef ref) =>
|
||||||
|
const _EtherTransferForm();
|
||||||
|
|
||||||
|
@override
|
||||||
|
SpecificGrant buildSpecificGrant(
|
||||||
|
Map<String, dynamic> formValues,
|
||||||
|
WidgetRef ref,
|
||||||
|
) {
|
||||||
|
final targets = ref.read(etherGrantTargetsProvider);
|
||||||
|
|
||||||
|
return SpecificGrant(
|
||||||
|
etherTransfer: EtherTransferSettings(
|
||||||
|
targets: targets
|
||||||
|
.where((e) => e.address.trim().isNotEmpty)
|
||||||
|
.map((e) => parseHexAddress(e.address))
|
||||||
|
.toList(),
|
||||||
|
limit: buildVolumeLimit(
|
||||||
|
formValues['etherVolume'] as String? ?? '',
|
||||||
|
formValues['etherVolumeWindow'] as String? ?? '',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Form widget
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class _EtherTransferForm extends ConsumerWidget {
|
||||||
|
const _EtherTransferForm();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final targets = ref.watch(etherGrantTargetsProvider);
|
||||||
|
final notifier = ref.read(etherGrantTargetsProvider.notifier);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
_EtherTargetsField(
|
||||||
|
values: targets,
|
||||||
|
onAdd: notifier.add,
|
||||||
|
onUpdate: notifier.update,
|
||||||
|
onRemove: notifier.remove,
|
||||||
|
),
|
||||||
|
SizedBox(height: 1.6.h),
|
||||||
|
Text(
|
||||||
|
'Ether volume limit',
|
||||||
|
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 0.8.h),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: FormBuilderTextField(
|
||||||
|
name: 'etherVolume',
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Max volume',
|
||||||
|
hintText: '1000000000000000000',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 1.w),
|
||||||
|
Expanded(
|
||||||
|
child: FormBuilderTextField(
|
||||||
|
name: 'etherVolumeWindow',
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Window (seconds)',
|
||||||
|
hintText: '86400',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Targets list widget
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class _EtherTargetsField extends StatelessWidget {
|
||||||
|
const _EtherTargetsField({
|
||||||
|
required this.values,
|
||||||
|
required this.onAdd,
|
||||||
|
required this.onUpdate,
|
||||||
|
required this.onRemove,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<EtherTargetEntry> values;
|
||||||
|
final VoidCallback onAdd;
|
||||||
|
final void Function(int index, EtherTargetEntry entry) onUpdate;
|
||||||
|
final void Function(int index) onRemove;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Ether targets',
|
||||||
|
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: onAdd,
|
||||||
|
icon: const Icon(Icons.add_rounded),
|
||||||
|
label: const Text('Add'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 0.8.h),
|
||||||
|
for (var i = 0; i < values.length; i++)
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: 1.h),
|
||||||
|
child: _EtherTargetRow(
|
||||||
|
key: ValueKey(values[i].id),
|
||||||
|
value: values[i],
|
||||||
|
onChanged: (entry) => onUpdate(i, entry),
|
||||||
|
onRemove: values.length == 1 ? null : () => onRemove(i),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EtherTargetRow extends HookWidget {
|
||||||
|
const _EtherTargetRow({
|
||||||
|
super.key,
|
||||||
|
required this.value,
|
||||||
|
required this.onChanged,
|
||||||
|
required this.onRemove,
|
||||||
|
});
|
||||||
|
|
||||||
|
final EtherTargetEntry value;
|
||||||
|
final ValueChanged<EtherTargetEntry> onChanged;
|
||||||
|
final VoidCallback? onRemove;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final addressController = useTextEditingController(text: value.address);
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: addressController,
|
||||||
|
onChanged: (next) => onChanged(value.copyWith(address: next)),
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Address',
|
||||||
|
hintText: '0x...',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 0.4.w),
|
||||||
|
IconButton(
|
||||||
|
onPressed: onRemove,
|
||||||
|
icon: const Icon(Icons.remove_circle_outline_rounded),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'ether_transfer_grant.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint, type=warning
|
||||||
|
|
||||||
|
@ProviderFor(EtherGrantTargets)
|
||||||
|
final etherGrantTargetsProvider = EtherGrantTargetsProvider._();
|
||||||
|
|
||||||
|
final class EtherGrantTargetsProvider
|
||||||
|
extends $NotifierProvider<EtherGrantTargets, List<EtherTargetEntry>> {
|
||||||
|
EtherGrantTargetsProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'etherGrantTargetsProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$etherGrantTargetsHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
EtherGrantTargets create() => EtherGrantTargets();
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(List<EtherTargetEntry> value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<List<EtherTargetEntry>>(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$etherGrantTargetsHash() => r'063aa3180d5e5bbc1525702272686f1fd8ca751d';
|
||||||
|
|
||||||
|
abstract class _$EtherGrantTargets extends $Notifier<List<EtherTargetEntry>> {
|
||||||
|
List<EtherTargetEntry> build();
|
||||||
|
@$mustCallSuper
|
||||||
|
@override
|
||||||
|
void runBuild() {
|
||||||
|
final ref =
|
||||||
|
this.ref as $Ref<List<EtherTargetEntry>, List<EtherTargetEntry>>;
|
||||||
|
final element =
|
||||||
|
ref.element
|
||||||
|
as $ClassProviderElement<
|
||||||
|
AnyNotifier<List<EtherTargetEntry>, List<EtherTargetEntry>>,
|
||||||
|
List<EtherTargetEntry>,
|
||||||
|
Object?,
|
||||||
|
Object?
|
||||||
|
>;
|
||||||
|
element.handleCreate(ref, build);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// lib/screens/dashboard/evm/grants/create/grants/grant_form_handler.dart
|
||||||
|
import 'package:arbiter/proto/evm.pb.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
|
abstract class GrantFormHandler {
|
||||||
|
/// Renders the grant-specific form section.
|
||||||
|
///
|
||||||
|
/// The returned widget must be a descendant of the [FormBuilder] in the
|
||||||
|
/// screen so its [FormBuilderField] children register automatically.
|
||||||
|
///
|
||||||
|
/// **Field name contract:** All `name:` values used by this handler must be
|
||||||
|
/// unique across ALL [GrantFormHandler] implementations. [FormBuilder]
|
||||||
|
/// retains field state across handler switches, so name collisions cause
|
||||||
|
/// silent data corruption.
|
||||||
|
Widget buildForm(BuildContext context, WidgetRef ref);
|
||||||
|
|
||||||
|
/// Assembles a [SpecificGrant] proto.
|
||||||
|
///
|
||||||
|
/// [formValues] — `formKey.currentState!.value` after `saveAndValidate()`.
|
||||||
|
/// [ref] — read any provider the handler owns (e.g. token volume limits).
|
||||||
|
SpecificGrant buildSpecificGrant(
|
||||||
|
Map<String, dynamic> formValues,
|
||||||
|
WidgetRef ref,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
// lib/screens/dashboard/evm/grants/create/grants/token_transfer_grant.dart
|
||||||
|
import 'package:arbiter/proto/evm.pb.dart';
|
||||||
|
import 'package:arbiter/screens/dashboard/evm/grants/create/grants/grant_form_handler.dart';
|
||||||
|
import 'package:arbiter/screens/dashboard/evm/grants/create/utils.dart';
|
||||||
|
import 'package:fixnum/fixnum.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:sizer/sizer.dart';
|
||||||
|
|
||||||
|
part 'token_transfer_grant.g.dart';
|
||||||
|
|
||||||
|
class VolumeLimitEntry {
|
||||||
|
VolumeLimitEntry({required this.id, this.amount = '', this.windowSeconds = ''});
|
||||||
|
|
||||||
|
final int id;
|
||||||
|
final String amount;
|
||||||
|
final String windowSeconds;
|
||||||
|
|
||||||
|
VolumeLimitEntry copyWith({String? amount, String? windowSeconds}) =>
|
||||||
|
VolumeLimitEntry(
|
||||||
|
id: id,
|
||||||
|
amount: amount ?? this.amount,
|
||||||
|
windowSeconds: windowSeconds ?? this.windowSeconds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
class TokenGrantLimits extends _$TokenGrantLimits {
|
||||||
|
int _nextId = 0;
|
||||||
|
int _newId() => _nextId++;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<VolumeLimitEntry> build() => [VolumeLimitEntry(id: _newId())];
|
||||||
|
|
||||||
|
void add() => state = [...state, VolumeLimitEntry(id: _newId())];
|
||||||
|
|
||||||
|
void update(int index, VolumeLimitEntry entry) {
|
||||||
|
final updated = [...state];
|
||||||
|
updated[index] = entry;
|
||||||
|
state = updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
void remove(int index) => state = [...state]..removeAt(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TokenTransferGrantHandler implements GrantFormHandler {
|
||||||
|
const TokenTransferGrantHandler();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildForm(BuildContext context, WidgetRef ref) =>
|
||||||
|
const _TokenTransferForm();
|
||||||
|
|
||||||
|
@override
|
||||||
|
SpecificGrant buildSpecificGrant(
|
||||||
|
Map<String, dynamic> formValues,
|
||||||
|
WidgetRef ref,
|
||||||
|
) {
|
||||||
|
final limits = ref.read(tokenGrantLimitsProvider);
|
||||||
|
final targetText = formValues['tokenTarget'] as String? ?? '';
|
||||||
|
|
||||||
|
return SpecificGrant(
|
||||||
|
tokenTransfer: TokenTransferSettings(
|
||||||
|
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)
|
||||||
|
.map(
|
||||||
|
(e) => VolumeRateLimit(
|
||||||
|
maxVolume: parseBigIntBytes(e.amount),
|
||||||
|
windowSecs: Int64.parseInt(e.windowSeconds),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Form widget
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class _TokenTransferForm extends ConsumerWidget {
|
||||||
|
const _TokenTransferForm();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final limits = ref.watch(tokenGrantLimitsProvider);
|
||||||
|
final notifier = ref.read(tokenGrantLimitsProvider.notifier);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
FormBuilderTextField(
|
||||||
|
name: 'tokenContract',
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Token contract',
|
||||||
|
hintText: '0x...',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 1.6.h),
|
||||||
|
FormBuilderTextField(
|
||||||
|
name: 'tokenTarget',
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Token recipient',
|
||||||
|
hintText: '0x... or leave empty for any recipient',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 1.6.h),
|
||||||
|
_TokenVolumeLimitsField(
|
||||||
|
values: limits,
|
||||||
|
onAdd: notifier.add,
|
||||||
|
onUpdate: notifier.update,
|
||||||
|
onRemove: notifier.remove,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Volume limits list widget
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class _TokenVolumeLimitsField extends StatelessWidget {
|
||||||
|
const _TokenVolumeLimitsField({
|
||||||
|
required this.values,
|
||||||
|
required this.onAdd,
|
||||||
|
required this.onUpdate,
|
||||||
|
required this.onRemove,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<VolumeLimitEntry> values;
|
||||||
|
final VoidCallback onAdd;
|
||||||
|
final void Function(int index, VolumeLimitEntry entry) onUpdate;
|
||||||
|
final void Function(int index) onRemove;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Token volume limits',
|
||||||
|
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: onAdd,
|
||||||
|
icon: const Icon(Icons.add_rounded),
|
||||||
|
label: const Text('Add'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: 0.8.h),
|
||||||
|
for (var i = 0; i < values.length; i++)
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(bottom: 1.h),
|
||||||
|
child: _TokenVolumeLimitRow(
|
||||||
|
key: ValueKey(values[i].id),
|
||||||
|
value: values[i],
|
||||||
|
onChanged: (entry) => onUpdate(i, entry),
|
||||||
|
onRemove: values.length == 1 ? null : () => onRemove(i),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TokenVolumeLimitRow extends HookWidget {
|
||||||
|
const _TokenVolumeLimitRow({
|
||||||
|
super.key,
|
||||||
|
required this.value,
|
||||||
|
required this.onChanged,
|
||||||
|
required this.onRemove,
|
||||||
|
});
|
||||||
|
|
||||||
|
final VolumeLimitEntry value;
|
||||||
|
final ValueChanged<VolumeLimitEntry> onChanged;
|
||||||
|
final VoidCallback? onRemove;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final amountController = useTextEditingController(text: value.amount);
|
||||||
|
final windowController = useTextEditingController(text: value.windowSeconds);
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: amountController,
|
||||||
|
onChanged: (next) => onChanged(value.copyWith(amount: next)),
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Max volume',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 1.w),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: windowController,
|
||||||
|
onChanged: (next) =>
|
||||||
|
onChanged(value.copyWith(windowSeconds: next)),
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Window (seconds)',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: 0.4.w),
|
||||||
|
IconButton(
|
||||||
|
onPressed: onRemove,
|
||||||
|
icon: const Icon(Icons.remove_circle_outline_rounded),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'token_transfer_grant.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint, type=warning
|
||||||
|
|
||||||
|
@ProviderFor(TokenGrantLimits)
|
||||||
|
final tokenGrantLimitsProvider = TokenGrantLimitsProvider._();
|
||||||
|
|
||||||
|
final class TokenGrantLimitsProvider
|
||||||
|
extends $NotifierProvider<TokenGrantLimits, List<VolumeLimitEntry>> {
|
||||||
|
TokenGrantLimitsProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'tokenGrantLimitsProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$tokenGrantLimitsHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
TokenGrantLimits create() => TokenGrantLimits();
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(List<VolumeLimitEntry> value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<List<VolumeLimitEntry>>(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$tokenGrantLimitsHash() => r'84db377f24940d215af82052e27863ab40c02b24';
|
||||||
|
|
||||||
|
abstract class _$TokenGrantLimits extends $Notifier<List<VolumeLimitEntry>> {
|
||||||
|
List<VolumeLimitEntry> build();
|
||||||
|
@$mustCallSuper
|
||||||
|
@override
|
||||||
|
void runBuild() {
|
||||||
|
final ref =
|
||||||
|
this.ref as $Ref<List<VolumeLimitEntry>, List<VolumeLimitEntry>>;
|
||||||
|
final element =
|
||||||
|
ref.element
|
||||||
|
as $ClassProviderElement<
|
||||||
|
AnyNotifier<List<VolumeLimitEntry>, List<VolumeLimitEntry>>,
|
||||||
|
List<VolumeLimitEntry>,
|
||||||
|
Object?,
|
||||||
|
Object?
|
||||||
|
>;
|
||||||
|
element.handleCreate(ref, build);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import 'package:arbiter/proto/evm.pb.dart';
|
||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'provider.freezed.dart';
|
||||||
|
part 'provider.g.dart';
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
abstract class GrantCreationState with _$GrantCreationState {
|
||||||
|
const factory GrantCreationState({
|
||||||
|
int? selectedClientId,
|
||||||
|
@Default(SpecificGrant_Grant.etherTransfer) SpecificGrant_Grant grantType,
|
||||||
|
}) = _GrantCreationState;
|
||||||
|
}
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
class GrantCreation extends _$GrantCreation {
|
||||||
|
@override
|
||||||
|
GrantCreationState build() => const GrantCreationState();
|
||||||
|
|
||||||
|
void setClientId(int? id) => state = state.copyWith(selectedClientId: id);
|
||||||
|
void setGrantType(SpecificGrant_Grant type) =>
|
||||||
|
state = state.copyWith(grantType: type);
|
||||||
|
}
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
// 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 'provider.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// FreezedGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// dart format off
|
||||||
|
T _$identity<T>(T value) => value;
|
||||||
|
/// @nodoc
|
||||||
|
mixin _$GrantCreationState {
|
||||||
|
|
||||||
|
int? get selectedClientId; SpecificGrant_Grant get grantType;
|
||||||
|
/// Create a copy of GrantCreationState
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
$GrantCreationStateCopyWith<GrantCreationState> get copyWith => _$GrantCreationStateCopyWithImpl<GrantCreationState>(this as GrantCreationState, _$identity);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is GrantCreationState&&(identical(other.selectedClientId, selectedClientId) || other.selectedClientId == selectedClientId)&&(identical(other.grantType, grantType) || other.grantType == grantType));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,selectedClientId,grantType);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'GrantCreationState(selectedClientId: $selectedClientId, grantType: $grantType)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class $GrantCreationStateCopyWith<$Res> {
|
||||||
|
factory $GrantCreationStateCopyWith(GrantCreationState value, $Res Function(GrantCreationState) _then) = _$GrantCreationStateCopyWithImpl;
|
||||||
|
@useResult
|
||||||
|
$Res call({
|
||||||
|
int? selectedClientId, SpecificGrant_Grant grantType
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class _$GrantCreationStateCopyWithImpl<$Res>
|
||||||
|
implements $GrantCreationStateCopyWith<$Res> {
|
||||||
|
_$GrantCreationStateCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final GrantCreationState _self;
|
||||||
|
final $Res Function(GrantCreationState) _then;
|
||||||
|
|
||||||
|
/// Create a copy of GrantCreationState
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@pragma('vm:prefer-inline') @override $Res call({Object? selectedClientId = freezed,Object? grantType = null,}) {
|
||||||
|
return _then(_self.copyWith(
|
||||||
|
selectedClientId: freezed == selectedClientId ? _self.selectedClientId : selectedClientId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int?,grantType: null == grantType ? _self.grantType : grantType // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SpecificGrant_Grant,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Adds pattern-matching-related methods to [GrantCreationState].
|
||||||
|
extension GrantCreationStatePatterns on GrantCreationState {
|
||||||
|
/// 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( _GrantCreationState value)? $default,{required TResult orElse(),}){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _GrantCreationState() 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( _GrantCreationState value) $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _GrantCreationState():
|
||||||
|
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( _GrantCreationState value)? $default,){
|
||||||
|
final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _GrantCreationState() 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( int? selectedClientId, SpecificGrant_Grant grantType)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _GrantCreationState() when $default != null:
|
||||||
|
return $default(_that.selectedClientId,_that.grantType);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( int? selectedClientId, SpecificGrant_Grant grantType) $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _GrantCreationState():
|
||||||
|
return $default(_that.selectedClientId,_that.grantType);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( int? selectedClientId, SpecificGrant_Grant grantType)? $default,) {final _that = this;
|
||||||
|
switch (_that) {
|
||||||
|
case _GrantCreationState() when $default != null:
|
||||||
|
return $default(_that.selectedClientId,_that.grantType);case _:
|
||||||
|
return null;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
|
||||||
|
|
||||||
|
class _GrantCreationState implements GrantCreationState {
|
||||||
|
const _GrantCreationState({this.selectedClientId, this.grantType = SpecificGrant_Grant.etherTransfer});
|
||||||
|
|
||||||
|
|
||||||
|
@override final int? selectedClientId;
|
||||||
|
@override@JsonKey() final SpecificGrant_Grant grantType;
|
||||||
|
|
||||||
|
/// Create a copy of GrantCreationState
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||||
|
@pragma('vm:prefer-inline')
|
||||||
|
_$GrantCreationStateCopyWith<_GrantCreationState> get copyWith => __$GrantCreationStateCopyWithImpl<_GrantCreationState>(this, _$identity);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _GrantCreationState&&(identical(other.selectedClientId, selectedClientId) || other.selectedClientId == selectedClientId)&&(identical(other.grantType, grantType) || other.grantType == grantType));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(runtimeType,selectedClientId,grantType);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'GrantCreationState(selectedClientId: $selectedClientId, grantType: $grantType)';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// @nodoc
|
||||||
|
abstract mixin class _$GrantCreationStateCopyWith<$Res> implements $GrantCreationStateCopyWith<$Res> {
|
||||||
|
factory _$GrantCreationStateCopyWith(_GrantCreationState value, $Res Function(_GrantCreationState) _then) = __$GrantCreationStateCopyWithImpl;
|
||||||
|
@override @useResult
|
||||||
|
$Res call({
|
||||||
|
int? selectedClientId, SpecificGrant_Grant grantType
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
/// @nodoc
|
||||||
|
class __$GrantCreationStateCopyWithImpl<$Res>
|
||||||
|
implements _$GrantCreationStateCopyWith<$Res> {
|
||||||
|
__$GrantCreationStateCopyWithImpl(this._self, this._then);
|
||||||
|
|
||||||
|
final _GrantCreationState _self;
|
||||||
|
final $Res Function(_GrantCreationState) _then;
|
||||||
|
|
||||||
|
/// Create a copy of GrantCreationState
|
||||||
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
|
@override @pragma('vm:prefer-inline') $Res call({Object? selectedClientId = freezed,Object? grantType = null,}) {
|
||||||
|
return _then(_GrantCreationState(
|
||||||
|
selectedClientId: freezed == selectedClientId ? _self.selectedClientId : selectedClientId // ignore: cast_nullable_to_non_nullable
|
||||||
|
as int?,grantType: null == grantType ? _self.grantType : grantType // ignore: cast_nullable_to_non_nullable
|
||||||
|
as SpecificGrant_Grant,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// dart format on
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'provider.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint, type=warning
|
||||||
|
|
||||||
|
@ProviderFor(GrantCreation)
|
||||||
|
final grantCreationProvider = GrantCreationProvider._();
|
||||||
|
|
||||||
|
final class GrantCreationProvider
|
||||||
|
extends $NotifierProvider<GrantCreation, GrantCreationState> {
|
||||||
|
GrantCreationProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'grantCreationProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$grantCreationHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
GrantCreation create() => GrantCreation();
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(GrantCreationState value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<GrantCreationState>(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$grantCreationHash() => r'3733d45da30990ef8ecbee946d2eae81bc7f5fc9';
|
||||||
|
|
||||||
|
abstract class _$GrantCreation extends $Notifier<GrantCreationState> {
|
||||||
|
GrantCreationState build();
|
||||||
|
@$mustCallSuper
|
||||||
|
@override
|
||||||
|
void runBuild() {
|
||||||
|
final ref = this.ref as $Ref<GrantCreationState, GrantCreationState>;
|
||||||
|
final element =
|
||||||
|
ref.element
|
||||||
|
as $ClassProviderElement<
|
||||||
|
AnyNotifier<GrantCreationState, GrantCreationState>,
|
||||||
|
GrantCreationState,
|
||||||
|
Object?,
|
||||||
|
Object?
|
||||||
|
>;
|
||||||
|
element.handleCreate(ref, build);
|
||||||
|
}
|
||||||
|
}
|
||||||
252
useragent/lib/screens/dashboard/evm/grants/create/screen.dart
Normal file
252
useragent/lib/screens/dashboard/evm/grants/create/screen.dart
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
// lib/screens/dashboard/evm/grants/create/screen.dart
|
||||||
|
import 'package:arbiter/proto/evm.pb.dart';
|
||||||
|
import 'package:arbiter/providers/evm/evm_grants.dart';
|
||||||
|
import 'package:arbiter/screens/dashboard/evm/grants/create/grants/ether_transfer_grant.dart';
|
||||||
|
import 'package:arbiter/screens/dashboard/evm/grants/create/grants/grant_form_handler.dart';
|
||||||
|
import 'package:arbiter/screens/dashboard/evm/grants/create/grants/token_transfer_grant.dart';
|
||||||
|
import 'package:arbiter/screens/dashboard/evm/grants/create/provider.dart';
|
||||||
|
import 'package:arbiter/screens/dashboard/evm/grants/create/shared_grant_fields.dart';
|
||||||
|
import 'package:arbiter/screens/dashboard/evm/grants/create/utils.dart';
|
||||||
|
import 'package:arbiter/theme/palette.dart';
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:fixnum/fixnum.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:hooks_riverpod/experimental/mutation.dart';
|
||||||
|
import 'package:sizer/sizer.dart';
|
||||||
|
|
||||||
|
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'),
|
||||||
|
};
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class CreateEvmGrantScreen extends HookConsumerWidget {
|
||||||
|
const CreateEvmGrantScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final formKey = useMemoized(() => GlobalKey<FormBuilderState>());
|
||||||
|
final createMutation = ref.watch(createEvmGrantMutation);
|
||||||
|
final state = ref.watch(grantCreationProvider);
|
||||||
|
final notifier = ref.read(grantCreationProvider.notifier);
|
||||||
|
final handler = _handlerFor(state.grantType);
|
||||||
|
|
||||||
|
Future<void> submit() async {
|
||||||
|
if (!(formKey.currentState?.saveAndValidate() ?? false)) return;
|
||||||
|
final formValues = formKey.currentState!.value;
|
||||||
|
|
||||||
|
final accessId = formValues['walletAccessId'] as int?;
|
||||||
|
if (accessId == null) {
|
||||||
|
_showSnackBar(context, 'Select a client and wallet access.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final specific = handler.buildSpecificGrant(formValues, ref);
|
||||||
|
final sharedSettings = SharedSettings(
|
||||||
|
walletAccessId: accessId,
|
||||||
|
chainId: Int64.parseInt(
|
||||||
|
(formValues['chainId'] as String? ?? '').trim(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final validFrom = formValues['validFrom'] as DateTime?;
|
||||||
|
final validUntil = formValues['validUntil'] as DateTime?;
|
||||||
|
if (validFrom != null) sharedSettings.validFrom = toTimestamp(validFrom);
|
||||||
|
if (validUntil != null) {
|
||||||
|
sharedSettings.validUntil = toTimestamp(validUntil);
|
||||||
|
}
|
||||||
|
final gasBytes =
|
||||||
|
optionalBigIntBytes(formValues['maxGasFeePerGas'] as String? ?? '');
|
||||||
|
if (gasBytes != null) sharedSettings.maxGasFeePerGas = gasBytes;
|
||||||
|
final priorityBytes = optionalBigIntBytes(
|
||||||
|
formValues['maxPriorityFeePerGas'] as String? ?? '',
|
||||||
|
);
|
||||||
|
if (priorityBytes != null) {
|
||||||
|
sharedSettings.maxPriorityFeePerGas = priorityBytes;
|
||||||
|
}
|
||||||
|
final rateLimit = buildRateLimit(
|
||||||
|
formValues['txCount'] as String? ?? '',
|
||||||
|
formValues['txWindow'] as String? ?? '',
|
||||||
|
);
|
||||||
|
if (rateLimit != null) sharedSettings.rateLimit = rateLimit;
|
||||||
|
|
||||||
|
await executeCreateEvmGrant(
|
||||||
|
ref,
|
||||||
|
sharedSettings: sharedSettings,
|
||||||
|
specific: specific,
|
||||||
|
);
|
||||||
|
if (!context.mounted) return;
|
||||||
|
context.router.pop();
|
||||||
|
} catch (error) {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
_showSnackBar(context, _formatError(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Create EVM Grant')),
|
||||||
|
body: SafeArea(
|
||||||
|
child: FormBuilder(
|
||||||
|
key: formKey,
|
||||||
|
child: ListView(
|
||||||
|
padding: EdgeInsets.fromLTRB(2.4.w, 2.h, 2.4.w, 3.2.h),
|
||||||
|
children: [
|
||||||
|
const _IntroCard(),
|
||||||
|
SizedBox(height: 1.8.h),
|
||||||
|
const _Section(
|
||||||
|
title: 'Shared grant options',
|
||||||
|
child: SharedGrantFields(),
|
||||||
|
),
|
||||||
|
SizedBox(height: 1.8.h),
|
||||||
|
_GrantTypeSelector(
|
||||||
|
value: state.grantType,
|
||||||
|
onChanged: notifier.setGrantType,
|
||||||
|
),
|
||||||
|
SizedBox(height: 1.8.h),
|
||||||
|
_Section(
|
||||||
|
title: 'Grant-specific options',
|
||||||
|
child: handler.buildForm(context, ref),
|
||||||
|
),
|
||||||
|
SizedBox(height: 2.2.h),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: FilledButton.icon(
|
||||||
|
onPressed:
|
||||||
|
createMutation is MutationPending ? null : submit,
|
||||||
|
icon: createMutation is MutationPending
|
||||||
|
? SizedBox(
|
||||||
|
width: 1.8.h,
|
||||||
|
height: 1.8.h,
|
||||||
|
child: const CircularProgressIndicator(
|
||||||
|
strokeWidth: 2.2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.check_rounded),
|
||||||
|
label: Text(
|
||||||
|
createMutation is MutationPending
|
||||||
|
? 'Creating...'
|
||||||
|
: 'Create grant',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Layout helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class _IntroCard extends StatelessWidget {
|
||||||
|
const _IntroCard();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.all(2.h),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
gradient: const LinearGradient(
|
||||||
|
colors: [Palette.introGradientStart, Palette.introGradientEnd],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
border: Border.all(color: Palette.cardBorder),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _Section extends StatelessWidget {
|
||||||
|
const _Section({required this.title, required this.child});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.all(2.h),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
color: Colors.white,
|
||||||
|
border: Border.all(color: Palette.cardBorder),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(height: 1.4.h),
|
||||||
|
child,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _GrantTypeSelector extends StatelessWidget {
|
||||||
|
const _GrantTypeSelector({required this.value, required this.onChanged});
|
||||||
|
|
||||||
|
final SpecificGrant_Grant value;
|
||||||
|
final ValueChanged<SpecificGrant_Grant> onChanged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SegmentedButton<SpecificGrant_Grant>(
|
||||||
|
segments: const [
|
||||||
|
ButtonSegment(
|
||||||
|
value: SpecificGrant_Grant.etherTransfer,
|
||||||
|
label: Text('Ether'),
|
||||||
|
icon: Icon(Icons.bolt_rounded),
|
||||||
|
),
|
||||||
|
ButtonSegment(
|
||||||
|
value: SpecificGrant_Grant.tokenTransfer,
|
||||||
|
label: Text('Token'),
|
||||||
|
icon: Icon(Icons.token_rounded),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
selected: {value},
|
||||||
|
onSelectionChanged: (selection) => onChanged(selection.first),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Utilities
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void _showSnackBar(BuildContext context, String message) {
|
||||||
|
if (!context.mounted) return;
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text(message), behavior: SnackBarBehavior.floating),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatError(Object error) {
|
||||||
|
final text = error.toString();
|
||||||
|
return text.startsWith('Exception: ')
|
||||||
|
? text.substring('Exception: '.length)
|
||||||
|
: text;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// lib/screens/dashboard/evm/grants/create/shared_grant_fields.dart
|
||||||
|
import 'package:arbiter/screens/dashboard/evm/grants/create/fields/chain_id_field.dart';
|
||||||
|
import 'package:arbiter/screens/dashboard/evm/grants/create/fields/client_picker_field.dart';
|
||||||
|
import 'package:arbiter/screens/dashboard/evm/grants/create/fields/gas_fee_options_field.dart';
|
||||||
|
import 'package:arbiter/screens/dashboard/evm/grants/create/fields/transaction_rate_limit_field.dart';
|
||||||
|
import 'package:arbiter/screens/dashboard/evm/grants/create/fields/validity_window_field.dart';
|
||||||
|
import 'package:arbiter/screens/dashboard/evm/grants/create/fields/wallet_access_picker_field.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:sizer/sizer.dart';
|
||||||
|
|
||||||
|
/// All shared grant fields in a single vertical layout.
|
||||||
|
///
|
||||||
|
/// Every [FormBuilderField] descendant auto-registers with the nearest
|
||||||
|
/// [FormBuilder] ancestor via [BuildContext] — no controllers passed.
|
||||||
|
class SharedGrantFields extends StatelessWidget {
|
||||||
|
const SharedGrantFields({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
const ClientPickerField(),
|
||||||
|
SizedBox(height: 1.6.h),
|
||||||
|
const WalletAccessPickerField(),
|
||||||
|
SizedBox(height: 1.6.h),
|
||||||
|
const ChainIdField(),
|
||||||
|
SizedBox(height: 1.6.h),
|
||||||
|
const ValidityWindowField(),
|
||||||
|
SizedBox(height: 1.6.h),
|
||||||
|
const GasFeeOptionsField(),
|
||||||
|
SizedBox(height: 1.6.h),
|
||||||
|
const TransactionRateLimitField(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
73
useragent/lib/screens/dashboard/evm/grants/create/utils.dart
Normal file
73
useragent/lib/screens/dashboard/evm/grants/create/utils.dart
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import 'package:arbiter/proto/evm.pb.dart';
|
||||||
|
import 'package:fixnum/fixnum.dart';
|
||||||
|
import 'package:protobuf/well_known_types/google/protobuf/timestamp.pb.dart';
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
return TransactionRateLimit(
|
||||||
|
count: int.parse(countText.trim()),
|
||||||
|
windowSecs: Int64.parseInt(windowText.trim()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
VolumeRateLimit? buildVolumeLimit(String amountText, String windowText) {
|
||||||
|
if (amountText.trim().isEmpty || windowText.trim().isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return VolumeRateLimit(
|
||||||
|
maxVolume: parseBigIntBytes(amountText),
|
||||||
|
windowSecs: Int64.parseInt(windowText.trim()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int>? optionalBigIntBytes(String value) {
|
||||||
|
if (value.trim().isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parseBigIntBytes(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int> parseBigIntBytes(String value) {
|
||||||
|
final number = BigInt.parse(value.trim());
|
||||||
|
if (number < BigInt.zero) {
|
||||||
|
throw Exception('Numeric values must be positive.');
|
||||||
|
}
|
||||||
|
if (number == BigInt.zero) {
|
||||||
|
return [0];
|
||||||
|
}
|
||||||
|
|
||||||
|
var remaining = number;
|
||||||
|
final bytes = <int>[];
|
||||||
|
while (remaining > BigInt.zero) {
|
||||||
|
bytes.insert(0, (remaining & BigInt.from(0xff)).toInt());
|
||||||
|
remaining >>= 8;
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int> parseHexAddress(String value) {
|
||||||
|
final normalized = value.trim().replaceFirst(RegExp(r'^0x'), '');
|
||||||
|
if (normalized.length != 40) {
|
||||||
|
throw Exception('Expected a 20-byte hex address.');
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
for (var i = 0; i < normalized.length; i += 2)
|
||||||
|
int.parse(normalized.substring(i, i + 2), radix: 16),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
String shortAddress(List<int> bytes) {
|
||||||
|
final hex = bytes
|
||||||
|
.map((byte) => byte.toRadixString(16).padLeft(2, '0'))
|
||||||
|
.join();
|
||||||
|
return '0x${hex.substring(0, 6)}...${hex.substring(hex.length - 4)}';
|
||||||
|
}
|
||||||
@@ -1,902 +0,0 @@
|
|||||||
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()
|
|
||||||
class CreateEvmGrantScreen extends HookConsumerWidget {
|
|
||||||
const CreateEvmGrantScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final createMutation = ref.watch(createEvmGrantMutation);
|
|
||||||
|
|
||||||
final selectedClientId = useState<int?>(null);
|
|
||||||
final selectedWalletAccessId = useState<int?>(null);
|
|
||||||
final chainIdController = useTextEditingController(text: '1');
|
|
||||||
final gasFeeController = useTextEditingController();
|
|
||||||
final priorityFeeController = useTextEditingController();
|
|
||||||
final txCountController = useTextEditingController();
|
|
||||||
final txWindowController = useTextEditingController();
|
|
||||||
final recipientsController = useTextEditingController();
|
|
||||||
final etherVolumeController = useTextEditingController();
|
|
||||||
final etherVolumeWindowController = useTextEditingController();
|
|
||||||
final tokenContractController = useTextEditingController();
|
|
||||||
final tokenTargetController = useTextEditingController();
|
|
||||||
final validFrom = useState<DateTime?>(null);
|
|
||||||
final validUntil = useState<DateTime?>(null);
|
|
||||||
final grantType = useState<SpecificGrant_Grant>(
|
|
||||||
SpecificGrant_Grant.etherTransfer,
|
|
||||||
);
|
|
||||||
final tokenVolumeLimits = useState<List<_VolumeLimitValue>>([
|
|
||||||
const _VolumeLimitValue(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
Future<void> submit() async {
|
|
||||||
final accessId = selectedWalletAccessId.value;
|
|
||||||
if (accessId == null) {
|
|
||||||
_showCreateMessage(context, 'Select a client and wallet access.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
final chainId = Int64.parseInt(chainIdController.text.trim());
|
|
||||||
final rateLimit = _buildRateLimit(
|
|
||||||
txCountController.text,
|
|
||||||
txWindowController.text,
|
|
||||||
);
|
|
||||||
final specific = switch (grantType.value) {
|
|
||||||
SpecificGrant_Grant.etherTransfer => SpecificGrant(
|
|
||||||
etherTransfer: EtherTransferSettings(
|
|
||||||
targets: _parseAddresses(recipientsController.text),
|
|
||||||
limit: _buildVolumeLimit(
|
|
||||||
etherVolumeController.text,
|
|
||||||
etherVolumeWindowController.text,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SpecificGrant_Grant.tokenTransfer => SpecificGrant(
|
|
||||||
tokenTransfer: TokenTransferSettings(
|
|
||||||
tokenContract: _parseHexAddress(tokenContractController.text),
|
|
||||||
target: tokenTargetController.text.trim().isEmpty
|
|
||||||
? null
|
|
||||||
: _parseHexAddress(tokenTargetController.text),
|
|
||||||
volumeLimits: tokenVolumeLimits.value
|
|
||||||
.where((item) => item.amount.trim().isNotEmpty)
|
|
||||||
.map(
|
|
||||||
(item) => VolumeRateLimit(
|
|
||||||
maxVolume: _parseBigIntBytes(item.amount),
|
|
||||||
windowSecs: Int64.parseInt(item.windowSeconds),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
_ => 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,
|
|
||||||
sharedSettings: sharedSettings,
|
|
||||||
specific: specific,
|
|
||||||
);
|
|
||||||
if (!context.mounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
context.router.pop();
|
|
||||||
} catch (error) {
|
|
||||||
if (!context.mounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_showCreateMessage(context, _formatCreateError(error));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(title: const Text('Create EVM Grant')),
|
|
||||||
body: SafeArea(
|
|
||||||
child: ListView(
|
|
||||||
padding: EdgeInsets.fromLTRB(2.4.w, 2.h, 2.4.w, 3.2.h),
|
|
||||||
children: [
|
|
||||||
const _CreateIntroCard(),
|
|
||||||
SizedBox(height: 1.8.h),
|
|
||||||
_CreateSection(
|
|
||||||
title: 'Shared grant options',
|
|
||||||
children: [
|
|
||||||
_ClientPickerField(
|
|
||||||
selectedClientId: selectedClientId.value,
|
|
||||||
onChanged: (clientId) {
|
|
||||||
selectedClientId.value = clientId;
|
|
||||||
selectedWalletAccessId.value = null;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
_WalletAccessPickerField(
|
|
||||||
selectedClientId: selectedClientId.value,
|
|
||||||
selectedAccessId: selectedWalletAccessId.value,
|
|
||||||
onChanged: (accessId) =>
|
|
||||||
selectedWalletAccessId.value = accessId,
|
|
||||||
),
|
|
||||||
_NumberInputField(
|
|
||||||
controller: chainIdController,
|
|
||||||
label: 'Chain ID',
|
|
||||||
hint: '1',
|
|
||||||
),
|
|
||||||
_ValidityWindowField(
|
|
||||||
validFrom: validFrom.value,
|
|
||||||
validUntil: validUntil.value,
|
|
||||||
onValidFromChanged: (value) => validFrom.value = value,
|
|
||||||
onValidUntilChanged: (value) => validUntil.value = value,
|
|
||||||
),
|
|
||||||
_GasFeeOptionsField(
|
|
||||||
gasFeeController: gasFeeController,
|
|
||||||
priorityFeeController: priorityFeeController,
|
|
||||||
),
|
|
||||||
_TransactionRateLimitField(
|
|
||||||
txCountController: txCountController,
|
|
||||||
txWindowController: txWindowController,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: 1.8.h),
|
|
||||||
_GrantTypeSelector(
|
|
||||||
value: grantType.value,
|
|
||||||
onChanged: (value) => grantType.value = value,
|
|
||||||
),
|
|
||||||
SizedBox(height: 1.8.h),
|
|
||||||
_CreateSection(
|
|
||||||
title: 'Grant-specific options',
|
|
||||||
children: [
|
|
||||||
if (grantType.value == SpecificGrant_Grant.etherTransfer) ...[
|
|
||||||
_EtherTargetsField(controller: recipientsController),
|
|
||||||
_VolumeLimitField(
|
|
||||||
amountController: etherVolumeController,
|
|
||||||
windowController: etherVolumeWindowController,
|
|
||||||
title: 'Ether volume limit',
|
|
||||||
),
|
|
||||||
] else ...[
|
|
||||||
_TokenContractField(controller: tokenContractController),
|
|
||||||
_TokenRecipientField(controller: tokenTargetController),
|
|
||||||
_TokenVolumeLimitsField(
|
|
||||||
values: tokenVolumeLimits.value,
|
|
||||||
onChanged: (values) => tokenVolumeLimits.value = values,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: 2.2.h),
|
|
||||||
Align(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: FilledButton.icon(
|
|
||||||
onPressed: createMutation is MutationPending ? null : submit,
|
|
||||||
icon: createMutation is MutationPending
|
|
||||||
? SizedBox(
|
|
||||||
width: 1.8.h,
|
|
||||||
height: 1.8.h,
|
|
||||||
child: const CircularProgressIndicator(strokeWidth: 2.2),
|
|
||||||
)
|
|
||||||
: const Icon(Icons.check_rounded),
|
|
||||||
label: Text(
|
|
||||||
createMutation is MutationPending
|
|
||||||
? 'Creating...'
|
|
||||||
: 'Create grant',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CreateIntroCard extends StatelessWidget {
|
|
||||||
const _CreateIntroCard();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
padding: EdgeInsets.all(2.h),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(24),
|
|
||||||
gradient: const LinearGradient(
|
|
||||||
colors: [Color(0xFFF7F9FC), Color(0xFFFDF5EA)],
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
),
|
|
||||||
border: Border.all(color: const Color(0x1A17324A)),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
'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),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CreateSection extends StatelessWidget {
|
|
||||||
const _CreateSection({required this.title, required this.children});
|
|
||||||
|
|
||||||
final String title;
|
|
||||||
final List<Widget> children;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
padding: EdgeInsets.all(2.h),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(24),
|
|
||||||
color: Colors.white,
|
|
||||||
border: Border.all(color: const Color(0x1A17324A)),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
title,
|
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
||||||
fontWeight: FontWeight.w800,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 1.4.h),
|
|
||||||
...children.map(
|
|
||||||
(child) => Padding(
|
|
||||||
padding: EdgeInsets.only(bottom: 1.6.h),
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ClientPickerField extends ConsumerWidget {
|
|
||||||
const _ClientPickerField({
|
|
||||||
required this.selectedClientId,
|
|
||||||
required this.onChanged,
|
|
||||||
});
|
|
||||||
|
|
||||||
final int? selectedClientId;
|
|
||||||
final ValueChanged<int?> onChanged;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final clients =
|
|
||||||
ref.watch(sdkClientsProvider).asData?.value ?? const <SdkClientEntry>[];
|
|
||||||
|
|
||||||
return DropdownButtonFormField<int>(
|
|
||||||
value: clients.any((c) => c.id == selectedClientId)
|
|
||||||
? selectedClientId
|
|
||||||
: null,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Client',
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
items: [
|
|
||||||
for (final c in clients)
|
|
||||||
DropdownMenuItem(
|
|
||||||
value: c.id,
|
|
||||||
child: Text(
|
|
||||||
c.info.name.isEmpty ? 'Client #${c.id}' : c.info.name,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _NumberInputField extends StatelessWidget {
|
|
||||||
const _NumberInputField({
|
|
||||||
required this.controller,
|
|
||||||
required this.label,
|
|
||||||
required this.hint,
|
|
||||||
this.helper,
|
|
||||||
});
|
|
||||||
|
|
||||||
final TextEditingController controller;
|
|
||||||
final String label;
|
|
||||||
final String hint;
|
|
||||||
final String? helper;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return TextField(
|
|
||||||
controller: controller,
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: label,
|
|
||||||
hintText: hint,
|
|
||||||
helperText: helper,
|
|
||||||
border: const OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ValidityWindowField extends StatelessWidget {
|
|
||||||
const _ValidityWindowField({
|
|
||||||
required this.validFrom,
|
|
||||||
required this.validUntil,
|
|
||||||
required this.onValidFromChanged,
|
|
||||||
required this.onValidUntilChanged,
|
|
||||||
});
|
|
||||||
|
|
||||||
final DateTime? validFrom;
|
|
||||||
final DateTime? validUntil;
|
|
||||||
final ValueChanged<DateTime?> onValidFromChanged;
|
|
||||||
final ValueChanged<DateTime?> onValidUntilChanged;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: _DateButtonField(
|
|
||||||
label: 'Valid from',
|
|
||||||
value: validFrom,
|
|
||||||
onChanged: onValidFromChanged,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 1.w),
|
|
||||||
Expanded(
|
|
||||||
child: _DateButtonField(
|
|
||||||
label: 'Valid until',
|
|
||||||
value: validUntil,
|
|
||||||
onChanged: onValidUntilChanged,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DateButtonField extends StatelessWidget {
|
|
||||||
const _DateButtonField({
|
|
||||||
required this.label,
|
|
||||||
required this.value,
|
|
||||||
required this.onChanged,
|
|
||||||
});
|
|
||||||
|
|
||||||
final String label;
|
|
||||||
final DateTime? value;
|
|
||||||
final ValueChanged<DateTime?> onChanged;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return OutlinedButton(
|
|
||||||
onPressed: () async {
|
|
||||||
final now = DateTime.now();
|
|
||||||
final date = await showDatePicker(
|
|
||||||
context: context,
|
|
||||||
firstDate: DateTime(now.year - 5),
|
|
||||||
lastDate: DateTime(now.year + 10),
|
|
||||||
initialDate: value ?? now,
|
|
||||||
);
|
|
||||||
if (date == null || !context.mounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final time = await showTimePicker(
|
|
||||||
context: context,
|
|
||||||
initialTime: TimeOfDay.fromDateTime(value ?? now),
|
|
||||||
);
|
|
||||||
if (time == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
onChanged(
|
|
||||||
DateTime(date.year, date.month, date.day, time.hour, time.minute),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onLongPress: value == null ? null : () => onChanged(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'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _GasFeeOptionsField extends StatelessWidget {
|
|
||||||
const _GasFeeOptionsField({
|
|
||||||
required this.gasFeeController,
|
|
||||||
required this.priorityFeeController,
|
|
||||||
});
|
|
||||||
|
|
||||||
final TextEditingController gasFeeController;
|
|
||||||
final TextEditingController priorityFeeController;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: _NumberInputField(
|
|
||||||
controller: gasFeeController,
|
|
||||||
label: 'Max gas fee / gas',
|
|
||||||
hint: '1000000000',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 1.w),
|
|
||||||
Expanded(
|
|
||||||
child: _NumberInputField(
|
|
||||||
controller: priorityFeeController,
|
|
||||||
label: 'Max priority fee / gas',
|
|
||||||
hint: '100000000',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TransactionRateLimitField extends StatelessWidget {
|
|
||||||
const _TransactionRateLimitField({
|
|
||||||
required this.txCountController,
|
|
||||||
required this.txWindowController,
|
|
||||||
});
|
|
||||||
|
|
||||||
final TextEditingController txCountController;
|
|
||||||
final TextEditingController txWindowController;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: _NumberInputField(
|
|
||||||
controller: txCountController,
|
|
||||||
label: 'Tx count limit',
|
|
||||||
hint: '10',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 1.w),
|
|
||||||
Expanded(
|
|
||||||
child: _NumberInputField(
|
|
||||||
controller: txWindowController,
|
|
||||||
label: 'Window (seconds)',
|
|
||||||
hint: '3600',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _GrantTypeSelector extends StatelessWidget {
|
|
||||||
const _GrantTypeSelector({required this.value, required this.onChanged});
|
|
||||||
|
|
||||||
final SpecificGrant_Grant value;
|
|
||||||
final ValueChanged<SpecificGrant_Grant> onChanged;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return SegmentedButton<SpecificGrant_Grant>(
|
|
||||||
segments: const [
|
|
||||||
ButtonSegment(
|
|
||||||
value: SpecificGrant_Grant.etherTransfer,
|
|
||||||
label: Text('Ether'),
|
|
||||||
icon: Icon(Icons.bolt_rounded),
|
|
||||||
),
|
|
||||||
ButtonSegment(
|
|
||||||
value: SpecificGrant_Grant.tokenTransfer,
|
|
||||||
label: Text('Token'),
|
|
||||||
icon: Icon(Icons.token_rounded),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
selected: {value},
|
|
||||||
onSelectionChanged: (selection) => onChanged(selection.first),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _EtherTargetsField extends StatelessWidget {
|
|
||||||
const _EtherTargetsField({required this.controller});
|
|
||||||
|
|
||||||
final TextEditingController controller;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return TextField(
|
|
||||||
controller: controller,
|
|
||||||
minLines: 3,
|
|
||||||
maxLines: 6,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Ether recipients',
|
|
||||||
hintText: 'One 0x address per line. Leave empty for unrestricted targets.',
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _VolumeLimitField extends StatelessWidget {
|
|
||||||
const _VolumeLimitField({
|
|
||||||
required this.amountController,
|
|
||||||
required this.windowController,
|
|
||||||
required this.title,
|
|
||||||
});
|
|
||||||
|
|
||||||
final TextEditingController amountController;
|
|
||||||
final TextEditingController windowController;
|
|
||||||
final String title;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
title,
|
|
||||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
|
||||||
fontWeight: FontWeight.w800,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(height: 0.8.h),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: _NumberInputField(
|
|
||||||
controller: amountController,
|
|
||||||
label: 'Max volume',
|
|
||||||
hint: '1000000000000000000',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 1.w),
|
|
||||||
Expanded(
|
|
||||||
child: _NumberInputField(
|
|
||||||
controller: windowController,
|
|
||||||
label: 'Window (seconds)',
|
|
||||||
hint: '86400',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TokenContractField extends StatelessWidget {
|
|
||||||
const _TokenContractField({required this.controller});
|
|
||||||
|
|
||||||
final TextEditingController controller;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return TextField(
|
|
||||||
controller: controller,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Token contract',
|
|
||||||
hintText: '0x...',
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TokenRecipientField extends StatelessWidget {
|
|
||||||
const _TokenRecipientField({required this.controller});
|
|
||||||
|
|
||||||
final TextEditingController controller;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return TextField(
|
|
||||||
controller: controller,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Token recipient',
|
|
||||||
hintText: '0x... or leave empty for any recipient',
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TokenVolumeLimitsField extends StatelessWidget {
|
|
||||||
const _TokenVolumeLimitsField({
|
|
||||||
required this.values,
|
|
||||||
required this.onChanged,
|
|
||||||
});
|
|
||||||
|
|
||||||
final List<_VolumeLimitValue> values;
|
|
||||||
final ValueChanged<List<_VolumeLimitValue>> onChanged;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
'Token volume limits',
|
|
||||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
|
||||||
fontWeight: FontWeight.w800,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextButton.icon(
|
|
||||||
onPressed: () =>
|
|
||||||
onChanged([...values, const _VolumeLimitValue()]),
|
|
||||||
icon: const Icon(Icons.add_rounded),
|
|
||||||
label: const Text('Add'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: 0.8.h),
|
|
||||||
for (var i = 0; i < values.length; i++)
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.only(bottom: 1.h),
|
|
||||||
child: _TokenVolumeLimitRow(
|
|
||||||
value: values[i],
|
|
||||||
onChanged: (next) {
|
|
||||||
final updated = [...values];
|
|
||||||
updated[i] = next;
|
|
||||||
onChanged(updated);
|
|
||||||
},
|
|
||||||
onRemove: values.length == 1
|
|
||||||
? null
|
|
||||||
: () {
|
|
||||||
final updated = [...values]..removeAt(i);
|
|
||||||
onChanged(updated);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TokenVolumeLimitRow extends StatelessWidget {
|
|
||||||
const _TokenVolumeLimitRow({
|
|
||||||
required this.value,
|
|
||||||
required this.onChanged,
|
|
||||||
required this.onRemove,
|
|
||||||
});
|
|
||||||
|
|
||||||
final _VolumeLimitValue value;
|
|
||||||
final ValueChanged<_VolumeLimitValue> onChanged;
|
|
||||||
final VoidCallback? onRemove;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final amountController = TextEditingController(text: value.amount);
|
|
||||||
final windowController = TextEditingController(text: value.windowSeconds);
|
|
||||||
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
controller: amountController,
|
|
||||||
onChanged: (next) =>
|
|
||||||
onChanged(value.copyWith(amount: next)),
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Max volume',
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 1.w),
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
controller: windowController,
|
|
||||||
onChanged: (next) =>
|
|
||||||
onChanged(value.copyWith(windowSeconds: next)),
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
labelText: 'Window (seconds)',
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
SizedBox(width: 0.4.w),
|
|
||||||
IconButton(
|
|
||||||
onPressed: onRemove,
|
|
||||||
icon: const Icon(Icons.remove_circle_outline_rounded),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _VolumeLimitValue {
|
|
||||||
const _VolumeLimitValue({this.amount = '', this.windowSeconds = ''});
|
|
||||||
|
|
||||||
final String amount;
|
|
||||||
final String windowSeconds;
|
|
||||||
|
|
||||||
_VolumeLimitValue copyWith({String? amount, String? windowSeconds}) {
|
|
||||||
return _VolumeLimitValue(
|
|
||||||
amount: amount ?? this.amount,
|
|
||||||
windowSeconds: windowSeconds ?? this.windowSeconds,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
return TransactionRateLimit(
|
|
||||||
count: int.parse(countText.trim()),
|
|
||||||
windowSecs: Int64.parseInt(windowText.trim()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
VolumeRateLimit? _buildVolumeLimit(String amountText, String windowText) {
|
|
||||||
if (amountText.trim().isEmpty || windowText.trim().isEmpty) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return VolumeRateLimit(
|
|
||||||
maxVolume: _parseBigIntBytes(amountText),
|
|
||||||
windowSecs: Int64.parseInt(windowText.trim()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<int>? _optionalBigIntBytes(String value) {
|
|
||||||
if (value.trim().isEmpty) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return _parseBigIntBytes(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
List<int> _parseBigIntBytes(String value) {
|
|
||||||
final number = BigInt.parse(value.trim());
|
|
||||||
if (number < BigInt.zero) {
|
|
||||||
throw Exception('Numeric values must be positive.');
|
|
||||||
}
|
|
||||||
if (number == BigInt.zero) {
|
|
||||||
return [0];
|
|
||||||
}
|
|
||||||
|
|
||||||
var remaining = number;
|
|
||||||
final bytes = <int>[];
|
|
||||||
while (remaining > BigInt.zero) {
|
|
||||||
bytes.insert(0, (remaining & BigInt.from(0xff)).toInt());
|
|
||||||
remaining >>= 8;
|
|
||||||
}
|
|
||||||
return bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<List<int>> _parseAddresses(String input) {
|
|
||||||
final parts = input
|
|
||||||
.split(RegExp(r'[\n,]'))
|
|
||||||
.map((part) => part.trim())
|
|
||||||
.where((part) => part.isNotEmpty);
|
|
||||||
return parts.map(_parseHexAddress).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
List<int> _parseHexAddress(String value) {
|
|
||||||
final normalized = value.trim().replaceFirst(RegExp(r'^0x'), '');
|
|
||||||
if (normalized.length != 40) {
|
|
||||||
throw Exception('Expected a 20-byte hex address.');
|
|
||||||
}
|
|
||||||
return [
|
|
||||||
for (var i = 0; i < normalized.length; i += 2)
|
|
||||||
int.parse(normalized.substring(i, i + 2), radix: 16),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
String _shortAddress(List<int> bytes) {
|
|
||||||
final hex = bytes
|
|
||||||
.map((byte) => byte.toRadixString(16).padLeft(2, '0'))
|
|
||||||
.join();
|
|
||||||
return '0x${hex.substring(0, 6)}...${hex.substring(hex.length - 4)}';
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showCreateMessage(BuildContext context, String message) {
|
|
||||||
if (!context.mounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(content: Text(message), behavior: SnackBarBehavior.floating),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _formatCreateError(Object error) {
|
|
||||||
final text = error.toString();
|
|
||||||
if (text.startsWith('Exception: ')) {
|
|
||||||
return text.substring('Exception: '.length);
|
|
||||||
}
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
@@ -6,4 +6,7 @@ class Palette {
|
|||||||
static const cream = Color(0xFFFFFAF4);
|
static const cream = Color(0xFFFFFAF4);
|
||||||
static const line = Color(0x1A15263C);
|
static const line = Color(0x1A15263C);
|
||||||
static const token = Color(0xFF5C6BC0);
|
static const token = Color(0xFF5C6BC0);
|
||||||
|
static const cardBorder = Color(0x1A17324A);
|
||||||
|
static const introGradientStart = Color(0xFFF7F9FC);
|
||||||
|
static const introGradientEnd = Color(0xFFFDF5EA);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,10 +45,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: async
|
name: async
|
||||||
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.13.0"
|
version: "2.13.1"
|
||||||
auto_route:
|
auto_route:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -69,10 +69,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: biometric_signature
|
name: biometric_signature
|
||||||
sha256: "0d22580388e5cc037f6f829b29ecda74e343fd61d26c55e33cbcf48a48e5c1dc"
|
sha256: "86a37a8b514eb3b56980ab6cc9df48ffc3c551147bdf8e3bf2afb2265a7f89e8"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.2.0"
|
version: "11.0.1"
|
||||||
bloc:
|
bloc:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -93,10 +93,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: build
|
name: build
|
||||||
sha256: "275bf6bb2a00a9852c28d4e0b410da1d833a734d57d39d44f94bfc895a484ec3"
|
sha256: aadd943f4f8cc946882c954c187e6115a84c98c81ad1d9c6cbf0895a8c85da9c
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.4"
|
version: "4.0.5"
|
||||||
build_config:
|
build_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -117,10 +117,10 @@ packages:
|
|||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: build_runner
|
name: build_runner
|
||||||
sha256: "7981eb922842c77033026eb4341d5af651562008cdb116bdfa31fc46516b6462"
|
sha256: "521daf8d189deb79ba474e43a696b41c49fb3987818dbacf3308f1e03673a75e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.12.2"
|
version: "2.13.1"
|
||||||
built_collection:
|
built_collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -133,10 +133,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: built_value
|
name: built_value
|
||||||
sha256: "6ae8a6435a8c6520c7077b107e77f1fb4ba7009633259a4d49a8afd8e7efc5e9"
|
sha256: "0730c18c770d05636a8f945c32a4d7d81cb6e0f0148c8db4ad12e7748f7e49af"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.12.4"
|
version: "8.12.5"
|
||||||
characters:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -245,10 +245,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: cupertino_icons
|
name: cupertino_icons
|
||||||
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
|
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.8"
|
version: "1.0.9"
|
||||||
dart_style:
|
dart_style:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -311,6 +311,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "9.1.1"
|
version: "9.1.1"
|
||||||
|
flutter_form_builder:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_form_builder
|
||||||
|
sha256: "1233251b4bc1d5deb245745d2a89dcebf4cdd382e1ec3f21f1c6703b700e574f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "10.3.0+2"
|
||||||
flutter_hooks:
|
flutter_hooks:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -653,10 +661,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: mockito
|
name: mockito
|
||||||
sha256: a45d1aa065b796922db7b9e7e7e45f921aed17adf3a8318a1f47097e7e695566
|
sha256: eff30d002f0c8bf073b6f929df4483b543133fcafce056870163587b03f1d422
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.6.3"
|
version: "5.6.4"
|
||||||
mtcore:
|
mtcore:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -669,10 +677,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: native_toolchain_c
|
name: native_toolchain_c
|
||||||
sha256: "92b2ca62c8bd2b8d2f267cdfccf9bfbdb7322f778f8f91b3ce5b5cda23a3899f"
|
sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.17.5"
|
version: "0.17.6"
|
||||||
nested:
|
nested:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -725,10 +733,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_android
|
name: path_provider_android
|
||||||
sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e
|
sha256: "149441ca6e4f38193b2e004c0ca6376a3d11f51fa5a77552d8bd4d2b0c0912ba"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.22"
|
version: "2.2.23"
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -938,10 +946,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: source_gen
|
name: source_gen
|
||||||
sha256: adc962c96fffb2de1728ef396a995aaedcafbe635abdca13d2a987ce17e57751
|
sha256: "732792cfd197d2161a65bb029606a46e0a18ff30ef9e141a7a82172b05ea8ecd"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2.1"
|
version: "4.2.2"
|
||||||
source_helper:
|
source_helper:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1018,26 +1026,26 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: talker
|
name: talker
|
||||||
sha256: "46a60c6014a870a71ab8e3fa22f9580a8d36faf9b39431dcf124940601c0c266"
|
sha256: c364edc0fbd6c648e1a78e6edd89cccd64df2150ca96d899ecd486b76c185042
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.1.15"
|
version: "5.1.16"
|
||||||
talker_flutter:
|
talker_flutter:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: talker_flutter
|
name: talker_flutter
|
||||||
sha256: "7e1819c505cdcbf2cf6497410b8fa3b33d170e6f137716bd278940c0e509f414"
|
sha256: "54cbbf852101721664faf4a05639fd2fdefdc37178327990abea00390690d4bc"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.1.15"
|
version: "5.1.16"
|
||||||
talker_logger:
|
talker_logger:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: talker_logger
|
name: talker_logger
|
||||||
sha256: "70112d016d7b978ccb7ef0b0f4941e0f0b0de88d80589db43143cea1d744eae0"
|
sha256: cea1b8283a28c2118a0b197057fc5beb5b0672c75e40a48725e5e452c0278ff3
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.1.15"
|
version: "5.1.16"
|
||||||
term_glyph:
|
term_glyph:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ dependencies:
|
|||||||
riverpod: ^3.1.0
|
riverpod: ^3.1.0
|
||||||
hooks_riverpod: ^3.1.0
|
hooks_riverpod: ^3.1.0
|
||||||
sizer: ^3.1.3
|
sizer: ^3.1.3
|
||||||
biometric_signature: ^10.2.0
|
biometric_signature: ^11.0.1
|
||||||
mtcore:
|
mtcore:
|
||||||
hosted: https://git.markettakers.org/api/packages/MarketTakers/pub/
|
hosted: https://git.markettakers.org/api/packages/MarketTakers/pub/
|
||||||
version: ^1.0.6
|
version: ^1.0.6
|
||||||
@@ -34,6 +34,7 @@ dependencies:
|
|||||||
freezed_annotation: ^3.1.0
|
freezed_annotation: ^3.1.0
|
||||||
json_annotation: ^4.9.0
|
json_annotation: ^4.9.0
|
||||||
timeago: ^3.7.1
|
timeago: ^3.7.1
|
||||||
|
flutter_form_builder: ^10.3.0+2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user