From 5f4a1adaa903e575fe7cb7966c2f9efa9b95b59e Mon Sep 17 00:00:00 2001 From: Sean Cheatham Date: Wed, 3 Jul 2024 15:06:14 -0400 Subject: [PATCH 1/8] Work-in-progress stream wallet --- .../bridge/providers/rpc_channel.dart | 21 ++++ .../bridge/providers/wallet_state.dart | 99 +++++++++++++++++++ pubspec.yaml | 11 ++- 3 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 lib/features/bridge/providers/rpc_channel.dart create mode 100644 lib/features/bridge/providers/wallet_state.dart diff --git a/lib/features/bridge/providers/rpc_channel.dart b/lib/features/bridge/providers/rpc_channel.dart new file mode 100644 index 0000000..e478909 --- /dev/null +++ b/lib/features/bridge/providers/rpc_channel.dart @@ -0,0 +1,21 @@ +import 'package:brambldart/brambldart.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'rpc_channel.g.dart'; + +@riverpod +class RpcChannel extends _$RpcChannel { + @override + RpcChannelState build() { + // TODO + return RpcChannelState(nodeRpcChannel: null!, genusRpcChannel: null!); + } +} + +class RpcChannelState { + final Channel nodeRpcChannel; + final Channel genusRpcChannel; + + RpcChannelState( + {required this.nodeRpcChannel, required this.genusRpcChannel}); +} diff --git a/lib/features/bridge/providers/wallet_state.dart b/lib/features/bridge/providers/wallet_state.dart new file mode 100644 index 0000000..7c0c5dd --- /dev/null +++ b/lib/features/bridge/providers/wallet_state.dart @@ -0,0 +1,99 @@ +import 'dart:convert'; + +import 'package:apparatus_wallet/features/bridge/providers/rpc_channel.dart'; +import 'package:biometric_storage/biometric_storage.dart'; +import 'package:brambldart/brambldart.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:topl_common/proto/brambl/models/address.pb.dart'; + +import 'package:topl_common/proto/genus/genus_models.pb.dart'; +import 'package:topl_common/proto/node/services/bifrost_rpc.pbgrpc.dart'; + +part 'wallet_state.freezed.dart'; +part 'wallet_state.g.dart'; + +@riverpod +class Wallet extends _$Wallet { + @override + Stream build() async* { + // TODO: Get addresses from wallet state / vault store + final addresses = []; + final rpcChannels = ref.watch(rpcChannelProvider); + final nodeRpcClient = NodeRpcClient(rpcChannels.nodeRpcChannel); + final genusRpcClient = GenusQueryAlgebra(rpcChannels.genusRpcChannel); + final base = WalletState.base(); + + await for (final _ in nodeRpcClient + .synchronizationTraversal(SynchronizationTraversalReq())) { + await Future.delayed(const Duration(seconds: 1)); + final utxosList = await Stream.fromIterable(addresses) + .asyncMap((address) => genusRpcClient.queryUtxo(fromAddress: address)) + .expand((l) => l) + .toList(); + final txos = + Map.fromEntries(utxosList.map((u) => MapEntry(u.outputAddress, u))); + yield base.copyWith(txos: txos); + } + } +} + +@freezed +class WalletState with _$WalletState { + const factory WalletState({ + required VaultStore? vaultStore, + required Map txos, + }) = _WalletState; + + factory WalletState.base() => const WalletState(vaultStore: null, txos: {}); +} + +class WalletKeyApi extends WalletKeyApiAlgebra { + @override + Future> deleteMainKeyVaultStore( + String name) async { + final store = await BiometricStorage().getStorage("$name.vault"); + await store.delete(); + return Either.right(const Unit()); + } + + @override + Future> getMainKeyVaultStore( + String name) async { + final store = await BiometricStorage().getStorage("$name.vault"); + final data = await store.read(); + if (data == null) { + return Either.left(WalletKeyException.vaultStoreDoesNotExist()); + } + final decoded = jsonDecode(data); + return VaultStore.fromJson(decoded).mapLeft( + (e) => WalletKeyException.decodeVaultStore(context: e.toString())); + } + + @override + Future> saveMainKeyVaultStore( + VaultStore mainKeyVaultStore, String name) async { + final store = await BiometricStorage().getStorage("$name.vault"); + final data = jsonEncode(mainKeyVaultStore.toJson()); + await store.write(data); + return Either.right(const Unit()); + } + + @override + Future> saveMnemonic( + List mnemonic, String mnemonicName) async { + final store = await BiometricStorage().getStorage("$mnemonicName.mnemonic"); + final data = jsonEncode(mnemonic); + await store.write(data); + return Either.right(const Unit()); + } + + @override + Future> updateMainKeyVaultStore( + VaultStore mainKeyVaultStore, String name) async { + final store = await BiometricStorage().getStorage("$name.vault"); + final data = jsonEncode(mainKeyVaultStore.toJson()); + await store.write(data); + return Either.right(const Unit()); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 2916eee..6982a46 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,8 +34,17 @@ dependencies: http: ^1.2.1 #TODO: Migrate to fetch_client #Misc - uuid: ^4.4.0 + uuid: ^4.2.1 crypto: ^3.0.3 + biometric_storage: ^5.0.1 + + # Blockchain + brambldart: ^2.0.0-beta.0 + topl_common: ^2.1.0 + servicekit: + git: + url: git@github.com:Topl/dart_service_kit.git + ref: beta-0-updates dev_dependencies: flutter_test: From 214d21426138b8761e95f2190aa3bbe6f8b670c2 Mon Sep 17 00:00:00 2001 From: Sean Cheatham Date: Tue, 9 Jul 2024 11:41:39 -0400 Subject: [PATCH 2/8] Work-in-progress wallet --- integration-test/docker-compose.yaml | 12 +- integration-test/envoy/config.yaml | 55 ++++ integration-test/envoy/envoy.Dockerfile | 6 + .../{ => init}/bridge-init.Dockerfile | 0 integration-test/{ => init}/bridge_init.sh | 0 .../bridge/providers/rpc_channel.dart | 11 +- .../bridge/providers/service_kit.dart | 40 +++ .../bridge/providers/wallet_state.dart | 117 +++---- lib/features/bridge/widgets/utxos_view.dart | 306 ++++++++++++++++++ lib/router.dart | 8 +- lib/utils/rpc/channel_factory.dart | 2 + lib/utils/rpc/channel_factory_non_web.dart | 11 + lib/utils/rpc/channel_factory_web.dart | 8 + pubspec.yaml | 10 +- 14 files changed, 498 insertions(+), 88 deletions(-) create mode 100644 integration-test/envoy/config.yaml create mode 100644 integration-test/envoy/envoy.Dockerfile rename integration-test/{ => init}/bridge-init.Dockerfile (100%) rename integration-test/{ => init}/bridge_init.sh (100%) create mode 100644 lib/features/bridge/providers/service_kit.dart create mode 100644 lib/features/bridge/widgets/utxos_view.dart create mode 100644 lib/utils/rpc/channel_factory.dart create mode 100644 lib/utils/rpc/channel_factory_non_web.dart create mode 100644 lib/utils/rpc/channel_factory_web.dart diff --git a/integration-test/docker-compose.yaml b/integration-test/docker-compose.yaml index 7672cd5..ef7fa65 100644 --- a/integration-test/docker-compose.yaml +++ b/integration-test/docker-compose.yaml @@ -39,9 +39,19 @@ services: - wallet:/app/wallet - ./peg-in-wallet.json:/app/btc-wallet/peg-in-wallet.json - ./btc-wallet.json:/app/btc-wallet/btc-wallet.json + envoy: + build: + context: envoy + dockerfile: envoy.Dockerfile + ports: + - "9094:9094" + links: + - bifrost + volumes: + - ./envoy/config.yaml:/etc/envoy/config.yaml bridge_init: build: - context: . + context: init dockerfile: bridge-init.Dockerfile volumes: - wallet:/app/wallet diff --git a/integration-test/envoy/config.yaml b/integration-test/envoy/config.yaml new file mode 100644 index 0000000..86b492e --- /dev/null +++ b/integration-test/envoy/config.yaml @@ -0,0 +1,55 @@ +static_resources: + listeners: + - name: listener_0 + address: + socket_address: { address: 0.0.0.0, port_value: 9094 } + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + codec_type: auto + stat_prefix: ingress_http + route_config: + name: local_route + virtual_hosts: + - name: local_service + domains: ["*"] + routes: + - match: { prefix: "/" } + route: + cluster: greeter_service + max_stream_duration: + grpc_timeout_header_max: 0s + cors: + allow_origin_string_match: + - prefix: "*" + allow_methods: GET, PUT, DELETE, POST, OPTIONS + allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout + max_age: "1728000" + expose_headers: custom-header-1,grpc-status,grpc-message + http_filters: + - name: envoy.filters.http.grpc_web + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb + - name: envoy.filters.http.cors + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors + - name: envoy.filters.http.router + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + clusters: + - name: greeter_service + connect_timeout: 0.25s + type: logical_dns + http2_protocol_options: {} + lb_policy: round_robin + load_assignment: + cluster_name: cluster_0 + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: bifrost + port_value: 9084 diff --git a/integration-test/envoy/envoy.Dockerfile b/integration-test/envoy/envoy.Dockerfile new file mode 100644 index 0000000..c3a266d --- /dev/null +++ b/integration-test/envoy/envoy.Dockerfile @@ -0,0 +1,6 @@ +FROM envoyproxy/envoy-alpine:v1.21-latest + +RUN apk update +RUN apk add ngrep + +CMD ["envoy", "-l","trace", "-c","/etc/envoy/config.yaml"] diff --git a/integration-test/bridge-init.Dockerfile b/integration-test/init/bridge-init.Dockerfile similarity index 100% rename from integration-test/bridge-init.Dockerfile rename to integration-test/init/bridge-init.Dockerfile diff --git a/integration-test/bridge_init.sh b/integration-test/init/bridge_init.sh similarity index 100% rename from integration-test/bridge_init.sh rename to integration-test/init/bridge_init.sh diff --git a/lib/features/bridge/providers/rpc_channel.dart b/lib/features/bridge/providers/rpc_channel.dart index e478909..dba64ac 100644 --- a/lib/features/bridge/providers/rpc_channel.dart +++ b/lib/features/bridge/providers/rpc_channel.dart @@ -1,4 +1,5 @@ -import 'package:brambldart/brambldart.dart'; +import 'package:apparatus_wallet/utils/rpc/channel_factory_web.dart'; +import 'package:grpc/grpc_connection_interface.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'rpc_channel.g.dart'; @@ -7,14 +8,14 @@ part 'rpc_channel.g.dart'; class RpcChannel extends _$RpcChannel { @override RpcChannelState build() { - // TODO - return RpcChannelState(nodeRpcChannel: null!, genusRpcChannel: null!); + final channel = makeChannel("localhost", 9094, false); + return RpcChannelState(nodeRpcChannel: channel, genusRpcChannel: channel); } } class RpcChannelState { - final Channel nodeRpcChannel; - final Channel genusRpcChannel; + final ClientChannelBase nodeRpcChannel; + final ClientChannelBase genusRpcChannel; RpcChannelState( {required this.nodeRpcChannel, required this.genusRpcChannel}); diff --git a/lib/features/bridge/providers/service_kit.dart b/lib/features/bridge/providers/service_kit.dart new file mode 100644 index 0000000..7d0efec --- /dev/null +++ b/lib/features/bridge/providers/service_kit.dart @@ -0,0 +1,40 @@ +import 'package:brambldart/brambldart.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:servicekit/servicekit.dart'; + +part 'service_kit.freezed.dart'; + +part 'service_kit.g.dart'; + +@riverpod +class ServiceKit extends _$ServiceKit { + @override + Future build() async { + final storage = await StorageApi.init(); + return ServiceKitState.base(storage); + } +} + +@freezed +class ServiceKitState with _$ServiceKitState { + factory ServiceKitState( + {required StorageApi storageApi, + required ContractStorageApi contractStorageApi, + required FellowshipStorageApi fellowshipStorageApi, + required WalletApi walletApi, + required WalletStateApi walletStateApi}) = _ServiceKitState; + + factory ServiceKitState.base(StorageApi storage) { + final contractStorage = ContractStorageApi(storage.sembast); + final fellowshipStorage = FellowshipStorageApi(storage.sembast); + final walletState = WalletStateApi(storage.sembast, storage.secureStorage); + final wallet = walletState.api; + return ServiceKitState( + storageApi: storage, + contractStorageApi: contractStorage, + fellowshipStorageApi: fellowshipStorage, + walletStateApi: walletState, + walletApi: wallet); + } +} diff --git a/lib/features/bridge/providers/wallet_state.dart b/lib/features/bridge/providers/wallet_state.dart index 7c0c5dd..4c759e1 100644 --- a/lib/features/bridge/providers/wallet_state.dart +++ b/lib/features/bridge/providers/wallet_state.dart @@ -1,99 +1,62 @@ -import 'dart:convert'; - import 'package:apparatus_wallet/features/bridge/providers/rpc_channel.dart'; -import 'package:biometric_storage/biometric_storage.dart'; +import 'package:apparatus_wallet/features/bridge/providers/service_kit.dart'; import 'package:brambldart/brambldart.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:topl_common/proto/brambl/models/address.pb.dart'; import 'package:topl_common/proto/genus/genus_models.pb.dart'; +import 'package:topl_common/proto/genus/genus_rpc.pbgrpc.dart'; import 'package:topl_common/proto/node/services/bifrost_rpc.pbgrpc.dart'; -part 'wallet_state.freezed.dart'; part 'wallet_state.g.dart'; @riverpod -class Wallet extends _$Wallet { +class WalletUtxos extends _$WalletUtxos { @override - Stream build() async* { - // TODO: Get addresses from wallet state / vault store - final addresses = []; + Stream build() async* { + final serviceKit = await ref.watch(serviceKitProvider.future); final rpcChannels = ref.watch(rpcChannelProvider); final nodeRpcClient = NodeRpcClient(rpcChannels.nodeRpcChannel); - final genusRpcClient = GenusQueryAlgebra(rpcChannels.genusRpcChannel); - final base = WalletState.base(); + final genusRpcClient = + TransactionServiceClient(rpcChannels.genusRpcChannel); + + final password = "test".toUtf8(); + final wallet = (await serviceKit.walletApi.createAndSaveNewWallet(password)) + .getOrThrow(); + final mainKey = serviceKit.walletApi + .extractMainKey(wallet.mainKeyVaultStore, password) + .getOrThrow(); + await serviceKit.walletStateApi.initWalletState( + NetworkConstants.privateNetworkId, + NetworkConstants.mainLedgerId, + mainKey.vk); + + Future fetchUtxos() async { + // Get the current address from the wallet + final encodedAddressOpt = serviceKit.walletStateApi.getCurrentAddress(); + // serviceKit.walletStateApi.getAddress("self", "default", null); + ArgumentError.checkNotNull(encodedAddressOpt); + final address = AddressCodecs.decode(encodedAddressOpt).getOrThrow(); + // Get the utxos associated with that address + final utxosList = (await genusRpcClient.getTxosByLockAddress( + QueryByLockAddressRequest(address: address, state: TxoState.UNSPENT), + )) + .txos; + final utxos = + Map.fromEntries(utxosList.map((u) => MapEntry(u.outputAddress, u))); + return utxos; + } + yield await fetchUtxos(); + + // For each block adoption or unadoption await for (final _ in nodeRpcClient .synchronizationTraversal(SynchronizationTraversalReq())) { + // Wait 1 second for Genus to catch up await Future.delayed(const Duration(seconds: 1)); - final utxosList = await Stream.fromIterable(addresses) - .asyncMap((address) => genusRpcClient.queryUtxo(fromAddress: address)) - .expand((l) => l) - .toList(); - final txos = - Map.fromEntries(utxosList.map((u) => MapEntry(u.outputAddress, u))); - yield base.copyWith(txos: txos); + yield await fetchUtxos(); } } } -@freezed -class WalletState with _$WalletState { - const factory WalletState({ - required VaultStore? vaultStore, - required Map txos, - }) = _WalletState; - - factory WalletState.base() => const WalletState(vaultStore: null, txos: {}); -} - -class WalletKeyApi extends WalletKeyApiAlgebra { - @override - Future> deleteMainKeyVaultStore( - String name) async { - final store = await BiometricStorage().getStorage("$name.vault"); - await store.delete(); - return Either.right(const Unit()); - } - - @override - Future> getMainKeyVaultStore( - String name) async { - final store = await BiometricStorage().getStorage("$name.vault"); - final data = await store.read(); - if (data == null) { - return Either.left(WalletKeyException.vaultStoreDoesNotExist()); - } - final decoded = jsonDecode(data); - return VaultStore.fromJson(decoded).mapLeft( - (e) => WalletKeyException.decodeVaultStore(context: e.toString())); - } - - @override - Future> saveMainKeyVaultStore( - VaultStore mainKeyVaultStore, String name) async { - final store = await BiometricStorage().getStorage("$name.vault"); - final data = jsonEncode(mainKeyVaultStore.toJson()); - await store.write(data); - return Either.right(const Unit()); - } - - @override - Future> saveMnemonic( - List mnemonic, String mnemonicName) async { - final store = await BiometricStorage().getStorage("$mnemonicName.mnemonic"); - final data = jsonEncode(mnemonic); - await store.write(data); - return Either.right(const Unit()); - } - - @override - Future> updateMainKeyVaultStore( - VaultStore mainKeyVaultStore, String name) async { - final store = await BiometricStorage().getStorage("$name.vault"); - final data = jsonEncode(mainKeyVaultStore.toJson()); - await store.write(data); - return Either.right(const Unit()); - } -} +typedef UtxosMap = Map; diff --git a/lib/features/bridge/widgets/utxos_view.dart b/lib/features/bridge/widgets/utxos_view.dart new file mode 100644 index 0000000..a4ff332 --- /dev/null +++ b/lib/features/bridge/widgets/utxos_view.dart @@ -0,0 +1,306 @@ +import 'package:apparatus_wallet/features/bridge/providers/wallet_state.dart'; +import 'package:brambldart/brambldart.dart'; +import 'package:fixnum/fixnum.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:fpdart/fpdart.dart' hide State; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:topl_common/proto/brambl/models/address.pb.dart'; +import 'package:topl_common/proto/brambl/models/box/value.pb.dart'; +import 'package:topl_common/proto/brambl/models/transaction/io_transaction.pb.dart'; +import 'package:topl_common/proto/brambl/models/transaction/spent_transaction_output.pb.dart'; +import 'package:topl_common/proto/brambl/models/transaction/unspent_transaction_output.pb.dart'; +import 'package:topl_common/proto/genus/genus_models.pb.dart'; + +class UtxosView extends HookConsumerWidget { + const UtxosView({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final utxos = ref.read(walletUtxosProvider); + + return Scaffold( + body: body(utxos), + ); + } + + Widget body(utxos) { + return switch (utxos) { + AsyncData(:final value) => const Text("Utxos Found"), + AsyncData(:final error) => const Text("Utxos failed to load"), + _ => const Text("Loading") + }; + } +} + +class TransactView extends StatefulWidget { + final UtxosMap utxos; + final Function(IoTransaction) submitTransaction; + + const TransactView( + {super.key, required this.utxos, required this.submitTransaction}); + @override + State createState() => TransactViewState(); +} + +class TransactViewState extends State { + Set _selectedInputs = {}; + List<(String valueStr, String addressStr)> _newOutputEntries = []; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(8), + child: Card( + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: ExpansionPanelList.radio(children: [ + ExpansionPanelRadio( + value: "Inputs", + headerBuilder: (context, isExpanded) => + const ListTile(title: Text("Inputs")), + body: _inputsTile(), + ), + ExpansionPanelRadio( + value: "Outputs", + headerBuilder: (context, isExpanded) => + const ListTile(title: Text("Outputs")), + body: _outputsTile()), + ]), + ), + ), + IconButton( + onPressed: _transact, + icon: const Icon(Icons.send), + ) + ], + ), + ), + ); + } + + Int64 _inputSum() => _selectedInputs + .toList() + .map((v) => widget.utxos[v]!.transactionOutput.value) + .map((v) => v.quantity) + .fold(Int64.ZERO, (a, b) => a + b); + + Future _createTransaction() async { + var tx = IoTransaction(); + + for (final ref in _selectedInputs) { + final output = widget.utxos[ref]!; + final input = SpentTransactionOutput() + ..value = output.transactionOutput.value + ..address = ref; + tx.inputs.add(input); + } + + for (final e in _newOutputEntries) { + // TODO: Error handling + final lockAddress = AddressCodecs.decode(e.$2).getOrThrow(); + // TODO: e.$1 value - is it a LVL? + final value = Value(); + final output = UnspentTransactionOutput() + ..address = lockAddress + ..value = value; + tx.outputs.add(output); + } + // TODO: Prove/sign + + return tx; + } + + _transact() async { + final tx = await _createTransaction(); + await widget.submitTransaction(tx); + setState(() { + _selectedInputs = {}; + _newOutputEntries = []; + }); + } + + Widget _outputsTile() { + return Column( + children: [ + DataTable( + columns: _outputTableHeader, + rows: [ + ..._newOutputEntries.mapWithIndex(_outputEntryRow), + _feeOutputRow() + ], + ), + IconButton(onPressed: _addOutputEntry, icon: const Icon(Icons.add)) + ], + ); + } + + static const _outputTableHeader = [ + DataColumn( + label: Expanded( + child: Text( + 'Quantity', + style: TextStyle(fontStyle: FontStyle.italic), + ), + ), + ), + DataColumn( + label: Expanded( + child: Text( + 'Lock Address', + style: TextStyle(fontStyle: FontStyle.italic), + ), + ), + ), + DataColumn( + label: Expanded( + child: Text( + 'Remove', + style: TextStyle(fontStyle: FontStyle.italic), + ), + ), + ), + ]; + + DataRow _outputEntryRow((String, String) entry, int index) { + return DataRow( + cells: [ + DataCell(TextFormField( + initialValue: entry.$1, + onChanged: (value) => _updateOutputEntryQuantity(index, value), + )), + DataCell(TextFormField( + initialValue: entry.$2, + onChanged: (value) => _updateOutputEntryAddress(index, value), + )), + DataCell( + IconButton( + icon: const Icon(Icons.delete), + onPressed: () => _deleteOutputEntry(index), + ), + ), + ], + ); + } + + DataRow _feeOutputRow() { + Int64 outputSum = Int64.ZERO; + String? errorText; + for (final t in _newOutputEntries) { + final parsed = Int64.tryParseInt(t.$1); + if (parsed == null) { + errorText = "?"; + break; + } else { + outputSum += parsed; + } + } + + return DataRow(cells: [ + DataCell(Text(errorText ?? (_inputSum() - outputSum).toString())), + const DataCell(Text("Tip")), + const DataCell(IconButton( + icon: Icon(Icons.cancel), + onPressed: null, + )), + ]); + } + + _updateOutputEntryQuantity(int index, String value) { + setState(() { + final (_, a) = _newOutputEntries[index]; + _newOutputEntries[index] = (value, a); + }); + } + + _updateOutputEntryAddress(int index, String value) { + setState(() { + final (v, _) = _newOutputEntries[index]; + _newOutputEntries[index] = (v, value); + }); + } + + _addOutputEntry() { + setState(() { + _newOutputEntries.add(const ("100", "")); + }); + } + + _deleteOutputEntry(int index) { + setState(() { + _newOutputEntries.removeAt(index); + }); + } + + Widget _inputsTile() { + return Container( + child: widget.utxos.isEmpty + ? const Text("Empty wallet") + : DataTable( + columns: header, + rows: widget.utxos.entries.map(_inputEntryRow).toList(), + )); + } + + DataRow _inputEntryRow(MapEntry entry) { + return DataRow( + cells: [ + DataCell(Text("${entry.value.transactionOutput.value.quantity}", + style: const TextStyle(fontSize: 12))), + DataCell(TextButton( + onPressed: () { + Clipboard.setData(ClipboardData( + text: AddressCodecs.encode( + entry.value.transactionOutput.address))); + }, + child: Text( + AddressCodecs.encode(entry.value.transactionOutput.address), + style: const TextStyle(fontSize: 12)), + )), + DataCell(Checkbox( + value: _selectedInputs.contains(entry.key), + onChanged: (newValue) => + _updateInputEntry(entry.key, newValue ?? false))), + ], + ); + } + + _updateInputEntry(TransactionOutputAddress ref, bool retain) { + setState(() { + if (retain) { + _selectedInputs.add(ref); + } else { + _selectedInputs.remove(ref); + } + }); + } + + static const header = [ + DataColumn( + label: Expanded( + child: Text( + 'Quantity', + style: TextStyle(fontStyle: FontStyle.italic), + ), + ), + ), + DataColumn( + label: Expanded( + child: Text( + 'Lock Address', + style: TextStyle(fontStyle: FontStyle.italic), + ), + ), + ), + DataColumn( + label: Expanded( + child: Text( + 'Selected', + style: TextStyle(fontStyle: FontStyle.italic), + ), + ), + ), + ]; +} diff --git a/lib/router.dart b/lib/router.dart index a39e745..9d3c81b 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -1,3 +1,4 @@ +import 'package:apparatus_wallet/features/bridge/widgets/utxos_view.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -9,13 +10,14 @@ import 'package:apparatus_wallet/features/peg_in/widgets/peg_in.dart'; /// const routes # TODO move to own file once we get more routes const homeRoute = '/'; const swapRoute = '/swap'; +const walletRoute = '/wallet'; final providedNavigator = GlobalKey(); //TODO: remove with provider refactor // GoRouter configuration final router = GoRouter( - initialLocation: homeRoute, + initialLocation: walletRoute, debugLogDiagnostics: !kReleaseMode ? true : false, // Turns logging off when in release mode routes: [ @@ -33,5 +35,9 @@ final router = GoRouter( path: swapRoute, builder: (context, state) => const BridgeUi(), ), + GoRoute( + path: walletRoute, + builder: (context, state) => const UtxosView(), + ), ], ); diff --git a/lib/utils/rpc/channel_factory.dart b/lib/utils/rpc/channel_factory.dart new file mode 100644 index 0000000..e879a2e --- /dev/null +++ b/lib/utils/rpc/channel_factory.dart @@ -0,0 +1,2 @@ +export 'channel_factory_non_web.dart' + if (dart.library.html) 'channel_factory_web.dart'; diff --git a/lib/utils/rpc/channel_factory_non_web.dart b/lib/utils/rpc/channel_factory_non_web.dart new file mode 100644 index 0000000..c37cc48 --- /dev/null +++ b/lib/utils/rpc/channel_factory_non_web.dart @@ -0,0 +1,11 @@ +import 'package:grpc/grpc_connection_interface.dart'; +import 'package:grpc/grpc.dart'; + +ClientChannelBase makeChannel(String host, int port, bool secure) { + return ClientChannel(host, + port: port, + options: ChannelOptions( + credentials: secure + ? const ChannelCredentials.secure() + : const ChannelCredentials.insecure())); +} diff --git a/lib/utils/rpc/channel_factory_web.dart b/lib/utils/rpc/channel_factory_web.dart new file mode 100644 index 0000000..e88ddac --- /dev/null +++ b/lib/utils/rpc/channel_factory_web.dart @@ -0,0 +1,8 @@ +import 'package:grpc/grpc_connection_interface.dart'; +import 'package:grpc/grpc_web.dart'; + +ClientChannelBase makeChannel(String host, int port, bool secure) { + final prefix = secure ? "https://" : "http://"; + final uri = "$prefix$host:$port"; + return GrpcWebClientChannel.xhr(Uri.parse(uri)); +} diff --git a/pubspec.yaml b/pubspec.yaml index 6982a46..425f48e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,19 +32,21 @@ dependencies: fpdart: ^1.1.0 fetch_client: ^1.1.2 http: ^1.2.1 #TODO: Migrate to fetch_client + fixnum: ^1.1.0 #Misc uuid: ^4.2.1 crypto: ^3.0.3 - biometric_storage: ^5.0.1 + grpc: ^3.2.4 # Blockchain brambldart: ^2.0.0-beta.0 topl_common: ^2.1.0 servicekit: - git: - url: git@github.com:Topl/dart_service_kit.git - ref: beta-0-updates + path: /media/sean/Shared Storage/git/dart_service_kit + # git: + # url: https://github.com/Topl/dart_service_kit.git + # ref: beta-0-updates dev_dependencies: flutter_test: From dfc0eee5cda88c39edb185469a26a601cf3d5352 Mon Sep 17 00:00:00 2001 From: Sean Cheatham Date: Tue, 9 Jul 2024 12:22:09 -0400 Subject: [PATCH 3/8] Set default password to 32 0s --- lib/features/bridge/providers/wallet_state.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/features/bridge/providers/wallet_state.dart b/lib/features/bridge/providers/wallet_state.dart index 4c759e1..6877775 100644 --- a/lib/features/bridge/providers/wallet_state.dart +++ b/lib/features/bridge/providers/wallet_state.dart @@ -20,7 +20,7 @@ class WalletUtxos extends _$WalletUtxos { final genusRpcClient = TransactionServiceClient(rpcChannels.genusRpcChannel); - final password = "test".toUtf8(); + final password = List.filled(32, 0); final wallet = (await serviceKit.walletApi.createAndSaveNewWallet(password)) .getOrThrow(); final mainKey = serviceKit.walletApi From 74bed961b2e103a325e297e633c843962eabf422 Mon Sep 17 00:00:00 2001 From: Sean Cheatham Date: Tue, 9 Jul 2024 15:27:04 -0400 Subject: [PATCH 4/8] Wallet init fixes --- integration-test/docker-compose.yaml | 1 + integration-test/init/bridge-init.Dockerfile | 2 - integration-test/init/bridge_init.sh | 4 +- .../bridge/providers/wallet_state.dart | 60 ++++++++++++++----- lib/features/bridge/widgets/utxos_view.dart | 9 +-- pubspec.yaml | 6 +- 6 files changed, 57 insertions(+), 25 deletions(-) diff --git a/integration-test/docker-compose.yaml b/integration-test/docker-compose.yaml index ef7fa65..264428b 100644 --- a/integration-test/docker-compose.yaml +++ b/integration-test/docker-compose.yaml @@ -55,6 +55,7 @@ services: dockerfile: bridge-init.Dockerfile volumes: - wallet:/app/wallet + - ./init/bridge_init.sh:/bridge_init.sh depends_on: - bifrost diff --git a/integration-test/init/bridge-init.Dockerfile b/integration-test/init/bridge-init.Dockerfile index 792f22f..8e5d64f 100644 --- a/integration-test/init/bridge-init.Dockerfile +++ b/integration-test/init/bridge-init.Dockerfile @@ -21,7 +21,5 @@ USER 1001:0 RUN cs setup --yes RUN cs fetch -r https://s01.oss.sonatype.org/content/repositories/releases co.topl:brambl-cli_2.13:2.0.0-beta5 -COPY bridge_init.sh /bridge_init.sh - ENTRYPOINT ["sh", "/bridge_init.sh"] CMD [] diff --git a/integration-test/init/bridge_init.sh b/integration-test/init/bridge_init.sh index 79dc1c4..700b1ea 100644 --- a/integration-test/init/bridge_init.sh +++ b/integration-test/init/bridge_init.sh @@ -23,7 +23,9 @@ export ADDRESS=$(brambl-cli wallet current-address --walletdb $TOPL_WALLET_DB) cd /app -brambl-cli simple-transaction create --from-fellowship nofellowship --from-template genesis --from-interaction 1 --change-fellowship nofellowship --change-template genesis --change-interaction 1 -t $ADDRESS -w $TOPL_WALLET_PASSWORD -o genesisTx.pbuf -n private -a 100000 -h bifrost --port 9084 --keyfile $TOPL_WALLET_JSON --walletdb $TOPL_WALLET_DB --fee 10 --transfer-token lvl +echo "Genesis UTxOs" +brambl-cli genus-query utxo-by-address --from-fellowship nofellowship --from-template genesis --host bifrost --port 9084 --secure false --walletdb $TOPL_WALLET_DB +brambl-cli simple-transaction create --from-fellowship nofellowship --from-template genesis --from-interaction 1 --change-fellowship nofellowship --change-template genesis --change-interaction 1 -t $ADDRESS -w $TOPL_WALLET_PASSWORD -o genesisTx.pbuf -n private -a 10000 -h bifrost --port 9084 --keyfile $TOPL_WALLET_JSON --walletdb $TOPL_WALLET_DB --fee 10 --transfer-token lvl brambl-cli tx prove -i genesisTx.pbuf --walletdb $TOPL_WALLET_DB --keyfile $TOPL_WALLET_JSON -w $TOPL_WALLET_PASSWORD -o genesisTxProved.pbuf export GROUP_UTXO=$(brambl-cli tx broadcast -i genesisTxProved.pbuf -h bifrost --port 9084 --secure false) echo "GROUP_UTXO: $GROUP_UTXO" diff --git a/lib/features/bridge/providers/wallet_state.dart b/lib/features/bridge/providers/wallet_state.dart index 6877775..1b3f9a9 100644 --- a/lib/features/bridge/providers/wallet_state.dart +++ b/lib/features/bridge/providers/wallet_state.dart @@ -20,41 +20,69 @@ class WalletUtxos extends _$WalletUtxos { final genusRpcClient = TransactionServiceClient(rpcChannels.genusRpcChannel); + // TODO: User provided final password = List.filled(32, 0); - final wallet = (await serviceKit.walletApi.createAndSaveNewWallet(password)) + // TODO: Load existing + final wallet = + (await serviceKit.walletApi.createNewWallet(password)).getOrThrow(); + (await serviceKit.walletApi.saveWallet(wallet.mainKeyVaultStore)) .getOrThrow(); final mainKey = serviceKit.walletApi .extractMainKey(wallet.mainKeyVaultStore, password) .getOrThrow(); - await serviceKit.walletStateApi.initWalletState( + try { + await serviceKit.walletStateApi.initWalletState( NetworkConstants.privateNetworkId, NetworkConstants.mainLedgerId, - mainKey.vk); + mainKey.vk, + ); + } catch (e) { + print(e); + rethrow; + } + final encodedGenesisAddress = + serviceKit.walletStateApi.getAddress("nofellowship", "genesis", 1)!; + final genesisAddress = + AddressCodecs.decode(encodedGenesisAddress).getOrThrow(); + final genesisLock = + serviceKit.walletStateApi.getLock("nofellowship", "genesis", 1)!; Future fetchUtxos() async { // Get the current address from the wallet - final encodedAddressOpt = serviceKit.walletStateApi.getCurrentAddress(); - // serviceKit.walletStateApi.getAddress("self", "default", null); - ArgumentError.checkNotNull(encodedAddressOpt); - final address = AddressCodecs.decode(encodedAddressOpt).getOrThrow(); - // Get the utxos associated with that address - final utxosList = (await genusRpcClient.getTxosByLockAddress( - QueryByLockAddressRequest(address: address, state: TxoState.UNSPENT), - )) - .txos; + final encodedAddress = serviceKit.walletStateApi.getCurrentAddress(); + final address = AddressCodecs.decode(encodedAddress).getOrThrow(); + final utxosList = [ + ...(await genusRpcClient.getTxosByLockAddress(QueryByLockAddressRequest( + address: address, state: TxoState.UNSPENT))) + .txos, + // In addition to the local wallet's funds, also fetch the public genesis funds + ...(await genusRpcClient.getTxosByLockAddress(QueryByLockAddressRequest( + address: genesisAddress, state: TxoState.UNSPENT))) + .txos, + ]; final utxos = Map.fromEntries(utxosList.map((u) => MapEntry(u.outputAddress, u))); return utxos; } - yield await fetchUtxos(); + try { + yield await fetchUtxos(); + } catch (e) { + print(e); + rethrow; + } // For each block adoption or unadoption await for (final _ in nodeRpcClient .synchronizationTraversal(SynchronizationTraversalReq())) { - // Wait 1 second for Genus to catch up - await Future.delayed(const Duration(seconds: 1)); - yield await fetchUtxos(); + try { + // Wait 1 second for Genus to catch up + await Future.delayed(const Duration(seconds: 1)); + yield await fetchUtxos(); + } catch (e) { + print(e); + rethrow; + } } } } diff --git a/lib/features/bridge/widgets/utxos_view.dart b/lib/features/bridge/widgets/utxos_view.dart index a4ff332..b9ee070 100644 --- a/lib/features/bridge/widgets/utxos_view.dart +++ b/lib/features/bridge/widgets/utxos_view.dart @@ -17,17 +17,18 @@ class UtxosView extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final utxos = ref.read(walletUtxosProvider); + final utxos = ref.watch(walletUtxosProvider); return Scaffold( body: body(utxos), ); } - Widget body(utxos) { + Widget body(AsyncValue utxos) { return switch (utxos) { - AsyncData(:final value) => const Text("Utxos Found"), - AsyncData(:final error) => const Text("Utxos failed to load"), + AsyncData(:final value) => + TransactView(utxos: value, submitTransaction: (tx) {}), + AsyncError(:final error) => Text("Utxos failed to load. Reason: $error"), _ => const Text("Loading") }; } diff --git a/pubspec.yaml b/pubspec.yaml index 425f48e..0cdeb48 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,10 +40,12 @@ dependencies: grpc: ^3.2.4 # Blockchain - brambldart: ^2.0.0-beta.0 + brambldart: + path: ../BramblDart + # ^2.0.0-beta.0 topl_common: ^2.1.0 servicekit: - path: /media/sean/Shared Storage/git/dart_service_kit + path: ../dart_service_kit # git: # url: https://github.com/Topl/dart_service_kit.git # ref: beta-0-updates From 77a0aa10ee55319091f7b3ead965b7fa47995aa5 Mon Sep 17 00:00:00 2001 From: Sean Cheatham Date: Mon, 15 Jul 2024 14:23:58 -0400 Subject: [PATCH 5/8] Cleanup UtxosView page. Separate UtxosState from WalletKeyVault --- .../bridge/providers/rpc_channel.dart | 1 + .../{wallet_state.dart => utxos_state.dart} | 47 +++--------------- .../bridge/providers/wallet_key_vault.dart | 49 +++++++++++++++++++ lib/features/bridge/widgets/utxos_view.dart | 38 +++++++------- 4 files changed, 79 insertions(+), 56 deletions(-) rename lib/features/bridge/providers/{wallet_state.dart => utxos_state.dart} (66%) create mode 100644 lib/features/bridge/providers/wallet_key_vault.dart diff --git a/lib/features/bridge/providers/rpc_channel.dart b/lib/features/bridge/providers/rpc_channel.dart index dba64ac..ef72a73 100644 --- a/lib/features/bridge/providers/rpc_channel.dart +++ b/lib/features/bridge/providers/rpc_channel.dart @@ -8,6 +8,7 @@ part 'rpc_channel.g.dart'; class RpcChannel extends _$RpcChannel { @override RpcChannelState build() { + // TODO: User Provided final channel = makeChannel("localhost", 9094, false); return RpcChannelState(nodeRpcChannel: channel, genusRpcChannel: channel); } diff --git a/lib/features/bridge/providers/wallet_state.dart b/lib/features/bridge/providers/utxos_state.dart similarity index 66% rename from lib/features/bridge/providers/wallet_state.dart rename to lib/features/bridge/providers/utxos_state.dart index 1b3f9a9..966f823 100644 --- a/lib/features/bridge/providers/wallet_state.dart +++ b/lib/features/bridge/providers/utxos_state.dart @@ -1,51 +1,30 @@ import 'package:apparatus_wallet/features/bridge/providers/rpc_channel.dart'; import 'package:apparatus_wallet/features/bridge/providers/service_kit.dart'; +import 'package:apparatus_wallet/features/bridge/providers/wallet_key_vault.dart'; import 'package:brambldart/brambldart.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:topl_common/proto/brambl/models/address.pb.dart'; - import 'package:topl_common/proto/genus/genus_models.pb.dart'; import 'package:topl_common/proto/genus/genus_rpc.pbgrpc.dart'; import 'package:topl_common/proto/node/services/bifrost_rpc.pbgrpc.dart'; -part 'wallet_state.g.dart'; +part 'utxos_state.g.dart'; @riverpod -class WalletUtxos extends _$WalletUtxos { +class UtxosState extends _$UtxosState { @override Stream build() async* { + await ref.watch(walletKeyVaultProvider.future); final serviceKit = await ref.watch(serviceKitProvider.future); final rpcChannels = ref.watch(rpcChannelProvider); final nodeRpcClient = NodeRpcClient(rpcChannels.nodeRpcChannel); final genusRpcClient = TransactionServiceClient(rpcChannels.genusRpcChannel); - // TODO: User provided - final password = List.filled(32, 0); - // TODO: Load existing - final wallet = - (await serviceKit.walletApi.createNewWallet(password)).getOrThrow(); - (await serviceKit.walletApi.saveWallet(wallet.mainKeyVaultStore)) - .getOrThrow(); - final mainKey = serviceKit.walletApi - .extractMainKey(wallet.mainKeyVaultStore, password) - .getOrThrow(); - try { - await serviceKit.walletStateApi.initWalletState( - NetworkConstants.privateNetworkId, - NetworkConstants.mainLedgerId, - mainKey.vk, - ); - } catch (e) { - print(e); - rethrow; - } final encodedGenesisAddress = serviceKit.walletStateApi.getAddress("nofellowship", "genesis", 1)!; final genesisAddress = AddressCodecs.decode(encodedGenesisAddress).getOrThrow(); - final genesisLock = - serviceKit.walletStateApi.getLock("nofellowship", "genesis", 1)!; Future fetchUtxos() async { // Get the current address from the wallet @@ -65,24 +44,14 @@ class WalletUtxos extends _$WalletUtxos { return utxos; } - try { - yield await fetchUtxos(); - } catch (e) { - print(e); - rethrow; - } + yield await fetchUtxos(); // For each block adoption or unadoption await for (final _ in nodeRpcClient .synchronizationTraversal(SynchronizationTraversalReq())) { - try { - // Wait 1 second for Genus to catch up - await Future.delayed(const Duration(seconds: 1)); - yield await fetchUtxos(); - } catch (e) { - print(e); - rethrow; - } + // Wait 1 second for Genus to catch up + await Future.delayed(const Duration(seconds: 1)); + yield await fetchUtxos(); } } } diff --git a/lib/features/bridge/providers/wallet_key_vault.dart b/lib/features/bridge/providers/wallet_key_vault.dart new file mode 100644 index 0000000..6324a73 --- /dev/null +++ b/lib/features/bridge/providers/wallet_key_vault.dart @@ -0,0 +1,49 @@ +import 'package:apparatus_wallet/features/bridge/providers/service_kit.dart'; +import 'package:brambldart/brambldart.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'wallet_key_vault.g.dart'; + +@riverpod +class WalletKeyVault extends _$WalletKeyVault { + @override + Future build() async { + final serviceKit = await ref.watch(serviceKitProvider.future); + + // TODO: User provided + final password = List.filled(32, 0); + + late VaultStore wallet; + // First, attempt to load an existing wallet + final loadResult = await serviceKit.walletApi.loadWallet(); + await loadResult.fold( + // Loading a wallet may fail for a variety of reasons + (e) async { + // If it failed because it doesn't exist, then we can take a happy-path and create a new one. + if (e.type == WalletApiFailureType.failedToLoadWallet) { + final saveResult = + (await serviceKit.walletApi.createNewWallet(password)) + .getOrThrow(); + wallet = saveResult.mainKeyVaultStore; + (await serviceKit.walletApi.saveWallet(wallet)).getOrThrow(); + final mainKey = serviceKit.walletApi + .extractMainKey(wallet, password) + .getOrThrow(); + await serviceKit.walletStateApi.initWalletState( + NetworkConstants.privateNetworkId, + NetworkConstants.mainLedgerId, + mainKey.vk, + ); + // Otherwise, if it failed for some other reason, then a wallet likely exists; it's just unparseable + // TODO: What to do in this situation? + } else { + throw e; + } + }, + // If the wallet loaded successfully, hooray! + (w) async => wallet = w, + ); + + return wallet; + } +} diff --git a/lib/features/bridge/widgets/utxos_view.dart b/lib/features/bridge/widgets/utxos_view.dart index b9ee070..d2ddbd4 100644 --- a/lib/features/bridge/widgets/utxos_view.dart +++ b/lib/features/bridge/widgets/utxos_view.dart @@ -1,6 +1,5 @@ -import 'package:apparatus_wallet/features/bridge/providers/wallet_state.dart'; +import 'package:apparatus_wallet/features/bridge/providers/utxos_state.dart'; import 'package:brambldart/brambldart.dart'; -import 'package:fixnum/fixnum.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:fpdart/fpdart.dart' hide State; @@ -11,13 +10,14 @@ import 'package:topl_common/proto/brambl/models/transaction/io_transaction.pb.da import 'package:topl_common/proto/brambl/models/transaction/spent_transaction_output.pb.dart'; import 'package:topl_common/proto/brambl/models/transaction/unspent_transaction_output.pb.dart'; import 'package:topl_common/proto/genus/genus_models.pb.dart'; +import 'package:topl_common/proto/quivr/models/shared.pb.dart'; class UtxosView extends HookConsumerWidget { const UtxosView({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final utxos = ref.watch(walletUtxosProvider); + final utxos = ref.watch(utxosStateProvider); return Scaffold( body: body(utxos), @@ -28,8 +28,9 @@ class UtxosView extends HookConsumerWidget { return switch (utxos) { AsyncData(:final value) => TransactView(utxos: value, submitTransaction: (tx) {}), - AsyncError(:final error) => Text("Utxos failed to load. Reason: $error"), - _ => const Text("Loading") + AsyncError(:final error) => + Center(child: Text("Utxos failed to load. Reason: $error")), + _ => const Center(child: CircularProgressIndicator()) }; } } @@ -45,6 +46,7 @@ class TransactView extends StatefulWidget { } class TransactViewState extends State { + // TODO: Move this state into Riverpod Set _selectedInputs = {}; List<(String valueStr, String addressStr)> _newOutputEntries = []; @@ -82,11 +84,11 @@ class TransactViewState extends State { ); } - Int64 _inputSum() => _selectedInputs + Int128 _inputSum() => _selectedInputs .toList() - .map((v) => widget.utxos[v]!.transactionOutput.value) - .map((v) => v.quantity) - .fold(Int64.ZERO, (a, b) => a + b); + .map((v) => widget.utxos[v]!.transactionOutput.value.quantity) + .nonNulls + .fold(BigInt.zero.toInt128(), (a, b) => a + b); Future _createTransaction() async { var tx = IoTransaction(); @@ -187,20 +189,21 @@ class TransactViewState extends State { } DataRow _feeOutputRow() { - Int64 outputSum = Int64.ZERO; + Int128 outputSum = BigInt.zero.toInt128(); String? errorText; for (final t in _newOutputEntries) { - final parsed = Int64.tryParseInt(t.$1); - if (parsed == null) { + try { + final parsed = BigInt.parse(t.$1).toInt128(); + outputSum += parsed; + } catch (_) { errorText = "?"; break; - } else { - outputSum += parsed; } } return DataRow(cells: [ - DataCell(Text(errorText ?? (_inputSum() - outputSum).toString())), + DataCell( + Text(errorText ?? (_inputSum() - outputSum).toBigInt().toString())), const DataCell(Text("Tip")), const DataCell(IconButton( icon: Icon(Icons.cancel), @@ -246,10 +249,11 @@ class TransactViewState extends State { } DataRow _inputEntryRow(MapEntry entry) { + final value = entry.value.transactionOutput.value; + final quantityString = value.quantity?.show ?? "-"; return DataRow( cells: [ - DataCell(Text("${entry.value.transactionOutput.value.quantity}", - style: const TextStyle(fontSize: 12))), + DataCell(Text(quantityString, style: const TextStyle(fontSize: 12))), DataCell(TextButton( onPressed: () { Clipboard.setData(ClipboardData( From 1d3d24042cdcba2bd51df82469e3993c281cf1b1 Mon Sep 17 00:00:00 2001 From: Sean Cheatham Date: Mon, 15 Jul 2024 16:32:51 -0400 Subject: [PATCH 6/8] Work-in-progress PegIn Riverpod --- ...dge_api.dart => bridge_api_interface.dart} | 4 +- lib/features/peg_in/providers/bridge_api.dart | 12 +++ lib/features/peg_in/providers/peg_in.dart | 82 +++++++++++++++++++ lib/features/peg_in/widgets/peg_in.dart | 9 +- lib/router.dart | 3 +- pubspec.yaml | 1 + 6 files changed, 104 insertions(+), 7 deletions(-) rename lib/features/peg_in/logic/{bridge_api.dart => bridge_api_interface.dart} (97%) create mode 100644 lib/features/peg_in/providers/bridge_api.dart create mode 100644 lib/features/peg_in/providers/peg_in.dart diff --git a/lib/features/peg_in/logic/bridge_api.dart b/lib/features/peg_in/logic/bridge_api_interface.dart similarity index 97% rename from lib/features/peg_in/logic/bridge_api.dart rename to lib/features/peg_in/logic/bridge_api_interface.dart index 447db2f..7f8a2e5 100644 --- a/lib/features/peg_in/logic/bridge_api.dart +++ b/lib/features/peg_in/logic/bridge_api_interface.dart @@ -2,10 +2,10 @@ import 'dart:convert'; import 'package:apparatus_wallet/features/peg_in/logic/http_client.dart'; -class BridgeApi { +class BridgeApiInterface { final String baseAddress; - BridgeApi({required this.baseAddress}); + BridgeApiInterface({required this.baseAddress}); Future startSession(StartSessionRequest request) async { final response = await httpClient.post( diff --git a/lib/features/peg_in/providers/bridge_api.dart b/lib/features/peg_in/providers/bridge_api.dart new file mode 100644 index 0000000..c34b92c --- /dev/null +++ b/lib/features/peg_in/providers/bridge_api.dart @@ -0,0 +1,12 @@ +import 'package:apparatus_wallet/features/peg_in/logic/bridge_api.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'bridge_api.g.dart'; + +@riverpod +class BridgeApi extends _$BridgeApi { + @override + BridgeApiInterface build() => + // TODO: Don't hardcode + BridgeApiInterface(baseAddress: "http://localhost:4000"); +} diff --git a/lib/features/peg_in/providers/peg_in.dart b/lib/features/peg_in/providers/peg_in.dart new file mode 100644 index 0000000..2936925 --- /dev/null +++ b/lib/features/peg_in/providers/peg_in.dart @@ -0,0 +1,82 @@ +import 'dart:convert'; + +import 'package:apparatus_wallet/features/peg_in/logic/bridge_api.dart'; +import 'package:apparatus_wallet/features/peg_in/providers/bridge_api.dart'; +import 'package:brambldart/brambldart.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:uuid/uuid.dart'; +import 'package:convert/convert.dart' show hex; + +part 'peg_in.freezed.dart'; +part 'peg_in.g.dart'; + +@riverpod +class PegIn extends _$PegIn { + @override + PegInState build() => PegInState.base(); + + startSession() async { + state = state.copyWith(sessionStarted: true); + final uuid = const Uuid().v4(); + final hashed = hex.encode(sha256.hash(utf8.encode(uuid))); + final request = StartSessionRequest( + pkey: + // TODO: This comes from the bridge demo - should it be replaced? + "0295bb5a3b80eeccb1e38ab2cbac2545e9af6c7012cdc8d53bd276754c54fc2e4a", + sha256: hashed); + try { + final response = await ref.watch(bridgeApiProvider).startSession(request); + state = state.copyWith( + sessionID: response.sessionID, + escrowAddress: response.escrowAddress, + ); + } catch (e) { + state = state.copyWith(error: "An error occurred. Lol! $e"); + } + } + + btcDeposited() { + state = state.copyWith(btcDeposited: true); + final bridgeApi = ref.watch(bridgeApiProvider); + final sub = Stream.periodic(const Duration(seconds: 5)) + .asyncMap((_) => bridgeApi.getMintingStatus(state.sessionID!)) + .where((status) => + // TODO: If null, raise error? + status != null && + status is MintingStatus_PeginSessionWaitingForRedemption) + // Using where + take(1) instead of firstWhere to avoid losing the cancelation benefits of the stream + .take(1) + .listen((_) { + state = state.copyWith(tBtcMinted: true); + }); + sub.onError( + (e) => state = state.copyWith(error: "An error occurred. Lol! $e")); + ref.onDispose(sub.cancel); + } + + tBtcAccepted() { + state = PegInState.base(); + } +} + +@freezed +class PegInState with _$PegInState { + const factory PegInState({ + required bool sessionStarted, + required String? sessionID, + required String? escrowAddress, + required bool btcDeposited, + required bool tBtcMinted, + required String? error, + }) = _PegInState; + + factory PegInState.base() => const PegInState( + sessionStarted: false, + sessionID: null, + escrowAddress: null, + btcDeposited: false, + tBtcMinted: false, + error: null, + ); +} diff --git a/lib/features/peg_in/widgets/peg_in.dart b/lib/features/peg_in/widgets/peg_in.dart index e3c3473..baea093 100644 --- a/lib/features/peg_in/widgets/peg_in.dart +++ b/lib/features/peg_in/widgets/peg_in.dart @@ -4,10 +4,11 @@ import 'package:crypto/crypto.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; -import 'package:apparatus_wallet/features/peg_in/logic/bridge_api.dart'; +import 'package:apparatus_wallet/features/peg_in/logic/bridge_api_interface.dart'; import 'package:uuid/uuid.dart'; /// A scaffolded page which allows a user to begin a new Peg-In session +// TODO: Update to use Riverpod class PegInPage extends StatefulWidget { const PegInPage({super.key}); @@ -44,14 +45,14 @@ class _PegInPageState extends State { } Widget startSessionButton(BuildContext context) { - final bridgeApi = context.watch(); + final bridgeApi = context.watch(); return TextButton.icon( onPressed: () => onStartSession(bridgeApi), icon: const Icon(Icons.start), label: const Text("Start Session")); } - void onStartSession(BridgeApi bridgeApi) async { + void onStartSession(BridgeApiInterface bridgeApi) async { setState(() => started = true); final uuid = const Uuid().v4(); final hashed = sha256.convert(utf8.encode(uuid)).toString(); @@ -170,7 +171,7 @@ class _PegInAwaitingFundsPageState extends State { Future awaitRedemption( BuildContext context) async { - BridgeApi client = context.watch(); + BridgeApiInterface client = context.watch(); MintingStatus? status = await client.getMintingStatus(widget.sessionID); while (status is! MintingStatus_PeginSessionWaitingForRedemption) { ArgumentError.checkNotNull(status); diff --git a/lib/router.dart b/lib/router.dart index 9d3c81b..158643e 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -24,7 +24,8 @@ final router = GoRouter( GoRoute( path: homeRoute, builder: (context, state) => Provider( - create: (context) => BridgeApi(baseAddress: "http://localhost:4000"), + create: (context) => + BridgeApiInterface(baseAddress: "http://localhost:4000"), child: Navigator( key: providedNavigator, onGenerateRoute: (settings) => MaterialPageRoute( diff --git a/pubspec.yaml b/pubspec.yaml index 0cdeb48..63a763e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -46,6 +46,7 @@ dependencies: topl_common: ^2.1.0 servicekit: path: ../dart_service_kit + convert: ^3.1.1 # git: # url: https://github.com/Topl/dart_service_kit.git # ref: beta-0-updates From 6908b191da9e6c33951845a720d1f253dae43042 Mon Sep 17 00:00:00 2001 From: Sean Cheatham Date: Wed, 17 Jul 2024 10:13:48 -0400 Subject: [PATCH 7/8] Added missing saved files --- lib/features/peg_in/providers/bridge_api.dart | 2 +- lib/features/peg_in/providers/peg_in.dart | 2 +- lib/router.dart | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/features/peg_in/providers/bridge_api.dart b/lib/features/peg_in/providers/bridge_api.dart index c34b92c..a7ae196 100644 --- a/lib/features/peg_in/providers/bridge_api.dart +++ b/lib/features/peg_in/providers/bridge_api.dart @@ -1,4 +1,4 @@ -import 'package:apparatus_wallet/features/peg_in/logic/bridge_api.dart'; +import 'package:apparatus_wallet/features/peg_in/logic/bridge_api_interface.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'bridge_api.g.dart'; diff --git a/lib/features/peg_in/providers/peg_in.dart b/lib/features/peg_in/providers/peg_in.dart index 2936925..e51406f 100644 --- a/lib/features/peg_in/providers/peg_in.dart +++ b/lib/features/peg_in/providers/peg_in.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:apparatus_wallet/features/peg_in/logic/bridge_api.dart'; +import 'package:apparatus_wallet/features/peg_in/logic/bridge_api_interface.dart'; import 'package:apparatus_wallet/features/peg_in/providers/bridge_api.dart'; import 'package:brambldart/brambldart.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; diff --git a/lib/router.dart b/lib/router.dart index 158643e..e0b4f70 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; import 'package:apparatus_wallet/features/bridge/bridge_ui.dart'; -import 'package:apparatus_wallet/features/peg_in/logic/bridge_api.dart'; +import 'package:apparatus_wallet/features/peg_in/logic/bridge_api_interface.dart'; import 'package:apparatus_wallet/features/peg_in/widgets/peg_in.dart'; /// const routes # TODO move to own file once we get more routes From 3da402ae2a45a17af7c86f94475a854a3f2321c0 Mon Sep 17 00:00:00 2001 From: Sean Cheatham Date: Wed, 17 Jul 2024 16:50:44 -0400 Subject: [PATCH 8/8] Update integration-test to newer bridge implementation. Update PegInPage to use riverpod --- .../bridge-consensus/application.conf | 40 +++ .../bridge-consensus.Dockerfile | 4 + .../bridge-consensus/bridge_custom_launch.sh | 5 + .../bridge-public-api/application.conf | 31 ++ integration-test/docker-compose.yaml | 42 +-- integration-test/init/bridge-init.Dockerfile | 4 +- integration-test/init/bridge_init.sh | 7 + .../init/extract_group_series_id.sh | 18 ++ lib/features/peg_in/providers/bridge_api.dart | 2 +- lib/features/peg_in/widgets/peg_in.dart | 273 ++++++------------ lib/router.dart | 17 +- 11 files changed, 221 insertions(+), 222 deletions(-) create mode 100644 integration-test/bridge-consensus/application.conf create mode 100644 integration-test/bridge-consensus/bridge-consensus.Dockerfile create mode 100644 integration-test/bridge-consensus/bridge_custom_launch.sh create mode 100644 integration-test/bridge-public-api/application.conf create mode 100644 integration-test/init/extract_group_series_id.sh diff --git a/integration-test/bridge-consensus/application.conf b/integration-test/bridge-consensus/application.conf new file mode 100644 index 0000000..7d70af9 --- /dev/null +++ b/integration-test/bridge-consensus/application.conf @@ -0,0 +1,40 @@ +bridge { + replica { + # the unique number that identifies this replica + replicaId = 0 + requests { + # the host where we are listening for requests + host = "[::]" + # the port where we are listening for requests + port = 4000 + } + # security configuration + security { + # path to the public key file + publicKeyFile = "/app/wallet/consensusPublicKey.pem" + # path to the private key file + privateKeyFile = "/app/wallet/consensusPrivateKey.pem" + } + consensus { + replicaCount = 1 + # map mapping each replica to its corresponding backend + replicas = { + 0 = { + publicKeyFile = "/app/wallet/publicKey.pem" + } + } + } + clients { + clientCount = 1 + # map mapping each client to its corresponding client + clients = { + 0 = { + publicKeyFile = "/app/wallet/clientPublicKey.pem" + host = "bridgepublicapi" + port = 6000 + secure = "false" + } + } + } + } +} \ No newline at end of file diff --git a/integration-test/bridge-consensus/bridge-consensus.Dockerfile b/integration-test/bridge-consensus/bridge-consensus.Dockerfile new file mode 100644 index 0000000..7825693 --- /dev/null +++ b/integration-test/bridge-consensus/bridge-consensus.Dockerfile @@ -0,0 +1,4 @@ +FROM ghcr.io/topl/topl-btc-bridge-consensus:latest + +ENTRYPOINT ["sh", "/bridge_custom_launch.sh"] +CMD [] diff --git a/integration-test/bridge-consensus/bridge_custom_launch.sh b/integration-test/bridge-consensus/bridge_custom_launch.sh new file mode 100644 index 0000000..e7e451e --- /dev/null +++ b/integration-test/bridge-consensus/bridge_custom_launch.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +export SERIES_ID=$(cat /app/wallet/seriesId.txt) +export GROUP_ID=$(cat /app/wallet/groupId.txt) +/opt/docker/bin/topl-btc-bridge-consensus --topl-host bifrost --btc-url http://bitcoin --zmq-host bitcoin --topl-wallet-seed-file /app/wallet/topl-wallet.json --topl-wallet-db /app/wallet/topl-wallet.db --btc-peg-in-seed-file /app/btc-wallet/peg-in-wallet.json --btc-wallet-seed-file /app/btc-wallet/btc-wallet.json --abtc-group-id $GROUP_ID --abtc-series-id $SERIES_ID --config-file /application.conf diff --git a/integration-test/bridge-public-api/application.conf b/integration-test/bridge-public-api/application.conf new file mode 100644 index 0000000..5ba2093 --- /dev/null +++ b/integration-test/bridge-public-api/application.conf @@ -0,0 +1,31 @@ +bridge { + client { + # the unique number that identifies this client + clientId = 0 + responses { + # the host where we are listening for responses + host = "[::]" + # the port where we are listening for responses + port = 6000 + } + # security configuration + security { + # path to the public key file + publicKeyFile = "/app/wallet/clientPublicKey.pem" + # path to the private key file + privateKeyFile = "/app/wallet/clientPrivateKey.pem" + } + consensus { + replicaCount = 1 + # map mapping each replica to its corresponding backend + replicas = { + 0 = { + publicKeyFile = "/app/wallet/consensusPublicKey.pem" + host = "bridgeconsensus" + port = 4000 + secure = "false" + } + } + } + } +} \ No newline at end of file diff --git a/integration-test/docker-compose.yaml b/integration-test/docker-compose.yaml index 264428b..4156fe3 100644 --- a/integration-test/docker-compose.yaml +++ b/integration-test/docker-compose.yaml @@ -9,25 +9,10 @@ services: - "18444:18444" - "18443:18443" - "28332:28332" - bridge: - image: toplprotocol/topl-btc-bridge:latest - command: - - "--topl-host" - - "bifrost" - - "--btc-url" - - "http://bitcoin" - - "--zmq-host" - - "bitcoin" - - "--topl-wallet-seed-file" - - "/app/wallet/topl-wallet.json" - - "--topl-wallet-db" - - "/app/wallet/topl-wallet.db" - - "--btc-peg-in-seed-file" - - "/app/btc-wallet/peg-in-wallet.json" - - "--btc-wallet-seed-file" - - "/app/btc-wallet/btc-wallet.json" - ports: - - "4000:4000" + bridgeconsensus: + build: + context: bridge-consensus + dockerfile: bridge-consensus.Dockerfile depends_on: bifrost: condition: service_started @@ -39,6 +24,25 @@ services: - wallet:/app/wallet - ./peg-in-wallet.json:/app/btc-wallet/peg-in-wallet.json - ./btc-wallet.json:/app/btc-wallet/btc-wallet.json + - ./bridge-consensus/bridge_custom_launch.sh:/bridge_custom_launch.sh + - ./bridge-consensus/application.conf:/application.conf + bridgepublicapi: + image: ghcr.io/topl/topl-btc-bridge-public-api:latest + command: + - "--config-file" + - "/application.conf" + ports: + - "5000:5000" + depends_on: + bifrost: + condition: service_started + bitcoin: + condition: service_started + bridge_init: + condition: service_completed_successfully + volumes: + - wallet:/app/wallet + - ./bridge-public-api/application.conf:/application.conf envoy: build: context: envoy diff --git a/integration-test/init/bridge-init.Dockerfile b/integration-test/init/bridge-init.Dockerfile index 8e5d64f..d085ef0 100644 --- a/integration-test/init/bridge-init.Dockerfile +++ b/integration-test/init/bridge-init.Dockerfile @@ -1,7 +1,7 @@ FROM eclipse-temurin:11-jdk RUN apt update \ - && apt install --no-install-recommends -y curl gzip wget \ + && apt install --no-install-recommends -y curl gzip wget openssl \ && apt-get clean RUN mkdir /bitcoin && wget -qO- https://bitcoin.org/bin/bitcoin-core-27.0/bitcoin-27.0-x86_64-linux-gnu.tar.gz | tar xvz -C /bitcoin && install -m 0755 -o root -g root -t /usr/local/bin /bitcoin/bitcoin-27.0/bin/* @@ -21,5 +21,7 @@ USER 1001:0 RUN cs setup --yes RUN cs fetch -r https://s01.oss.sonatype.org/content/repositories/releases co.topl:brambl-cli_2.13:2.0.0-beta5 +COPY extract_group_series_id.sh /extract_group_series_id.sh + ENTRYPOINT ["sh", "/bridge_init.sh"] CMD [] diff --git a/integration-test/init/bridge_init.sh b/integration-test/init/bridge_init.sh index 700b1ea..6d1f483 100644 --- a/integration-test/init/bridge_init.sh +++ b/integration-test/init/bridge_init.sh @@ -13,6 +13,11 @@ export TOPL_WALLET_MNEMONIC=/app/wallet/topl-mnemonic.txt rm -f $TOPL_WALLET_DB $TOPL_WALLET_JSON $TOPL_WALLET_MNEMONIC +openssl ecparam -name secp256k1 -genkey -noout -out /app/wallet/consensusPrivateKey.pem +openssl ec -in /app/wallet/consensusPrivateKey.pem -pubout -out /app/wallet/consensusPublicKey.pem +openssl ecparam -name secp256k1 -genkey -noout -out /app/wallet/clientPrivateKey.pem +openssl ec -in /app/wallet/clientPrivateKey.pem -pubout -out /app/wallet/clientPublicKey.pem + bitcoin-cli -regtest -named -rpcconnect=bitcoin -rpcuser=bitcoin -rpcpassword=password createwallet wallet_name=testwallet export BTC_ADDRESS=`bitcoin-cli -rpcconnect=bitcoin -rpcuser=$BTC_USER -rpcpassword=$BTC_PASSWORD -rpcwallet=testwallet -regtest getnewaddress` echo BTC Address: $BTC_ADDRESS @@ -47,3 +52,5 @@ export ASSET_UTXO=$(brambl-cli tx broadcast -i seriesMintingTxProved.pbuf -h bif echo "ASSET_UTXO: $ASSET_UTXO" until brambl-cli genus-query utxo-by-address --host bifrost --port 9084 --secure false --walletdb $TOPL_WALLET_DB; do sleep 5; done brambl-cli wallet balance --from-fellowship self --from-template default --walletdb $TOPL_WALLET_DB --host bifrost --port 9084 --secure false +echo $(brambl-cli genus-query utxo-by-address -h bifrost --port 9084 --secure false --walletdb $TOPL_WALLET_DB | /bin/bash /extract_group_series_id.sh "Group Constructor") > /app/wallet/groupId.txt +echo $(brambl-cli genus-query utxo-by-address -h bifrost --port 9084 --secure false --walletdb $TOPL_WALLET_DB | /bin/bash /extract_group_series_id.sh "Series Constructor") > /app/wallet/seriesId.txt diff --git a/integration-test/init/extract_group_series_id.sh b/integration-test/init/extract_group_series_id.sh new file mode 100644 index 0000000..6e52772 --- /dev/null +++ b/integration-test/init/extract_group_series_id.sh @@ -0,0 +1,18 @@ + +#!/bin/bash + +TOKEN_TYPE=$1 +# Extract the Id of series or group tokens from the input + +while IFS= read -r line; do + # Check if the line contains "Type" and if the next line should be read + if [[ $line == *"Type"* && $line == *$TOKEN_TYPE* ]]; then + read_next_line=true + elif [ "$read_next_line" = true ]; then + # If the next line should be read and contains "Id", extract and print the Id + if [[ $line == "Id"* ]]; then + echo "$line" | awk '{print $NF}' + fi + read_next_line=false + fi +done < /dev/stdin # Read from stdin diff --git a/lib/features/peg_in/providers/bridge_api.dart b/lib/features/peg_in/providers/bridge_api.dart index a7ae196..30285d9 100644 --- a/lib/features/peg_in/providers/bridge_api.dart +++ b/lib/features/peg_in/providers/bridge_api.dart @@ -8,5 +8,5 @@ class BridgeApi extends _$BridgeApi { @override BridgeApiInterface build() => // TODO: Don't hardcode - BridgeApiInterface(baseAddress: "http://localhost:4000"); + BridgeApiInterface(baseAddress: "http://localhost:5000"); } diff --git a/lib/features/peg_in/widgets/peg_in.dart b/lib/features/peg_in/widgets/peg_in.dart index baea093..fab7cd5 100644 --- a/lib/features/peg_in/widgets/peg_in.dart +++ b/lib/features/peg_in/widgets/peg_in.dart @@ -1,83 +1,56 @@ -import 'dart:convert'; - -import 'package:crypto/crypto.dart'; +import 'package:apparatus_wallet/features/peg_in/providers/peg_in.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:provider/provider.dart'; -import 'package:apparatus_wallet/features/peg_in/logic/bridge_api_interface.dart'; -import 'package:uuid/uuid.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; /// A scaffolded page which allows a user to begin a new Peg-In session -// TODO: Update to use Riverpod -class PegInPage extends StatefulWidget { +class PegInPage extends HookConsumerWidget { const PegInPage({super.key}); @override - State createState() => _PegInPageState(); -} - -class _PegInPageState extends State { - bool started = false; - String? error; - StartSessionResponse? session; - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: const Text("Acquire tBTC"), ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - (session != null) - ? continueButton(context, session!) - : (error != null) - ? errorWidget(error!) - : started - ? const CircularProgressIndicator() - : startSessionButton(context), - ], - ), - ), + body: Center(child: body(ref)), ); } - Widget startSessionButton(BuildContext context) { - final bridgeApi = context.watch(); - return TextButton.icon( - onPressed: () => onStartSession(bridgeApi), - icon: const Icon(Icons.start), - label: const Text("Start Session")); - } - - void onStartSession(BridgeApiInterface bridgeApi) async { - setState(() => started = true); - final uuid = const Uuid().v4(); - final hashed = sha256.convert(utf8.encode(uuid)).toString(); - final request = StartSessionRequest( - pkey: - "0295bb5a3b80eeccb1e38ab2cbac2545e9af6c7012cdc8d53bd276754c54fc2e4a", - sha256: hashed); - try { - final response = await bridgeApi.startSession(request); - setState(() => session = response); - } catch (e) { - setState(() => error = "An error occurred. Lol! $e"); + Widget body(WidgetRef ref) { + final state = ref.watch(pegInProvider); + if (state.sessionStarted) { + if (state.escrowAddress == null || state.sessionID == null) { + return const CircularProgressIndicator(); + } else { + if (state.tBtcMinted) { + return PegInClaimFundsPage( + onAccepted: () => + ref.watch(pegInProvider.notifier).tBtcAccepted()); + } + if (state.btcDeposited) { + return PegInAwaitingFundsStage(sessionID: state.sessionID!); + } else { + return PegInDepositFundsStage( + sessionID: state.sessionID!, + escrowAddress: state.escrowAddress!, + onDeposit: () => ref.watch(pegInProvider.notifier).btcDeposited(), + ); + } + } + } else { + return startSessionButton(ref); } } - Widget continueButton(BuildContext context, StartSessionResponse session) => - TextButton.icon( - onPressed: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => PegInDepositFundsPage( - sessionID: session.sessionID, - escrowAddress: session.escrowAddress))), - icon: const Icon(Icons.forward), - label: const Text("Continue")); + Widget startSessionButton(WidgetRef ref) { + return TextButton.icon( + onPressed: () => ref.read(pegInProvider.notifier).startSession(), + icon: const Icon(Icons.start), + label: const Text("Start Session"), + ); + } Widget errorWidget(String message) => Text(message, style: const TextStyle( @@ -85,152 +58,80 @@ class _PegInPageState extends State { } /// A scaffolded page instructing users where to deposit BTC funds -class PegInDepositFundsPage extends StatefulWidget { - const PegInDepositFundsPage( - {super.key, required this.sessionID, required this.escrowAddress}); +class PegInDepositFundsStage extends StatelessWidget { + const PegInDepositFundsStage( + {super.key, + required this.sessionID, + required this.escrowAddress, + required this.onDeposit}); final String sessionID; final String escrowAddress; + final Function() onDeposit; @override - State createState() => _PegInDepositFundsPageState(); -} - -class _PegInDepositFundsPageState extends State { - @override - Widget build(BuildContext context) => Scaffold( - appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - title: const Text("Acquire tBTC: Deposit BTC Funds"), - ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Please send BTC to the following address.', - style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), - TextButton.icon( - onPressed: () => Clipboard.setData( - ClipboardData(text: widget.escrowAddress)), - icon: const Icon(Icons.copy), - label: Text(widget.escrowAddress, - style: const TextStyle( - fontSize: 14, fontWeight: FontWeight.w100)), - ), - TextButton.icon( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => PegInAwaitingFundsPage( - sessionID: widget.sessionID))); - }, - icon: const Icon(Icons.start), - label: const Text("Next")), - ], - ), + Widget build(BuildContext context) => Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Please send BTC to the following address.', + style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), + TextButton.icon( + onPressed: () => + Clipboard.setData(ClipboardData(text: escrowAddress)), + icon: const Icon(Icons.copy), + label: Text(escrowAddress, + style: const TextStyle( + fontSize: 14, fontWeight: FontWeight.w100)), + ), + TextButton.icon( + onPressed: onDeposit, + icon: const Icon(Icons.start), + label: const Text("Next"), + ), + ], ), ); } /// A scaffolded page which waits for the API to indicate funds have been deposited -class PegInAwaitingFundsPage extends StatefulWidget { - const PegInAwaitingFundsPage({super.key, required this.sessionID}); +class PegInAwaitingFundsStage extends StatelessWidget { + const PegInAwaitingFundsStage({super.key, required this.sessionID}); final String sessionID; @override - State createState() => _PegInAwaitingFundsPageState(); -} - -class _PegInAwaitingFundsPageState extends State { - @override - Widget build(BuildContext context) => Scaffold( - appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - title: const Text("Acquire tBTC: Awaiting BTC Transfer"), - ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text('Please wait while we confirm the BTC transfer.', - style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), - FutureBuilder( - future: awaitRedemption(context), - builder: (context, snapshot) => snapshot.hasError - ? errorOccurred(snapshot.error) - : snapshot.hasData - ? successButton - : waiting, - ), - ], + Widget build(BuildContext context) => const Column( + children: [ + Center( + child: Text('Please wait while we confirm the BTC transfer.', + style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), ), - ), + CircularProgressIndicator(), + ], ); - - Future awaitRedemption( - BuildContext context) async { - BridgeApiInterface client = context.watch(); - MintingStatus? status = await client.getMintingStatus(widget.sessionID); - while (status is! MintingStatus_PeginSessionWaitingForRedemption) { - ArgumentError.checkNotNull(status); - await Future.delayed(const Duration(seconds: 5)); - status = await client.getMintingStatus(widget.sessionID); - } - return status; - } - - Widget get waiting => const CircularProgressIndicator(); - - Widget get successButton => TextButton.icon( - onPressed: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const PegInClaimFundsPage())); - }, - icon: const Icon(Icons.wallet), - label: const Text("Next")); - Widget errorOccurred(Object? e) => Text('An error occurred. Lol. $e', - style: const TextStyle( - fontSize: 14, fontWeight: FontWeight.bold, color: Colors.red)); } /// A scaffolded page instructing users that tBTC funds are now available in their wallet -class PegInClaimFundsPage extends StatefulWidget { - const PegInClaimFundsPage({super.key}); +class PegInClaimFundsPage extends StatelessWidget { + const PegInClaimFundsPage({super.key, required this.onAccepted}); - @override - State createState() => _PegInClaimFundsPageState(); -} + final Function() onAccepted; -class _PegInClaimFundsPageState extends State { @override - Widget build(BuildContext context) => Scaffold( - appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - title: const Text("Acquire tBTC: Done"), - ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text( - 'Your tBTC is ready and available for transfer in your wallet.', - style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), - TextButton.icon( - onPressed: () { - Navigator.pushAndRemoveUntil( - context, - MaterialPageRoute( - builder: (context) => const PegInPage()), - (_) => true); - }, - icon: const Icon(Icons.done), - label: const Text("Back")), - ], - ), + Widget build(BuildContext context) => Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'Your tBTC is ready and available for transfer in your wallet.', + style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)), + TextButton.icon( + onPressed: onAccepted, + icon: const Icon(Icons.done), + label: const Text("Back"), + ), + ], ), ); } diff --git a/lib/router.dart b/lib/router.dart index e0b4f70..ea4213a 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -1,10 +1,7 @@ import 'package:apparatus_wallet/features/bridge/widgets/utxos_view.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:provider/provider.dart'; import 'package:apparatus_wallet/features/bridge/bridge_ui.dart'; -import 'package:apparatus_wallet/features/peg_in/logic/bridge_api_interface.dart'; import 'package:apparatus_wallet/features/peg_in/widgets/peg_in.dart'; /// const routes # TODO move to own file once we get more routes @@ -12,25 +9,15 @@ const homeRoute = '/'; const swapRoute = '/swap'; const walletRoute = '/wallet'; -final providedNavigator = - GlobalKey(); //TODO: remove with provider refactor - // GoRouter configuration final router = GoRouter( - initialLocation: walletRoute, + initialLocation: homeRoute, debugLogDiagnostics: !kReleaseMode ? true : false, // Turns logging off when in release mode routes: [ GoRoute( path: homeRoute, - builder: (context, state) => Provider( - create: (context) => - BridgeApiInterface(baseAddress: "http://localhost:4000"), - child: Navigator( - key: providedNavigator, - onGenerateRoute: (settings) => MaterialPageRoute( - builder: (context) => const PegInPage(), - settings: settings))), + builder: (context, state) => const PegInPage(), ), GoRoute( path: swapRoute,