diff --git a/packages/espressocash_app/lib/config.dart b/packages/espressocash_app/lib/config.dart index 93cde2b565..5b861de475 100644 --- a/packages/espressocash_app/lib/config.dart +++ b/packages/espressocash_app/lib/config.dart @@ -77,3 +77,6 @@ const guardarianBaseUrl = 'https://guardarian.com/calculator/v1'; const guardarianApiKey = 'ef6d4999-80ad-4ab2-bcfc-4c0669d2e070'; const maxPayloadsPerSigningRequest = 10; + +// Escrow payment address +const escrowScAddress = '7rE2We9zMQzj2xmhJRTvYXKP22VKDGh3krujdBqWibBL'; diff --git a/packages/espressocash_app/lib/core/escrow_private_key.dart b/packages/espressocash_app/lib/core/escrow_private_key.dart index 42fdaffd4b..254553953b 100644 --- a/packages/espressocash_app/lib/core/escrow_private_key.dart +++ b/packages/espressocash_app/lib/core/escrow_private_key.dart @@ -3,6 +3,8 @@ import 'package:solana/solana.dart'; part 'escrow_private_key.freezed.dart'; +typedef EscrowPublicKey = Ed25519HDPublicKey; + @freezed class EscrowPrivateKey with _$EscrowPrivateKey { factory EscrowPrivateKey(List bytes) = _EscrowPrivateKey; diff --git a/packages/espressocash_app/lib/data/db/db.dart b/packages/espressocash_app/lib/data/db/db.dart index d2fa17deb8..4580494d1f 100644 --- a/packages/espressocash_app/lib/data/db/db.dart +++ b/packages/espressocash_app/lib/data/db/db.dart @@ -27,7 +27,7 @@ class OutgoingTransferRows extends Table { Set> get primaryKey => {id}; } -const int latestVersion = 36; +const int latestVersion = 37; const _tables = [ OutgoingTransferRows, @@ -151,6 +151,9 @@ class MyDatabase extends _$MyDatabase { if (from < 36) { await m.deleteTable('i_s_l_p_rows'); } + if (from >= 16 && from < 37) { + await m.addColumn(oSKPRows, oSKPRows.publicKey); + } }, ); diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/data/repository.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/data/repository.dart index 3a1fa7e80a..45c905c308 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/data/repository.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/data/repository.dart @@ -1,5 +1,6 @@ // ignore_for_file: avoid-non-null-assertion +import 'package:collection/collection.dart'; import 'package:dfunc/dfunc.dart'; import 'package:drift/drift.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; @@ -7,6 +8,7 @@ import 'package:injectable/injectable.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:solana/base58.dart'; import 'package:solana/encoder.dart'; +import 'package:solana/solana.dart'; import '../../../core/amount.dart'; import '../../../core/api_version.dart'; @@ -72,6 +74,7 @@ class OSKPRepository { OSKPStatusDto.cancelTxSent, OSKPStatusDto.cancelTxWaitFailure, OSKPStatusDto.cancelTxFailure, + OSKPStatusDto.recovered, ]); Stream> watchTxCreated() => @@ -103,6 +106,22 @@ class OSKPRepository { OSKPStatusDto.cancelTxWaitFailure, ]); + Stream> watchRecoverCancelTxCreated() => + _watchWithStatuses([ + OSKPStatusDto.recoveredCancelTxCreated, + ]); + + Stream> watchRecoverCancelTxSent() => + _watchWithStatuses([ + OSKPStatusDto.recoveredCancelTxSent, + ]); + + Stream> watchPending() => _watchWithStatuses( + OSKPStatusDto.values.whereNot( + (e) => e == OSKPStatusDto.withdrawn || e == OSKPStatusDto.canceled, + ), + ); + Future clear() => _db.delete(_db.oSKPRows).go(); Stream> _watchWithStatuses( @@ -141,6 +160,7 @@ class OSKPRows extends Table with AmountMixin, EntityMixin { TextColumn get slot => text().nullable()(); IntColumn get apiVersion => intEnum().withDefault(const Constant(0))(); + TextColumn get publicKey => text().nullable()(); } enum OSKPStatusDto { @@ -160,6 +180,10 @@ enum OSKPStatusDto { cancelTxSent, cancelTxSendFailure, cancelTxWaitFailure, + recovered, + recoveredCancelTxFailure, + recoveredCancelTxSent, + recoveredCancelTxCreated, } extension OSKPRowExt on OSKPRow { @@ -188,6 +212,7 @@ extension on OSKPStatusDto { final txId = row.txId; final withdrawTxId = row.withdrawTxId; final escrow = row.privateKey?.let(base58decode).let(EscrowPrivateKey.new); + final escrowPubkey = row.publicKey?.let(Ed25519HDPublicKey.fromBase58); final link1 = row.link1?.let(Uri.parse); final link2 = row.link2?.let(Uri.parse); final link3 = row.link3?.let(Uri.tryParse); @@ -259,6 +284,27 @@ extension on OSKPStatusDto { escrow: escrow!, slot: slot ?? BigInt.zero, ); + case OSKPStatusDto.recovered: + return OSKPStatus.recovered( + escrowPubKey: escrowPubkey!, + ); + case OSKPStatusDto.recoveredCancelTxFailure: + return OSKPStatus.recoveredCancelTxFailure( + escrowPubKey: escrowPubkey!, + reason: row.txFailureReason ?? TxFailureReason.unknown, + ); + case OSKPStatusDto.recoveredCancelTxSent: + return OSKPStatus.recoveredCancelTxSent( + cancelTx!, + escrowPubKey: escrowPubkey!, + slot: slot ?? BigInt.zero, + ); + case OSKPStatusDto.recoveredCancelTxCreated: + return OSKPStatus.recoveredCancelTxCreated( + cancelTx!, + escrowPubKey: escrowPubkey!, + slot: slot ?? BigInt.zero, + ); } } } @@ -284,6 +330,7 @@ extension on OutgoingSplitKeyPayment { generatedLinksAt: linksGeneratedAt, resolvedAt: status.toResolvedAt(), apiVersion: apiVersion.toDto(), + publicKey: status.toPublicKey(), ); } @@ -299,6 +346,12 @@ extension on OSKPStatus { cancelTxCreated: always(OSKPStatusDto.cancelTxCreated), cancelTxFailure: always(OSKPStatusDto.cancelTxFailure), cancelTxSent: always(OSKPStatusDto.cancelTxSent), + recovered: always(OSKPStatusDto.recovered), + recoveredCancelTxFailure: + always(OSKPStatusDto.recoveredCancelTxFailure), + recoveredCancelTxSent: always(OSKPStatusDto.recoveredCancelTxSent), + recoveredCancelTxCreated: + always(OSKPStatusDto.recoveredCancelTxCreated), ); String? toTx() => mapOrNull( @@ -316,12 +369,14 @@ extension on OSKPStatus { String? toCancelTx() => mapOrNull( cancelTxCreated: (it) => it.tx.encode(), cancelTxSent: (it) => it.tx.encode(), + recoveredCancelTxCreated: (it) => it.tx.encode(), ); String? toCancelTxId() => mapOrNull( cancelTxCreated: (it) => it.tx.id, cancelTxSent: (it) => it.tx.id, canceled: (it) => it.txId, + recoveredCancelTxCreated: (it) => it.tx.id, ); Future toPrivateKey() async => this.map( @@ -332,9 +387,13 @@ extension on OSKPStatus { withdrawn: (it) async => null, canceled: (it) async => null, txFailure: (it) async => null, - cancelTxCreated: (it) async => base58encode(it.escrow.bytes), + cancelTxCreated: (it) async => null, cancelTxFailure: (it) async => base58encode(it.escrow.bytes), cancelTxSent: (it) async => base58encode(it.escrow.bytes), + recovered: (it) async => null, + recoveredCancelTxFailure: (it) async => null, + recoveredCancelTxSent: (it) async => null, + recoveredCancelTxCreated: (it) async => null, ); String? toLink1() => mapOrNull( @@ -365,6 +424,13 @@ extension on OSKPStatus { cancelTxCreated: (it) => it.slot, cancelTxSent: (it) => it.slot, ); + + String? toPublicKey() => mapOrNull( + recovered: (it) => it.escrowPubKey.toBase58(), + recoveredCancelTxCreated: (it) => it.escrowPubKey.toBase58(), + recoveredCancelTxSent: (it) => it.escrowPubKey.toBase58(), + recoveredCancelTxFailure: (it) => it.escrowPubKey.toBase58(), + ); } extension on SplitKeyApiVersion { diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/models/outgoing_split_key_payment.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/models/outgoing_split_key_payment.dart index 0872d8526c..d03e1808d0 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/models/outgoing_split_key_payment.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/models/outgoing_split_key_payment.dart @@ -1,5 +1,6 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:solana/encoder.dart'; +import 'package:solana/solana.dart'; import '../../../core/amount.dart'; import '../../../core/api_version.dart'; @@ -94,4 +95,30 @@ class OSKPStatus with _$OSKPStatus { required BigInt slot, required EscrowPrivateKey escrow, }) = OSKPStatusCancelTxSent; + + /// Tx was recovered from the blockchain. + /// These transactions cannot be resent, they can only be cancelled + const factory OSKPStatus.recovered({ + required EscrowPublicKey escrowPubKey, + }) = OSKPStatusRecovered; + + /// Similar to [OSKPStatus.cancelTxCreated], but only contains public key + const factory OSKPStatus.recoveredCancelTxCreated( + SignedTx tx, { + required BigInt slot, + required EscrowPublicKey escrowPubKey, + }) = OSKPStatusRecoveredCancelTxCreated; + + /// Similar to [OSKPStatus.cancelTxSent], but only contains public key + const factory OSKPStatus.recoveredCancelTxSent( + SignedTx tx, { + required BigInt slot, + required EscrowPublicKey escrowPubKey, + }) = OSKPStatusRecoveredCancelTxSent; + + /// Similar to [OSKPStatus.cancelTxFailure], but only contains public key + const factory OSKPStatus.recoveredCancelTxFailure({ + required TxFailureReason reason, + required EscrowPublicKey escrowPubKey, + }) = OSKPStatusRecoveredCancelTxFailure; } diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/module.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/module.dart index 90d575dc61..af4dd3f877 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/module.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/module.dart @@ -9,6 +9,9 @@ import '../balances/widgets/context_ext.dart'; import 'data/repository.dart'; import 'services/cancel_tx_created_watcher.dart'; import 'services/cancel_tx_sent_watcher.dart'; +import 'services/recover_cancel_tx_created_watcher.dart'; +import 'services/recover_cancel_tx_sent_watcher.dart'; +import 'services/recover_pending_watcher.dart'; import 'services/tx_confirmed_watcher.dart'; import 'services/tx_created_watcher.dart'; import 'services/tx_ready_watcher.dart'; @@ -57,6 +60,24 @@ class OSKPModule extends SingleChildStatelessWidget { ..call(onBalanceAffected: () => context.notifyBalanceAffected()), dispose: (_, value) => value.dispose(), ), + Provider( + lazy: false, + create: (context) => sl() + ..call(onBalanceAffected: () => context.notifyBalanceAffected()), + dispose: (_, value) => value.dispose(), + ), + Provider( + lazy: false, + create: (context) => sl() + ..call(onBalanceAffected: () => context.notifyBalanceAffected()), + dispose: (_, value) => value.dispose(), + ), + Provider( + lazy: false, + create: (context) => sl( + param1: context.read().wallet.publicKey, + )..init(), + ), ], child: LogoutListener( onLogout: (_) => sl().clear(), diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/screens/oskp_screen.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/screens/oskp_screen.dart index 99b1dfcdd8..d301a31ea2 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/screens/oskp_screen.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/screens/oskp_screen.dart @@ -139,6 +139,7 @@ class _OSKPScreenState extends State { onPressed: onCancel, ), ], + recovered: (s) => [cancelButton], orElse: () => const [], ); @@ -153,6 +154,10 @@ class _OSKPScreenState extends State { cancelTxCreated: always(CpStatusType.info), cancelTxFailure: always(CpStatusType.error), cancelTxSent: always(CpStatusType.info), + recovered: always(CpStatusType.info), + recoveredCancelTxFailure: always(CpStatusType.error), + recoveredCancelTxCreated: always(CpStatusType.info), + recoveredCancelTxSent: always(CpStatusType.info), ); final String? statusTitle = payment.status.mapOrNull( @@ -190,6 +195,7 @@ class _OSKPScreenState extends State { canceled: always(CpTimelineStatus.neutral), txFailure: always(CpTimelineStatus.failure), cancelTxFailure: always(CpTimelineStatus.failure), + recoveredCancelTxFailure: always(CpTimelineStatus.failure), ) ?? CpTimelineStatus.inProgress; @@ -204,6 +210,10 @@ class _OSKPScreenState extends State { txConfirmed: always(1), linksReady: always(1), withdrawn: always(2), + recovered: always(1), + recoveredCancelTxFailure: always(0), + recoveredCancelTxCreated: always(1), + recoveredCancelTxSent: always(1), ); final paymentInitiated = CpTimelineItem( @@ -241,11 +251,14 @@ class _OSKPScreenState extends State { cancelTxCreated: always(cancelingItems), cancelTxFailure: always(cancelingItems), cancelTxSent: always(cancelingItems), + recoveredCancelTxCreated: always(cancelingItems), + recoveredCancelTxFailure: always(cancelingItems), + recoveredCancelTxSent: always(cancelingItems), ) ?? normalItems; final animated = timelineStatus == CpTimelineStatus.inProgress && - payment.status.maybeMap(orElse: T, linksReady: F); + payment.status.maybeMap(orElse: T, linksReady: F, recovered: F); return StatusScreen( onBackButtonPressed: () => context.router.pop(), diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/services/oskp_service.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/services/oskp_service.dart index dfdae6e045..d6be54afe7 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/services/oskp_service.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/services/oskp_service.dart @@ -77,6 +77,11 @@ class OSKPService { txId: null, timestamp: DateTime.now(), ); + } else if (status is OSKPStatusRecovered) { + newStatus = await _createRecoveredCancelTx( + escrow: status.escrowPubKey, + account: account, + ); } else { final escrow = status.mapOrNull( linksReady: (it) => it.escrow, @@ -191,4 +196,36 @@ class OSKPService { ); } } + + Future _createRecoveredCancelTx({ + required EscrowPublicKey escrow, + required ECWallet account, + }) async { + try { + final dto = CancelPaymentRequestDto( + senderAccount: account.address, + escrowAccount: escrow.toBase58(), + cluster: apiCluster, + ); + + final response = await _client.cancelPaymentEc(dto); + + final transaction = response.transaction; + final slot = response.slot; + final tx = await transaction + .let(SignedTx.decode) + .let((it) => it.resign(account)); + + return OSKPStatus.recoveredCancelTxCreated( + tx, + escrowPubKey: escrow, + slot: slot, + ); + } on Exception { + return OSKPStatus.recoveredCancelTxFailure( + escrowPubKey: escrow, + reason: TxFailureReason.creatingFailure, + ); + } + } } diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/services/payment_watcher.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/services/payment_watcher.dart index 2c70fa9460..669d4c1ffb 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/services/payment_watcher.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/services/payment_watcher.dart @@ -76,5 +76,9 @@ extension on OSKPStatus { cancelTxCreated: F, cancelTxFailure: F, cancelTxSent: F, + recovered: F, + recoveredCancelTxCreated: F, + recoveredCancelTxSent: F, + recoveredCancelTxFailure: F, ); } diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/services/recover_cancel_tx_created_watcher.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/services/recover_cancel_tx_created_watcher.dart new file mode 100644 index 0000000000..d0bb287268 --- /dev/null +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/services/recover_cancel_tx_created_watcher.dart @@ -0,0 +1,68 @@ +import 'dart:async'; + +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:injectable/injectable.dart'; + +import '../../../../core/cancelable_job.dart'; +import '../../transactions/models/tx_sender.dart'; +import '../../transactions/services/tx_sender.dart'; +import '../data/repository.dart'; +import '../models/outgoing_split_key_payment.dart'; +import 'payment_watcher.dart'; + +/// Watches for [OSKPStatus.recoveredCancelTxCreated] payments and sends the tx. +/// +/// The watcher will try to submit the tx until it's accepted or rejected. +@injectable +class RecoverCancelTxCreatedWatcher extends PaymentWatcher { + RecoverCancelTxCreatedWatcher(super._repository, this._sender); + + final TxSender _sender; + + @override + CancelableJob createJob( + OutgoingSplitKeyPayment payment, + ) => + _OSKPRecoverCancelTxCreatedJob(payment, _sender); + + @override + Stream> watchPayments( + OSKPRepository repository, + ) => + repository.watchRecoverCancelTxCreated(); +} + +class _OSKPRecoverCancelTxCreatedJob + extends CancelableJob { + const _OSKPRecoverCancelTxCreatedJob(this.payment, this.sender); + + final OutgoingSplitKeyPayment payment; + final TxSender sender; + + @override + Future process() async { + final status = payment.status; + if (status is! OSKPStatusRecoveredCancelTxCreated) return payment; + + final tx = await sender.send(status.tx, minContextSlot: status.slot); + + final OSKPStatus? newStatus = tx.map( + sent: (_) => OSKPStatus.recoveredCancelTxSent( + status.tx, + escrowPubKey: status.escrowPubKey, + slot: status.slot, + ), + invalidBlockhash: (_) => OSKPStatus.recoveredCancelTxFailure( + reason: TxFailureReason.invalidBlockhashSending, + escrowPubKey: status.escrowPubKey, + ), + failure: (it) => OSKPStatus.recoveredCancelTxFailure( + reason: it.reason, + escrowPubKey: status.escrowPubKey, + ), + networkError: (_) => null, + ); + + return newStatus == null ? null : payment.copyWith(status: newStatus); + } +} diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/services/recover_cancel_tx_sent_watcher.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/services/recover_cancel_tx_sent_watcher.dart new file mode 100644 index 0000000000..5566d7de6c --- /dev/null +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/services/recover_cancel_tx_sent_watcher.dart @@ -0,0 +1,61 @@ +import 'dart:async'; + +import 'package:fast_immutable_collections/fast_immutable_collections.dart'; +import 'package:injectable/injectable.dart'; + +import '../../../../core/cancelable_job.dart'; +import '../../transactions/services/tx_sender.dart'; +import '../data/repository.dart'; +import '../models/outgoing_split_key_payment.dart'; +import 'payment_watcher.dart'; + +/// Watches for [OSKPStatus.recoveredCancelTxSent] payments and waits for the tx to be +/// confirmed. +@injectable +class RecoverCancelTxSentWatcher extends PaymentWatcher { + RecoverCancelTxSentWatcher(super._repository, this._sender); + + final TxSender _sender; + + @override + CancelableJob createJob( + OutgoingSplitKeyPayment payment, + ) => + _OSKPRecoverCancelTxSentJob(payment, _sender); + + @override + Stream> watchPayments( + OSKPRepository repository, + ) => + repository.watchRecoverCancelTxSent(); +} + +class _OSKPRecoverCancelTxSentJob + extends CancelableJob { + const _OSKPRecoverCancelTxSentJob(this.payment, this.sender); + + final OutgoingSplitKeyPayment payment; + final TxSender sender; + + @override + Future process() async { + final status = payment.status; + if (status is! OSKPStatusRecoveredCancelTxSent) return payment; + + final tx = await sender.wait(status.tx, minContextSlot: status.slot); + + final OSKPStatus? newStatus = tx.map( + success: (_) => OSKPStatus.canceled( + txId: status.tx.id, + timestamp: DateTime.now(), + ), + failure: (tx) => OSKPStatus.recoveredCancelTxFailure( + reason: tx.reason, + escrowPubKey: status.escrowPubKey, + ), + networkError: (_) => null, + ); + + return newStatus == null ? null : payment.copyWith(status: newStatus); + } +} diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/services/recover_pending_watcher.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/services/recover_pending_watcher.dart new file mode 100644 index 0000000000..c58cd2308c --- /dev/null +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/services/recover_pending_watcher.dart @@ -0,0 +1,134 @@ +import 'package:collection/collection.dart'; +import 'package:dfunc/dfunc.dart'; +import 'package:injectable/injectable.dart'; +import 'package:solana/dto.dart'; +import 'package:solana/encoder.dart'; +import 'package:solana/solana.dart'; +import 'package:uuid/uuid.dart'; + +import '../../../../config.dart'; +import '../../../../core/amount.dart'; +import '../../../../core/api_version.dart'; +import '../../../../core/currency.dart'; +import '../../../../core/escrow_private_key.dart'; +import '../../../../core/tokens/token.dart'; +import '../data/repository.dart'; +import '../models/outgoing_split_key_payment.dart'; + +@injectable +class RecoverPendingWatcher { + const RecoverPendingWatcher( + this._client, + this._repository, { + @factoryParam required Ed25519HDPublicKey userPublicKey, + }) : _userPublicKey = userPublicKey; + + final SolanaClient _client; + final OSKPRepository _repository; + final Ed25519HDPublicKey _userPublicKey; + + Future init() async { + const fetchLimit = 100; + + final details = await _client.rpcClient.getTransactionsList( + limit: fetchLimit, + _userPublicKey, + encoding: Encoding.base64, + commitment: Commitment.confirmed, + ); + + final pendingEscrows = await _pendingEscrows(); + + for (final detail in details) { + final rawTx = detail.transaction as RawTransaction; + final tx = SignedTx.fromBytes(rawTx.data); + + // Check if the transaction has interacted with the escrow smart contract + final accounts = tx.compiledMessage.accountKeys; + final hasInteractedWithEscrow = accounts.contains( + Ed25519HDPublicKey.fromBase58(escrowScAddress), + ); + + if (!hasInteractedWithEscrow) continue; + + // Find the escrow address from accounts. It should either be in index 1 or 2. + // Index 0 is the platforms account, index 1 or 2 should either be the user or the escrow. + final escrow = + accounts.getRange(1, 2).where((e) => e != _userPublicKey).firstOrNull; + + if (escrow == null) continue; + + if (pendingEscrows.contains(escrow)) continue; + + final txList = await _client.rpcClient.getTransactionsList( + escrow, + limit: 2, + commitment: Commitment.confirmed, + encoding: Encoding.jsonParsed, + ); + + if (txList.length < 2) { + final id = const Uuid().v4(); + + final tx = txList.first; + + int amount = 0; + + for (final ix in tx.meta?.innerInstructions?.last.instructions ?? []) { + if (ix is ParsedInstructionSplToken && + ix.parsed is ParsedSplTokenTransferInstruction) { + final parsed = ix.parsed as ParsedSplTokenTransferInstruction; + + amount = int.parse(parsed.info.amount); + } + } + + final timestamp = detail.blockTime?.let( + (it) => DateTime.fromMillisecondsSinceEpoch(it * 1000), + ) ?? + DateTime.now(); + + await _repository.save( + OutgoingSplitKeyPayment( + id: id, + amount: CryptoAmount( + value: amount, + cryptoCurrency: const CryptoCurrency(token: Token.usdc), + ), + status: OSKPStatus.recovered(escrowPubKey: escrow), + created: timestamp, + linksGeneratedAt: timestamp, + apiVersion: SplitKeyApiVersion.smartContract, + ), + ); + } + } + } + + Future> _pendingEscrows() async { + final pending = await _repository.watchPending().first; + + final List results = []; + + for (final p in pending) { + final escrow = await p.status.mapOrNull( + txCreated: (it) async => it.escrow.keyPair.then((v) => v.publicKey), + txSent: (it) async => it.escrow.keyPair.then((v) => v.publicKey), + txConfirmed: (it) async => it.escrow.keyPair.then((v) => v.publicKey), + linksReady: (it) async => it.escrow.keyPair.then((v) => v.publicKey), + cancelTxCreated: (it) async => + it.escrow.keyPair.then((v) => v.publicKey), + cancelTxFailure: (it) async => + it.escrow.keyPair.then((v) => v.publicKey), + cancelTxSent: (it) async => it.escrow.keyPair.then((v) => v.publicKey), + recovered: (it) async => it.escrowPubKey, + ); + + if (escrow != null) { + results.add(escrow); + } + } + + return results; + } +} diff --git a/packages/espressocash_app/lib/features/outgoing_split_key_payments/services/tx_ready_watcher.dart b/packages/espressocash_app/lib/features/outgoing_split_key_payments/services/tx_ready_watcher.dart index 013e6ce022..dc5e07e4a5 100644 --- a/packages/espressocash_app/lib/features/outgoing_split_key_payments/services/tx_ready_watcher.dart +++ b/packages/espressocash_app/lib/features/outgoing_split_key_payments/services/tx_ready_watcher.dart @@ -69,13 +69,21 @@ class TxReadyWatcher { } final status = payment.status; - if (status is! OSKPStatusLinksReady) continue; + if (status is! OSKPStatusLinksReady && status is! OSKPStatusRecovered) { + continue; + } + + final escrowAccount = await status.mapOrNull( + linksReady: (it) async => it.escrow.keyPair + .then((e) => Ed25519HDPublicKey.fromBase58(e.address)), + recovered: (it) async => it.escrowPubKey, + ); - final escrowAccount = await status.escrow.keyPair; + if (escrowAccount == null) continue; if (!_subscriptions.containsKey(payment.id)) { _subscriptions[payment.id] = - _createStream(account: escrowAccount.publicKey).listen(onSuccess); + _createStream(account: escrowAccount).listen(onSuccess); } } }); diff --git a/packages/espressocash_app/moor_schemas/moor_schema_v37.json b/packages/espressocash_app/moor_schemas/moor_schema_v37.json new file mode 100644 index 0000000000..589ecaf3aa --- /dev/null +++ b/packages/espressocash_app/moor_schemas/moor_schema_v37.json @@ -0,0 +1 @@ +{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.0.0"},"options":{"store_date_time_values_as_text":false},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"outgoing_transfer_rows","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created","getter_name":"created","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"data","getter_name":"data","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":1,"references":[],"type":"table","data":{"name":"payment_request_rows","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created","getter_name":"created","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"payer_name","getter_name":"payerName","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"dynamic_link","getter_name":"dynamicLink","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"state","getter_name":"state","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(PaymentRequestStateDto.values)","dart_type_name":"PaymentRequestStateDto"}},{"name":"transaction_id","getter_name":"transactionId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"recipient","getter_name":"recipient","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"amount","getter_name":"amount","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"splt_token","getter_name":"spltToken","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"reference","getter_name":"reference","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"label","getter_name":"label","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"message","getter_name":"message","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"memo","getter_name":"memo","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":2,"references":[],"type":"table","data":{"name":"o_d_p_rows","was_declared_in_moor":false,"columns":[{"name":"amount","getter_name":"amount","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"token","getter_name":"token","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created","getter_name":"created","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"receiver","getter_name":"receiver","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"reference","getter_name":"reference","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"status","getter_name":"status","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(ODPStatusDto.values)","dart_type_name":"ODPStatusDto"}},{"name":"tx","getter_name":"tx","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx_id","getter_name":"txId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx_failure_reason","getter_name":"txFailureReason","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(TxFailureReason.values)","dart_type_name":"TxFailureReason"}},{"name":"slot","getter_name":"slot","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":3,"references":[],"type":"table","data":{"name":"o_s_k_p_rows","was_declared_in_moor":false,"columns":[{"name":"amount","getter_name":"amount","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"token","getter_name":"token","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created","getter_name":"created","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"status","getter_name":"status","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(OSKPStatusDto.values)","dart_type_name":"OSKPStatusDto"}},{"name":"tx","getter_name":"tx","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx_id","getter_name":"txId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"withdraw_tx_id","getter_name":"withdrawTxId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"private_key","getter_name":"privateKey","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"link1","getter_name":"link1","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"link2","getter_name":"link2","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"link3","getter_name":"link3","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx_failure_reason","getter_name":"txFailureReason","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(TxFailureReason.values)","dart_type_name":"TxFailureReason"}},{"name":"cancel_tx","getter_name":"cancelTx","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"cancel_tx_id","getter_name":"cancelTxId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"generated_links_at","getter_name":"generatedLinksAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"resolved_at","getter_name":"resolvedAt","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"slot","getter_name":"slot","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"api_version","getter_name":"apiVersion","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const Constant(0)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(OskpApiVersionDto.values)","dart_type_name":"OskpApiVersionDto"}},{"name":"public_key","getter_name":"publicKey","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":4,"references":[],"type":"table","data":{"name":"i_s_k_p_rows","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created","getter_name":"created","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx","getter_name":"tx","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx_id","getter_name":"txId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx_failure_reason","getter_name":"txFailureReason","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(TxFailureReason.values)","dart_type_name":"TxFailureReason"}},{"name":"slot","getter_name":"slot","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"private_key","getter_name":"privateKey","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"status","getter_name":"status","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(ISKPStatusDto.values)","dart_type_name":"ISKPStatusDto"}},{"name":"api_version","getter_name":"apiVersion","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":"const Constant(0)","default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(IskpApiVersionDto.values)","dart_type_name":"IskpApiVersionDto"}}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":5,"references":[],"type":"table","data":{"name":"swap_rows","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created","getter_name":"created","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx","getter_name":"tx","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx_id","getter_name":"txId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx_failure_reason","getter_name":"txFailureReason","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(TxFailureReason.values)","dart_type_name":"TxFailureReason"}},{"name":"slot","getter_name":"slot","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"status","getter_name":"status","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(SwapStatusDto.values)","dart_type_name":"SwapStatusDto"}},{"name":"amount","getter_name":"amount","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"token","getter_name":"token","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"input_mint","getter_name":"inputMint","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"output_mint","getter_name":"outputMint","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"slippage","getter_name":"slippage","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(SlippageDto.values)","dart_type_name":"SlippageDto"}}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":6,"references":[],"type":"table","data":{"name":"transaction_rows","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created","getter_name":"created","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"encoded_tx","getter_name":"encodedTx","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"status","getter_name":"status","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(TxCommonStatus.values)","dart_type_name":"TxCommonStatus"}}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":7,"references":[],"type":"table","data":{"name":"favorite_token_rows","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"symbol","getter_name":"symbol","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"logo_uri","getter_name":"logoUri","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":8,"references":[],"type":"table","data":{"name":"popular_token_rows","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"symbol","getter_name":"symbol","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"logo_uri","getter_name":"logoUri","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"price","getter_name":"price","moor_type":"double","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":9,"references":[],"type":"table","data":{"name":"o_t_rows","was_declared_in_moor":false,"columns":[{"name":"amount","getter_name":"amount","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"token","getter_name":"token","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created","getter_name":"created","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"status","getter_name":"status","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(OTStatusDto.values)","dart_type_name":"OTStatusDto"}},{"name":"tx","getter_name":"tx","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx_id","getter_name":"txId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"withdraw_tx_id","getter_name":"withdrawTxId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"private_key","getter_name":"privateKey","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"link","getter_name":"link","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx_failure_reason","getter_name":"txFailureReason","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(TxFailureReason.values)","dart_type_name":"TxFailureReason"}},{"name":"cancel_tx","getter_name":"cancelTx","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"cancel_tx_id","getter_name":"cancelTxId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":10,"references":[],"type":"table","data":{"name":"i_t_rows","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"created","getter_name":"created","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"private_key","getter_name":"privateKey","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"status","getter_name":"status","moor_type":"int","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumIndexConverter(ITStatusDto.values)","dart_type_name":"ITStatusDto"}},{"name":"tx","getter_name":"tx","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"tx_id","getter_name":"txId","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}}]} \ No newline at end of file diff --git a/packages/solana/lib/src/rpc/dto/dto.dart b/packages/solana/lib/src/rpc/dto/dto.dart index ed0adac0b3..119bede6ca 100644 --- a/packages/solana/lib/src/rpc/dto/dto.dart +++ b/packages/solana/lib/src/rpc/dto/dto.dart @@ -47,6 +47,7 @@ export 'recent_blockhash.dart'; export 'reward.dart'; export 'reward_type.dart'; export 'signature_status.dart'; +export 'simple_instruction.dart'; export 'simulate_transaction_accounts.dart'; export 'slot.dart'; export 'solana_version.dart';