diff --git a/packages/espressocash_api/lib/src/dto/scalex.dart b/packages/espressocash_api/lib/src/dto/scalex.dart index 26753c8171..681c426340 100644 --- a/packages/espressocash_api/lib/src/dto/scalex.dart +++ b/packages/espressocash_api/lib/src/dto/scalex.dart @@ -46,6 +46,7 @@ class OrderStatusScalexResponseDto with _$OrderStatusScalexResponseDto { @JsonKey(unknownEnumValue: ScalexOrderStatus.unknown) required ScalexOrderStatus status, OnRampScalexDetails? onRampDetails, + OffRampScalexDetails? offRampDetails, }) = _OrderStatusScalexResponseDto; factory OrderStatusScalexResponseDto.fromJson(Map json) => @@ -89,6 +90,17 @@ class OnRampScalexDetails with _$OnRampScalexDetails { _$OnRampScalexDetailsFromJson(json); } +@freezed +class OffRampScalexDetails with _$OffRampScalexDetails { + 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 e9eebe4389..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> @@ -1317,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 5165cc79bc..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 = { @@ -131,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( 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 8ce87c66f0..a0ed0586ec 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 = {}; @@ -115,27 +115,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 { @@ -145,7 +138,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/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 1ecf66b6bd..fd15dab0c9 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,36 +1,37 @@ 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 '../../balances/services/refresh_balance.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_durable_sender.dart'; import '../data/repository.dart'; import '../models/outgoing_direct_payment.dart'; @Singleton(scope: authScope) class ODPService { ODPService( - this._client, this._repository, - this._txSender, + this._txDurableSender, this._analyticsManager, + this._createDirectPayment, + this._refreshBalance, ); - final EspressoCashClient _client; final ODPRepository _repository; - final TxSender _txSender; + final TxDurableSender _txDurableSender; final AnalyticsManager _analyticsManager; + final CreateDirectPayment _createDirectPayment; + final RefreshBalance _refreshBalance; final Map> _subscriptions = {}; @@ -87,20 +88,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, @@ -132,17 +129,16 @@ class ODPService { return payment; } - final tx = await _txSender.send(status.tx, minContextSlot: status.slot); + final tx = await _txDurableSender.send(status.tx); final ODPStatus? newStatus = tx.map( - sent: (_) => ODPStatus.txSent( + sent: (it) => ODPStatus.txSent( status.tx, - slot: status.slot, + signature: it.signature ?? '', ), - invalidBlockhash: (_) => const ODPStatus.txFailure( - reason: TxFailureReason.invalidBlockhashSending, - ), - failure: (it) => ODPStatus.txFailure(reason: it.reason), + invalidBlockhash: (_) => null, + failure: (it) => + const ODPStatus.txFailure(reason: TxFailureReason.creatingFailure), networkError: (_) => null, ); @@ -154,20 +150,16 @@ class ODPService { if (status is! ODPStatusTxSent) { return payment; } + final tx = await _txDurableSender.wait(txId: status.signature); - final tx = await _txSender.wait( - status.tx, - minContextSlot: status.slot, - txType: 'OutgoingDirectPayment', - ); - - final ODPStatus? newStatus = tx.map( - success: (_) => ODPStatus.success(txId: status.tx.id), + final ODPStatus? newStatus = tx?.map( + success: (_) => ODPStatus.success(txId: status.signature), failure: (tx) => ODPStatus.txFailure(reason: tx.reason), networkError: (_) => null, ); if (newStatus is ODPStatusSuccess) { + _refreshBalance(); _analyticsManager.directPaymentSent(amount: payment.amount.decimal); } 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 bc6abbd345..152e24ce99 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; @@ -201,32 +198,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 { @@ -235,7 +221,7 @@ class OLPService implements Disposable { return payment; } - await _txConfirm(txId: status.signature); + await _txDurableSender.wait(txId: status.signature); final token = payment.amount.token; @@ -265,32 +251,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( @@ -302,7 +279,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/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/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 c7b59a834f..154bd18632 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 @@ -26,7 +26,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'; @@ -48,14 +48,14 @@ class MoneygramOffRampOrderService implements Disposable { this._moneygramClient, this._allbridgeApiClient, this._solanaClient, - this._txConfirm, + this._txDurableSender, this._refreshBalance, this._analytics, ); final MyDatabase _db; final TxSender _sender; - final TxConfirm _txConfirm; + final TxDurableSender _txDurableSender; final RefreshBalance _refreshBalance; final AnalyticsManager _analytics; @@ -383,6 +383,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()) { @@ -780,7 +781,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 05fe51d42f..e24927d2bf 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 @@ -23,7 +23,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; @@ -40,13 +40,13 @@ class MoneygramOnRampOrderService implements Disposable { this._stellarClient, this._moneygramClient, this._allbridgeApiClient, - this._txConfirm, + this._txDurableSender, this._refreshBalance, this._analytics, ); final MyDatabase _db; - final TxConfirm _txConfirm; + final TxDurableSender _txDurableSender; final RefreshBalance _refreshBalance; final AnalyticsManager _analytics; @@ -365,7 +365,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/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, + ); + } +} 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 389ede9605..ce52ebbadf 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'; @@ -20,15 +19,17 @@ import '../../accounts/models/ec_wallet.dart'; import '../../analytics/analytics_manager.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_durable_sender.dart'; import '../models/ramp_type.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 = ({ @@ -56,7 +57,9 @@ class OffRampOrderService implements Disposable { OffRampOrderService( this._account, this._client, - this._sender, + this._scalexWithdrawPayment, + this._createDirectPayment, + this._txDurableSender, this._db, this._tokens, this._analytics, @@ -67,7 +70,9 @@ class OffRampOrderService implements Disposable { final ECWallet _account; final EspressoCashClient _client; - final TxSender _sender; + final ScalexWithdrawPayment _scalexWithdrawPayment; + final CreateDirectPayment _createDirectPayment; + final TxDurableSender _txDurableSender; final MyDatabase _db; final TokenList _tokens; final AnalyticsManager _analytics; @@ -391,8 +396,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: @@ -439,52 +443,54 @@ 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, - ); + return _signAndUpdateRow(directPaymentResult.transaction); } Future _createScalexTx({ required String partnerOrderId, }) async { - final dto = ScalexWithdrawRequestDto( - orderId: partnerOrderId, - cluster: apiCluster, - ); - 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), + ); + } - return _signAndUpdateRow( - encodedTx: response.transaction, - slot: response.slot, + final scalexPaymentResult = await _scalexWithdrawPayment( + aReceiver: Ed25519HDPublicKey.fromBase58(details.depositAddress), + aSender: _account.publicKey, + amount: details.amount, + commitment: Commitment.confirmed, ); + + return _signAndUpdateRow(scalexPaymentResult.transaction); } - Future _signAndUpdateRow({ - required String encodedTx, - required BigInt slot, - }) 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), transaction: Value(tx.encode()), - slot: Value(slot), ); } - Future _sendTx((SignedTx, BigInt) tx) async { - final sent = await _sender.send(tx.$1, minContextSlot: tx.$2); + Future _sendTx(SignedTx tx) async { + final sent = await _txDurableSender.send(tx); switch (sent) { case TxSendSent(): break; @@ -506,11 +512,7 @@ class OffRampOrderService implements Disposable { return _depositError; } - final confirmed = await _sender.wait( - tx.$1, - minContextSlot: tx.$2, - txType: 'OffRamp', - ); + final confirmed = await _txDurableSender.wait(txId: sent.signature ?? ''); switch (confirmed) { case TxWaitSuccess(): return const OffRampOrderRowsCompanion( @@ -525,6 +527,7 @@ class OffRampOrderService implements Disposable { slot: Value(BigInt.zero), ); case TxWaitNetworkError(): + case null: return _depositError; } } 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/pubspec.lock b/packages/espressocash_app/pubspec.lock index 3edd8e54ac..7868ce9b45 100644 --- a/packages/espressocash_app/pubspec.lock +++ b/packages/espressocash_app/pubspec.lock @@ -298,7 +298,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 a53598a36f..41a293e6ab 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 dart_jsonwebtoken: ^2.14.0 decimal: ^2.3.3 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..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 @@ -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_durable_sender.dart'; import 'package:fast_immutable_collections/fast_immutable_collections.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; @@ -22,20 +22,24 @@ 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 sender = MockTxSender(); -final client = MockEspressoCashClient(); +final txDurableSender = MockTxDurableSender(); +final createDirectPayment = MockCreateDirectPayment(); -@GenerateMocks([TxSender, EspressoCashClient]) +@GenerateMocks([ + TxDurableSender, + CreateDirectPayment, +]) Future main() async { final account = LocalWallet(await Ed25519HDKeyPair.random()); final receiver = await Ed25519HDKeyPair.random(); final repository = MemoryRepository(); setUp(() { - reset(sender); - reset(client); + reset(txDurableSender); + reset(createDirectPayment); }); tearDown( @@ -58,13 +62,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( @@ -73,10 +77,11 @@ Future main() async { ); ODPService createService() => ODPService( - client, repository, - sender, + txDurableSender, const StubAnalyticsManager(), + createDirectPayment, + const StubRefreshBalance(), ); Future createODP(ODPService service) async { @@ -94,18 +99,21 @@ 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(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); 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(txDurableSender.send(any)).called(1); + verify(txDurableSender.wait(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(txDurableSender.send(any)); + verifyNever(txDurableSender.wait(txId: anyNamed('txId'))); }); } 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 {} +}