From 19bb04b112fed2ee71cd8d7fe237ca6432c38a5e Mon Sep 17 00:00:00 2001 From: Vlad Sumin Date: Fri, 12 Jul 2024 00:45:59 +0300 Subject: [PATCH 01/20] add durable nonce for direct payments --- .../data/repository.dart | 15 +- .../models/outgoing_direct_payment.dart | 11 +- .../services/odp_service.dart | 74 ++++----- .../payments/create_direct_payment.dart | 141 ++++++++++++++++++ .../services/odp_service_test.dart | 85 ++++++----- 5 files changed, 229 insertions(+), 97 deletions(-) create mode 100644 packages/espressocash_app/lib/features/payments/create_direct_payment.dart diff --git a/packages/espressocash_app/lib/features/outgoing_direct_payments/data/repository.dart b/packages/espressocash_app/lib/features/outgoing_direct_payments/data/repository.dart index 7053993a46..d28830ecf4 100644 --- a/packages/espressocash_app/lib/features/outgoing_direct_payments/data/repository.dart +++ b/packages/espressocash_app/lib/features/outgoing_direct_payments/data/repository.dart @@ -106,20 +106,16 @@ extension ODPRowExt on ODPRow { extension on ODPStatusDto { ODPStatus toModel(ODPRow row) { final tx = row.tx?.let(SignedTx.decode); - final slot = row.slot?.let(BigInt.tryParse); switch (this) { case ODPStatusDto.txCreated: case ODPStatusDto.txSendFailure: - return ODPStatus.txCreated( - tx!, - slot: slot ?? BigInt.zero, - ); + return ODPStatus.txCreated(tx!); case ODPStatusDto.txSent: case ODPStatusDto.txWaitFailure: return ODPStatus.txSent( tx ?? StubSignedTx(row.txId!), - slot: slot ?? BigInt.zero, + signature: row.txId ?? '', ); case ODPStatusDto.success: return ODPStatus.success(txId: row.txId!); @@ -141,7 +137,6 @@ extension on OutgoingDirectPayment { tx: status.toTx(), txId: status.toTxId(), txFailureReason: status.toTxFailureReason(), - slot: status.toSlot()?.toString(), ); } @@ -159,15 +154,11 @@ extension on ODPStatus { ); String? toTxId() => mapOrNull( + txSent: (it) => it.signature, success: (it) => it.txId, ); TxFailureReason? toTxFailureReason() => mapOrNull( txFailure: (it) => it.reason, ); - - BigInt? toSlot() => mapOrNull( - txCreated: (it) => it.slot, - txSent: (it) => it.slot, - ); } diff --git a/packages/espressocash_app/lib/features/outgoing_direct_payments/models/outgoing_direct_payment.dart b/packages/espressocash_app/lib/features/outgoing_direct_payments/models/outgoing_direct_payment.dart index 6cd2058870..632c900e7e 100644 --- a/packages/espressocash_app/lib/features/outgoing_direct_payments/models/outgoing_direct_payment.dart +++ b/packages/espressocash_app/lib/features/outgoing_direct_payments/models/outgoing_direct_payment.dart @@ -21,16 +21,13 @@ class OutgoingDirectPayment with _$OutgoingDirectPayment { @freezed sealed class ODPStatus with _$ODPStatus { - /// Tx created, but not sent yet. At this stage, it's safe to recreate it. - const factory ODPStatus.txCreated( - SignedTx tx, { - required BigInt slot, - }) = ODPStatusTxCreated; + /// Tx created, but not sent yet. At this stage, it's safe to cancel it. + const factory ODPStatus.txCreated(SignedTx tx) = ODPStatusTxCreated; - /// Tx sent, but not confirmed yet. We cannot say if it was accepted. + /// Tx sent to backend. Should be good as confirmed at this point. const factory ODPStatus.txSent( SignedTx tx, { - required BigInt slot, + required String signature, }) = ODPStatusTxSent; /// Money is received by the recipient address. The payment is complete. diff --git a/packages/espressocash_app/lib/features/outgoing_direct_payments/services/odp_service.dart b/packages/espressocash_app/lib/features/outgoing_direct_payments/services/odp_service.dart index cad00298da..8a057190a9 100644 --- a/packages/espressocash_app/lib/features/outgoing_direct_payments/services/odp_service.dart +++ b/packages/espressocash_app/lib/features/outgoing_direct_payments/services/odp_service.dart @@ -3,18 +3,17 @@ import 'dart:async'; import 'package:dfunc/dfunc.dart'; import 'package:espressocash_api/espressocash_api.dart'; import 'package:injectable/injectable.dart'; -import 'package:solana/encoder.dart'; import 'package:solana/solana.dart'; import 'package:uuid/uuid.dart'; -import '../../../config.dart'; import '../../accounts/auth_scope.dart'; import '../../accounts/models/ec_wallet.dart'; import '../../analytics/analytics_manager.dart'; import '../../currency/models/amount.dart'; +import '../../payments/create_direct_payment.dart'; import '../../transactions/models/tx_results.dart'; import '../../transactions/services/resign_tx.dart'; -import '../../transactions/services/tx_sender.dart'; +import '../../transactions/services/tx_confirm.dart'; import '../data/repository.dart'; import '../models/outgoing_direct_payment.dart'; @@ -23,14 +22,16 @@ class ODPService { ODPService( this._client, this._repository, - this._txSender, + this._txConfirm, this._analyticsManager, + this._createDirectPayment, ); final EspressoCashClient _client; final ODPRepository _repository; - final TxSender _txSender; + final TxConfirm _txConfirm; final AnalyticsManager _analyticsManager; + final CreateDirectPayment _createDirectPayment; final Map> _subscriptions = {}; @@ -107,20 +108,16 @@ class ODPService { required Ed25519HDPublicKey? reference, }) async { try { - final dto = CreateDirectPaymentRequestDto( - senderAccount: account.address, - receiverAccount: receiver.toBase58(), - referenceAccount: reference?.toBase58(), + final directPaymentResult = await _createDirectPayment( + aReceiver: receiver, + aSender: account.publicKey, + aReference: reference, amount: amount.value, - cluster: apiCluster, + commitment: Commitment.confirmed, ); - final response = await _client.createDirectPayment(dto); - final tx = await response - .let((it) => it.transaction) - .let(SignedTx.decode) - .let((it) => it.resign(account)); + final tx = await directPaymentResult.transaction.resign(account); - return ODPStatus.txCreated(tx, slot: response.slot); + return ODPStatus.txCreated(tx); } on Exception { return const ODPStatus.txFailure( reason: TxFailureReason.creatingFailure, @@ -152,21 +149,29 @@ class ODPService { return payment; } - final tx = await _txSender.send(status.tx, minContextSlot: status.slot); - - final ODPStatus? newStatus = tx.map( - sent: (_) => ODPStatus.txSent( - status.tx, - slot: status.slot, - ), - invalidBlockhash: (_) => const ODPStatus.txFailure( - reason: TxFailureReason.invalidBlockhashSending, - ), - failure: (it) => ODPStatus.txFailure(reason: it.reason), - networkError: (_) => null, - ); + final tx = status.tx; - return newStatus == null ? payment : payment.copyWith(status: newStatus); + try { + final signature = await _client + .submitDurableTx( + SubmitDurableTxRequestDto( + tx: tx.encode(), + ), + ) + .then((e) => e.signature); + + return payment.copyWith( + status: ODPStatus.txSent( + tx, + signature: signature, + ), + ); + } on Exception { + return payment.copyWith( + status: + const ODPStatus.txFailure(reason: TxFailureReason.creatingFailure), + ); + } } Future _wait(OutgoingDirectPayment payment) async { @@ -174,14 +179,9 @@ class ODPService { if (status is! ODPStatusTxSent) { return payment; } + final tx = await _txConfirm(txId: status.signature); - final tx = await _txSender.wait( - status.tx, - minContextSlot: status.slot, - txType: 'OutgoingDirectPayment', - ); - - final ODPStatus? newStatus = tx.map( + final ODPStatus? newStatus = tx?.map( success: (_) => ODPStatus.success(txId: status.tx.id), failure: (tx) => ODPStatus.txFailure(reason: tx.reason), networkError: (_) => null, diff --git a/packages/espressocash_app/lib/features/payments/create_direct_payment.dart b/packages/espressocash_app/lib/features/payments/create_direct_payment.dart new file mode 100644 index 0000000000..b659f5189d --- /dev/null +++ b/packages/espressocash_app/lib/features/payments/create_direct_payment.dart @@ -0,0 +1,141 @@ +import 'package:dfunc/dfunc.dart'; +import 'package:espressocash_api/espressocash_api.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:injectable/injectable.dart'; +import 'package:solana/encoder.dart'; +import 'package:solana/solana.dart'; + +import '../../utils/transactions.dart'; +import '../priority_fees/services/add_priority_fees.dart'; +import '../tokens/token.dart'; + +part 'create_direct_payment.freezed.dart'; + +@freezed +class DirectPaymentResult with _$DirectPaymentResult { + const factory DirectPaymentResult({ + required int fee, + required SignedTx transaction, + }) = _DirectPaymentResult; +} + +@injectable +class CreateDirectPayment { + const CreateDirectPayment( + this._client, + this._addPriorityFees, + this._ecClient, + ); + + final SolanaClient _client; + final AddPriorityFees _addPriorityFees; + final EspressoCashClient _ecClient; + + Future call({ + required Ed25519HDPublicKey aSender, + required Ed25519HDPublicKey aReceiver, + required Ed25519HDPublicKey? aReference, + required int amount, + required Commitment commitment, + }) async { + final mint = Token.usdc.publicKey; + + final nonceData = await _ecClient.getFreeNonce(); + final platformAccount = Ed25519HDPublicKey.fromBase58(nonceData.authority); + + final shouldCreateAta = !await _client.hasAssociatedTokenAccount( + owner: aReceiver, + mint: mint, + commitment: commitment, + ); + + final instructions = []; + + final ataSender = await findAssociatedTokenAddress( + owner: aSender, + mint: mint, + ); + + final ataReceiver = await findAssociatedTokenAddress( + owner: aReceiver, + mint: mint, + ); + + if (shouldCreateAta) { + final iCreateATA = AssociatedTokenAccountInstruction.createAccount( + funder: platformAccount, + address: ataReceiver, + owner: aReceiver, + mint: mint, + ); + instructions.add(iCreateATA); + } + + final transactionFees = await _ecClient.getFees(); + + final iTransfer = TokenInstruction.transfer( + amount: amount, + source: ataSender, + destination: ataReceiver, + owner: aSender, + ); + if (aReference != null) { + iTransfer.accounts + .add(AccountMeta.readonly(pubKey: aReference, isSigner: false)); + } + instructions.add(iTransfer); + + final fee = shouldCreateAta + ? transactionFees.directPayment.ataDoesNotExist + : transactionFees.directPayment.ataExists; + + final ataPlatform = await findAssociatedTokenAddress( + owner: platformAccount, + mint: mint, + ); + final iTransferFee = TokenInstruction.transfer( + amount: fee, + source: ataSender, + destination: ataPlatform, + owner: aSender, + ); + instructions.add(iTransferFee); + + final message = Message( + instructions: [ + SystemInstruction.advanceNonceAccount( + nonce: Ed25519HDPublicKey.fromBase58(nonceData.nonceAccount), + nonceAuthority: platformAccount, + ), + ...instructions, + ], + ); + + final compiled = message.compile( + recentBlockhash: nonceData.nonce, + feePayer: platformAccount, + ); + + final priorityFees = await _ecClient.getDurableFees(); + + final tx = await SignedTx( + compiledMessage: compiled, + signatures: [ + platformAccount.emptySignature(), + aSender.emptySignature(), + ], + ).let( + (tx) => _addPriorityFees( + tx: tx, + commitment: commitment, + maxPriorityFee: priorityFees.outgoingLink, + platform: platformAccount, + ), + ); + + return DirectPaymentResult( + fee: fee, + transaction: tx, + ); + } +} diff --git a/packages/espressocash_app/test/features/outgoing_direct_payments/services/odp_service_test.dart b/packages/espressocash_app/test/features/outgoing_direct_payments/services/odp_service_test.dart index 1dacd63a81..95f411b74d 100644 --- a/packages/espressocash_app/test/features/outgoing_direct_payments/services/odp_service_test.dart +++ b/packages/espressocash_app/test/features/outgoing_direct_payments/services/odp_service_test.dart @@ -9,10 +9,10 @@ import 'package:espressocash_app/features/currency/models/currency.dart'; import 'package:espressocash_app/features/outgoing_direct_payments/data/repository.dart'; import 'package:espressocash_app/features/outgoing_direct_payments/models/outgoing_direct_payment.dart'; import 'package:espressocash_app/features/outgoing_direct_payments/services/odp_service.dart'; +import 'package:espressocash_app/features/payments/create_direct_payment.dart'; import 'package:espressocash_app/features/tokens/token.dart'; import 'package:espressocash_app/features/transactions/models/tx_results.dart'; -import 'package:espressocash_app/features/transactions/services/tx_sender.dart'; - +import 'package:espressocash_app/features/transactions/services/tx_confirm.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; @@ -24,17 +24,23 @@ import 'package:solana/solana.dart'; import '../../../stub_analytics_manager.dart'; import 'odp_service_test.mocks.dart'; -final sender = MockTxSender(); +final confirm = MockTxConfirm(); +final createDirectPayment = MockCreateDirectPayment(); final client = MockEspressoCashClient(); -@GenerateMocks([TxSender, EspressoCashClient]) +@GenerateMocks([ + TxConfirm, + CreateDirectPayment, + EspressoCashClient, +]) Future main() async { final account = LocalWallet(await Ed25519HDKeyPair.random()); final receiver = await Ed25519HDKeyPair.random(); final repository = MemoryRepository(); setUp(() { - reset(sender); + reset(confirm); + reset(createDirectPayment); reset(client); }); @@ -58,13 +64,13 @@ Future main() async { [it.toByteArray().toList().let(Uint8List.fromList)], ), ), - ) - .then((it) => it.encode()); + ); - final testApiResponse = CreateDirectPaymentResponseDto( - fee: 100, - transaction: stubTx, - slot: BigInt.zero, + final testDirectPaymentResult = + DirectPaymentResult(fee: 100, transaction: stubTx); + + final testApiResponse = SubmitDurableTxResponseDto( + signature: stubTx.encode(), ); const testAmount = CryptoAmount( @@ -75,8 +81,9 @@ Future main() async { ODPService createService() => ODPService( client, repository, - sender, + confirm, const StubAnalyticsManager(), + createDirectPayment, ); Future createODP(ODPService service) async { @@ -94,18 +101,19 @@ Future main() async { provideDummy(const TxSendSent()); provideDummy(const TxWaitSuccess()); - when(client.createDirectPayment(any)) - .thenAnswer((_) async => testApiResponse); - - when(sender.send(any, minContextSlot: anyNamed('minContextSlot'))) - .thenAnswer((_) async => const TxSendResult.sent()); when( - sender.wait( - any, - minContextSlot: anyNamed('minContextSlot'), - txType: anyNamed('txType'), + createDirectPayment( + aReceiver: anyNamed('aReceiver'), + aReference: anyNamed('aReference'), + aSender: anyNamed('aSender'), + amount: anyNamed('amount'), + commitment: anyNamed('commitment'), ), - ).thenAnswer((_) async => const TxWaitResult.success()); + ).thenAnswer((_) async => testDirectPaymentResult); + + when(client.submitDurableTx(any)).thenAnswer((_) async => testApiResponse); + when(confirm(txId: anyNamed('txId'))) + .thenAnswer((_) async => const TxWaitResult.success()); final paymentId = await createService().let(createODP); final payment = repository.watch(paymentId); @@ -124,18 +132,11 @@ Future main() async { ), ); - verify(sender.send(any, minContextSlot: anyNamed('minContextSlot'))) - .called(1); - verify( - sender.wait( - any, - minContextSlot: anyNamed('minContextSlot'), - txType: anyNamed('txType'), - ), - ).called(1); + verify(client.submitDurableTx(any)).called(1); + verify(confirm(txId: anyNamed('txId'))).called(1); }); - test('Failed to get tx from API', () async { + test('Failed to create direct durable payment', () async { provideDummy( const TxSendFailure(reason: TxFailureReason.unknown), ); @@ -143,7 +144,15 @@ Future main() async { const TxWaitFailure(reason: TxFailureReason.unknown), ); - when(client.createDirectPayment(any)).thenAnswer((_) => throw Exception()); + when( + createDirectPayment( + aReceiver: anyNamed('aReceiver'), + aReference: anyNamed('aReference'), + aSender: anyNamed('aSender'), + amount: anyNamed('amount'), + commitment: anyNamed('commitment'), + ), + ).thenAnswer((_) async => throw Exception()); final paymentId = await createService().let(createODP); final payment = repository.watch(paymentId); @@ -158,14 +167,8 @@ Future main() async { ), ); - verifyNever(sender.send(any, minContextSlot: anyNamed('minContextSlot'))); - verifyNever( - sender.wait( - any, - minContextSlot: anyNamed('minContextSlot'), - txType: anyNamed('txType'), - ), - ); + verifyNever(client.submitDurableTx(any)); + verifyNever(confirm(txId: anyNamed('txId'))); }); } From 06fe514506a136786d6146e2ba3b337ca46604b4 Mon Sep 17 00:00:00 2001 From: Vlad Sumin Date: Fri, 12 Jul 2024 15:41:00 +0300 Subject: [PATCH 02/20] Offramp service changes to use durable --- .../ramp/services/off_ramp_order_service.dart | 85 ++++++++++--------- 1 file changed, 43 insertions(+), 42 deletions(-) diff --git a/packages/espressocash_app/lib/features/ramp/services/off_ramp_order_service.dart b/packages/espressocash_app/lib/features/ramp/services/off_ramp_order_service.dart index 0114b22af2..d34f9ee326 100644 --- a/packages/espressocash_app/lib/features/ramp/services/off_ramp_order_service.dart +++ b/packages/espressocash_app/lib/features/ramp/services/off_ramp_order_service.dart @@ -19,11 +19,12 @@ import '../../accounts/auth_scope.dart'; import '../../accounts/models/ec_wallet.dart'; import '../../currency/models/amount.dart'; import '../../currency/models/currency.dart'; +import '../../payments/create_direct_payment.dart'; import '../../ramp_partner/models/ramp_partner.dart'; import '../../tokens/token_list.dart'; import '../../transactions/models/tx_results.dart'; import '../../transactions/services/resign_tx.dart'; -import '../../transactions/services/tx_sender.dart'; +import '../../transactions/services/tx_confirm.dart'; import '../models/ramp_watcher.dart'; import '../partners/coinflow/services/coinflow_off_ramp_order_watcher.dart'; import '../partners/kado/services/kado_off_ramp_order_watcher.dart'; @@ -47,7 +48,8 @@ class OffRampOrderService implements Disposable { OffRampOrderService( this._account, this._client, - this._sender, + this._createDirectPayment, + this._txConfirm, this._db, this._tokens, ); @@ -57,7 +59,8 @@ class OffRampOrderService implements Disposable { final ECWallet _account; final EspressoCashClient _client; - final TxSender _sender; + final CreateDirectPayment _createDirectPayment; + final TxConfirm _txConfirm; final MyDatabase _db; final TokenList _tokens; @@ -327,8 +330,7 @@ class OffRampOrderService implements Disposable { ), ); case OffRampOrderStatus.sendingDepositTx: - final tx = - SignedTx.decode(order.transaction).let((it) => (it, order.slot)); + final tx = SignedTx.decode(order.transaction).let((it) => it); return Stream.fromFuture(_sendTx(tx)); case OffRampOrderStatus.depositTxReady: @@ -372,24 +374,23 @@ class OffRampOrderService implements Disposable { required CryptoAmount amount, required Ed25519HDPublicKey receiver, }) async { - final dto = CreateDirectPaymentRequestDto( - senderAccount: _account.address, - receiverAccount: receiver.toBase58(), + final directPaymentResult = await _createDirectPayment( + aReceiver: receiver, + aSender: _account.publicKey, + aReference: null, amount: amount.value, - referenceAccount: null, - cluster: apiCluster, + commitment: Commitment.confirmed, ); - final response = await _client.createDirectPayment(dto); return _signAndUpdateRow( - encodedTx: response.transaction, - slot: response.slot, + encodedTx: directPaymentResult.transaction.encode(), ); } Future _createScalexTx({ required String partnerOrderId, }) async { + // TODO(vsumin): make scalex durable final dto = ScalexWithdrawRequestDto( orderId: partnerOrderId, cluster: apiCluster, @@ -398,13 +399,12 @@ class OffRampOrderService implements Disposable { return _signAndUpdateRow( encodedTx: response.transaction, - slot: response.slot, + // slot: response.slot, ); } Future _signAndUpdateRow({ required String encodedTx, - required BigInt slot, }) async { final tx = await SignedTx.decode(encodedTx).let((it) => it.resign(_account)); @@ -412,38 +412,38 @@ class OffRampOrderService implements Disposable { return OffRampOrderRowsCompanion( status: const Value(OffRampOrderStatus.depositTxReady), transaction: Value(tx.encode()), - slot: Value(slot), ); } - Future _sendTx((SignedTx, BigInt) tx) async { - final sent = await _sender.send(tx.$1, minContextSlot: tx.$2); - switch (sent) { - case TxSendSent(): - break; - case TxSendInvalidBlockhash(): - return OffRampOrderRowsCompanion( - status: const Value(OffRampOrderStatus.depositError), - transaction: const Value(''), - slot: Value(BigInt.zero), - ); - case TxSendFailure(:final reason): - return OffRampOrderRowsCompanion( - status: reason == TxFailureReason.insufficientFunds - ? const Value(OffRampOrderStatus.insufficientFunds) - : const Value(OffRampOrderStatus.depositError), - transaction: const Value(''), - slot: Value(BigInt.zero), - ); - case TxSendNetworkError(): - return _depositError; + Future _sendTx(SignedTx tx) async { + final signature = await _submitDurableTx(tx); + + return signature.isEmpty + ? OffRampOrderRowsCompanion( + status: const Value(OffRampOrderStatus.depositError), + transaction: const Value(''), + slot: Value(BigInt.zero), + ) + : await _confirmTransaction(signature); + } + + Future _submitDurableTx(SignedTx tx) async { + try { + final response = await _client.submitDurableTx( + SubmitDurableTxRequestDto(tx: tx.encode()), + ); + + return response.signature; + } on Exception { + return ''; } + } + + Future _confirmTransaction( + String signature, + ) async { + final confirmed = await _txConfirm(txId: signature); - final confirmed = await _sender.wait( - tx.$1, - minContextSlot: tx.$2, - txType: 'OffRamp', - ); switch (confirmed) { case TxWaitSuccess(): return const OffRampOrderRowsCompanion( @@ -458,6 +458,7 @@ class OffRampOrderService implements Disposable { slot: Value(BigInt.zero), ); case TxWaitNetworkError(): + case null: return _depositError; } } From 90215194d9e438d4475f1f9ea2d0b961f76471b0 Mon Sep 17 00:00:00 2001 From: Vlad Sumin Date: Fri, 12 Jul 2024 18:10:24 +0300 Subject: [PATCH 03/20] Added durable scalex tx --- .../scalex/scalex_withdraw_payment.dart | 146 ++++++++++++++++++ 1 file changed, 146 insertions(+) create mode 100644 packages/espressocash_app/lib/features/ramp/partners/scalex/scalex_withdraw_payment.dart diff --git a/packages/espressocash_app/lib/features/ramp/partners/scalex/scalex_withdraw_payment.dart b/packages/espressocash_app/lib/features/ramp/partners/scalex/scalex_withdraw_payment.dart new file mode 100644 index 0000000000..2402cc6c4b --- /dev/null +++ b/packages/espressocash_app/lib/features/ramp/partners/scalex/scalex_withdraw_payment.dart @@ -0,0 +1,146 @@ +import 'dart:math'; + +import 'package:dfunc/dfunc.dart'; +import 'package:espressocash_api/espressocash_api.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:injectable/injectable.dart'; +import 'package:solana/encoder.dart'; +import 'package:solana/solana.dart'; + +import '../../../../utils/transactions.dart'; +import '../../../priority_fees/services/add_priority_fees.dart'; +import '../../../tokens/token.dart'; + +part 'scalex_withdraw_payment.freezed.dart'; + +@freezed +class WithdrawPaymentResult with _$WithdrawPaymentResult { + const factory WithdrawPaymentResult({ + required int fee, + required SignedTx transaction, + }) = _WithdrawPaymentResult; +} + +@injectable +class ScalexWithdrawPayment { + const ScalexWithdrawPayment( + this._client, + this._addPriorityFees, + this._ecClient, + ); + + final SolanaClient _client; + final AddPriorityFees _addPriorityFees; + final EspressoCashClient _ecClient; + + Future call({ + required Ed25519HDPublicKey aSender, + required Ed25519HDPublicKey aReceiver, + required int amount, + required Commitment commitment, + }) async { + final mint = Token.usdc.publicKey; + + final nonceData = await _ecClient.getFreeNonce(); + final platformAccount = Ed25519HDPublicKey.fromBase58(nonceData.authority); + + final shouldCreateAta = !await _client.hasAssociatedTokenAccount( + owner: aReceiver, + mint: mint, + commitment: commitment, + ); + + final instructions = []; + + final ataSender = await findAssociatedTokenAddress( + owner: aSender, + mint: mint, + ); + + final ataReceiver = await findAssociatedTokenAddress( + owner: aReceiver, + mint: mint, + ); + + if (shouldCreateAta) { + final iCreateATA = AssociatedTokenAccountInstruction.createAccount( + funder: platformAccount, + address: ataReceiver, + owner: aReceiver, + mint: mint, + ); + instructions.add(iCreateATA); + } + + final transactionFees = await _ecClient.getFees(); + + final iTransfer = TokenInstruction.transfer( + amount: amount, + source: ataSender, + destination: ataReceiver, + owner: aSender, + ); + + instructions.add(iTransfer); + + final txFee = shouldCreateAta + ? transactionFees.directPayment.ataDoesNotExist + : transactionFees.directPayment.ataExists; + + final ataPlatform = await findAssociatedTokenAddress( + owner: platformAccount, + mint: mint, + ); + + final espressoFee = await _ecClient.fetchScalexFeesAndRate(); + + final scalexOffRampFeeFraction = espressoFee.espressoFeePercentage; + + final percentageFeeAmount = (amount * scalexOffRampFeeFraction).ceil(); + final totalFee = max(percentageFeeAmount, txFee); + final iTransferPercentageFee = TokenInstruction.transfer( + amount: totalFee, + source: ataSender, + destination: ataPlatform, + owner: aSender, + ); + instructions.add(iTransferPercentageFee); + + final message = Message( + instructions: [ + SystemInstruction.advanceNonceAccount( + nonce: Ed25519HDPublicKey.fromBase58(nonceData.nonceAccount), + nonceAuthority: platformAccount, + ), + ...instructions, + ], + ); + + final compiled = message.compile( + recentBlockhash: nonceData.nonce, + feePayer: platformAccount, + ); + + final priorityFees = await _ecClient.getDurableFees(); + + final tx = await SignedTx( + compiledMessage: compiled, + signatures: [ + platformAccount.emptySignature(), + aSender.emptySignature(), + ], + ).let( + (tx) => _addPriorityFees( + tx: tx, + commitment: commitment, + maxPriorityFee: priorityFees.outgoingLink, + platform: platformAccount, + ), + ); + + return WithdrawPaymentResult( + fee: totalFee, + transaction: tx, + ); + } +} From fda65610a65f98b14f315191999b02407dc06ef7 Mon Sep 17 00:00:00 2001 From: Vlad Sumin Date: Mon, 15 Jul 2024 00:52:28 +0300 Subject: [PATCH 04/20] Scalex changes --- .../espressocash_api/lib/src/dto/scalex.dart | 6 +- .../lib/src/dto/scalex.freezed.dart | 119 +++++++----------- .../lib/src/dto/scalex.g.dart | 17 +-- .../ramp/services/off_ramp_order_service.dart | 33 +++-- 4 files changed, 64 insertions(+), 111 deletions(-) diff --git a/packages/espressocash_api/lib/src/dto/scalex.dart b/packages/espressocash_api/lib/src/dto/scalex.dart index 26753c8171..8afca10eb1 100644 --- a/packages/espressocash_api/lib/src/dto/scalex.dart +++ b/packages/espressocash_api/lib/src/dto/scalex.dart @@ -56,7 +56,6 @@ class OrderStatusScalexResponseDto with _$OrderStatusScalexResponseDto { class ScalexWithdrawRequestDto with _$ScalexWithdrawRequestDto { const factory ScalexWithdrawRequestDto({ required String orderId, - required Cluster cluster, }) = _ScalexWithdrawRequestDto; factory ScalexWithdrawRequestDto.fromJson(Map json) => @@ -66,9 +65,8 @@ class ScalexWithdrawRequestDto with _$ScalexWithdrawRequestDto { @freezed class ScalexWithdrawResponseDto with _$ScalexWithdrawResponseDto { const factory ScalexWithdrawResponseDto({ - required int fee, - required String transaction, - required BigInt slot, + required String depositAddress, + required int amount, }) = _WithdrawPaymentResponseDto; factory ScalexWithdrawResponseDto.fromJson(Map json) => diff --git a/packages/espressocash_api/lib/src/dto/scalex.freezed.dart b/packages/espressocash_api/lib/src/dto/scalex.freezed.dart index e9eebe4389..b1700f9200 100644 --- a/packages/espressocash_api/lib/src/dto/scalex.freezed.dart +++ b/packages/espressocash_api/lib/src/dto/scalex.freezed.dart @@ -756,7 +756,6 @@ ScalexWithdrawRequestDto _$ScalexWithdrawRequestDtoFromJson( /// @nodoc mixin _$ScalexWithdrawRequestDto { String get orderId => throw _privateConstructorUsedError; - Cluster get cluster => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -770,7 +769,7 @@ abstract class $ScalexWithdrawRequestDtoCopyWith<$Res> { $Res Function(ScalexWithdrawRequestDto) then) = _$ScalexWithdrawRequestDtoCopyWithImpl<$Res, ScalexWithdrawRequestDto>; @useResult - $Res call({String orderId, Cluster cluster}); + $Res call({String orderId}); } /// @nodoc @@ -788,17 +787,12 @@ class _$ScalexWithdrawRequestDtoCopyWithImpl<$Res, @override $Res call({ Object? orderId = null, - Object? cluster = null, }) { return _then(_value.copyWith( orderId: null == orderId ? _value.orderId : orderId // ignore: cast_nullable_to_non_nullable as String, - cluster: null == cluster - ? _value.cluster - : cluster // ignore: cast_nullable_to_non_nullable - as Cluster, ) as $Val); } } @@ -812,7 +806,7 @@ abstract class _$$ScalexWithdrawRequestDtoImplCopyWith<$Res> __$$ScalexWithdrawRequestDtoImplCopyWithImpl<$Res>; @override @useResult - $Res call({String orderId, Cluster cluster}); + $Res call({String orderId}); } /// @nodoc @@ -829,17 +823,12 @@ class __$$ScalexWithdrawRequestDtoImplCopyWithImpl<$Res> @override $Res call({ Object? orderId = null, - Object? cluster = null, }) { return _then(_$ScalexWithdrawRequestDtoImpl( orderId: null == orderId ? _value.orderId : orderId // ignore: cast_nullable_to_non_nullable as String, - cluster: null == cluster - ? _value.cluster - : cluster // ignore: cast_nullable_to_non_nullable - as Cluster, )); } } @@ -847,20 +836,17 @@ class __$$ScalexWithdrawRequestDtoImplCopyWithImpl<$Res> /// @nodoc @JsonSerializable() class _$ScalexWithdrawRequestDtoImpl implements _ScalexWithdrawRequestDto { - const _$ScalexWithdrawRequestDtoImpl( - {required this.orderId, required this.cluster}); + const _$ScalexWithdrawRequestDtoImpl({required this.orderId}); factory _$ScalexWithdrawRequestDtoImpl.fromJson(Map json) => _$$ScalexWithdrawRequestDtoImplFromJson(json); @override final String orderId; - @override - final Cluster cluster; @override String toString() { - return 'ScalexWithdrawRequestDto(orderId: $orderId, cluster: $cluster)'; + return 'ScalexWithdrawRequestDto(orderId: $orderId)'; } @override @@ -868,13 +854,12 @@ class _$ScalexWithdrawRequestDtoImpl implements _ScalexWithdrawRequestDto { return identical(this, other) || (other.runtimeType == runtimeType && other is _$ScalexWithdrawRequestDtoImpl && - (identical(other.orderId, orderId) || other.orderId == orderId) && - (identical(other.cluster, cluster) || other.cluster == cluster)); + (identical(other.orderId, orderId) || other.orderId == orderId)); } @JsonKey(ignore: true) @override - int get hashCode => Object.hash(runtimeType, orderId, cluster); + int get hashCode => Object.hash(runtimeType, orderId); @JsonKey(ignore: true) @override @@ -892,9 +877,8 @@ class _$ScalexWithdrawRequestDtoImpl implements _ScalexWithdrawRequestDto { } abstract class _ScalexWithdrawRequestDto implements ScalexWithdrawRequestDto { - const factory _ScalexWithdrawRequestDto( - {required final String orderId, - required final Cluster cluster}) = _$ScalexWithdrawRequestDtoImpl; + const factory _ScalexWithdrawRequestDto({required final String orderId}) = + _$ScalexWithdrawRequestDtoImpl; factory _ScalexWithdrawRequestDto.fromJson(Map json) = _$ScalexWithdrawRequestDtoImpl.fromJson; @@ -902,8 +886,6 @@ abstract class _ScalexWithdrawRequestDto implements ScalexWithdrawRequestDto { @override String get orderId; @override - Cluster get cluster; - @override @JsonKey(ignore: true) _$$ScalexWithdrawRequestDtoImplCopyWith<_$ScalexWithdrawRequestDtoImpl> get copyWith => throw _privateConstructorUsedError; @@ -916,9 +898,8 @@ ScalexWithdrawResponseDto _$ScalexWithdrawResponseDtoFromJson( /// @nodoc mixin _$ScalexWithdrawResponseDto { - int get fee => throw _privateConstructorUsedError; - String get transaction => throw _privateConstructorUsedError; - BigInt get slot => throw _privateConstructorUsedError; + String get depositAddress => throw _privateConstructorUsedError; + int get amount => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -932,7 +913,7 @@ abstract class $ScalexWithdrawResponseDtoCopyWith<$Res> { $Res Function(ScalexWithdrawResponseDto) then) = _$ScalexWithdrawResponseDtoCopyWithImpl<$Res, ScalexWithdrawResponseDto>; @useResult - $Res call({int fee, String transaction, BigInt slot}); + $Res call({String depositAddress, int amount}); } /// @nodoc @@ -949,23 +930,18 @@ class _$ScalexWithdrawResponseDtoCopyWithImpl<$Res, @pragma('vm:prefer-inline') @override $Res call({ - Object? fee = null, - Object? transaction = null, - Object? slot = null, + Object? depositAddress = null, + Object? amount = null, }) { return _then(_value.copyWith( - fee: null == fee - ? _value.fee - : fee // ignore: cast_nullable_to_non_nullable - as int, - transaction: null == transaction - ? _value.transaction - : transaction // ignore: cast_nullable_to_non_nullable + depositAddress: null == depositAddress + ? _value.depositAddress + : depositAddress // ignore: cast_nullable_to_non_nullable as String, - slot: null == slot - ? _value.slot - : slot // ignore: cast_nullable_to_non_nullable - as BigInt, + amount: null == amount + ? _value.amount + : amount // ignore: cast_nullable_to_non_nullable + as int, ) as $Val); } } @@ -979,7 +955,7 @@ abstract class _$$WithdrawPaymentResponseDtoImplCopyWith<$Res> __$$WithdrawPaymentResponseDtoImplCopyWithImpl<$Res>; @override @useResult - $Res call({int fee, String transaction, BigInt slot}); + $Res call({String depositAddress, int amount}); } /// @nodoc @@ -995,23 +971,18 @@ class __$$WithdrawPaymentResponseDtoImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ - Object? fee = null, - Object? transaction = null, - Object? slot = null, + Object? depositAddress = null, + Object? amount = null, }) { return _then(_$WithdrawPaymentResponseDtoImpl( - fee: null == fee - ? _value.fee - : fee // ignore: cast_nullable_to_non_nullable - as int, - transaction: null == transaction - ? _value.transaction - : transaction // ignore: cast_nullable_to_non_nullable + depositAddress: null == depositAddress + ? _value.depositAddress + : depositAddress // ignore: cast_nullable_to_non_nullable as String, - slot: null == slot - ? _value.slot - : slot // ignore: cast_nullable_to_non_nullable - as BigInt, + amount: null == amount + ? _value.amount + : amount // ignore: cast_nullable_to_non_nullable + as int, )); } } @@ -1020,22 +991,20 @@ class __$$WithdrawPaymentResponseDtoImplCopyWithImpl<$Res> @JsonSerializable() class _$WithdrawPaymentResponseDtoImpl implements _WithdrawPaymentResponseDto { const _$WithdrawPaymentResponseDtoImpl( - {required this.fee, required this.transaction, required this.slot}); + {required this.depositAddress, required this.amount}); factory _$WithdrawPaymentResponseDtoImpl.fromJson( Map json) => _$$WithdrawPaymentResponseDtoImplFromJson(json); @override - final int fee; - @override - final String transaction; + final String depositAddress; @override - final BigInt slot; + final int amount; @override String toString() { - return 'ScalexWithdrawResponseDto(fee: $fee, transaction: $transaction, slot: $slot)'; + return 'ScalexWithdrawResponseDto(depositAddress: $depositAddress, amount: $amount)'; } @override @@ -1043,15 +1012,14 @@ class _$WithdrawPaymentResponseDtoImpl implements _WithdrawPaymentResponseDto { return identical(this, other) || (other.runtimeType == runtimeType && other is _$WithdrawPaymentResponseDtoImpl && - (identical(other.fee, fee) || other.fee == fee) && - (identical(other.transaction, transaction) || - other.transaction == transaction) && - (identical(other.slot, slot) || other.slot == slot)); + (identical(other.depositAddress, depositAddress) || + other.depositAddress == depositAddress) && + (identical(other.amount, amount) || other.amount == amount)); } @JsonKey(ignore: true) @override - int get hashCode => Object.hash(runtimeType, fee, transaction, slot); + int get hashCode => Object.hash(runtimeType, depositAddress, amount); @JsonKey(ignore: true) @override @@ -1071,19 +1039,16 @@ class _$WithdrawPaymentResponseDtoImpl implements _WithdrawPaymentResponseDto { abstract class _WithdrawPaymentResponseDto implements ScalexWithdrawResponseDto { const factory _WithdrawPaymentResponseDto( - {required final int fee, - required final String transaction, - required final BigInt slot}) = _$WithdrawPaymentResponseDtoImpl; + {required final String depositAddress, + required final int amount}) = _$WithdrawPaymentResponseDtoImpl; factory _WithdrawPaymentResponseDto.fromJson(Map json) = _$WithdrawPaymentResponseDtoImpl.fromJson; @override - int get fee; - @override - String get transaction; + String get depositAddress; @override - BigInt get slot; + int get amount; @override @JsonKey(ignore: true) _$$WithdrawPaymentResponseDtoImplCopyWith<_$WithdrawPaymentResponseDtoImpl> diff --git a/packages/espressocash_api/lib/src/dto/scalex.g.dart b/packages/espressocash_api/lib/src/dto/scalex.g.dart index 5165cc79bc..914fdfc9e4 100644 --- a/packages/espressocash_api/lib/src/dto/scalex.g.dart +++ b/packages/espressocash_api/lib/src/dto/scalex.g.dart @@ -80,35 +80,26 @@ _$ScalexWithdrawRequestDtoImpl _$$ScalexWithdrawRequestDtoImplFromJson( Map json) => _$ScalexWithdrawRequestDtoImpl( orderId: json['orderId'] as String, - cluster: $enumDecode(_$ClusterEnumMap, json['cluster']), ); Map _$$ScalexWithdrawRequestDtoImplToJson( _$ScalexWithdrawRequestDtoImpl instance) => { 'orderId': instance.orderId, - 'cluster': _$ClusterEnumMap[instance.cluster]!, }; -const _$ClusterEnumMap = { - Cluster.mainnet: 'mainnet', - Cluster.devnet: 'devnet', -}; - _$WithdrawPaymentResponseDtoImpl _$$WithdrawPaymentResponseDtoImplFromJson( Map json) => _$WithdrawPaymentResponseDtoImpl( - fee: (json['fee'] as num).toInt(), - transaction: json['transaction'] as String, - slot: BigInt.parse(json['slot'] as String), + depositAddress: json['depositAddress'] as String, + amount: (json['amount'] as num).toInt(), ); Map _$$WithdrawPaymentResponseDtoImplToJson( _$WithdrawPaymentResponseDtoImpl instance) => { - 'fee': instance.fee, - 'transaction': instance.transaction, - 'slot': instance.slot.toString(), + 'depositAddress': instance.depositAddress, + 'amount': instance.amount, }; _$OnRampScalexDetailsImpl _$$OnRampScalexDetailsImplFromJson( diff --git a/packages/espressocash_app/lib/features/ramp/services/off_ramp_order_service.dart b/packages/espressocash_app/lib/features/ramp/services/off_ramp_order_service.dart index d34f9ee326..028a956e1b 100644 --- a/packages/espressocash_app/lib/features/ramp/services/off_ramp_order_service.dart +++ b/packages/espressocash_app/lib/features/ramp/services/off_ramp_order_service.dart @@ -12,7 +12,6 @@ import 'package:solana/encoder.dart'; import 'package:solana/solana.dart'; import 'package:uuid/uuid.dart'; -import '../../../config.dart'; import '../../../data/db/db.dart'; import '../../../di.dart'; import '../../accounts/auth_scope.dart'; @@ -28,6 +27,7 @@ import '../../transactions/services/tx_confirm.dart'; import '../models/ramp_watcher.dart'; import '../partners/coinflow/services/coinflow_off_ramp_order_watcher.dart'; import '../partners/kado/services/kado_off_ramp_order_watcher.dart'; +import '../partners/scalex/scalex_withdraw_payment.dart'; import '../partners/scalex/services/scalex_off_ramp_order_watcher.dart'; typedef OffRampOrder = ({ @@ -48,6 +48,7 @@ class OffRampOrderService implements Disposable { OffRampOrderService( this._account, this._client, + this._scalexWithdrawPayment, this._createDirectPayment, this._txConfirm, this._db, @@ -59,6 +60,7 @@ class OffRampOrderService implements Disposable { final ECWallet _account; final EspressoCashClient _client; + final ScalexWithdrawPayment _scalexWithdrawPayment; final CreateDirectPayment _createDirectPayment; final TxConfirm _txConfirm; final MyDatabase _db; @@ -382,32 +384,29 @@ class OffRampOrderService implements Disposable { commitment: Commitment.confirmed, ); - return _signAndUpdateRow( - encodedTx: directPaymentResult.transaction.encode(), - ); + return _signAndUpdateRow(directPaymentResult.transaction); } Future _createScalexTx({ required String partnerOrderId, }) async { - // TODO(vsumin): make scalex durable - final dto = ScalexWithdrawRequestDto( - orderId: partnerOrderId, - cluster: apiCluster, - ); + final dto = ScalexWithdrawRequestDto(orderId: partnerOrderId); final response = await _client.createScalexWithdraw(dto); - return _signAndUpdateRow( - encodedTx: response.transaction, - // slot: response.slot, + final scalexPaymentResult = await _scalexWithdrawPayment( + aReceiver: Ed25519HDPublicKey.fromBase58(response.depositAddress), + aSender: _account.publicKey, + amount: response.amount, + commitment: Commitment.confirmed, ); + + return _signAndUpdateRow(scalexPaymentResult.transaction); } - Future _signAndUpdateRow({ - required String encodedTx, - }) async { - final tx = - await SignedTx.decode(encodedTx).let((it) => it.resign(_account)); + Future _signAndUpdateRow( + SignedTx encodedTx, + ) async { + final tx = await encodedTx.let((it) => it.resign(_account)); return OffRampOrderRowsCompanion( status: const Value(OffRampOrderStatus.depositTxReady), From aae525f215e8d4b39a8d47cc95a1ffe78925e393 Mon Sep 17 00:00:00 2001 From: Vlad Sumin Date: Mon, 15 Jul 2024 00:56:01 +0300 Subject: [PATCH 05/20] Fix analyzer --- packages/espressocash_api/lib/src/dto/scalex.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/espressocash_api/lib/src/dto/scalex.dart b/packages/espressocash_api/lib/src/dto/scalex.dart index 8afca10eb1..dc2749f013 100644 --- a/packages/espressocash_api/lib/src/dto/scalex.dart +++ b/packages/espressocash_api/lib/src/dto/scalex.dart @@ -1,6 +1,5 @@ // ignore_for_file: invalid_annotation_target -import 'package:espressocash_api/espressocash_api.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'scalex.freezed.dart'; From ac7671df5f7e1feeb999821220fd64f831f4bc2a Mon Sep 17 00:00:00 2001 From: Vlad Sumin Date: Tue, 16 Jul 2024 01:34:03 +0300 Subject: [PATCH 06/20] upd models --- .../espressocash_api/lib/src/dto/scalex.dart | 19 +- .../lib/src/dto/scalex.freezed.dart | 330 +++++++++++++++--- .../lib/src/dto/scalex.g.dart | 36 +- 3 files changed, 330 insertions(+), 55 deletions(-) diff --git a/packages/espressocash_api/lib/src/dto/scalex.dart b/packages/espressocash_api/lib/src/dto/scalex.dart index dc2749f013..064fe0bf7d 100644 --- a/packages/espressocash_api/lib/src/dto/scalex.dart +++ b/packages/espressocash_api/lib/src/dto/scalex.dart @@ -1,5 +1,6 @@ // ignore_for_file: invalid_annotation_target +import 'package:espressocash_api/espressocash_api.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; part 'scalex.freezed.dart'; @@ -45,6 +46,7 @@ class OrderStatusScalexResponseDto with _$OrderStatusScalexResponseDto { @JsonKey(unknownEnumValue: ScalexOrderStatus.unknown) required ScalexOrderStatus status, OnRampScalexDetails? onRampDetails, + OffRampScalexDetails? offRampDetails, }) = _OrderStatusScalexResponseDto; factory OrderStatusScalexResponseDto.fromJson(Map json) => @@ -55,6 +57,7 @@ class OrderStatusScalexResponseDto with _$OrderStatusScalexResponseDto { class ScalexWithdrawRequestDto with _$ScalexWithdrawRequestDto { const factory ScalexWithdrawRequestDto({ required String orderId, + required Cluster cluster, }) = _ScalexWithdrawRequestDto; factory ScalexWithdrawRequestDto.fromJson(Map json) => @@ -64,8 +67,9 @@ class ScalexWithdrawRequestDto with _$ScalexWithdrawRequestDto { @freezed class ScalexWithdrawResponseDto with _$ScalexWithdrawResponseDto { const factory ScalexWithdrawResponseDto({ - required String depositAddress, - required int amount, + required int fee, + required String transaction, + required BigInt slot, }) = _WithdrawPaymentResponseDto; factory ScalexWithdrawResponseDto.fromJson(Map json) => @@ -86,6 +90,17 @@ class OnRampScalexDetails with _$OnRampScalexDetails { _$OnRampScalexDetailsFromJson(json); } +@freezed +class OffRampScalexDetails with _$OnRampScalexDetails { + const factory OffRampScalexDetails({ + required String depositAddress, + required int amount, + }) = _OffRampScalexDetails; + + factory OffRampScalexDetails.fromJson(Map json) => + _$OffRampScalexDetailsFromJson(json); +} + @freezed class ScalexRateFeeResponseDto with _$ScalexRateFeeResponseDto { const factory ScalexRateFeeResponseDto({ diff --git a/packages/espressocash_api/lib/src/dto/scalex.freezed.dart b/packages/espressocash_api/lib/src/dto/scalex.freezed.dart index b1700f9200..832e031b0f 100644 --- a/packages/espressocash_api/lib/src/dto/scalex.freezed.dart +++ b/packages/espressocash_api/lib/src/dto/scalex.freezed.dart @@ -559,6 +559,8 @@ mixin _$OrderStatusScalexResponseDto { @JsonKey(unknownEnumValue: ScalexOrderStatus.unknown) ScalexOrderStatus get status => throw _privateConstructorUsedError; OnRampScalexDetails? get onRampDetails => throw _privateConstructorUsedError; + OffRampScalexDetails? get offRampDetails => + throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -577,9 +579,11 @@ abstract class $OrderStatusScalexResponseDtoCopyWith<$Res> { $Res call( {@JsonKey(unknownEnumValue: ScalexOrderStatus.unknown) ScalexOrderStatus status, - OnRampScalexDetails? onRampDetails}); + OnRampScalexDetails? onRampDetails, + OffRampScalexDetails? offRampDetails}); $OnRampScalexDetailsCopyWith<$Res>? get onRampDetails; + $OffRampScalexDetailsCopyWith<$Res>? get offRampDetails; } /// @nodoc @@ -598,6 +602,7 @@ class _$OrderStatusScalexResponseDtoCopyWithImpl<$Res, $Res call({ Object? status = null, Object? onRampDetails = freezed, + Object? offRampDetails = freezed, }) { return _then(_value.copyWith( status: null == status @@ -608,6 +613,10 @@ class _$OrderStatusScalexResponseDtoCopyWithImpl<$Res, ? _value.onRampDetails : onRampDetails // ignore: cast_nullable_to_non_nullable as OnRampScalexDetails?, + offRampDetails: freezed == offRampDetails + ? _value.offRampDetails + : offRampDetails // ignore: cast_nullable_to_non_nullable + as OffRampScalexDetails?, ) as $Val); } @@ -622,6 +631,18 @@ class _$OrderStatusScalexResponseDtoCopyWithImpl<$Res, return _then(_value.copyWith(onRampDetails: value) as $Val); }); } + + @override + @pragma('vm:prefer-inline') + $OffRampScalexDetailsCopyWith<$Res>? get offRampDetails { + if (_value.offRampDetails == null) { + return null; + } + + return $OffRampScalexDetailsCopyWith<$Res>(_value.offRampDetails!, (value) { + return _then(_value.copyWith(offRampDetails: value) as $Val); + }); + } } /// @nodoc @@ -636,10 +657,13 @@ abstract class _$$OrderStatusScalexResponseDtoImplCopyWith<$Res> $Res call( {@JsonKey(unknownEnumValue: ScalexOrderStatus.unknown) ScalexOrderStatus status, - OnRampScalexDetails? onRampDetails}); + OnRampScalexDetails? onRampDetails, + OffRampScalexDetails? offRampDetails}); @override $OnRampScalexDetailsCopyWith<$Res>? get onRampDetails; + @override + $OffRampScalexDetailsCopyWith<$Res>? get offRampDetails; } /// @nodoc @@ -657,6 +681,7 @@ class __$$OrderStatusScalexResponseDtoImplCopyWithImpl<$Res> $Res call({ Object? status = null, Object? onRampDetails = freezed, + Object? offRampDetails = freezed, }) { return _then(_$OrderStatusScalexResponseDtoImpl( status: null == status @@ -667,6 +692,10 @@ class __$$OrderStatusScalexResponseDtoImplCopyWithImpl<$Res> ? _value.onRampDetails : onRampDetails // ignore: cast_nullable_to_non_nullable as OnRampScalexDetails?, + offRampDetails: freezed == offRampDetails + ? _value.offRampDetails + : offRampDetails // ignore: cast_nullable_to_non_nullable + as OffRampScalexDetails?, )); } } @@ -678,7 +707,8 @@ class _$OrderStatusScalexResponseDtoImpl const _$OrderStatusScalexResponseDtoImpl( {@JsonKey(unknownEnumValue: ScalexOrderStatus.unknown) required this.status, - this.onRampDetails}); + this.onRampDetails, + this.offRampDetails}); factory _$OrderStatusScalexResponseDtoImpl.fromJson( Map json) => @@ -689,10 +719,12 @@ class _$OrderStatusScalexResponseDtoImpl final ScalexOrderStatus status; @override final OnRampScalexDetails? onRampDetails; + @override + final OffRampScalexDetails? offRampDetails; @override String toString() { - return 'OrderStatusScalexResponseDto(status: $status, onRampDetails: $onRampDetails)'; + return 'OrderStatusScalexResponseDto(status: $status, onRampDetails: $onRampDetails, offRampDetails: $offRampDetails)'; } @override @@ -702,12 +734,15 @@ class _$OrderStatusScalexResponseDtoImpl other is _$OrderStatusScalexResponseDtoImpl && (identical(other.status, status) || other.status == status) && (identical(other.onRampDetails, onRampDetails) || - other.onRampDetails == onRampDetails)); + other.onRampDetails == onRampDetails) && + (identical(other.offRampDetails, offRampDetails) || + other.offRampDetails == offRampDetails)); } @JsonKey(ignore: true) @override - int get hashCode => Object.hash(runtimeType, status, onRampDetails); + int get hashCode => + Object.hash(runtimeType, status, onRampDetails, offRampDetails); @JsonKey(ignore: true) @override @@ -730,7 +765,8 @@ abstract class _OrderStatusScalexResponseDto const factory _OrderStatusScalexResponseDto( {@JsonKey(unknownEnumValue: ScalexOrderStatus.unknown) required final ScalexOrderStatus status, - final OnRampScalexDetails? onRampDetails}) = + final OnRampScalexDetails? onRampDetails, + final OffRampScalexDetails? offRampDetails}) = _$OrderStatusScalexResponseDtoImpl; factory _OrderStatusScalexResponseDto.fromJson(Map json) = @@ -742,6 +778,8 @@ abstract class _OrderStatusScalexResponseDto @override OnRampScalexDetails? get onRampDetails; @override + OffRampScalexDetails? get offRampDetails; + @override @JsonKey(ignore: true) _$$OrderStatusScalexResponseDtoImplCopyWith< _$OrderStatusScalexResponseDtoImpl> @@ -756,6 +794,7 @@ ScalexWithdrawRequestDto _$ScalexWithdrawRequestDtoFromJson( /// @nodoc mixin _$ScalexWithdrawRequestDto { String get orderId => throw _privateConstructorUsedError; + Cluster get cluster => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -769,7 +808,7 @@ abstract class $ScalexWithdrawRequestDtoCopyWith<$Res> { $Res Function(ScalexWithdrawRequestDto) then) = _$ScalexWithdrawRequestDtoCopyWithImpl<$Res, ScalexWithdrawRequestDto>; @useResult - $Res call({String orderId}); + $Res call({String orderId, Cluster cluster}); } /// @nodoc @@ -787,12 +826,17 @@ class _$ScalexWithdrawRequestDtoCopyWithImpl<$Res, @override $Res call({ Object? orderId = null, + Object? cluster = null, }) { return _then(_value.copyWith( orderId: null == orderId ? _value.orderId : orderId // ignore: cast_nullable_to_non_nullable as String, + cluster: null == cluster + ? _value.cluster + : cluster // ignore: cast_nullable_to_non_nullable + as Cluster, ) as $Val); } } @@ -806,7 +850,7 @@ abstract class _$$ScalexWithdrawRequestDtoImplCopyWith<$Res> __$$ScalexWithdrawRequestDtoImplCopyWithImpl<$Res>; @override @useResult - $Res call({String orderId}); + $Res call({String orderId, Cluster cluster}); } /// @nodoc @@ -823,12 +867,17 @@ class __$$ScalexWithdrawRequestDtoImplCopyWithImpl<$Res> @override $Res call({ Object? orderId = null, + Object? cluster = null, }) { return _then(_$ScalexWithdrawRequestDtoImpl( orderId: null == orderId ? _value.orderId : orderId // ignore: cast_nullable_to_non_nullable as String, + cluster: null == cluster + ? _value.cluster + : cluster // ignore: cast_nullable_to_non_nullable + as Cluster, )); } } @@ -836,17 +885,20 @@ class __$$ScalexWithdrawRequestDtoImplCopyWithImpl<$Res> /// @nodoc @JsonSerializable() class _$ScalexWithdrawRequestDtoImpl implements _ScalexWithdrawRequestDto { - const _$ScalexWithdrawRequestDtoImpl({required this.orderId}); + const _$ScalexWithdrawRequestDtoImpl( + {required this.orderId, required this.cluster}); factory _$ScalexWithdrawRequestDtoImpl.fromJson(Map json) => _$$ScalexWithdrawRequestDtoImplFromJson(json); @override final String orderId; + @override + final Cluster cluster; @override String toString() { - return 'ScalexWithdrawRequestDto(orderId: $orderId)'; + return 'ScalexWithdrawRequestDto(orderId: $orderId, cluster: $cluster)'; } @override @@ -854,12 +906,13 @@ class _$ScalexWithdrawRequestDtoImpl implements _ScalexWithdrawRequestDto { return identical(this, other) || (other.runtimeType == runtimeType && other is _$ScalexWithdrawRequestDtoImpl && - (identical(other.orderId, orderId) || other.orderId == orderId)); + (identical(other.orderId, orderId) || other.orderId == orderId) && + (identical(other.cluster, cluster) || other.cluster == cluster)); } @JsonKey(ignore: true) @override - int get hashCode => Object.hash(runtimeType, orderId); + int get hashCode => Object.hash(runtimeType, orderId, cluster); @JsonKey(ignore: true) @override @@ -877,8 +930,9 @@ class _$ScalexWithdrawRequestDtoImpl implements _ScalexWithdrawRequestDto { } abstract class _ScalexWithdrawRequestDto implements ScalexWithdrawRequestDto { - const factory _ScalexWithdrawRequestDto({required final String orderId}) = - _$ScalexWithdrawRequestDtoImpl; + const factory _ScalexWithdrawRequestDto( + {required final String orderId, + required final Cluster cluster}) = _$ScalexWithdrawRequestDtoImpl; factory _ScalexWithdrawRequestDto.fromJson(Map json) = _$ScalexWithdrawRequestDtoImpl.fromJson; @@ -886,6 +940,8 @@ abstract class _ScalexWithdrawRequestDto implements ScalexWithdrawRequestDto { @override String get orderId; @override + Cluster get cluster; + @override @JsonKey(ignore: true) _$$ScalexWithdrawRequestDtoImplCopyWith<_$ScalexWithdrawRequestDtoImpl> get copyWith => throw _privateConstructorUsedError; @@ -898,8 +954,9 @@ ScalexWithdrawResponseDto _$ScalexWithdrawResponseDtoFromJson( /// @nodoc mixin _$ScalexWithdrawResponseDto { - String get depositAddress => throw _privateConstructorUsedError; - int get amount => throw _privateConstructorUsedError; + int get fee => throw _privateConstructorUsedError; + String get transaction => throw _privateConstructorUsedError; + BigInt get slot => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -913,7 +970,7 @@ abstract class $ScalexWithdrawResponseDtoCopyWith<$Res> { $Res Function(ScalexWithdrawResponseDto) then) = _$ScalexWithdrawResponseDtoCopyWithImpl<$Res, ScalexWithdrawResponseDto>; @useResult - $Res call({String depositAddress, int amount}); + $Res call({int fee, String transaction, BigInt slot}); } /// @nodoc @@ -930,18 +987,23 @@ class _$ScalexWithdrawResponseDtoCopyWithImpl<$Res, @pragma('vm:prefer-inline') @override $Res call({ - Object? depositAddress = null, - Object? amount = null, + Object? fee = null, + Object? transaction = null, + Object? slot = null, }) { return _then(_value.copyWith( - depositAddress: null == depositAddress - ? _value.depositAddress - : depositAddress // ignore: cast_nullable_to_non_nullable - as String, - amount: null == amount - ? _value.amount - : amount // ignore: cast_nullable_to_non_nullable + fee: null == fee + ? _value.fee + : fee // ignore: cast_nullable_to_non_nullable as int, + transaction: null == transaction + ? _value.transaction + : transaction // ignore: cast_nullable_to_non_nullable + as String, + slot: null == slot + ? _value.slot + : slot // ignore: cast_nullable_to_non_nullable + as BigInt, ) as $Val); } } @@ -955,7 +1017,7 @@ abstract class _$$WithdrawPaymentResponseDtoImplCopyWith<$Res> __$$WithdrawPaymentResponseDtoImplCopyWithImpl<$Res>; @override @useResult - $Res call({String depositAddress, int amount}); + $Res call({int fee, String transaction, BigInt slot}); } /// @nodoc @@ -971,18 +1033,23 @@ class __$$WithdrawPaymentResponseDtoImplCopyWithImpl<$Res> @pragma('vm:prefer-inline') @override $Res call({ - Object? depositAddress = null, - Object? amount = null, + Object? fee = null, + Object? transaction = null, + Object? slot = null, }) { return _then(_$WithdrawPaymentResponseDtoImpl( - depositAddress: null == depositAddress - ? _value.depositAddress - : depositAddress // ignore: cast_nullable_to_non_nullable - as String, - amount: null == amount - ? _value.amount - : amount // ignore: cast_nullable_to_non_nullable + fee: null == fee + ? _value.fee + : fee // ignore: cast_nullable_to_non_nullable as int, + transaction: null == transaction + ? _value.transaction + : transaction // ignore: cast_nullable_to_non_nullable + as String, + slot: null == slot + ? _value.slot + : slot // ignore: cast_nullable_to_non_nullable + as BigInt, )); } } @@ -991,20 +1058,22 @@ class __$$WithdrawPaymentResponseDtoImplCopyWithImpl<$Res> @JsonSerializable() class _$WithdrawPaymentResponseDtoImpl implements _WithdrawPaymentResponseDto { const _$WithdrawPaymentResponseDtoImpl( - {required this.depositAddress, required this.amount}); + {required this.fee, required this.transaction, required this.slot}); factory _$WithdrawPaymentResponseDtoImpl.fromJson( Map json) => _$$WithdrawPaymentResponseDtoImplFromJson(json); @override - final String depositAddress; + final int fee; @override - final int amount; + final String transaction; + @override + final BigInt slot; @override String toString() { - return 'ScalexWithdrawResponseDto(depositAddress: $depositAddress, amount: $amount)'; + return 'ScalexWithdrawResponseDto(fee: $fee, transaction: $transaction, slot: $slot)'; } @override @@ -1012,14 +1081,15 @@ class _$WithdrawPaymentResponseDtoImpl implements _WithdrawPaymentResponseDto { return identical(this, other) || (other.runtimeType == runtimeType && other is _$WithdrawPaymentResponseDtoImpl && - (identical(other.depositAddress, depositAddress) || - other.depositAddress == depositAddress) && - (identical(other.amount, amount) || other.amount == amount)); + (identical(other.fee, fee) || other.fee == fee) && + (identical(other.transaction, transaction) || + other.transaction == transaction) && + (identical(other.slot, slot) || other.slot == slot)); } @JsonKey(ignore: true) @override - int get hashCode => Object.hash(runtimeType, depositAddress, amount); + int get hashCode => Object.hash(runtimeType, fee, transaction, slot); @JsonKey(ignore: true) @override @@ -1039,16 +1109,19 @@ class _$WithdrawPaymentResponseDtoImpl implements _WithdrawPaymentResponseDto { abstract class _WithdrawPaymentResponseDto implements ScalexWithdrawResponseDto { const factory _WithdrawPaymentResponseDto( - {required final String depositAddress, - required final int amount}) = _$WithdrawPaymentResponseDtoImpl; + {required final int fee, + required final String transaction, + required final BigInt slot}) = _$WithdrawPaymentResponseDtoImpl; factory _WithdrawPaymentResponseDto.fromJson(Map json) = _$WithdrawPaymentResponseDtoImpl.fromJson; @override - String get depositAddress; + int get fee; @override - int get amount; + String get transaction; + @override + BigInt get slot; @override @JsonKey(ignore: true) _$$WithdrawPaymentResponseDtoImplCopyWith<_$WithdrawPaymentResponseDtoImpl> @@ -1282,6 +1355,165 @@ abstract class _OnRampScalexDetails implements OnRampScalexDetails { throw _privateConstructorUsedError; } +OffRampScalexDetails _$OffRampScalexDetailsFromJson(Map json) { + return _OffRampScalexDetails.fromJson(json); +} + +/// @nodoc +mixin _$OffRampScalexDetails { + String get depositAddress => throw _privateConstructorUsedError; + int get amount => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $OffRampScalexDetailsCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $OffRampScalexDetailsCopyWith<$Res> { + factory $OffRampScalexDetailsCopyWith(OffRampScalexDetails value, + $Res Function(OffRampScalexDetails) then) = + _$OffRampScalexDetailsCopyWithImpl<$Res, OffRampScalexDetails>; + @useResult + $Res call({String depositAddress, int amount}); +} + +/// @nodoc +class _$OffRampScalexDetailsCopyWithImpl<$Res, + $Val extends OffRampScalexDetails> + implements $OffRampScalexDetailsCopyWith<$Res> { + _$OffRampScalexDetailsCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? depositAddress = null, + Object? amount = null, + }) { + return _then(_value.copyWith( + depositAddress: null == depositAddress + ? _value.depositAddress + : depositAddress // ignore: cast_nullable_to_non_nullable + as String, + amount: null == amount + ? _value.amount + : amount // ignore: cast_nullable_to_non_nullable + as int, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$OffRampScalexDetailsImplCopyWith<$Res> + implements $OffRampScalexDetailsCopyWith<$Res> { + factory _$$OffRampScalexDetailsImplCopyWith(_$OffRampScalexDetailsImpl value, + $Res Function(_$OffRampScalexDetailsImpl) then) = + __$$OffRampScalexDetailsImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String depositAddress, int amount}); +} + +/// @nodoc +class __$$OffRampScalexDetailsImplCopyWithImpl<$Res> + extends _$OffRampScalexDetailsCopyWithImpl<$Res, _$OffRampScalexDetailsImpl> + implements _$$OffRampScalexDetailsImplCopyWith<$Res> { + __$$OffRampScalexDetailsImplCopyWithImpl(_$OffRampScalexDetailsImpl _value, + $Res Function(_$OffRampScalexDetailsImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? depositAddress = null, + Object? amount = null, + }) { + return _then(_$OffRampScalexDetailsImpl( + depositAddress: null == depositAddress + ? _value.depositAddress + : depositAddress // ignore: cast_nullable_to_non_nullable + as String, + amount: null == amount + ? _value.amount + : amount // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$OffRampScalexDetailsImpl implements _OffRampScalexDetails { + const _$OffRampScalexDetailsImpl( + {required this.depositAddress, required this.amount}); + + factory _$OffRampScalexDetailsImpl.fromJson(Map json) => + _$$OffRampScalexDetailsImplFromJson(json); + + @override + final String depositAddress; + @override + final int amount; + + @override + String toString() { + return 'OffRampScalexDetails(depositAddress: $depositAddress, amount: $amount)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$OffRampScalexDetailsImpl && + (identical(other.depositAddress, depositAddress) || + other.depositAddress == depositAddress) && + (identical(other.amount, amount) || other.amount == amount)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, depositAddress, amount); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$OffRampScalexDetailsImplCopyWith<_$OffRampScalexDetailsImpl> + get copyWith => + __$$OffRampScalexDetailsImplCopyWithImpl<_$OffRampScalexDetailsImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$OffRampScalexDetailsImplToJson( + this, + ); + } +} + +abstract class _OffRampScalexDetails implements OffRampScalexDetails { + const factory _OffRampScalexDetails( + {required final String depositAddress, + required final int amount}) = _$OffRampScalexDetailsImpl; + + factory _OffRampScalexDetails.fromJson(Map json) = + _$OffRampScalexDetailsImpl.fromJson; + + @override + String get depositAddress; + @override + int get amount; + @override + @JsonKey(ignore: true) + _$$OffRampScalexDetailsImplCopyWith<_$OffRampScalexDetailsImpl> + get copyWith => throw _privateConstructorUsedError; +} + ScalexRateFeeResponseDto _$ScalexRateFeeResponseDtoFromJson( Map json) { return _ScalexRateFeeResponseDto.fromJson(json); diff --git a/packages/espressocash_api/lib/src/dto/scalex.g.dart b/packages/espressocash_api/lib/src/dto/scalex.g.dart index 914fdfc9e4..a887d2864c 100644 --- a/packages/espressocash_api/lib/src/dto/scalex.g.dart +++ b/packages/espressocash_api/lib/src/dto/scalex.g.dart @@ -59,6 +59,10 @@ _$OrderStatusScalexResponseDtoImpl _$$OrderStatusScalexResponseDtoImplFromJson( ? null : OnRampScalexDetails.fromJson( json['onRampDetails'] as Map), + offRampDetails: json['offRampDetails'] == null + ? null + : OffRampScalexDetails.fromJson( + json['offRampDetails'] as Map), ); Map _$$OrderStatusScalexResponseDtoImplToJson( @@ -66,6 +70,7 @@ Map _$$OrderStatusScalexResponseDtoImplToJson( { 'status': _$ScalexOrderStatusEnumMap[instance.status]!, 'onRampDetails': instance.onRampDetails, + 'offRampDetails': instance.offRampDetails, }; const _$ScalexOrderStatusEnumMap = { @@ -80,26 +85,35 @@ _$ScalexWithdrawRequestDtoImpl _$$ScalexWithdrawRequestDtoImplFromJson( Map json) => _$ScalexWithdrawRequestDtoImpl( orderId: json['orderId'] as String, + cluster: $enumDecode(_$ClusterEnumMap, json['cluster']), ); Map _$$ScalexWithdrawRequestDtoImplToJson( _$ScalexWithdrawRequestDtoImpl instance) => { 'orderId': instance.orderId, + 'cluster': _$ClusterEnumMap[instance.cluster]!, }; +const _$ClusterEnumMap = { + Cluster.mainnet: 'mainnet', + Cluster.devnet: 'devnet', +}; + _$WithdrawPaymentResponseDtoImpl _$$WithdrawPaymentResponseDtoImplFromJson( Map json) => _$WithdrawPaymentResponseDtoImpl( - depositAddress: json['depositAddress'] as String, - amount: (json['amount'] as num).toInt(), + fee: (json['fee'] as num).toInt(), + transaction: json['transaction'] as String, + slot: BigInt.parse(json['slot'] as String), ); Map _$$WithdrawPaymentResponseDtoImplToJson( _$WithdrawPaymentResponseDtoImpl instance) => { - 'depositAddress': instance.depositAddress, - 'amount': instance.amount, + 'fee': instance.fee, + 'transaction': instance.transaction, + 'slot': instance.slot.toString(), }; _$OnRampScalexDetailsImpl _$$OnRampScalexDetailsImplFromJson( @@ -122,6 +136,20 @@ Map _$$OnRampScalexDetailsImplToJson( 'fiatCurrency': instance.fiatCurrency, }; +_$OffRampScalexDetailsImpl _$$OffRampScalexDetailsImplFromJson( + Map json) => + _$OffRampScalexDetailsImpl( + depositAddress: json['depositAddress'] as String, + amount: (json['amount'] as num).toInt(), + ); + +Map _$$OffRampScalexDetailsImplToJson( + _$OffRampScalexDetailsImpl instance) => + { + 'depositAddress': instance.depositAddress, + 'amount': instance.amount, + }; + _$ScalexRateFeeResponseDtoImpl _$$ScalexRateFeeResponseDtoImplFromJson( Map json) => _$ScalexRateFeeResponseDtoImpl( From 8a1a9f2426c81b4f010efccea28ba02ffe6dec1c Mon Sep 17 00:00:00 2001 From: Vlad Sumin Date: Tue, 16 Jul 2024 01:53:43 +0300 Subject: [PATCH 07/20] upd --- .../espressocash_api/lib/src/dto/scalex.dart | 2 +- .../ramp/services/off_ramp_order_service.dart | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/espressocash_api/lib/src/dto/scalex.dart b/packages/espressocash_api/lib/src/dto/scalex.dart index 064fe0bf7d..681c426340 100644 --- a/packages/espressocash_api/lib/src/dto/scalex.dart +++ b/packages/espressocash_api/lib/src/dto/scalex.dart @@ -91,7 +91,7 @@ class OnRampScalexDetails with _$OnRampScalexDetails { } @freezed -class OffRampScalexDetails with _$OnRampScalexDetails { +class OffRampScalexDetails with _$OffRampScalexDetails { const factory OffRampScalexDetails({ required String depositAddress, required int amount, diff --git a/packages/espressocash_app/lib/features/ramp/services/off_ramp_order_service.dart b/packages/espressocash_app/lib/features/ramp/services/off_ramp_order_service.dart index 028a956e1b..787209cb8c 100644 --- a/packages/espressocash_app/lib/features/ramp/services/off_ramp_order_service.dart +++ b/packages/espressocash_app/lib/features/ramp/services/off_ramp_order_service.dart @@ -390,13 +390,21 @@ class OffRampOrderService implements Disposable { Future _createScalexTx({ required String partnerOrderId, }) async { - final dto = ScalexWithdrawRequestDto(orderId: partnerOrderId); - final response = await _client.createScalexWithdraw(dto); + final dto = OrderStatusScalexRequestDto(referenceId: partnerOrderId); + final response = await _client.fetchScalexTransaction(dto); + + final details = response.offRampDetails; + + if (details == null) { + return const OffRampOrderRowsCompanion( + status: Value(OffRampOrderStatus.depositError), + ); + } final scalexPaymentResult = await _scalexWithdrawPayment( - aReceiver: Ed25519HDPublicKey.fromBase58(response.depositAddress), + aReceiver: Ed25519HDPublicKey.fromBase58(details.depositAddress), aSender: _account.publicKey, - amount: response.amount, + amount: details.amount, commitment: Commitment.confirmed, ); From 1a8c85f7e78767d67ecce9dc78e831e0cfb62072 Mon Sep 17 00:00:00 2001 From: Vlad Sumin Date: Tue, 16 Jul 2024 13:25:18 +0300 Subject: [PATCH 08/20] Made dln payments durable --- .../outgoing_dln_payments/client/chains.dart | 52 ++++ .../outgoing_dln_payments/client/client.dart | 45 +++ .../outgoing_dln_payments/client/dto.dart | 153 ++++++++++ .../outgoing_dln_payments/client/models.dart | 130 ++++++++ .../create_dln_payment.dart | 287 ++++++++++++++++++ .../data/quote_repository.dart | 74 +++-- .../data/repository.dart | 16 +- .../models/outgoing_payment.dart | 10 +- .../models/payment_quote.dart | 1 - .../models/quote_info.dart | 14 + .../services/confirm_payment_bloc.dart | 39 ++- .../services/dln_order_service.dart | 53 ++-- .../screens/outgoing_dln_order_screen.dart | 5 +- packages/espressocash_app/pubspec.lock | 2 +- packages/espressocash_app/pubspec.yaml | 1 + 15 files changed, 790 insertions(+), 92 deletions(-) create mode 100644 packages/espressocash_app/lib/features/outgoing_dln_payments/client/chains.dart create mode 100644 packages/espressocash_app/lib/features/outgoing_dln_payments/client/client.dart create mode 100644 packages/espressocash_app/lib/features/outgoing_dln_payments/client/dto.dart create mode 100644 packages/espressocash_app/lib/features/outgoing_dln_payments/client/models.dart create mode 100644 packages/espressocash_app/lib/features/outgoing_dln_payments/create_dln_payment.dart create mode 100644 packages/espressocash_app/lib/features/outgoing_dln_payments/models/quote_info.dart diff --git a/packages/espressocash_app/lib/features/outgoing_dln_payments/client/chains.dart b/packages/espressocash_app/lib/features/outgoing_dln_payments/client/chains.dart new file mode 100644 index 0000000000..5739483c97 --- /dev/null +++ b/packages/espressocash_app/lib/features/outgoing_dln_payments/client/chains.dart @@ -0,0 +1,52 @@ +enum DlnChains { + arbitrum, + avalanche, + bnb, + ethereum, + polygon, + solana, + linea, + base, + optimism +} + +extension DlnChainsExt on DlnChains { + String get chainId => switch (this) { + DlnChains.arbitrum => '42161', + DlnChains.avalanche => '43114', + DlnChains.bnb => '56', + DlnChains.ethereum => '1', + DlnChains.polygon => '137', + DlnChains.solana => '7565164', + DlnChains.linea => '59144', + DlnChains.base => '8453', + DlnChains.optimism => '10', + }; + + String get usdcAddress => switch (this) { + DlnChains.arbitrum => '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', + DlnChains.avalanche => '0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E', + DlnChains.bnb => '0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d', + DlnChains.ethereum => '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + DlnChains.polygon => '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', + DlnChains.solana => 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', + DlnChains.linea => '0x176211869ca2b568f2a7d4ee941e073a821ee1ff', + DlnChains.base => '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', + DlnChains.optimism => '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', + }; +} + +extension StringExt on String { + DlnChains? get toDlnChain => switch (this) { + 'arbitrum' => DlnChains.arbitrum, + 'avalanche' => DlnChains.avalanche, + 'bnb' => DlnChains.bnb, + 'ethereum' => DlnChains.ethereum, + 'polygon' => DlnChains.polygon, + 'solana' => DlnChains.solana, + 'linea' => DlnChains.linea, + 'base' => DlnChains.base, + 'optimism' => DlnChains.optimism, + _ => null, + }; +} diff --git a/packages/espressocash_app/lib/features/outgoing_dln_payments/client/client.dart b/packages/espressocash_app/lib/features/outgoing_dln_payments/client/client.dart new file mode 100644 index 0000000000..a79f715e34 --- /dev/null +++ b/packages/espressocash_app/lib/features/outgoing_dln_payments/client/client.dart @@ -0,0 +1,45 @@ +import 'package:dio/dio.dart'; +import 'package:injectable/injectable.dart'; +import 'package:retrofit/retrofit.dart'; + +import 'dto.dart'; + +export 'chains.dart'; +export 'dto.dart'; + +part 'client.g.dart'; + +/// For docs head to https://api.dln.trade/v1.0/#/DLN +@RestApi(baseUrl: 'https://api.dln.trade/v1.0/dln') +@injectable +abstract class DlnApiClient { + @factoryMethod + factory DlnApiClient() => _DlnApiClient(Dio()); + + /// This endpoint returns the recommended take amount for creating DLN order. + @GET('/order/quote') + Future getQuote( + @Queries() DlnQuoteRequestDto quoteRequestDto, + ); + + /// This endpoint returns the data for a transaction to place a cross-chain DLN order. + @GET('/order/create-tx') + Future createTx( + @Queries() CreateTxRequestDto createTxRequestDto, + ); + + /// This endpoint returns the data of order. + @GET('/order/{id}') + Future getOrder(@Path() String id); + + /// This endpoint returns the status of order. + @GET('/order/{id}/status') + Future getStatus(@Path() String id); + + @GET('/tx/{hash}/order-ids') + Future getOrderIdByHash(@Path() String hash); + + /// This endpoint generates a transaction that cancels the given order. + @GET('/order/{id}/cancel-tx') + Future cancelTx(@Path() String id); +} diff --git a/packages/espressocash_app/lib/features/outgoing_dln_payments/client/dto.dart b/packages/espressocash_app/lib/features/outgoing_dln_payments/client/dto.dart new file mode 100644 index 0000000000..faeb0b616a --- /dev/null +++ b/packages/espressocash_app/lib/features/outgoing_dln_payments/client/dto.dart @@ -0,0 +1,153 @@ +// ignore_for_file: invalid_annotation_target + +import 'package:freezed_annotation/freezed_annotation.dart'; + +import 'models.dart'; + +part 'dto.freezed.dart'; +part 'dto.g.dart'; + +@freezed +class DlnQuoteRequestDto with _$DlnQuoteRequestDto { + const factory DlnQuoteRequestDto({ + required String srcChainId, + required String srcChainTokenIn, + required String srcChainTokenInAmount, + required String dstChainId, + required String dstChainTokenOut, + String? dstChainTokenOutAmount, + int? additionalTakerRewardBps, + String? srcIntermediaryTokenAddress, + String? dstIntermediaryTokenAddress, + String? dstIntermediaryTokenSpenderAddress, + int? intermediaryTokenUSDPrice, + int? slippage, + double? affiliateFeePercent, + bool? prependOperatingExpenses, + }) = _DlnQuoteRequestDto; + + factory DlnQuoteRequestDto.fromJson(Map json) => + _$DlnQuoteRequestDtoFromJson(json); +} + +@freezed +class DlnQuoteResponseDto with _$DlnQuoteResponseDto { + const factory DlnQuoteResponseDto({ + required OrderEstimation estimation, + String? prependedOperatingExpenseCost, + TxQuote? tx, + required Order order, + required String fixFee, + }) = _DlnQuoteResponseDto; + + factory DlnQuoteResponseDto.fromJson(Map json) => + _$DlnQuoteResponseDtoFromJson(json); +} + +@freezed +class CreateTxRequestDto with _$CreateTxRequestDto { + const factory CreateTxRequestDto({ + required String srcChainId, + required String srcChainTokenIn, + required String srcChainTokenInAmount, + required String dstChainId, + required String dstChainTokenOut, + required String dstChainTokenOutRecipient, + required String srcChainOrderAuthorityAddress, + required String dstChainOrderAuthorityAddress, + String? dstChainTokenOutAmount, + double? additionalTakerRewardBps, + String? srcIntermediaryTokenAddress, + String? dstIntermediaryTokenAddress, + String? dstIntermediaryTokenSpenderAddress, + double? intermediaryTokenUSDPrice, + double? slippage, + String? senderAddress, + int? referralCode, + double? affiliateFeePercent, + String? affiliateFeeRecipient, + String? srcChainTokenInSenderPermit, + bool? enableEstimate, + String? allowedTaker, + String? externalCall, + bool? prependOperatingExpenses, + }) = _CreateTxRequestDto; + + factory CreateTxRequestDto.fromJson(Map json) => + _$CreateTxRequestDtoFromJson(json); +} + +@freezed +class CreateTxResponseDto with _$CreateTxResponseDto { + const factory CreateTxResponseDto({ + required OrderEstimation estimation, + required DlnTx tx, + String? prependedOperatingExpenseCost, + required Order order, + required String fixFee, + }) = _CreateTxResponseDto; + + factory CreateTxResponseDto.fromJson(Map json) => + _$CreateTxResponseDtoFromJson(json); +} + +@freezed +class OrderResponseDto with _$OrderResponseDto { + const factory OrderResponseDto({ + required String orderId, + required String status, + required String externalCallState, + required OrderStruct orderStruct, + }) = _OrderResponseDto; + + factory OrderResponseDto.fromJson(Map json) => + _$OrderResponseDtoFromJson(json); +} + +@freezed +class OrderStatusResponseDto with _$OrderStatusResponseDto { + const factory OrderStatusResponseDto({ + required String orderId, + @JsonKey(unknownEnumValue: OrderStatus.unknown) required OrderStatus status, + }) = _OrderStatusResponseDto; + + factory OrderStatusResponseDto.fromJson(Map json) => + _$OrderStatusResponseDtoFromJson(json); +} + +@freezed +class OrderIdTxResponseDto with _$OrderIdTxResponseDto { + const factory OrderIdTxResponseDto({ + required List orderIds, + }) = _OrderIdTxResponseDto; + + factory OrderIdTxResponseDto.fromJson(Map json) => + _$OrderIdTxResponseDtoFromJson(json); +} + +@freezed +class CancelTxResponseDto with _$CancelTxResponseDto { + const factory CancelTxResponseDto({ + required String to, + required String data, + required String value, + required double chainId, + required String from, + String? cancelBeneficiary, + }) = _CancelTxResponseDto; + + factory CancelTxResponseDto.fromJson(Map json) => + _$CancelTxResponseDtoFromJson(json); +} + +@JsonEnum(fieldRename: FieldRename.pascal) +enum OrderStatus { + created, + fulfilled, + sentUnlock, + claimedUnlock, + orderCancelled, + sentOrderCancel, + claimedOrderCancel, + unknown +} diff --git a/packages/espressocash_app/lib/features/outgoing_dln_payments/client/models.dart b/packages/espressocash_app/lib/features/outgoing_dln_payments/client/models.dart new file mode 100644 index 0000000000..73a458af61 --- /dev/null +++ b/packages/espressocash_app/lib/features/outgoing_dln_payments/client/models.dart @@ -0,0 +1,130 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'models.freezed.dart'; +part 'models.g.dart'; + +@freezed +class OrderEstimation with _$OrderEstimation { + const factory OrderEstimation({ + required TokenWithApproximateOperatingExpense srcChainTokenIn, + TokenWithMaxRefundAmount? srcChainTokenOut, + required DstChainTokenOutResponseType dstChainTokenOut, + double? recommendedSlippage, + required List costsDetails, + }) = _OrderEstimation; + + factory OrderEstimation.fromJson(Map json) => + _$OrderEstimationFromJson(json); +} + +@freezed +class TokenWithMaxRefundAmount with _$TokenWithMaxRefundAmount { + const factory TokenWithMaxRefundAmount({ + required String address, + required String name, + required String symbol, + required int decimals, + required String amount, + required String maxRefundAmount, + }) = _TokenWithMaxRefundAmount; + + factory TokenWithMaxRefundAmount.fromJson(Map json) => + _$TokenWithMaxRefundAmountFromJson(json); +} + +@freezed +class TokenWithApproximateOperatingExpense + with _$TokenWithApproximateOperatingExpense { + const factory TokenWithApproximateOperatingExpense({ + required String address, + required String name, + required String symbol, + required int decimals, + required String amount, + required String approximateOperatingExpense, + required bool mutatedWithOperatingExpense, + }) = _TokenWithApproximateOperatingExpense; + + factory TokenWithApproximateOperatingExpense.fromJson( + Map json, + ) => + _$TokenWithApproximateOperatingExpenseFromJson(json); +} + +@freezed +class DstChainTokenOutResponseType with _$DstChainTokenOutResponseType { + const factory DstChainTokenOutResponseType({ + required String address, + required String name, + required String symbol, + required int decimals, + required String amount, + required String recommendedAmount, + String? withoutAdditionalTakerRewardsAmount, + }) = _DstChainTokenOutResponseType; + + factory DstChainTokenOutResponseType.fromJson(Map json) => + _$DstChainTokenOutResponseTypeFromJson(json); +} + +@freezed +class TxQuote with _$TxQuote { + const factory TxQuote({ + required String allowanceTarget, + required String allowanceValue, + }) = _TxQuote; + + factory TxQuote.fromJson(Map json) => + _$TxQuoteFromJson(json); +} + +@freezed +class Order with _$Order { + const factory Order({ + required double approximateFulfillmentDelay, + }) = _Order; + + factory Order.fromJson(Map json) => _$OrderFromJson(json); +} + +@freezed +class DlnTx with _$DlnTx { + const factory DlnTx({ + required String data, + String? to, + String? value, + double? gasLimit, + }) = _DlnTx; + + factory DlnTx.fromJson(Map json) => _$DlnTxFromJson(json); +} + +@freezed +class Offer with _$Offer { + const factory Offer({ + required int chainId, + required String tokenAddress, + required String amount, + }) = _Offer; + + factory Offer.fromJson(Map json) => _$OfferFromJson(json); +} + +@freezed +class OrderStruct with _$OrderStruct { + const factory OrderStruct({ + required int makerOrderNonce, + required String makerSrc, + required Offer giveOffer, + required String receiverDst, + required Offer takeOffer, + required String givePatchAuthoritySrc, + required String orderAuthorityAddressDst, + String? allowedTakerDst, + String? allowedCancelBeneficiarySrc, + String? externalCall, + }) = _OrderStruct; + + factory OrderStruct.fromJson(Map json) => + _$OrderStructFromJson(json); +} diff --git a/packages/espressocash_app/lib/features/outgoing_dln_payments/create_dln_payment.dart b/packages/espressocash_app/lib/features/outgoing_dln_payments/create_dln_payment.dart new file mode 100644 index 0000000000..c828d87ee0 --- /dev/null +++ b/packages/espressocash_app/lib/features/outgoing_dln_payments/create_dln_payment.dart @@ -0,0 +1,287 @@ +import 'dart:math'; + +import 'package:borsh_annotation/borsh_annotation.dart'; +import 'package:collection/collection.dart'; +import 'package:convert/convert.dart'; +import 'package:dfunc/dfunc.dart'; +import 'package:espressocash_api/espressocash_api.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:injectable/injectable.dart'; +import 'package:solana/dto.dart' as dto; +import 'package:solana/encoder.dart'; +import 'package:solana/solana.dart'; + +import '../../utils/transactions.dart'; +import '../priority_fees/services/add_priority_fees.dart'; +import '../tokens/token.dart'; +import 'client/chains.dart'; +import 'data/quote_repository.dart'; + +part 'create_dln_payment.freezed.dart'; + +@freezed +class QuoteTransaction with _$QuoteTransaction { + const factory QuoteTransaction({ + required int senderDeductAmount, + required int receiverAmount, + required int fee, + required SignedTx transaction, + }) = _QuoteTransaction; +} + +@injectable +class CreateDlnPayment { + const CreateDlnPayment( + this._quoteRepository, + this._ecClient, + this._client, + this._addPriorityFees, + ); + + final QuoteRepository _quoteRepository; + final EspressoCashClient _ecClient; + final SolanaClient _client; + final AddPriorityFees _addPriorityFees; + + Future call({ + required int amount, + required String senderAddress, + required String receiverAddress, + required Commitment commitment, + required String receiverChain, + }) async { + final nonceData = await _ecClient.getFreeNonce(); + final platformAccount = Ed25519HDPublicKey.fromBase58(nonceData.authority); + + final senderAccount = Ed25519HDPublicKey.fromBase58(senderAddress); + + final chain = receiverChain.toDlnChain; + + if (chain == null) { + throw ArgumentError.value( + receiverChain, + 'receiverChain', + 'Invalid chain', + ); + } + + final feePayer = platformAccount; + + final instructions = []; + + final feesFromTransaction = await _client.calculateDlnTxFee( + sender: senderAccount, + commitment: commitment, + ); + final solTransferIx = SystemInstruction.transfer( + fundingAccount: feePayer, + recipientAccount: senderAccount, + lamports: feesFromTransaction, + ); + + instructions.add(solTransferIx); + + final percentageFeeAmount = (amount * outgoingDlnPaymentFeeFraction).ceil(); + final transferFeeAmount = max(percentageFeeAmount, minimumDlnPaymentFee); + + final feeIx = await _createFeePayment( + aSender: senderAccount, + aReceiver: feePayer, + amount: transferFeeAmount, + ); + + instructions.add(feeIx); + + final quote = await _quoteRepository.getQuoteAndTransaction( + amount: amount, + senderAddress: senderAddress, + receiverAddress: receiverAddress, + receiverChain: chain, + ); + + if (quote.senderDeductAmount != quote.totalFees + quote.inputAmount) { + throw ArgumentError.value( + quote, + 'quote', + 'Invalid quote', + ); + } + + final dlnTx = SignedTx.fromBytes(hex.decode(quote.tx.replaceAll('0x', ''))); + + final addressTableLookups = dlnTx.compiledMessage.map( + legacy: (_) => [], + v0: (v0) => v0.addressTableLookups, + ); + + final lookUpTables = await _client.rpcClient + .getAddressLookUpTableAccounts(addressTableLookups); + + final dlnMessage = dlnTx.let( + (tx) => tx.decompileMessage(addressLookupTableAccounts: lookUpTables), + ); + + final instructionsWithNonce = [ + SystemInstruction.advanceNonceAccount( + nonce: Ed25519HDPublicKey.fromBase58(nonceData.nonceAccount), + nonceAuthority: platformAccount, + ), + ...instructions, + ]; + + final message = dlnMessage + .let((m) => m.removeDefaultComputeIx()) + .let((m) => m.addIxsToFront(instructionsWithNonce)); + + final compiled = message.compileV0( + recentBlockhash: nonceData.nonce, + feePayer: feePayer, + addressLookupTableAccounts: lookUpTables, + ); + + final tx = await SignedTx( + compiledMessage: compiled, + signatures: [ + platformAccount.emptySignature(), + senderAccount.emptySignature(), + ], + ).let( + (tx) => _addPriorityFees( + tx: tx, + commitment: commitment, + maxPriorityFee: _maxTxCostUsdc, + platform: platformAccount, + ), + ); + + final totalFees = quote.totalFees.toInt() + transferFeeAmount; + + return QuoteTransaction( + senderDeductAmount: quote.senderDeductAmount, + receiverAmount: quote.receiverAmount, + fee: totalFees, + transaction: tx, + ); + } +} + +extension on Message { + Message addIxsToFront(List ix) => + Message(instructions: [...ix, ...instructions]); + + Message removeDefaultComputeIx() => Message( + instructions: instructions + .where((e) => e.programId != ComputeBudgetProgram.id) + .toList(), + ); +} + +Future _createFeePayment({ + required Ed25519HDPublicKey aReceiver, + required Ed25519HDPublicKey aSender, + required int amount, +}) async { + final mint = Token.usdc.publicKey; + + final ataSender = await findAssociatedTokenAddress( + owner: aSender, + mint: mint, + ); + + final ataReceiver = await findAssociatedTokenAddress( + owner: aReceiver, + mint: mint, + ); + + return TokenInstruction.transfer( + amount: amount, + source: ataSender, + destination: ataReceiver, + owner: aSender, + ); +} + +extension on SolanaClient { + Future calculateDlnTxFee({ + required Ed25519HDPublicKey sender, + required Commitment commitment, + }) async { + final results = await Future.wait([ + _fetchDlnProtocolFee(commitment: commitment), + rpcClient.getMinimumBalanceForRentExemption(211), + rpcClient + .getMinimumBalanceForRentExemption(TokenProgram.neededAccountSpace), + ]); + + final nonceAccountCreationFee = await _hasNonceAccount( + sender: sender, + commitment: commitment, + ).letAsync( + (p) async => p + ? 0 + : await rpcClient.getMinimumBalanceForRentExemption(16) + 900000, + ); + + return results.sum + nonceAccountCreationFee; + } + + Future _hasNonceAccount({ + required Ed25519HDPublicKey sender, + required Commitment commitment, + }) async { + final pda = await Ed25519HDPublicKey.findProgramAddress( + seeds: ['NONCE'.codeUnits, sender.bytes], + programId: _dlnSourceAddress, + ); + + final info = await rpcClient + .getAccountInfo( + pda.toBase58(), + commitment: commitment, + encoding: dto.Encoding.base64, + ) + .value; + + return info?.data != null; + } + + Future _fetchDlnProtocolFee({ + required Commitment commitment, + }) async { + final pda = await Ed25519HDPublicKey.findProgramAddress( + seeds: ['STATE'.codeUnits], + programId: _dlnSourceAddress, + ); + + final info = await rpcClient + .getAccountInfo( + pda.toBase58(), + commitment: commitment, + encoding: dto.Encoding.base64, + ) + .value; + + // ignore: avoid-non-null-assertion, required data + final data = info!.data! as dto.BinaryAccountData; + + return data.data + .skip(8) + .skip(32) + .toList() + .let(Uint8List.fromList) + .buffer + .asByteData() + .let(BinaryReader.new) + .readU64() + .toInt(); + } +} + +final _dlnSourceAddress = Ed25519HDPublicKey.fromBase58( + 'src5qyZHqTqecJV4aY6Cb6zDZLMDzrDKKezs22MPHr4', +); + +const _maxTxCostUsdc = 2000000; // 2 USDC +const outgoingDlnPaymentFeeFraction = 0.03; // 3% +const incomingDlnPaymentFeePercentage = 3.0; // 3% +const minimumDlnPaymentFee = 3000000; // 3 USDC diff --git a/packages/espressocash_app/lib/features/outgoing_dln_payments/data/quote_repository.dart b/packages/espressocash_app/lib/features/outgoing_dln_payments/data/quote_repository.dart index 9dee264b67..ea093f8b3b 100644 --- a/packages/espressocash_app/lib/features/outgoing_dln_payments/data/quote_repository.dart +++ b/packages/espressocash_app/lib/features/outgoing_dln_payments/data/quote_repository.dart @@ -1,54 +1,50 @@ -import 'package:espressocash_api/espressocash_api.dart'; - import 'package:injectable/injectable.dart'; -import '../../blockchain/models/blockchain.dart'; -import '../../currency/models/amount.dart'; -import '../../currency/models/currency.dart'; -import '../models/dln_payment.dart'; -import '../models/payment_quote.dart'; +import '../../tokens/token.dart'; +import '../client/client.dart'; +import '../models/quote_info.dart'; @injectable class QuoteRepository { const QuoteRepository({ - required EspressoCashClient ecClient, - }) : _client = ecClient; - - final EspressoCashClient _client; + required DlnApiClient dlnApiClient, + }) : _dlnApiClient = dlnApiClient; + final DlnApiClient _dlnApiClient; - Future getQuote({ - required CryptoAmount amount, + Future getQuoteAndTransaction({ + required int amount, required String receiverAddress, - required Blockchain receiverBlockchain, + required String senderAddress, + required DlnChains receiverChain, }) async { - final quote = await _client.getDlnQuote( - PaymentQuoteRequestDto( - amount: amount.value, - receiverAddress: receiverAddress, - receiverBlockchain: receiverBlockchain.name, + final response = await _dlnApiClient.createTx( + CreateTxRequestDto( + srcChainId: DlnChains.solana.chainId, + srcChainTokenIn: Token.usdc.publicKey.toBase58(), + srcChainTokenInAmount: 'auto', + dstChainId: receiverChain.chainId, + dstChainTokenOut: receiverChain.usdcAddress, + dstChainTokenOutAmount: amount.toString(), + dstChainTokenOutRecipient: receiverAddress, + srcChainOrderAuthorityAddress: senderAddress, + dstChainOrderAuthorityAddress: receiverAddress, + referralCode: espressoDlnRefCode, ), ); - return PaymentQuote( - payment: DlnPayment( - inputAmount: amount, - receiverAddress: receiverAddress, - receiverBlockchain: receiverBlockchain, - ), - receiverAmount: CryptoAmount( - cryptoCurrency: Currency.usdc, - value: quote.receiverAmount, - ), - inputAmount: CryptoAmount( - cryptoCurrency: Currency.usdc, - value: quote.inputAmount, - ), - fee: CryptoAmount( - cryptoCurrency: Currency.usdc, - value: quote.feeInUsdc, - ), - encodedTx: quote.encodedTx, - slot: quote.slot, + final tx = response.tx.data; + final estimation = response.estimation; + + final totalFees = int.parse(estimation.srcChainTokenIn.amount) - amount; + + return QuoteInfo( + tx: tx, + inputAmount: amount, + senderDeductAmount: int.parse(estimation.srcChainTokenIn.amount), + receiverAmount: int.parse(estimation.dstChainTokenOut.recommendedAmount), + totalFees: totalFees, ); } } + +const espressoDlnRefCode = 8435; diff --git a/packages/espressocash_app/lib/features/outgoing_dln_payments/data/repository.dart b/packages/espressocash_app/lib/features/outgoing_dln_payments/data/repository.dart index c30365a69b..63376ecf14 100644 --- a/packages/espressocash_app/lib/features/outgoing_dln_payments/data/repository.dart +++ b/packages/espressocash_app/lib/features/outgoing_dln_payments/data/repository.dart @@ -100,16 +100,12 @@ extension OutgoingDlnPaymentRowExt on OutgoingDlnPaymentRow { extension on ODLNPaymentStatusDto { OutgoingDlnPaymentStatus toModel(OutgoingDlnPaymentRow row) { final tx = row.tx?.let(SignedTx.decode); - final slot = row.slot?.let(BigInt.tryParse); switch (this) { case ODLNPaymentStatusDto.txCreated: - return OutgoingDlnPaymentStatus.txCreated( - tx!, - slot: slot ?? BigInt.zero, - ); + return OutgoingDlnPaymentStatus.txCreated(tx!); case ODLNPaymentStatusDto.txSent: - return OutgoingDlnPaymentStatus.txSent(tx!, slot: slot ?? BigInt.zero); + return OutgoingDlnPaymentStatus.txSent(tx!, signature: tx.id); case ODLNPaymentStatusDto.success: return OutgoingDlnPaymentStatus.success( tx!, @@ -159,7 +155,6 @@ extension on OutgoingDlnPayment { tx: status.toTx(), txId: status.toTxId(), amount: amount.value, - slot: status.toSlot()?.toString(), receiverBlockchain: payment.receiverBlockchain.toDto(), receiverAddress: payment.receiverAddress, txFailureReason: status.toTxFailureReason(), @@ -185,7 +180,7 @@ extension on OutgoingDlnPaymentStatus { ); String? toTxId() => mapOrNull( - txSent: (it) => it.tx.id, + txSent: (it) => it.signature, success: (it) => it.tx.id, txCreated: (it) => it.tx.id, unfulfilled: (it) => it.tx.id, @@ -200,9 +195,4 @@ extension on OutgoingDlnPaymentStatus { TxFailureReason? toTxFailureReason() => mapOrNull( txFailure: (it) => it.reason, ); - - BigInt? toSlot() => mapOrNull( - txCreated: (it) => it.slot, - txSent: (it) => it.slot, - ); } diff --git a/packages/espressocash_app/lib/features/outgoing_dln_payments/models/outgoing_payment.dart b/packages/espressocash_app/lib/features/outgoing_dln_payments/models/outgoing_payment.dart index 8ed6e5dee4..47ef6ec9e6 100644 --- a/packages/espressocash_app/lib/features/outgoing_dln_payments/models/outgoing_payment.dart +++ b/packages/espressocash_app/lib/features/outgoing_dln_payments/models/outgoing_payment.dart @@ -21,15 +21,13 @@ class OutgoingDlnPayment with _$OutgoingDlnPayment { @freezed sealed class OutgoingDlnPaymentStatus with _$OutgoingDlnPaymentStatus { /// Tx is successfully created and ready to be sent. - const factory OutgoingDlnPaymentStatus.txCreated( - SignedTx tx, { - required BigInt slot, - }) = OutgoingDlnPaymentStatusTxCreated; + const factory OutgoingDlnPaymentStatus.txCreated(SignedTx tx) = + OutgoingDlnPaymentStatusTxCreated; - /// Tx is successfully sent. + /// Tx sent to backend. Should be good as confirmed at this point. const factory OutgoingDlnPaymentStatus.txSent( SignedTx tx, { - required BigInt slot, + required String signature, }) = OutgoingDlnPaymentStatusTxSent; /// Tx is successfully confirmed and order awaiting fulfillment. diff --git a/packages/espressocash_app/lib/features/outgoing_dln_payments/models/payment_quote.dart b/packages/espressocash_app/lib/features/outgoing_dln_payments/models/payment_quote.dart index 215645d0ec..766a578583 100644 --- a/packages/espressocash_app/lib/features/outgoing_dln_payments/models/payment_quote.dart +++ b/packages/espressocash_app/lib/features/outgoing_dln_payments/models/payment_quote.dart @@ -13,6 +13,5 @@ class PaymentQuote with _$PaymentQuote { required CryptoAmount receiverAmount, required CryptoAmount fee, required String encodedTx, - required BigInt slot, }) = _PaymentQuote; } diff --git a/packages/espressocash_app/lib/features/outgoing_dln_payments/models/quote_info.dart b/packages/espressocash_app/lib/features/outgoing_dln_payments/models/quote_info.dart new file mode 100644 index 0000000000..05755b4657 --- /dev/null +++ b/packages/espressocash_app/lib/features/outgoing_dln_payments/models/quote_info.dart @@ -0,0 +1,14 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'quote_info.freezed.dart'; + +@freezed +class QuoteInfo with _$QuoteInfo { + const factory QuoteInfo({ + required String tx, + required int inputAmount, + required int senderDeductAmount, + required int receiverAmount, + required num totalFees, + }) = _QuoteInfo; +} diff --git a/packages/espressocash_app/lib/features/outgoing_dln_payments/services/confirm_payment_bloc.dart b/packages/espressocash_app/lib/features/outgoing_dln_payments/services/confirm_payment_bloc.dart index 818d517619..e8dcb6e86c 100644 --- a/packages/espressocash_app/lib/features/outgoing_dln_payments/services/confirm_payment_bloc.dart +++ b/packages/espressocash_app/lib/features/outgoing_dln_payments/services/confirm_payment_bloc.dart @@ -6,12 +6,14 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:injectable/injectable.dart'; import 'package:rxdart/rxdart.dart'; +import 'package:solana/solana.dart'; import '../../../utils/flow.dart'; +import '../../accounts/models/ec_wallet.dart'; import '../../balances/data/repository.dart'; import '../../currency/models/amount.dart'; import '../../currency/models/currency.dart'; import '../../tokens/token.dart'; -import '../data/quote_repository.dart'; +import '../create_dln_payment.dart'; import '../models/dln_payment.dart'; import '../models/payment_quote.dart'; @@ -24,10 +26,12 @@ typedef _Emitter = Emitter<_State>; @injectable class ConfirmPaymentBloc extends Bloc<_Event, _State> { ConfirmPaymentBloc({ - required QuoteRepository quoteRepository, + required CreateDlnPayment createDlnPayment, required TokenBalancesRepository balancesRepository, - }) : _quoteRepository = quoteRepository, + required ECWallet account, + }) : _createDlnPayment = createDlnPayment, _balancesRepository = balancesRepository, + _account = account, super(ConfirmPaymentState(flowState: const Flow.initial())) { on(_onInit); on(_onConfirmed); @@ -39,8 +43,9 @@ class ConfirmPaymentBloc extends Bloc<_Event, _State> { ); } - final QuoteRepository _quoteRepository; + final CreateDlnPayment _createDlnPayment; final TokenBalancesRepository _balancesRepository; + final ECWallet _account; Timer? _timer; @@ -85,10 +90,30 @@ class ConfirmPaymentBloc extends Bloc<_Event, _State> { } try { - final quote = await _quoteRepository.getQuote( - amount: payment.inputAmount, + final createDlnPayment = await _createDlnPayment( + amount: payment.inputAmount.value, + senderAddress: _account.publicKey.toBase58(), receiverAddress: payment.receiverAddress, - receiverBlockchain: payment.receiverBlockchain, + receiverChain: payment.receiverBlockchain.name, + commitment: Commitment.confirmed, + ); + + final quote = PaymentQuote( + payment: DlnPayment( + inputAmount: payment.inputAmount, + receiverAddress: payment.receiverAddress, + receiverBlockchain: payment.receiverBlockchain, + ), + receiverAmount: CryptoAmount( + cryptoCurrency: Currency.usdc, + value: createDlnPayment.receiverAmount, + ), + inputAmount: payment.inputAmount, + fee: CryptoAmount( + cryptoCurrency: Currency.usdc, + value: createDlnPayment.fee, + ), + encodedTx: createDlnPayment.transaction.encode(), ); _startTimer(); diff --git a/packages/espressocash_app/lib/features/outgoing_dln_payments/services/dln_order_service.dart b/packages/espressocash_app/lib/features/outgoing_dln_payments/services/dln_order_service.dart index e224f347de..ae06e2b7c9 100644 --- a/packages/espressocash_app/lib/features/outgoing_dln_payments/services/dln_order_service.dart +++ b/packages/espressocash_app/lib/features/outgoing_dln_payments/services/dln_order_service.dart @@ -13,7 +13,7 @@ import '../../accounts/models/ec_wallet.dart'; import '../../currency/models/amount.dart'; import '../../transactions/models/tx_results.dart'; import '../../transactions/services/resign_tx.dart'; -import '../../transactions/services/tx_sender.dart'; +import '../../transactions/services/tx_confirm.dart'; import '../data/repository.dart'; import '../models/outgoing_payment.dart'; import '../models/payment_quote.dart'; @@ -23,7 +23,7 @@ class OutgoingDlnPaymentService implements Disposable { OutgoingDlnPaymentService( this._account, this._client, - this._sender, + this._txConfirm, this._repository, ); @@ -32,7 +32,7 @@ class OutgoingDlnPaymentService implements Disposable { final ECWallet _account; final EspressoCashClient _client; - final TxSender _sender; + final TxConfirm _txConfirm; final OutgoingDlnPaymentRepository _repository; @PostConstruct(preResolve: true) @@ -111,7 +111,7 @@ class OutgoingDlnPaymentService implements Disposable { try { final tx = await SignedTx.decode(quote.encodedTx).resign(_account); - return OutgoingDlnPaymentStatus.txCreated(tx, slot: quote.slot); + return OutgoingDlnPaymentStatus.txCreated(tx); } on Exception { return const OutgoingDlnPaymentStatus.txFailure( reason: TxFailureReason.creatingFailure, @@ -125,20 +125,30 @@ class OutgoingDlnPaymentService implements Disposable { return payment; } - final tx = await _sender.send(status.tx, minContextSlot: status.slot); - final OutgoingDlnPaymentStatus? newStatus = tx.map( - sent: (_) => OutgoingDlnPaymentStatus.txSent( - status.tx, - slot: status.slot, - ), - invalidBlockhash: (_) => const OutgoingDlnPaymentStatus.txFailure( - reason: TxFailureReason.invalidBlockhashSending, - ), - failure: (it) => OutgoingDlnPaymentStatus.txFailure(reason: it.reason), - networkError: (_) => null, - ); + final tx = status.tx; - return newStatus == null ? payment : payment.copyWith(status: newStatus); + try { + final signature = await _client + .submitDurableTx( + SubmitDurableTxRequestDto( + tx: tx.encode(), + ), + ) + .then((e) => e.signature); + + return payment.copyWith( + status: OutgoingDlnPaymentStatus.txSent( + tx, + signature: signature, + ), + ); + } on Exception { + return payment.copyWith( + status: const OutgoingDlnPaymentStatus.txFailure( + reason: TxFailureReason.creatingFailure, + ), + ); + } } Future _wait(OutgoingDlnPayment payment) async { @@ -147,12 +157,9 @@ class OutgoingDlnPaymentService implements Disposable { return payment; } - final tx = await _sender.wait( - status.tx, - minContextSlot: status.slot, - txType: 'OutgoingDlnPayment', - ); - final OutgoingDlnPaymentStatus? newStatus = await tx.map( + final tx = await _txConfirm(txId: status.signature); + + final OutgoingDlnPaymentStatus? newStatus = tx?.map( success: (_) => OutgoingDlnPaymentStatus.success( status.tx, orderId: null, diff --git a/packages/espressocash_app/lib/storybook/stories/screens/outgoing_dln_order_screen.dart b/packages/espressocash_app/lib/storybook/stories/screens/outgoing_dln_order_screen.dart index 7632b9b465..37b512a2c9 100644 --- a/packages/espressocash_app/lib/storybook/stories/screens/outgoing_dln_order_screen.dart +++ b/packages/espressocash_app/lib/storybook/stories/screens/outgoing_dln_order_screen.dart @@ -45,9 +45,10 @@ final outgoingDlnScreenStory = Story( const dummyOrderId = 'ORDER_ID'; const dummyTx = StubSignedTx(dummyOrderId); -final dummyBigInt = BigInt.from(0); +const dummySignature = 'Signature'; -final txSent = OutgoingDlnPaymentStatus.txSent(dummyTx, slot: dummyBigInt); +const txSent = + OutgoingDlnPaymentStatus.txSent(dummyTx, signature: dummySignature); const success = OutgoingDlnPaymentStatus.success(dummyTx, orderId: dummyOrderId); const txFailure = OutgoingDlnPaymentStatus.txFailure( diff --git a/packages/espressocash_app/pubspec.lock b/packages/espressocash_app/pubspec.lock index 419b5049a6..55ce3e5205 100644 --- a/packages/espressocash_app/pubspec.lock +++ b/packages/espressocash_app/pubspec.lock @@ -290,7 +290,7 @@ packages: source: hosted version: "3.0.0" convert: - dependency: transitive + dependency: "direct main" description: name: convert sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" diff --git a/packages/espressocash_app/pubspec.yaml b/packages/espressocash_app/pubspec.yaml index d0fbfd57db..b1a8108472 100644 --- a/packages/espressocash_app/pubspec.yaml +++ b/packages/espressocash_app/pubspec.yaml @@ -14,6 +14,7 @@ dependencies: borsh_annotation: ^0.3.1+5 cached_network_image: ^3.3.1 collection: ^1.18.0 + convert: ^3.1.1 crypto: ^3.0.2 decimal: ^2.3.3 device_preview: ^1.1.0 From 44e63e574a6372c1566a73f653397c3165c9e211 Mon Sep 17 00:00:00 2001 From: Vlad Sumin Date: Tue, 16 Jul 2024 13:32:25 +0300 Subject: [PATCH 09/20] Fix analyzer --- .../lib/features/outgoing_dln_payments/create_dln_payment.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/espressocash_app/lib/features/outgoing_dln_payments/create_dln_payment.dart b/packages/espressocash_app/lib/features/outgoing_dln_payments/create_dln_payment.dart index c828d87ee0..37d477603c 100644 --- a/packages/espressocash_app/lib/features/outgoing_dln_payments/create_dln_payment.dart +++ b/packages/espressocash_app/lib/features/outgoing_dln_payments/create_dln_payment.dart @@ -60,7 +60,7 @@ class CreateDlnPayment { if (chain == null) { throw ArgumentError.value( receiverChain, - 'receiverChain', + 'receiver_Chain', 'Invalid chain', ); } From 4570c29f61984a8e97c21817f5f2b3083235c475 Mon Sep 17 00:00:00 2001 From: Vlad Sumin Date: Wed, 17 Jul 2024 13:52:15 +0300 Subject: [PATCH 10/20] added refresh balance after tx success --- .../outgoing_direct_payments/services/odp_service.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/espressocash_app/lib/features/outgoing_direct_payments/services/odp_service.dart b/packages/espressocash_app/lib/features/outgoing_direct_payments/services/odp_service.dart index 8a057190a9..c38b75491e 100644 --- a/packages/espressocash_app/lib/features/outgoing_direct_payments/services/odp_service.dart +++ b/packages/espressocash_app/lib/features/outgoing_direct_payments/services/odp_service.dart @@ -9,6 +9,7 @@ import 'package:uuid/uuid.dart'; import '../../accounts/auth_scope.dart'; import '../../accounts/models/ec_wallet.dart'; import '../../analytics/analytics_manager.dart'; +import '../../balances/services/refresh_balance.dart'; import '../../currency/models/amount.dart'; import '../../payments/create_direct_payment.dart'; import '../../transactions/models/tx_results.dart'; @@ -25,6 +26,7 @@ class ODPService { this._txConfirm, this._analyticsManager, this._createDirectPayment, + this._refreshBalance, ); final EspressoCashClient _client; @@ -32,6 +34,7 @@ class ODPService { final TxConfirm _txConfirm; final AnalyticsManager _analyticsManager; final CreateDirectPayment _createDirectPayment; + final RefreshBalance _refreshBalance; final Map> _subscriptions = {}; @@ -188,6 +191,7 @@ class ODPService { ); if (newStatus is ODPStatusSuccess) { + _refreshBalance(); _analyticsManager.directPaymentSent(amount: payment.amount.decimal); } From e1775e8955963b7a91e95104ca20a81f803f5f71 Mon Sep 17 00:00:00 2001 From: Vlad Sumin Date: Wed, 17 Jul 2024 14:19:13 +0300 Subject: [PATCH 11/20] Fix test --- .../services/odp_service_test.dart | 2 ++ packages/espressocash_app/test/stub_refresh_balance.dart | 8 ++++++++ 2 files changed, 10 insertions(+) create mode 100644 packages/espressocash_app/test/stub_refresh_balance.dart diff --git a/packages/espressocash_app/test/features/outgoing_direct_payments/services/odp_service_test.dart b/packages/espressocash_app/test/features/outgoing_direct_payments/services/odp_service_test.dart index 95f411b74d..012388a0df 100644 --- a/packages/espressocash_app/test/features/outgoing_direct_payments/services/odp_service_test.dart +++ b/packages/espressocash_app/test/features/outgoing_direct_payments/services/odp_service_test.dart @@ -22,6 +22,7 @@ import 'package:solana/encoder.dart'; import 'package:solana/solana.dart'; import '../../../stub_analytics_manager.dart'; +import '../../../stub_refresh_balance.dart'; import 'odp_service_test.mocks.dart'; final confirm = MockTxConfirm(); @@ -84,6 +85,7 @@ Future main() async { confirm, const StubAnalyticsManager(), createDirectPayment, + const StubRefreshBalance(), ); Future createODP(ODPService service) async { diff --git a/packages/espressocash_app/test/stub_refresh_balance.dart b/packages/espressocash_app/test/stub_refresh_balance.dart new file mode 100644 index 0000000000..c6a410f6b1 --- /dev/null +++ b/packages/espressocash_app/test/stub_refresh_balance.dart @@ -0,0 +1,8 @@ +import 'package:espressocash_app/features/balances/services/refresh_balance.dart'; + +class StubRefreshBalance implements RefreshBalance { + const StubRefreshBalance(); + + @override + Future call() async {} +} From 47fb4e8837ff7bb38e3898c281cee1a91adaf530 Mon Sep 17 00:00:00 2001 From: Vlad Sumin Date: Mon, 29 Jul 2024 19:11:30 +0300 Subject: [PATCH 12/20] Small fix upd --- .../lib/features/outgoing_dln_payments/client/dto.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/espressocash_app/lib/features/outgoing_dln_payments/client/dto.dart b/packages/espressocash_app/lib/features/outgoing_dln_payments/client/dto.dart index faeb0b616a..ddd7ad238a 100644 --- a/packages/espressocash_app/lib/features/outgoing_dln_payments/client/dto.dart +++ b/packages/espressocash_app/lib/features/outgoing_dln_payments/client/dto.dart @@ -46,6 +46,7 @@ class DlnQuoteResponseDto with _$DlnQuoteResponseDto { @freezed class CreateTxRequestDto with _$CreateTxRequestDto { + @JsonSerializable(includeIfNull: false) const factory CreateTxRequestDto({ required String srcChainId, required String srcChainTokenIn, From 99ab7bf3388a862bbca835f32ba670fd8f1e1763 Mon Sep 17 00:00:00 2001 From: Vlad Sumin Date: Sun, 11 Aug 2024 20:43:46 +0300 Subject: [PATCH 13/20] Restructure, added new service --- .../services/ilp_service.dart | 41 ++++---- .../services/odp_service.dart | 46 ++++----- .../services/dln_order_service.dart | 43 ++++----- .../services/olp_service.dart | 93 +++++++------------ .../service/moneygram_off_ramp_service.dart | 9 +- .../service/moneygram_on_ramp_service.dart | 8 +- .../ramp/services/off_ramp_order_service.dart | 53 +++++------ .../transactions/models/tx_results.dart | 2 +- ...tx_confirm.dart => tx_durable_sender.dart} | 45 ++++++--- .../services/odp_service_test.dart | 28 +++--- 10 files changed, 167 insertions(+), 201 deletions(-) rename packages/espressocash_app/lib/features/transactions/services/{tx_confirm.dart => tx_durable_sender.dart} (77%) diff --git a/packages/espressocash_app/lib/features/incoming_link_payments/services/ilp_service.dart b/packages/espressocash_app/lib/features/incoming_link_payments/services/ilp_service.dart index e292129697..4e2e032ea6 100644 --- a/packages/espressocash_app/lib/features/incoming_link_payments/services/ilp_service.dart +++ b/packages/espressocash_app/lib/features/incoming_link_payments/services/ilp_service.dart @@ -19,7 +19,7 @@ import '../../escrow_payments/create_incoming_escrow.dart'; import '../../escrow_payments/escrow_exception.dart'; import '../../transactions/models/tx_results.dart'; import '../../transactions/services/resign_tx.dart'; -import '../../transactions/services/tx_confirm.dart'; +import '../../transactions/services/tx_durable_sender.dart'; import '../data/ilp_repository.dart'; import '../models/incoming_link_payment.dart'; @@ -30,14 +30,14 @@ class ILPService implements Disposable { this._createIncomingEscrow, this._ecClient, this._refreshBalance, - this._txConfirm, + this._txDurableSender, ); final ILPRepository _repository; final CreateIncomingEscrow _createIncomingEscrow; final EspressoCashClient _ecClient; final RefreshBalance _refreshBalance; - final TxConfirm _txConfirm; + final TxDurableSender _txDurableSender; final Map> _subscriptions = {}; @@ -132,27 +132,20 @@ class ILPService implements Disposable { return payment; } - final tx = status.tx; + final tx = await _txDurableSender.send(status.tx); - try { - final signature = await _ecClient - .submitDurableTx( - SubmitDurableTxRequestDto( - tx: tx.encode(), - ), - ) - .then((e) => e.signature); - - return payment.copyWith( - status: ILPStatus.txSent(tx, signature: signature), - ); - } on Exception { - return payment.copyWith( - status: const ILPStatus.txFailure( - reason: TxFailureReason.creatingFailure, - ), - ); - } + final ILPStatus? newStatus = tx.map( + sent: (it) => ILPStatus.txSent( + status.tx, + signature: it.signature ?? '', + ), + invalidBlockhash: (_) => null, + failure: (it) => + const ILPStatus.txFailure(reason: TxFailureReason.creatingFailure), + networkError: (_) => null, + ); + + return newStatus == null ? payment : payment.copyWith(status: newStatus); } Future _wait(IncomingLinkPayment payment) async { @@ -162,7 +155,7 @@ class ILPService implements Disposable { return payment; } - await _txConfirm(txId: status.signature); + await _txDurableSender.wait(txId: status.signature); int? fee; try { diff --git a/packages/espressocash_app/lib/features/outgoing_direct_payments/services/odp_service.dart b/packages/espressocash_app/lib/features/outgoing_direct_payments/services/odp_service.dart index c38b75491e..93332a7c06 100644 --- a/packages/espressocash_app/lib/features/outgoing_direct_payments/services/odp_service.dart +++ b/packages/espressocash_app/lib/features/outgoing_direct_payments/services/odp_service.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:dfunc/dfunc.dart'; -import 'package:espressocash_api/espressocash_api.dart'; import 'package:injectable/injectable.dart'; import 'package:solana/solana.dart'; import 'package:uuid/uuid.dart'; @@ -14,24 +13,22 @@ import '../../currency/models/amount.dart'; import '../../payments/create_direct_payment.dart'; import '../../transactions/models/tx_results.dart'; import '../../transactions/services/resign_tx.dart'; -import '../../transactions/services/tx_confirm.dart'; +import '../../transactions/services/tx_durable_sender.dart'; import '../data/repository.dart'; import '../models/outgoing_direct_payment.dart'; @Singleton(scope: authScope) class ODPService { ODPService( - this._client, this._repository, - this._txConfirm, + this._txDurableSender, this._analyticsManager, this._createDirectPayment, this._refreshBalance, ); - final EspressoCashClient _client; final ODPRepository _repository; - final TxConfirm _txConfirm; + final TxDurableSender _txDurableSender; final AnalyticsManager _analyticsManager; final CreateDirectPayment _createDirectPayment; final RefreshBalance _refreshBalance; @@ -152,29 +149,20 @@ class ODPService { return payment; } - final tx = status.tx; + final tx = await _txDurableSender.send(status.tx); - try { - final signature = await _client - .submitDurableTx( - SubmitDurableTxRequestDto( - tx: tx.encode(), - ), - ) - .then((e) => e.signature); - - return payment.copyWith( - status: ODPStatus.txSent( - tx, - signature: signature, - ), - ); - } on Exception { - return payment.copyWith( - status: - const ODPStatus.txFailure(reason: TxFailureReason.creatingFailure), - ); - } + final ODPStatus? newStatus = tx.map( + sent: (it) => ODPStatus.txSent( + status.tx, + signature: it.signature ?? '', + ), + invalidBlockhash: (_) => null, + failure: (it) => + const ODPStatus.txFailure(reason: TxFailureReason.creatingFailure), + networkError: (_) => null, + ); + + return newStatus == null ? payment : payment.copyWith(status: newStatus); } Future _wait(OutgoingDirectPayment payment) async { @@ -182,7 +170,7 @@ class ODPService { if (status is! ODPStatusTxSent) { return payment; } - final tx = await _txConfirm(txId: status.signature); + final tx = await _txDurableSender.wait(txId: status.signature); final ODPStatus? newStatus = tx?.map( success: (_) => ODPStatus.success(txId: status.tx.id), diff --git a/packages/espressocash_app/lib/features/outgoing_dln_payments/services/dln_order_service.dart b/packages/espressocash_app/lib/features/outgoing_dln_payments/services/dln_order_service.dart index ae06e2b7c9..50eee3a1c0 100644 --- a/packages/espressocash_app/lib/features/outgoing_dln_payments/services/dln_order_service.dart +++ b/packages/espressocash_app/lib/features/outgoing_dln_payments/services/dln_order_service.dart @@ -13,7 +13,7 @@ import '../../accounts/models/ec_wallet.dart'; import '../../currency/models/amount.dart'; import '../../transactions/models/tx_results.dart'; import '../../transactions/services/resign_tx.dart'; -import '../../transactions/services/tx_confirm.dart'; +import '../../transactions/services/tx_durable_sender.dart'; import '../data/repository.dart'; import '../models/outgoing_payment.dart'; import '../models/payment_quote.dart'; @@ -23,7 +23,7 @@ class OutgoingDlnPaymentService implements Disposable { OutgoingDlnPaymentService( this._account, this._client, - this._txConfirm, + this._txDurableSender, this._repository, ); @@ -32,7 +32,7 @@ class OutgoingDlnPaymentService implements Disposable { final ECWallet _account; final EspressoCashClient _client; - final TxConfirm _txConfirm; + final TxDurableSender _txDurableSender; final OutgoingDlnPaymentRepository _repository; @PostConstruct(preResolve: true) @@ -125,30 +125,21 @@ class OutgoingDlnPaymentService implements Disposable { return payment; } - final tx = status.tx; + final tx = await _txDurableSender.send(status.tx); - try { - final signature = await _client - .submitDurableTx( - SubmitDurableTxRequestDto( - tx: tx.encode(), - ), - ) - .then((e) => e.signature); + final OutgoingDlnPaymentStatus? newStatus = tx.map( + sent: (it) => OutgoingDlnPaymentStatus.txSent( + status.tx, + signature: it.signature ?? '', + ), + invalidBlockhash: (_) => null, + failure: (it) => const OutgoingDlnPaymentStatus.txFailure( + reason: TxFailureReason.creatingFailure, + ), + networkError: (_) => null, + ); - return payment.copyWith( - status: OutgoingDlnPaymentStatus.txSent( - tx, - signature: signature, - ), - ); - } on Exception { - return payment.copyWith( - status: const OutgoingDlnPaymentStatus.txFailure( - reason: TxFailureReason.creatingFailure, - ), - ); - } + return newStatus == null ? payment : payment.copyWith(status: newStatus); } Future _wait(OutgoingDlnPayment payment) async { @@ -157,7 +148,7 @@ class OutgoingDlnPaymentService implements Disposable { return payment; } - final tx = await _txConfirm(txId: status.signature); + final tx = await _txDurableSender.wait(txId: status.signature); final OutgoingDlnPaymentStatus? newStatus = tx?.map( success: (_) => OutgoingDlnPaymentStatus.success( diff --git a/packages/espressocash_app/lib/features/outgoing_link_payments/services/olp_service.dart b/packages/espressocash_app/lib/features/outgoing_link_payments/services/olp_service.dart index f05d7b4fef..ba61b097c8 100644 --- a/packages/espressocash_app/lib/features/outgoing_link_payments/services/olp_service.dart +++ b/packages/espressocash_app/lib/features/outgoing_link_payments/services/olp_service.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:dfunc/dfunc.dart'; -import 'package:espressocash_api/espressocash_api.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:get_it/get_it.dart'; import 'package:injectable/injectable.dart'; @@ -21,7 +20,7 @@ import '../../escrow_payments/escrow_exception.dart'; import '../../link_payments/models/link_payment.dart'; import '../../transactions/models/tx_results.dart'; import '../../transactions/services/resign_tx.dart'; -import '../../transactions/services/tx_confirm.dart'; +import '../../transactions/services/tx_durable_sender.dart'; import '../data/repository.dart'; import '../models/outgoing_link_payment.dart'; @@ -31,8 +30,7 @@ class OLPService implements Disposable { this._repository, this._createOutgoingEscrow, this._createCanceledEscrow, - this._ecClient, - this._txConfirm, + this._txDurableSender, this._analyticsManager, this._refreshBalance, ); @@ -40,8 +38,7 @@ class OLPService implements Disposable { final OLPRepository _repository; final CreateOutgoingEscrow _createOutgoingEscrow; final CreateCanceledEscrow _createCanceledEscrow; - final EspressoCashClient _ecClient; - final TxConfirm _txConfirm; + final TxDurableSender _txDurableSender; final AnalyticsManager _analyticsManager; final RefreshBalance _refreshBalance; @@ -218,32 +215,21 @@ class OLPService implements Disposable { return payment; } - final tx = status.tx; + final tx = await _txDurableSender.send(status.tx); - try { - final signature = await _ecClient - .submitDurableTx( - SubmitDurableTxRequestDto( - tx: tx.encode(), - ), - ) - .then((e) => e.signature); - - _analyticsManager.singleLinkCreated(amount: payment.amount.decimal); - - return payment.copyWith( - status: OLPStatus.txSent( - tx, - escrow: status.escrow, - signature: signature, - ), - ); - } on Exception { - return payment.copyWith( - status: - const OLPStatus.txFailure(reason: TxFailureReason.creatingFailure), - ); - } + final OLPStatus? newStatus = tx.map( + sent: (it) => OLPStatus.txSent( + status.tx, + escrow: status.escrow, + signature: it.signature ?? '', + ), + invalidBlockhash: (_) => null, + failure: (it) => + const OLPStatus.txFailure(reason: TxFailureReason.creatingFailure), + networkError: (_) => null, + ); + + return newStatus == null ? payment : payment.copyWith(status: newStatus); } Future _wait(OutgoingLinkPayment payment) async { @@ -252,7 +238,7 @@ class OLPService implements Disposable { return payment; } - await _txConfirm(txId: status.signature); + await _txDurableSender.wait(txId: status.signature); final token = payment.amount.token; @@ -282,32 +268,23 @@ class OLPService implements Disposable { return payment; } - final tx = status.tx; + final tx = await _txDurableSender.send(status.tx); - try { - final signature = await _ecClient - .submitDurableTx( - SubmitDurableTxRequestDto( - tx: tx.encode(), - ), - ) - .then((e) => e.signature); - - return payment.copyWith( - status: OLPStatus.cancelTxSent( - tx, - escrow: status.escrow, - signature: signature, - ), - ); - } on Exception { - return payment.copyWith( - status: OLPStatus.cancelTxFailure( - reason: TxFailureReason.creatingFailure, - escrow: status.escrow, - ), - ); - } + final OLPStatus? newStatus = tx.map( + sent: (it) => OLPStatus.cancelTxSent( + status.tx, + escrow: status.escrow, + signature: it.signature ?? '', + ), + invalidBlockhash: (_) => null, + failure: (it) => OLPStatus.cancelTxFailure( + reason: TxFailureReason.creatingFailure, + escrow: status.escrow, + ), + networkError: (_) => null, + ); + + return newStatus == null ? payment : payment.copyWith(status: newStatus); } Future _processCanceled( @@ -319,7 +296,7 @@ class OLPService implements Disposable { return payment; } - await _txConfirm(txId: status.signature); + await _txDurableSender.wait(txId: status.signature); _analyticsManager.singleLinkCanceled(amount: payment.amount.decimal); diff --git a/packages/espressocash_app/lib/features/ramp/partners/moneygram/service/moneygram_off_ramp_service.dart b/packages/espressocash_app/lib/features/ramp/partners/moneygram/service/moneygram_off_ramp_service.dart index fd3fb71c95..011b310f41 100644 --- a/packages/espressocash_app/lib/features/ramp/partners/moneygram/service/moneygram_off_ramp_service.dart +++ b/packages/espressocash_app/lib/features/ramp/partners/moneygram/service/moneygram_off_ramp_service.dart @@ -24,7 +24,7 @@ import '../../../../stellar/service/stellar_client.dart'; import '../../../../tokens/token.dart'; import '../../../../transactions/models/tx_results.dart'; import '../../../../transactions/services/resign_tx.dart'; -import '../../../../transactions/services/tx_confirm.dart'; +import '../../../../transactions/services/tx_durable_sender.dart'; import '../../../../transactions/services/tx_sender.dart'; import '../../../data/my_database_ext.dart'; import '../../../models/ramp_type.dart'; @@ -46,13 +46,13 @@ class MoneygramOffRampOrderService implements Disposable { this._moneygramClient, this._allbridgeApiClient, this._solanaClient, - this._txConfirm, + this._txDurableSender, this._refreshBalance, ); final MyDatabase _db; final TxSender _sender; - final TxConfirm _txConfirm; + final TxDurableSender _txDurableSender; final RefreshBalance _refreshBalance; final ECWallet _ecWallet; @@ -362,6 +362,7 @@ class MoneygramOffRampOrderService implements Disposable { final slot = latestBlockhash.context.slot; + // TODO(vsumin): Check, if this can be durable final send = await _sender.send(tx, minContextSlot: slot); if (send != const TxSendSent()) { @@ -741,7 +742,7 @@ class MoneygramOffRampOrderService implements Disposable { final solanaTxId = status.txId; - final waitResult = await _txConfirm(txId: solanaTxId); + final waitResult = await _txDurableSender.wait(txId: solanaTxId); if (waitResult != const TxWaitSuccess()) { return; } diff --git a/packages/espressocash_app/lib/features/ramp/partners/moneygram/service/moneygram_on_ramp_service.dart b/packages/espressocash_app/lib/features/ramp/partners/moneygram/service/moneygram_on_ramp_service.dart index e70e22a951..d45f5b4369 100644 --- a/packages/espressocash_app/lib/features/ramp/partners/moneygram/service/moneygram_on_ramp_service.dart +++ b/packages/espressocash_app/lib/features/ramp/partners/moneygram/service/moneygram_on_ramp_service.dart @@ -21,7 +21,7 @@ import '../../../../stellar/models/stellar_wallet.dart'; import '../../../../stellar/service/stellar_client.dart'; import '../../../../tokens/token.dart'; import '../../../../transactions/models/tx_results.dart'; -import '../../../../transactions/services/tx_confirm.dart'; +import '../../../../transactions/services/tx_durable_sender.dart'; import '../../../models/ramp_type.dart'; import '../data/allbridge_client.dart'; import '../data/allbridge_dto.dart' hide TransactionStatus; @@ -38,12 +38,12 @@ class MoneygramOnRampOrderService implements Disposable { this._stellarClient, this._moneygramClient, this._allbridgeApiClient, - this._txConfirm, + this._txDurableSender, this._refreshBalance, ); final MyDatabase _db; - final TxConfirm _txConfirm; + final TxDurableSender _txDurableSender; final RefreshBalance _refreshBalance; final ECWallet _ecWallet; @@ -343,7 +343,7 @@ class MoneygramOnRampOrderService implements Disposable { final solanaTxId = status.txId; final receiveAmount = int.tryParse(status.amount); - final waitResult = await _txConfirm(txId: solanaTxId); + final waitResult = await _txDurableSender.wait(txId: solanaTxId); if (waitResult != const TxWaitSuccess()) { return; } diff --git a/packages/espressocash_app/lib/features/ramp/services/off_ramp_order_service.dart b/packages/espressocash_app/lib/features/ramp/services/off_ramp_order_service.dart index a14520713b..7e744b4757 100644 --- a/packages/espressocash_app/lib/features/ramp/services/off_ramp_order_service.dart +++ b/packages/espressocash_app/lib/features/ramp/services/off_ramp_order_service.dart @@ -23,7 +23,7 @@ import '../../ramp_partner/models/ramp_partner.dart'; import '../../tokens/token_list.dart'; import '../../transactions/models/tx_results.dart'; import '../../transactions/services/resign_tx.dart'; -import '../../transactions/services/tx_confirm.dart'; +import '../../transactions/services/tx_durable_sender.dart'; import '../models/ramp_watcher.dart'; import '../partners/coinflow/services/coinflow_off_ramp_order_watcher.dart'; import '../partners/kado/services/kado_off_ramp_order_watcher.dart'; @@ -56,7 +56,7 @@ class OffRampOrderService implements Disposable { this._client, this._scalexWithdrawPayment, this._createDirectPayment, - this._txConfirm, + this._txDurableSender, this._db, this._tokens, ); @@ -68,7 +68,7 @@ class OffRampOrderService implements Disposable { final EspressoCashClient _client; final ScalexWithdrawPayment _scalexWithdrawPayment; final CreateDirectPayment _createDirectPayment; - final TxConfirm _txConfirm; + final TxDurableSender _txDurableSender; final MyDatabase _db; final TokenList _tokens; @@ -470,34 +470,29 @@ class OffRampOrderService implements Disposable { } Future _sendTx(SignedTx tx) async { - final signature = await _submitDurableTx(tx); - - return signature.isEmpty - ? OffRampOrderRowsCompanion( - status: const Value(OffRampOrderStatus.depositError), - transaction: const Value(''), - slot: Value(BigInt.zero), - ) - : await _confirmTransaction(signature); - } - - Future _submitDurableTx(SignedTx tx) async { - try { - final response = await _client.submitDurableTx( - SubmitDurableTxRequestDto(tx: tx.encode()), - ); - - return response.signature; - } on Exception { - return ''; + final sent = await _txDurableSender.send(tx); + switch (sent) { + case TxSendSent(): + break; + case TxSendInvalidBlockhash(): + return OffRampOrderRowsCompanion( + status: const Value(OffRampOrderStatus.depositError), + transaction: const Value(''), + slot: Value(BigInt.zero), + ); + case TxSendFailure(:final reason): + return OffRampOrderRowsCompanion( + status: reason == TxFailureReason.insufficientFunds + ? const Value(OffRampOrderStatus.insufficientFunds) + : const Value(OffRampOrderStatus.depositError), + transaction: const Value(''), + slot: Value(BigInt.zero), + ); + case TxSendNetworkError(): + return _depositError; } - } - - Future _confirmTransaction( - String signature, - ) async { - final confirmed = await _txConfirm(txId: signature); + final confirmed = await _txDurableSender.wait(txId: sent.signature ?? ''); switch (confirmed) { case TxWaitSuccess(): return const OffRampOrderRowsCompanion( diff --git a/packages/espressocash_app/lib/features/transactions/models/tx_results.dart b/packages/espressocash_app/lib/features/transactions/models/tx_results.dart index fc2b3aff39..055f7c6f01 100644 --- a/packages/espressocash_app/lib/features/transactions/models/tx_results.dart +++ b/packages/espressocash_app/lib/features/transactions/models/tx_results.dart @@ -43,7 +43,7 @@ class StubSignedTx implements SignedTx { @freezed sealed class TxSendResult with _$TxSendResult { - const factory TxSendResult.sent() = TxSendSent; + const factory TxSendResult.sent({String? signature}) = TxSendSent; const factory TxSendResult.invalidBlockhash() = TxSendInvalidBlockhash; const factory TxSendResult.failure({ required TxFailureReason reason, diff --git a/packages/espressocash_app/lib/features/transactions/services/tx_confirm.dart b/packages/espressocash_app/lib/features/transactions/services/tx_durable_sender.dart similarity index 77% rename from packages/espressocash_app/lib/features/transactions/services/tx_confirm.dart rename to packages/espressocash_app/lib/features/transactions/services/tx_durable_sender.dart index 28f7b93d06..73ce05fde7 100644 --- a/packages/espressocash_app/lib/features/transactions/services/tx_confirm.dart +++ b/packages/espressocash_app/lib/features/transactions/services/tx_durable_sender.dart @@ -1,26 +1,49 @@ import 'package:dfunc/dfunc.dart'; +import 'package:espressocash_api/espressocash_api.dart'; import 'package:injectable/injectable.dart'; import 'package:logging/logging.dart'; import 'package:rxdart/rxdart.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:solana/dto.dart'; +import 'package:solana/encoder.dart'; import 'package:solana/solana.dart'; import '../../../config.dart'; import '../models/tx_results.dart'; @injectable -class TxConfirm { - const TxConfirm({ - required SolanaClient client, - }) : _client = client; - - final SolanaClient _client; +class TxDurableSender { + const TxDurableSender({ + required EspressoCashClient ecClient, + required SolanaClient solanaClient, + }) : _ecClient = ecClient, + _solanaClient = solanaClient; + + final EspressoCashClient _ecClient; + final SolanaClient _solanaClient; + + Future send(SignedTx tx) async { + try { + final signature = await _ecClient + .submitDurableTx( + SubmitDurableTxRequestDto( + tx: tx.encode(), + ), + ) + .then((e) => e.signature); + + return TxSendResult.sent(signature: signature); + } on Exception { + return const TxSendResult.failure( + reason: TxFailureReason.creatingFailure, + ); + } + } - Future call({required String txId}) { + Future wait({required String txId}) { final sentryTx = Sentry.startTransaction( 'Wait TX confirmation', - 'TxConfirm.wait()', + 'TxDurableSender.wait()', waitForChildren: true, // ignore: avoid-missing-interpolation, as intended )..setData('txId', txId); @@ -31,7 +54,7 @@ class TxConfirm { final innerSpan = span.startChild('getSignatureStatus()'); _logger.fine('$txId: Checking tx status.'); - final statuses = await _client.rpcClient.getSignatureStatuses( + final statuses = await _solanaClient.rpcClient.getSignatureStatuses( [txId], searchTransactionHistory: true, ); @@ -72,7 +95,7 @@ class TxConfirm { Future waitForSignatureStatus(ISentrySpan span) async { final innerSpan = span.startChild('waitForSignatureStatus()'); try { - await _client.waitForSignatureStatus( + await _solanaClient.waitForSignatureStatus( txId, status: commitment, pingInterval: pingDefaultInterval, @@ -125,4 +148,4 @@ class TxConfirm { } } -final _logger = Logger('TxConfirm'); +final _logger = Logger('TxDurableSender'); diff --git a/packages/espressocash_app/test/features/outgoing_direct_payments/services/odp_service_test.dart b/packages/espressocash_app/test/features/outgoing_direct_payments/services/odp_service_test.dart index 012388a0df..0d90f8ae11 100644 --- a/packages/espressocash_app/test/features/outgoing_direct_payments/services/odp_service_test.dart +++ b/packages/espressocash_app/test/features/outgoing_direct_payments/services/odp_service_test.dart @@ -12,7 +12,7 @@ import 'package:espressocash_app/features/outgoing_direct_payments/services/odp_ import 'package:espressocash_app/features/payments/create_direct_payment.dart'; import 'package:espressocash_app/features/tokens/token.dart'; import 'package:espressocash_app/features/transactions/models/tx_results.dart'; -import 'package:espressocash_app/features/transactions/services/tx_confirm.dart'; +import 'package:espressocash_app/features/transactions/services/tx_durable_sender.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; @@ -25,14 +25,12 @@ import '../../../stub_analytics_manager.dart'; import '../../../stub_refresh_balance.dart'; import 'odp_service_test.mocks.dart'; -final confirm = MockTxConfirm(); +final txDurableSender = MockTxDurableSender(); final createDirectPayment = MockCreateDirectPayment(); -final client = MockEspressoCashClient(); @GenerateMocks([ - TxConfirm, + TxDurableSender, CreateDirectPayment, - EspressoCashClient, ]) Future main() async { final account = LocalWallet(await Ed25519HDKeyPair.random()); @@ -40,9 +38,8 @@ Future main() async { final repository = MemoryRepository(); setUp(() { - reset(confirm); + reset(txDurableSender); reset(createDirectPayment); - reset(client); }); tearDown( @@ -80,9 +77,8 @@ Future main() async { ); ODPService createService() => ODPService( - client, repository, - confirm, + txDurableSender, const StubAnalyticsManager(), createDirectPayment, const StubRefreshBalance(), @@ -113,8 +109,10 @@ Future main() async { ), ).thenAnswer((_) async => testDirectPaymentResult); - when(client.submitDurableTx(any)).thenAnswer((_) async => testApiResponse); - when(confirm(txId: anyNamed('txId'))) + when(txDurableSender.send(any)).thenAnswer( + (_) async => TxSendResult.sent(signature: testApiResponse.signature), + ); + when(txDurableSender.wait(txId: anyNamed('txId'))) .thenAnswer((_) async => const TxWaitResult.success()); final paymentId = await createService().let(createODP); @@ -134,8 +132,8 @@ Future main() async { ), ); - verify(client.submitDurableTx(any)).called(1); - verify(confirm(txId: anyNamed('txId'))).called(1); + verify(txDurableSender.send(any)).called(1); + verify(txDurableSender.wait(txId: anyNamed('txId'))).called(1); }); test('Failed to create direct durable payment', () async { @@ -169,8 +167,8 @@ Future main() async { ), ); - verifyNever(client.submitDurableTx(any)); - verifyNever(confirm(txId: anyNamed('txId'))); + verifyNever(txDurableSender.send(any)); + verifyNever(txDurableSender.wait(txId: anyNamed('txId'))); }); } From 10dd25f42592c4b08d1fd9ca6cee91ab0d0a2de5 Mon Sep 17 00:00:00 2001 From: Vlad Sumin Date: Mon, 12 Aug 2024 14:38:17 +0300 Subject: [PATCH 14/20] Fix --- .../features/outgoing_direct_payments/services/odp_service.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/espressocash_app/lib/features/outgoing_direct_payments/services/odp_service.dart b/packages/espressocash_app/lib/features/outgoing_direct_payments/services/odp_service.dart index 93332a7c06..6ddea6569f 100644 --- a/packages/espressocash_app/lib/features/outgoing_direct_payments/services/odp_service.dart +++ b/packages/espressocash_app/lib/features/outgoing_direct_payments/services/odp_service.dart @@ -173,7 +173,7 @@ class ODPService { final tx = await _txDurableSender.wait(txId: status.signature); final ODPStatus? newStatus = tx?.map( - success: (_) => ODPStatus.success(txId: status.tx.id), + success: (_) => ODPStatus.success(txId: status.signature), failure: (tx) => ODPStatus.txFailure(reason: tx.reason), networkError: (_) => null, ); From bd93c6a56b12179af70aec39a47bd8b860d1e103 Mon Sep 17 00:00:00 2001 From: Vlad Sumin Date: Tue, 13 Aug 2024 02:58:30 +0300 Subject: [PATCH 15/20] Upd --- packages/espressocash_app/lib/config.dart | 2 ++ packages/espressocash_app/lib/di.dart | 10 ++++++ .../create_dln_payment.dart | 35 +++++++++---------- .../data/repository.dart | 16 +++++++-- .../models/outgoing_payment.dart | 10 +++--- .../models/payment_quote.dart | 1 + .../services/confirm_payment_bloc.dart | 1 + .../services/dln_order_service.dart | 30 ++++++++-------- .../services/add_priority_fees.dart | 9 ++++- .../screens/outgoing_dln_order_screen.dart | 5 ++- 10 files changed, 76 insertions(+), 43 deletions(-) diff --git a/packages/espressocash_app/lib/config.dart b/packages/espressocash_app/lib/config.dart index 5e66260151..f727a27b95 100644 --- a/packages/espressocash_app/lib/config.dart +++ b/packages/espressocash_app/lib/config.dart @@ -77,6 +77,8 @@ const coinflowKycUrl = isProd ? 'https://coinflow.cash/withdraw/espresso' : 'https://sandbox.coinflow.cash/withdraw/espresso'; +const platformMnemonic = String.fromEnvironment('PLATFORM_MNEMONIC_MAINNET'); + const maxPayloadsPerSigningRequest = 10; const playstoreName = 'com.pleasecrypto.flutter'; diff --git a/packages/espressocash_app/lib/di.dart b/packages/espressocash_app/lib/di.dart index c58c67dfcc..b33b821951 100644 --- a/packages/espressocash_app/lib/di.dart +++ b/packages/espressocash_app/lib/di.dart @@ -43,6 +43,14 @@ abstract class AppModule { @preResolve Future prefs() => SharedPreferences.getInstance(); + @preResolve + @platformAccount + Future platformKeyPair() => Ed25519HDKeyPair.fromMnemonic( + platformMnemonic, + account: 0, + change: 0, + ); + @singleton @preResolve Future mixpanel() async { @@ -55,3 +63,5 @@ abstract class AppModule { return mixpanel; } } + +const platformAccount = Named('PlatformAccount'); diff --git a/packages/espressocash_app/lib/features/outgoing_dln_payments/create_dln_payment.dart b/packages/espressocash_app/lib/features/outgoing_dln_payments/create_dln_payment.dart index 37d477603c..b0d61bbbc9 100644 --- a/packages/espressocash_app/lib/features/outgoing_dln_payments/create_dln_payment.dart +++ b/packages/espressocash_app/lib/features/outgoing_dln_payments/create_dln_payment.dart @@ -11,6 +11,7 @@ import 'package:solana/dto.dart' as dto; import 'package:solana/encoder.dart'; import 'package:solana/solana.dart'; +import '../../di.dart'; import '../../utils/transactions.dart'; import '../priority_fees/services/add_priority_fees.dart'; import '../tokens/token.dart'; @@ -26,6 +27,7 @@ class QuoteTransaction with _$QuoteTransaction { required int receiverAmount, required int fee, required SignedTx transaction, + required BigInt slot, }) = _QuoteTransaction; } @@ -36,11 +38,13 @@ class CreateDlnPayment { this._ecClient, this._client, this._addPriorityFees, + @platformAccount this._platform, ); final QuoteRepository _quoteRepository; final EspressoCashClient _ecClient; final SolanaClient _client; + final Ed25519HDKeyPair _platform; final AddPriorityFees _addPriorityFees; Future call({ @@ -50,9 +54,6 @@ class CreateDlnPayment { required Commitment commitment, required String receiverChain, }) async { - final nonceData = await _ecClient.getFreeNonce(); - final platformAccount = Ed25519HDPublicKey.fromBase58(nonceData.authority); - final senderAccount = Ed25519HDPublicKey.fromBase58(senderAddress); final chain = receiverChain.toDlnChain; @@ -60,12 +61,12 @@ class CreateDlnPayment { if (chain == null) { throw ArgumentError.value( receiverChain, - 'receiver_Chain', + 'receiverChain', 'Invalid chain', ); } - final feePayer = platformAccount; + final feePayer = _platform.publicKey; final instructions = []; @@ -121,20 +122,16 @@ class CreateDlnPayment { (tx) => tx.decompileMessage(addressLookupTableAccounts: lookUpTables), ); - final instructionsWithNonce = [ - SystemInstruction.advanceNonceAccount( - nonce: Ed25519HDPublicKey.fromBase58(nonceData.nonceAccount), - nonceAuthority: platformAccount, - ), - ...instructions, - ]; - final message = dlnMessage .let((m) => m.removeDefaultComputeIx()) - .let((m) => m.addIxsToFront(instructionsWithNonce)); + .let((m) => m.addIxsToFront(instructions)); + + final latestBlockhash = await _client.rpcClient.getLatestBlockhash( + commitment: commitment, + ); final compiled = message.compileV0( - recentBlockhash: nonceData.nonce, + recentBlockhash: latestBlockhash.value.blockhash, feePayer: feePayer, addressLookupTableAccounts: lookUpTables, ); @@ -142,15 +139,16 @@ class CreateDlnPayment { final tx = await SignedTx( compiledMessage: compiled, signatures: [ - platformAccount.emptySignature(), - senderAccount.emptySignature(), + await _platform.sign(compiled.toByteArray()), + Signature(List.filled(64, 0), publicKey: senderAccount), ], ).let( (tx) => _addPriorityFees( tx: tx, commitment: commitment, maxPriorityFee: _maxTxCostUsdc, - platform: platformAccount, + sign: _platform.sign, + platform: _platform.publicKey, ), ); @@ -161,6 +159,7 @@ class CreateDlnPayment { receiverAmount: quote.receiverAmount, fee: totalFees, transaction: tx, + slot: latestBlockhash.context.slot, ); } } diff --git a/packages/espressocash_app/lib/features/outgoing_dln_payments/data/repository.dart b/packages/espressocash_app/lib/features/outgoing_dln_payments/data/repository.dart index 63376ecf14..c30365a69b 100644 --- a/packages/espressocash_app/lib/features/outgoing_dln_payments/data/repository.dart +++ b/packages/espressocash_app/lib/features/outgoing_dln_payments/data/repository.dart @@ -100,12 +100,16 @@ extension OutgoingDlnPaymentRowExt on OutgoingDlnPaymentRow { extension on ODLNPaymentStatusDto { OutgoingDlnPaymentStatus toModel(OutgoingDlnPaymentRow row) { final tx = row.tx?.let(SignedTx.decode); + final slot = row.slot?.let(BigInt.tryParse); switch (this) { case ODLNPaymentStatusDto.txCreated: - return OutgoingDlnPaymentStatus.txCreated(tx!); + return OutgoingDlnPaymentStatus.txCreated( + tx!, + slot: slot ?? BigInt.zero, + ); case ODLNPaymentStatusDto.txSent: - return OutgoingDlnPaymentStatus.txSent(tx!, signature: tx.id); + return OutgoingDlnPaymentStatus.txSent(tx!, slot: slot ?? BigInt.zero); case ODLNPaymentStatusDto.success: return OutgoingDlnPaymentStatus.success( tx!, @@ -155,6 +159,7 @@ extension on OutgoingDlnPayment { tx: status.toTx(), txId: status.toTxId(), amount: amount.value, + slot: status.toSlot()?.toString(), receiverBlockchain: payment.receiverBlockchain.toDto(), receiverAddress: payment.receiverAddress, txFailureReason: status.toTxFailureReason(), @@ -180,7 +185,7 @@ extension on OutgoingDlnPaymentStatus { ); String? toTxId() => mapOrNull( - txSent: (it) => it.signature, + txSent: (it) => it.tx.id, success: (it) => it.tx.id, txCreated: (it) => it.tx.id, unfulfilled: (it) => it.tx.id, @@ -195,4 +200,9 @@ extension on OutgoingDlnPaymentStatus { TxFailureReason? toTxFailureReason() => mapOrNull( txFailure: (it) => it.reason, ); + + BigInt? toSlot() => mapOrNull( + txCreated: (it) => it.slot, + txSent: (it) => it.slot, + ); } diff --git a/packages/espressocash_app/lib/features/outgoing_dln_payments/models/outgoing_payment.dart b/packages/espressocash_app/lib/features/outgoing_dln_payments/models/outgoing_payment.dart index 47ef6ec9e6..8ed6e5dee4 100644 --- a/packages/espressocash_app/lib/features/outgoing_dln_payments/models/outgoing_payment.dart +++ b/packages/espressocash_app/lib/features/outgoing_dln_payments/models/outgoing_payment.dart @@ -21,13 +21,15 @@ class OutgoingDlnPayment with _$OutgoingDlnPayment { @freezed sealed class OutgoingDlnPaymentStatus with _$OutgoingDlnPaymentStatus { /// Tx is successfully created and ready to be sent. - const factory OutgoingDlnPaymentStatus.txCreated(SignedTx tx) = - OutgoingDlnPaymentStatusTxCreated; + const factory OutgoingDlnPaymentStatus.txCreated( + SignedTx tx, { + required BigInt slot, + }) = OutgoingDlnPaymentStatusTxCreated; - /// Tx sent to backend. Should be good as confirmed at this point. + /// Tx is successfully sent. const factory OutgoingDlnPaymentStatus.txSent( SignedTx tx, { - required String signature, + required BigInt slot, }) = OutgoingDlnPaymentStatusTxSent; /// Tx is successfully confirmed and order awaiting fulfillment. diff --git a/packages/espressocash_app/lib/features/outgoing_dln_payments/models/payment_quote.dart b/packages/espressocash_app/lib/features/outgoing_dln_payments/models/payment_quote.dart index 766a578583..215645d0ec 100644 --- a/packages/espressocash_app/lib/features/outgoing_dln_payments/models/payment_quote.dart +++ b/packages/espressocash_app/lib/features/outgoing_dln_payments/models/payment_quote.dart @@ -13,5 +13,6 @@ class PaymentQuote with _$PaymentQuote { required CryptoAmount receiverAmount, required CryptoAmount fee, required String encodedTx, + required BigInt slot, }) = _PaymentQuote; } diff --git a/packages/espressocash_app/lib/features/outgoing_dln_payments/services/confirm_payment_bloc.dart b/packages/espressocash_app/lib/features/outgoing_dln_payments/services/confirm_payment_bloc.dart index e8dcb6e86c..10c7d42116 100644 --- a/packages/espressocash_app/lib/features/outgoing_dln_payments/services/confirm_payment_bloc.dart +++ b/packages/espressocash_app/lib/features/outgoing_dln_payments/services/confirm_payment_bloc.dart @@ -114,6 +114,7 @@ class ConfirmPaymentBloc extends Bloc<_Event, _State> { value: createDlnPayment.fee, ), encodedTx: createDlnPayment.transaction.encode(), + slot: createDlnPayment.slot, ); _startTimer(); diff --git a/packages/espressocash_app/lib/features/outgoing_dln_payments/services/dln_order_service.dart b/packages/espressocash_app/lib/features/outgoing_dln_payments/services/dln_order_service.dart index 50eee3a1c0..e224f347de 100644 --- a/packages/espressocash_app/lib/features/outgoing_dln_payments/services/dln_order_service.dart +++ b/packages/espressocash_app/lib/features/outgoing_dln_payments/services/dln_order_service.dart @@ -13,7 +13,7 @@ import '../../accounts/models/ec_wallet.dart'; import '../../currency/models/amount.dart'; import '../../transactions/models/tx_results.dart'; import '../../transactions/services/resign_tx.dart'; -import '../../transactions/services/tx_durable_sender.dart'; +import '../../transactions/services/tx_sender.dart'; import '../data/repository.dart'; import '../models/outgoing_payment.dart'; import '../models/payment_quote.dart'; @@ -23,7 +23,7 @@ class OutgoingDlnPaymentService implements Disposable { OutgoingDlnPaymentService( this._account, this._client, - this._txDurableSender, + this._sender, this._repository, ); @@ -32,7 +32,7 @@ class OutgoingDlnPaymentService implements Disposable { final ECWallet _account; final EspressoCashClient _client; - final TxDurableSender _txDurableSender; + final TxSender _sender; final OutgoingDlnPaymentRepository _repository; @PostConstruct(preResolve: true) @@ -111,7 +111,7 @@ class OutgoingDlnPaymentService implements Disposable { try { final tx = await SignedTx.decode(quote.encodedTx).resign(_account); - return OutgoingDlnPaymentStatus.txCreated(tx); + return OutgoingDlnPaymentStatus.txCreated(tx, slot: quote.slot); } on Exception { return const OutgoingDlnPaymentStatus.txFailure( reason: TxFailureReason.creatingFailure, @@ -125,17 +125,16 @@ class OutgoingDlnPaymentService implements Disposable { return payment; } - final tx = await _txDurableSender.send(status.tx); - + final tx = await _sender.send(status.tx, minContextSlot: status.slot); final OutgoingDlnPaymentStatus? newStatus = tx.map( - sent: (it) => OutgoingDlnPaymentStatus.txSent( + sent: (_) => OutgoingDlnPaymentStatus.txSent( status.tx, - signature: it.signature ?? '', + slot: status.slot, ), - invalidBlockhash: (_) => null, - failure: (it) => const OutgoingDlnPaymentStatus.txFailure( - reason: TxFailureReason.creatingFailure, + invalidBlockhash: (_) => const OutgoingDlnPaymentStatus.txFailure( + reason: TxFailureReason.invalidBlockhashSending, ), + failure: (it) => OutgoingDlnPaymentStatus.txFailure(reason: it.reason), networkError: (_) => null, ); @@ -148,9 +147,12 @@ class OutgoingDlnPaymentService implements Disposable { return payment; } - final tx = await _txDurableSender.wait(txId: status.signature); - - final OutgoingDlnPaymentStatus? newStatus = tx?.map( + final tx = await _sender.wait( + status.tx, + minContextSlot: status.slot, + txType: 'OutgoingDlnPayment', + ); + final OutgoingDlnPaymentStatus? newStatus = await tx.map( success: (_) => OutgoingDlnPaymentStatus.success( status.tx, orderId: null, diff --git a/packages/espressocash_app/lib/features/priority_fees/services/add_priority_fees.dart b/packages/espressocash_app/lib/features/priority_fees/services/add_priority_fees.dart index caef9c5653..c375982c88 100644 --- a/packages/espressocash_app/lib/features/priority_fees/services/add_priority_fees.dart +++ b/packages/espressocash_app/lib/features/priority_fees/services/add_priority_fees.dart @@ -23,6 +23,7 @@ class AddPriorityFees { required SignedTx tx, required Commitment commitment, required int maxPriorityFee, + Future Function(Iterable data)? sign, required Ed25519HDPublicKey platform, }) async { final priorityFees = await _ecClient.getPriorityFeeEstimate( @@ -35,6 +36,7 @@ class AddPriorityFees { tx, recommendedCuPrice: veryHighFee ?? (pow(2, 32).toInt()), maxTxFee: maxPriorityFee, + sign: sign, platform: platform, commitment: commitment, ) ?? @@ -45,6 +47,7 @@ class AddPriorityFees { SignedTx tx, { required int recommendedCuPrice, required int maxTxFee, + Future Function(Iterable data)? sign, required Ed25519HDPublicKey platform, required Commitment commitment, }) async { @@ -146,10 +149,14 @@ class AddPriorityFees { feePayer: platform, ); + final signature = sign != null + ? await sign(newCompiledMessage.toByteArray()) + : platform.emptySignature(); + return SignedTx( compiledMessage: newCompiledMessage, signatures: [ - platform.emptySignature(), + signature, ...tx.signatures.where((s) => s.publicKey != platform), ], ); diff --git a/packages/espressocash_app/lib/storybook/stories/screens/outgoing_dln_order_screen.dart b/packages/espressocash_app/lib/storybook/stories/screens/outgoing_dln_order_screen.dart index 37b512a2c9..7632b9b465 100644 --- a/packages/espressocash_app/lib/storybook/stories/screens/outgoing_dln_order_screen.dart +++ b/packages/espressocash_app/lib/storybook/stories/screens/outgoing_dln_order_screen.dart @@ -45,10 +45,9 @@ final outgoingDlnScreenStory = Story( const dummyOrderId = 'ORDER_ID'; const dummyTx = StubSignedTx(dummyOrderId); -const dummySignature = 'Signature'; +final dummyBigInt = BigInt.from(0); -const txSent = - OutgoingDlnPaymentStatus.txSent(dummyTx, signature: dummySignature); +final txSent = OutgoingDlnPaymentStatus.txSent(dummyTx, slot: dummyBigInt); const success = OutgoingDlnPaymentStatus.success(dummyTx, orderId: dummyOrderId); const txFailure = OutgoingDlnPaymentStatus.txFailure( From 879d838300003d19d0a9a2bf86591bebf5eccdc0 Mon Sep 17 00:00:00 2001 From: Vlad Sumin Date: Tue, 13 Aug 2024 03:03:36 +0300 Subject: [PATCH 16/20] fixes --- .../features/outgoing_dln_payments/create_dln_payment.dart | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/espressocash_app/lib/features/outgoing_dln_payments/create_dln_payment.dart b/packages/espressocash_app/lib/features/outgoing_dln_payments/create_dln_payment.dart index b0d61bbbc9..482e8493a0 100644 --- a/packages/espressocash_app/lib/features/outgoing_dln_payments/create_dln_payment.dart +++ b/packages/espressocash_app/lib/features/outgoing_dln_payments/create_dln_payment.dart @@ -4,7 +4,6 @@ import 'package:borsh_annotation/borsh_annotation.dart'; import 'package:collection/collection.dart'; import 'package:convert/convert.dart'; import 'package:dfunc/dfunc.dart'; -import 'package:espressocash_api/espressocash_api.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:injectable/injectable.dart'; import 'package:solana/dto.dart' as dto; @@ -12,7 +11,6 @@ import 'package:solana/encoder.dart'; import 'package:solana/solana.dart'; import '../../di.dart'; -import '../../utils/transactions.dart'; import '../priority_fees/services/add_priority_fees.dart'; import '../tokens/token.dart'; import 'client/chains.dart'; @@ -35,14 +33,12 @@ class QuoteTransaction with _$QuoteTransaction { class CreateDlnPayment { const CreateDlnPayment( this._quoteRepository, - this._ecClient, this._client, this._addPriorityFees, @platformAccount this._platform, ); final QuoteRepository _quoteRepository; - final EspressoCashClient _ecClient; final SolanaClient _client; final Ed25519HDKeyPair _platform; final AddPriorityFees _addPriorityFees; From 38c40c15338da6ef4f801c50c3aaa189990e86c3 Mon Sep 17 00:00:00 2001 From: Vlad Sumin Date: Tue, 13 Aug 2024 03:11:12 +0300 Subject: [PATCH 17/20] fix --- .../lib/features/outgoing_dln_payments/create_dln_payment.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/espressocash_app/lib/features/outgoing_dln_payments/create_dln_payment.dart b/packages/espressocash_app/lib/features/outgoing_dln_payments/create_dln_payment.dart index 482e8493a0..e73d3f5aca 100644 --- a/packages/espressocash_app/lib/features/outgoing_dln_payments/create_dln_payment.dart +++ b/packages/espressocash_app/lib/features/outgoing_dln_payments/create_dln_payment.dart @@ -57,7 +57,7 @@ class CreateDlnPayment { if (chain == null) { throw ArgumentError.value( receiverChain, - 'receiverChain', + 'receiver_Chain', 'Invalid chain', ); } From c823a0cb5a9829c83d551028ce68d7cb4cd79b55 Mon Sep 17 00:00:00 2001 From: Vlad Sumin Date: Tue, 13 Aug 2024 13:50:38 +0300 Subject: [PATCH 18/20] revert --- .../outgoing_dln_payments/client/chains.dart | 52 ---- .../outgoing_dln_payments/client/client.dart | 45 --- .../outgoing_dln_payments/client/dto.dart | 154 ---------- .../outgoing_dln_payments/client/models.dart | 130 -------- .../create_dln_payment.dart | 282 ------------------ .../data/quote_repository.dart | 74 ++--- .../models/quote_info.dart | 14 - .../services/confirm_payment_bloc.dart | 40 +-- 8 files changed, 46 insertions(+), 745 deletions(-) delete mode 100644 packages/espressocash_app/lib/features/outgoing_dln_payments/client/chains.dart delete mode 100644 packages/espressocash_app/lib/features/outgoing_dln_payments/client/client.dart delete mode 100644 packages/espressocash_app/lib/features/outgoing_dln_payments/client/dto.dart delete mode 100644 packages/espressocash_app/lib/features/outgoing_dln_payments/client/models.dart delete mode 100644 packages/espressocash_app/lib/features/outgoing_dln_payments/create_dln_payment.dart delete mode 100644 packages/espressocash_app/lib/features/outgoing_dln_payments/models/quote_info.dart diff --git a/packages/espressocash_app/lib/features/outgoing_dln_payments/client/chains.dart b/packages/espressocash_app/lib/features/outgoing_dln_payments/client/chains.dart deleted file mode 100644 index 5739483c97..0000000000 --- a/packages/espressocash_app/lib/features/outgoing_dln_payments/client/chains.dart +++ /dev/null @@ -1,52 +0,0 @@ -enum DlnChains { - arbitrum, - avalanche, - bnb, - ethereum, - polygon, - solana, - linea, - base, - optimism -} - -extension DlnChainsExt on DlnChains { - String get chainId => switch (this) { - DlnChains.arbitrum => '42161', - DlnChains.avalanche => '43114', - DlnChains.bnb => '56', - DlnChains.ethereum => '1', - DlnChains.polygon => '137', - DlnChains.solana => '7565164', - DlnChains.linea => '59144', - DlnChains.base => '8453', - DlnChains.optimism => '10', - }; - - String get usdcAddress => switch (this) { - DlnChains.arbitrum => '0xaf88d065e77c8cC2239327C5EDb3A432268e5831', - DlnChains.avalanche => '0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E', - DlnChains.bnb => '0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d', - DlnChains.ethereum => '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', - DlnChains.polygon => '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', - DlnChains.solana => 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', - DlnChains.linea => '0x176211869ca2b568f2a7d4ee941e073a821ee1ff', - DlnChains.base => '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', - DlnChains.optimism => '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85', - }; -} - -extension StringExt on String { - DlnChains? get toDlnChain => switch (this) { - 'arbitrum' => DlnChains.arbitrum, - 'avalanche' => DlnChains.avalanche, - 'bnb' => DlnChains.bnb, - 'ethereum' => DlnChains.ethereum, - 'polygon' => DlnChains.polygon, - 'solana' => DlnChains.solana, - 'linea' => DlnChains.linea, - 'base' => DlnChains.base, - 'optimism' => DlnChains.optimism, - _ => null, - }; -} diff --git a/packages/espressocash_app/lib/features/outgoing_dln_payments/client/client.dart b/packages/espressocash_app/lib/features/outgoing_dln_payments/client/client.dart deleted file mode 100644 index a79f715e34..0000000000 --- a/packages/espressocash_app/lib/features/outgoing_dln_payments/client/client.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:injectable/injectable.dart'; -import 'package:retrofit/retrofit.dart'; - -import 'dto.dart'; - -export 'chains.dart'; -export 'dto.dart'; - -part 'client.g.dart'; - -/// For docs head to https://api.dln.trade/v1.0/#/DLN -@RestApi(baseUrl: 'https://api.dln.trade/v1.0/dln') -@injectable -abstract class DlnApiClient { - @factoryMethod - factory DlnApiClient() => _DlnApiClient(Dio()); - - /// This endpoint returns the recommended take amount for creating DLN order. - @GET('/order/quote') - Future getQuote( - @Queries() DlnQuoteRequestDto quoteRequestDto, - ); - - /// This endpoint returns the data for a transaction to place a cross-chain DLN order. - @GET('/order/create-tx') - Future createTx( - @Queries() CreateTxRequestDto createTxRequestDto, - ); - - /// This endpoint returns the data of order. - @GET('/order/{id}') - Future getOrder(@Path() String id); - - /// This endpoint returns the status of order. - @GET('/order/{id}/status') - Future getStatus(@Path() String id); - - @GET('/tx/{hash}/order-ids') - Future getOrderIdByHash(@Path() String hash); - - /// This endpoint generates a transaction that cancels the given order. - @GET('/order/{id}/cancel-tx') - Future cancelTx(@Path() String id); -} diff --git a/packages/espressocash_app/lib/features/outgoing_dln_payments/client/dto.dart b/packages/espressocash_app/lib/features/outgoing_dln_payments/client/dto.dart deleted file mode 100644 index ddd7ad238a..0000000000 --- a/packages/espressocash_app/lib/features/outgoing_dln_payments/client/dto.dart +++ /dev/null @@ -1,154 +0,0 @@ -// ignore_for_file: invalid_annotation_target - -import 'package:freezed_annotation/freezed_annotation.dart'; - -import 'models.dart'; - -part 'dto.freezed.dart'; -part 'dto.g.dart'; - -@freezed -class DlnQuoteRequestDto with _$DlnQuoteRequestDto { - const factory DlnQuoteRequestDto({ - required String srcChainId, - required String srcChainTokenIn, - required String srcChainTokenInAmount, - required String dstChainId, - required String dstChainTokenOut, - String? dstChainTokenOutAmount, - int? additionalTakerRewardBps, - String? srcIntermediaryTokenAddress, - String? dstIntermediaryTokenAddress, - String? dstIntermediaryTokenSpenderAddress, - int? intermediaryTokenUSDPrice, - int? slippage, - double? affiliateFeePercent, - bool? prependOperatingExpenses, - }) = _DlnQuoteRequestDto; - - factory DlnQuoteRequestDto.fromJson(Map json) => - _$DlnQuoteRequestDtoFromJson(json); -} - -@freezed -class DlnQuoteResponseDto with _$DlnQuoteResponseDto { - const factory DlnQuoteResponseDto({ - required OrderEstimation estimation, - String? prependedOperatingExpenseCost, - TxQuote? tx, - required Order order, - required String fixFee, - }) = _DlnQuoteResponseDto; - - factory DlnQuoteResponseDto.fromJson(Map json) => - _$DlnQuoteResponseDtoFromJson(json); -} - -@freezed -class CreateTxRequestDto with _$CreateTxRequestDto { - @JsonSerializable(includeIfNull: false) - const factory CreateTxRequestDto({ - required String srcChainId, - required String srcChainTokenIn, - required String srcChainTokenInAmount, - required String dstChainId, - required String dstChainTokenOut, - required String dstChainTokenOutRecipient, - required String srcChainOrderAuthorityAddress, - required String dstChainOrderAuthorityAddress, - String? dstChainTokenOutAmount, - double? additionalTakerRewardBps, - String? srcIntermediaryTokenAddress, - String? dstIntermediaryTokenAddress, - String? dstIntermediaryTokenSpenderAddress, - double? intermediaryTokenUSDPrice, - double? slippage, - String? senderAddress, - int? referralCode, - double? affiliateFeePercent, - String? affiliateFeeRecipient, - String? srcChainTokenInSenderPermit, - bool? enableEstimate, - String? allowedTaker, - String? externalCall, - bool? prependOperatingExpenses, - }) = _CreateTxRequestDto; - - factory CreateTxRequestDto.fromJson(Map json) => - _$CreateTxRequestDtoFromJson(json); -} - -@freezed -class CreateTxResponseDto with _$CreateTxResponseDto { - const factory CreateTxResponseDto({ - required OrderEstimation estimation, - required DlnTx tx, - String? prependedOperatingExpenseCost, - required Order order, - required String fixFee, - }) = _CreateTxResponseDto; - - factory CreateTxResponseDto.fromJson(Map json) => - _$CreateTxResponseDtoFromJson(json); -} - -@freezed -class OrderResponseDto with _$OrderResponseDto { - const factory OrderResponseDto({ - required String orderId, - required String status, - required String externalCallState, - required OrderStruct orderStruct, - }) = _OrderResponseDto; - - factory OrderResponseDto.fromJson(Map json) => - _$OrderResponseDtoFromJson(json); -} - -@freezed -class OrderStatusResponseDto with _$OrderStatusResponseDto { - const factory OrderStatusResponseDto({ - required String orderId, - @JsonKey(unknownEnumValue: OrderStatus.unknown) required OrderStatus status, - }) = _OrderStatusResponseDto; - - factory OrderStatusResponseDto.fromJson(Map json) => - _$OrderStatusResponseDtoFromJson(json); -} - -@freezed -class OrderIdTxResponseDto with _$OrderIdTxResponseDto { - const factory OrderIdTxResponseDto({ - required List orderIds, - }) = _OrderIdTxResponseDto; - - factory OrderIdTxResponseDto.fromJson(Map json) => - _$OrderIdTxResponseDtoFromJson(json); -} - -@freezed -class CancelTxResponseDto with _$CancelTxResponseDto { - const factory CancelTxResponseDto({ - required String to, - required String data, - required String value, - required double chainId, - required String from, - String? cancelBeneficiary, - }) = _CancelTxResponseDto; - - factory CancelTxResponseDto.fromJson(Map json) => - _$CancelTxResponseDtoFromJson(json); -} - -@JsonEnum(fieldRename: FieldRename.pascal) -enum OrderStatus { - created, - fulfilled, - sentUnlock, - claimedUnlock, - orderCancelled, - sentOrderCancel, - claimedOrderCancel, - unknown -} diff --git a/packages/espressocash_app/lib/features/outgoing_dln_payments/client/models.dart b/packages/espressocash_app/lib/features/outgoing_dln_payments/client/models.dart deleted file mode 100644 index 73a458af61..0000000000 --- a/packages/espressocash_app/lib/features/outgoing_dln_payments/client/models.dart +++ /dev/null @@ -1,130 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'models.freezed.dart'; -part 'models.g.dart'; - -@freezed -class OrderEstimation with _$OrderEstimation { - const factory OrderEstimation({ - required TokenWithApproximateOperatingExpense srcChainTokenIn, - TokenWithMaxRefundAmount? srcChainTokenOut, - required DstChainTokenOutResponseType dstChainTokenOut, - double? recommendedSlippage, - required List costsDetails, - }) = _OrderEstimation; - - factory OrderEstimation.fromJson(Map json) => - _$OrderEstimationFromJson(json); -} - -@freezed -class TokenWithMaxRefundAmount with _$TokenWithMaxRefundAmount { - const factory TokenWithMaxRefundAmount({ - required String address, - required String name, - required String symbol, - required int decimals, - required String amount, - required String maxRefundAmount, - }) = _TokenWithMaxRefundAmount; - - factory TokenWithMaxRefundAmount.fromJson(Map json) => - _$TokenWithMaxRefundAmountFromJson(json); -} - -@freezed -class TokenWithApproximateOperatingExpense - with _$TokenWithApproximateOperatingExpense { - const factory TokenWithApproximateOperatingExpense({ - required String address, - required String name, - required String symbol, - required int decimals, - required String amount, - required String approximateOperatingExpense, - required bool mutatedWithOperatingExpense, - }) = _TokenWithApproximateOperatingExpense; - - factory TokenWithApproximateOperatingExpense.fromJson( - Map json, - ) => - _$TokenWithApproximateOperatingExpenseFromJson(json); -} - -@freezed -class DstChainTokenOutResponseType with _$DstChainTokenOutResponseType { - const factory DstChainTokenOutResponseType({ - required String address, - required String name, - required String symbol, - required int decimals, - required String amount, - required String recommendedAmount, - String? withoutAdditionalTakerRewardsAmount, - }) = _DstChainTokenOutResponseType; - - factory DstChainTokenOutResponseType.fromJson(Map json) => - _$DstChainTokenOutResponseTypeFromJson(json); -} - -@freezed -class TxQuote with _$TxQuote { - const factory TxQuote({ - required String allowanceTarget, - required String allowanceValue, - }) = _TxQuote; - - factory TxQuote.fromJson(Map json) => - _$TxQuoteFromJson(json); -} - -@freezed -class Order with _$Order { - const factory Order({ - required double approximateFulfillmentDelay, - }) = _Order; - - factory Order.fromJson(Map json) => _$OrderFromJson(json); -} - -@freezed -class DlnTx with _$DlnTx { - const factory DlnTx({ - required String data, - String? to, - String? value, - double? gasLimit, - }) = _DlnTx; - - factory DlnTx.fromJson(Map json) => _$DlnTxFromJson(json); -} - -@freezed -class Offer with _$Offer { - const factory Offer({ - required int chainId, - required String tokenAddress, - required String amount, - }) = _Offer; - - factory Offer.fromJson(Map json) => _$OfferFromJson(json); -} - -@freezed -class OrderStruct with _$OrderStruct { - const factory OrderStruct({ - required int makerOrderNonce, - required String makerSrc, - required Offer giveOffer, - required String receiverDst, - required Offer takeOffer, - required String givePatchAuthoritySrc, - required String orderAuthorityAddressDst, - String? allowedTakerDst, - String? allowedCancelBeneficiarySrc, - String? externalCall, - }) = _OrderStruct; - - factory OrderStruct.fromJson(Map json) => - _$OrderStructFromJson(json); -} diff --git a/packages/espressocash_app/lib/features/outgoing_dln_payments/create_dln_payment.dart b/packages/espressocash_app/lib/features/outgoing_dln_payments/create_dln_payment.dart deleted file mode 100644 index e73d3f5aca..0000000000 --- a/packages/espressocash_app/lib/features/outgoing_dln_payments/create_dln_payment.dart +++ /dev/null @@ -1,282 +0,0 @@ -import 'dart:math'; - -import 'package:borsh_annotation/borsh_annotation.dart'; -import 'package:collection/collection.dart'; -import 'package:convert/convert.dart'; -import 'package:dfunc/dfunc.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:injectable/injectable.dart'; -import 'package:solana/dto.dart' as dto; -import 'package:solana/encoder.dart'; -import 'package:solana/solana.dart'; - -import '../../di.dart'; -import '../priority_fees/services/add_priority_fees.dart'; -import '../tokens/token.dart'; -import 'client/chains.dart'; -import 'data/quote_repository.dart'; - -part 'create_dln_payment.freezed.dart'; - -@freezed -class QuoteTransaction with _$QuoteTransaction { - const factory QuoteTransaction({ - required int senderDeductAmount, - required int receiverAmount, - required int fee, - required SignedTx transaction, - required BigInt slot, - }) = _QuoteTransaction; -} - -@injectable -class CreateDlnPayment { - const CreateDlnPayment( - this._quoteRepository, - this._client, - this._addPriorityFees, - @platformAccount this._platform, - ); - - final QuoteRepository _quoteRepository; - final SolanaClient _client; - final Ed25519HDKeyPair _platform; - final AddPriorityFees _addPriorityFees; - - Future call({ - required int amount, - required String senderAddress, - required String receiverAddress, - required Commitment commitment, - required String receiverChain, - }) async { - final senderAccount = Ed25519HDPublicKey.fromBase58(senderAddress); - - final chain = receiverChain.toDlnChain; - - if (chain == null) { - throw ArgumentError.value( - receiverChain, - 'receiver_Chain', - 'Invalid chain', - ); - } - - final feePayer = _platform.publicKey; - - final instructions = []; - - final feesFromTransaction = await _client.calculateDlnTxFee( - sender: senderAccount, - commitment: commitment, - ); - final solTransferIx = SystemInstruction.transfer( - fundingAccount: feePayer, - recipientAccount: senderAccount, - lamports: feesFromTransaction, - ); - - instructions.add(solTransferIx); - - final percentageFeeAmount = (amount * outgoingDlnPaymentFeeFraction).ceil(); - final transferFeeAmount = max(percentageFeeAmount, minimumDlnPaymentFee); - - final feeIx = await _createFeePayment( - aSender: senderAccount, - aReceiver: feePayer, - amount: transferFeeAmount, - ); - - instructions.add(feeIx); - - final quote = await _quoteRepository.getQuoteAndTransaction( - amount: amount, - senderAddress: senderAddress, - receiverAddress: receiverAddress, - receiverChain: chain, - ); - - if (quote.senderDeductAmount != quote.totalFees + quote.inputAmount) { - throw ArgumentError.value( - quote, - 'quote', - 'Invalid quote', - ); - } - - final dlnTx = SignedTx.fromBytes(hex.decode(quote.tx.replaceAll('0x', ''))); - - final addressTableLookups = dlnTx.compiledMessage.map( - legacy: (_) => [], - v0: (v0) => v0.addressTableLookups, - ); - - final lookUpTables = await _client.rpcClient - .getAddressLookUpTableAccounts(addressTableLookups); - - final dlnMessage = dlnTx.let( - (tx) => tx.decompileMessage(addressLookupTableAccounts: lookUpTables), - ); - - final message = dlnMessage - .let((m) => m.removeDefaultComputeIx()) - .let((m) => m.addIxsToFront(instructions)); - - final latestBlockhash = await _client.rpcClient.getLatestBlockhash( - commitment: commitment, - ); - - final compiled = message.compileV0( - recentBlockhash: latestBlockhash.value.blockhash, - feePayer: feePayer, - addressLookupTableAccounts: lookUpTables, - ); - - final tx = await SignedTx( - compiledMessage: compiled, - signatures: [ - await _platform.sign(compiled.toByteArray()), - Signature(List.filled(64, 0), publicKey: senderAccount), - ], - ).let( - (tx) => _addPriorityFees( - tx: tx, - commitment: commitment, - maxPriorityFee: _maxTxCostUsdc, - sign: _platform.sign, - platform: _platform.publicKey, - ), - ); - - final totalFees = quote.totalFees.toInt() + transferFeeAmount; - - return QuoteTransaction( - senderDeductAmount: quote.senderDeductAmount, - receiverAmount: quote.receiverAmount, - fee: totalFees, - transaction: tx, - slot: latestBlockhash.context.slot, - ); - } -} - -extension on Message { - Message addIxsToFront(List ix) => - Message(instructions: [...ix, ...instructions]); - - Message removeDefaultComputeIx() => Message( - instructions: instructions - .where((e) => e.programId != ComputeBudgetProgram.id) - .toList(), - ); -} - -Future _createFeePayment({ - required Ed25519HDPublicKey aReceiver, - required Ed25519HDPublicKey aSender, - required int amount, -}) async { - final mint = Token.usdc.publicKey; - - final ataSender = await findAssociatedTokenAddress( - owner: aSender, - mint: mint, - ); - - final ataReceiver = await findAssociatedTokenAddress( - owner: aReceiver, - mint: mint, - ); - - return TokenInstruction.transfer( - amount: amount, - source: ataSender, - destination: ataReceiver, - owner: aSender, - ); -} - -extension on SolanaClient { - Future calculateDlnTxFee({ - required Ed25519HDPublicKey sender, - required Commitment commitment, - }) async { - final results = await Future.wait([ - _fetchDlnProtocolFee(commitment: commitment), - rpcClient.getMinimumBalanceForRentExemption(211), - rpcClient - .getMinimumBalanceForRentExemption(TokenProgram.neededAccountSpace), - ]); - - final nonceAccountCreationFee = await _hasNonceAccount( - sender: sender, - commitment: commitment, - ).letAsync( - (p) async => p - ? 0 - : await rpcClient.getMinimumBalanceForRentExemption(16) + 900000, - ); - - return results.sum + nonceAccountCreationFee; - } - - Future _hasNonceAccount({ - required Ed25519HDPublicKey sender, - required Commitment commitment, - }) async { - final pda = await Ed25519HDPublicKey.findProgramAddress( - seeds: ['NONCE'.codeUnits, sender.bytes], - programId: _dlnSourceAddress, - ); - - final info = await rpcClient - .getAccountInfo( - pda.toBase58(), - commitment: commitment, - encoding: dto.Encoding.base64, - ) - .value; - - return info?.data != null; - } - - Future _fetchDlnProtocolFee({ - required Commitment commitment, - }) async { - final pda = await Ed25519HDPublicKey.findProgramAddress( - seeds: ['STATE'.codeUnits], - programId: _dlnSourceAddress, - ); - - final info = await rpcClient - .getAccountInfo( - pda.toBase58(), - commitment: commitment, - encoding: dto.Encoding.base64, - ) - .value; - - // ignore: avoid-non-null-assertion, required data - final data = info!.data! as dto.BinaryAccountData; - - return data.data - .skip(8) - .skip(32) - .toList() - .let(Uint8List.fromList) - .buffer - .asByteData() - .let(BinaryReader.new) - .readU64() - .toInt(); - } -} - -final _dlnSourceAddress = Ed25519HDPublicKey.fromBase58( - 'src5qyZHqTqecJV4aY6Cb6zDZLMDzrDKKezs22MPHr4', -); - -const _maxTxCostUsdc = 2000000; // 2 USDC -const outgoingDlnPaymentFeeFraction = 0.03; // 3% -const incomingDlnPaymentFeePercentage = 3.0; // 3% -const minimumDlnPaymentFee = 3000000; // 3 USDC diff --git a/packages/espressocash_app/lib/features/outgoing_dln_payments/data/quote_repository.dart b/packages/espressocash_app/lib/features/outgoing_dln_payments/data/quote_repository.dart index ea093f8b3b..9dee264b67 100644 --- a/packages/espressocash_app/lib/features/outgoing_dln_payments/data/quote_repository.dart +++ b/packages/espressocash_app/lib/features/outgoing_dln_payments/data/quote_repository.dart @@ -1,50 +1,54 @@ +import 'package:espressocash_api/espressocash_api.dart'; + import 'package:injectable/injectable.dart'; -import '../../tokens/token.dart'; -import '../client/client.dart'; -import '../models/quote_info.dart'; +import '../../blockchain/models/blockchain.dart'; +import '../../currency/models/amount.dart'; +import '../../currency/models/currency.dart'; +import '../models/dln_payment.dart'; +import '../models/payment_quote.dart'; @injectable class QuoteRepository { const QuoteRepository({ - required DlnApiClient dlnApiClient, - }) : _dlnApiClient = dlnApiClient; - final DlnApiClient _dlnApiClient; + required EspressoCashClient ecClient, + }) : _client = ecClient; + + final EspressoCashClient _client; - Future getQuoteAndTransaction({ - required int amount, + Future getQuote({ + required CryptoAmount amount, required String receiverAddress, - required String senderAddress, - required DlnChains receiverChain, + required Blockchain receiverBlockchain, }) async { - final response = await _dlnApiClient.createTx( - CreateTxRequestDto( - srcChainId: DlnChains.solana.chainId, - srcChainTokenIn: Token.usdc.publicKey.toBase58(), - srcChainTokenInAmount: 'auto', - dstChainId: receiverChain.chainId, - dstChainTokenOut: receiverChain.usdcAddress, - dstChainTokenOutAmount: amount.toString(), - dstChainTokenOutRecipient: receiverAddress, - srcChainOrderAuthorityAddress: senderAddress, - dstChainOrderAuthorityAddress: receiverAddress, - referralCode: espressoDlnRefCode, + final quote = await _client.getDlnQuote( + PaymentQuoteRequestDto( + amount: amount.value, + receiverAddress: receiverAddress, + receiverBlockchain: receiverBlockchain.name, ), ); - final tx = response.tx.data; - final estimation = response.estimation; - - final totalFees = int.parse(estimation.srcChainTokenIn.amount) - amount; - - return QuoteInfo( - tx: tx, - inputAmount: amount, - senderDeductAmount: int.parse(estimation.srcChainTokenIn.amount), - receiverAmount: int.parse(estimation.dstChainTokenOut.recommendedAmount), - totalFees: totalFees, + return PaymentQuote( + payment: DlnPayment( + inputAmount: amount, + receiverAddress: receiverAddress, + receiverBlockchain: receiverBlockchain, + ), + receiverAmount: CryptoAmount( + cryptoCurrency: Currency.usdc, + value: quote.receiverAmount, + ), + inputAmount: CryptoAmount( + cryptoCurrency: Currency.usdc, + value: quote.inputAmount, + ), + fee: CryptoAmount( + cryptoCurrency: Currency.usdc, + value: quote.feeInUsdc, + ), + encodedTx: quote.encodedTx, + slot: quote.slot, ); } } - -const espressoDlnRefCode = 8435; diff --git a/packages/espressocash_app/lib/features/outgoing_dln_payments/models/quote_info.dart b/packages/espressocash_app/lib/features/outgoing_dln_payments/models/quote_info.dart deleted file mode 100644 index 05755b4657..0000000000 --- a/packages/espressocash_app/lib/features/outgoing_dln_payments/models/quote_info.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'quote_info.freezed.dart'; - -@freezed -class QuoteInfo with _$QuoteInfo { - const factory QuoteInfo({ - required String tx, - required int inputAmount, - required int senderDeductAmount, - required int receiverAmount, - required num totalFees, - }) = _QuoteInfo; -} diff --git a/packages/espressocash_app/lib/features/outgoing_dln_payments/services/confirm_payment_bloc.dart b/packages/espressocash_app/lib/features/outgoing_dln_payments/services/confirm_payment_bloc.dart index 10c7d42116..818d517619 100644 --- a/packages/espressocash_app/lib/features/outgoing_dln_payments/services/confirm_payment_bloc.dart +++ b/packages/espressocash_app/lib/features/outgoing_dln_payments/services/confirm_payment_bloc.dart @@ -6,14 +6,12 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:injectable/injectable.dart'; import 'package:rxdart/rxdart.dart'; -import 'package:solana/solana.dart'; import '../../../utils/flow.dart'; -import '../../accounts/models/ec_wallet.dart'; import '../../balances/data/repository.dart'; import '../../currency/models/amount.dart'; import '../../currency/models/currency.dart'; import '../../tokens/token.dart'; -import '../create_dln_payment.dart'; +import '../data/quote_repository.dart'; import '../models/dln_payment.dart'; import '../models/payment_quote.dart'; @@ -26,12 +24,10 @@ typedef _Emitter = Emitter<_State>; @injectable class ConfirmPaymentBloc extends Bloc<_Event, _State> { ConfirmPaymentBloc({ - required CreateDlnPayment createDlnPayment, + required QuoteRepository quoteRepository, required TokenBalancesRepository balancesRepository, - required ECWallet account, - }) : _createDlnPayment = createDlnPayment, + }) : _quoteRepository = quoteRepository, _balancesRepository = balancesRepository, - _account = account, super(ConfirmPaymentState(flowState: const Flow.initial())) { on(_onInit); on(_onConfirmed); @@ -43,9 +39,8 @@ class ConfirmPaymentBloc extends Bloc<_Event, _State> { ); } - final CreateDlnPayment _createDlnPayment; + final QuoteRepository _quoteRepository; final TokenBalancesRepository _balancesRepository; - final ECWallet _account; Timer? _timer; @@ -90,31 +85,10 @@ class ConfirmPaymentBloc extends Bloc<_Event, _State> { } try { - final createDlnPayment = await _createDlnPayment( - amount: payment.inputAmount.value, - senderAddress: _account.publicKey.toBase58(), + final quote = await _quoteRepository.getQuote( + amount: payment.inputAmount, receiverAddress: payment.receiverAddress, - receiverChain: payment.receiverBlockchain.name, - commitment: Commitment.confirmed, - ); - - final quote = PaymentQuote( - payment: DlnPayment( - inputAmount: payment.inputAmount, - receiverAddress: payment.receiverAddress, - receiverBlockchain: payment.receiverBlockchain, - ), - receiverAmount: CryptoAmount( - cryptoCurrency: Currency.usdc, - value: createDlnPayment.receiverAmount, - ), - inputAmount: payment.inputAmount, - fee: CryptoAmount( - cryptoCurrency: Currency.usdc, - value: createDlnPayment.fee, - ), - encodedTx: createDlnPayment.transaction.encode(), - slot: createDlnPayment.slot, + receiverBlockchain: payment.receiverBlockchain, ); _startTimer(); From ca709cddf9b9a482713255b216e391498c4b951d Mon Sep 17 00:00:00 2001 From: Vlad Sumin Date: Tue, 13 Aug 2024 13:53:19 +0300 Subject: [PATCH 19/20] revert --- packages/espressocash_app/lib/config.dart | 2 -- packages/espressocash_app/lib/di.dart | 10 ---------- 2 files changed, 12 deletions(-) diff --git a/packages/espressocash_app/lib/config.dart b/packages/espressocash_app/lib/config.dart index f727a27b95..5e66260151 100644 --- a/packages/espressocash_app/lib/config.dart +++ b/packages/espressocash_app/lib/config.dart @@ -77,8 +77,6 @@ const coinflowKycUrl = isProd ? 'https://coinflow.cash/withdraw/espresso' : 'https://sandbox.coinflow.cash/withdraw/espresso'; -const platformMnemonic = String.fromEnvironment('PLATFORM_MNEMONIC_MAINNET'); - const maxPayloadsPerSigningRequest = 10; const playstoreName = 'com.pleasecrypto.flutter'; diff --git a/packages/espressocash_app/lib/di.dart b/packages/espressocash_app/lib/di.dart index b33b821951..c58c67dfcc 100644 --- a/packages/espressocash_app/lib/di.dart +++ b/packages/espressocash_app/lib/di.dart @@ -43,14 +43,6 @@ abstract class AppModule { @preResolve Future prefs() => SharedPreferences.getInstance(); - @preResolve - @platformAccount - Future platformKeyPair() => Ed25519HDKeyPair.fromMnemonic( - platformMnemonic, - account: 0, - change: 0, - ); - @singleton @preResolve Future mixpanel() async { @@ -63,5 +55,3 @@ abstract class AppModule { return mixpanel; } } - -const platformAccount = Named('PlatformAccount'); From 447ef7f85a5a06f72bcd8bdf50d52c75363d00cb Mon Sep 17 00:00:00 2001 From: Vlad Sumin Date: Tue, 13 Aug 2024 13:54:47 +0300 Subject: [PATCH 20/20] revert --- .../priority_fees/services/add_priority_fees.dart | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/espressocash_app/lib/features/priority_fees/services/add_priority_fees.dart b/packages/espressocash_app/lib/features/priority_fees/services/add_priority_fees.dart index c375982c88..caef9c5653 100644 --- a/packages/espressocash_app/lib/features/priority_fees/services/add_priority_fees.dart +++ b/packages/espressocash_app/lib/features/priority_fees/services/add_priority_fees.dart @@ -23,7 +23,6 @@ class AddPriorityFees { required SignedTx tx, required Commitment commitment, required int maxPriorityFee, - Future Function(Iterable data)? sign, required Ed25519HDPublicKey platform, }) async { final priorityFees = await _ecClient.getPriorityFeeEstimate( @@ -36,7 +35,6 @@ class AddPriorityFees { tx, recommendedCuPrice: veryHighFee ?? (pow(2, 32).toInt()), maxTxFee: maxPriorityFee, - sign: sign, platform: platform, commitment: commitment, ) ?? @@ -47,7 +45,6 @@ class AddPriorityFees { SignedTx tx, { required int recommendedCuPrice, required int maxTxFee, - Future Function(Iterable data)? sign, required Ed25519HDPublicKey platform, required Commitment commitment, }) async { @@ -149,14 +146,10 @@ class AddPriorityFees { feePayer: platform, ); - final signature = sign != null - ? await sign(newCompiledMessage.toByteArray()) - : platform.emptySignature(); - return SignedTx( compiledMessage: newCompiledMessage, signatures: [ - signature, + platform.emptySignature(), ...tx.signatures.where((s) => s.publicKey != platform), ], );