diff --git a/packages/komodo_defi_rpc_methods/lib/src/common_structures/transaction_history/transaction_info.dart b/packages/komodo_defi_rpc_methods/lib/src/common_structures/transaction_history/transaction_info.dart index d773df9..2dc9905 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/common_structures/transaction_history/transaction_info.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/common_structures/transaction_history/transaction_info.dart @@ -1,4 +1,3 @@ -import 'package:decimal/decimal.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; class TransactionInfo { @@ -71,19 +70,4 @@ class TransactionInfo { if (transactionFee != null) 'transaction_fee': transactionFee, if (memo != null) 'memo': memo, }; - - Transaction asTransaction(AssetId assetId) => Transaction( - id: txHash, - internalId: internalId, - assetId: assetId, - amount: Decimal.parse(myBalanceChange), - timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000), - confirmations: confirmations, - blockHeight: blockHeight, - from: from, - to: to, - fee: feeDetails, - txHash: txHash, - memo: memo, - ); } diff --git a/packages/komodo_defi_sdk/example/lib/screens/withdrawal_page.dart b/packages/komodo_defi_sdk/example/lib/screens/withdrawal_page.dart index e720038..2d2287d 100644 --- a/packages/komodo_defi_sdk/example/lib/screens/withdrawal_page.dart +++ b/packages/komodo_defi_sdk/example/lib/screens/withdrawal_page.dart @@ -104,8 +104,9 @@ class _WithdrawalScreenState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('Amount: ${_preview!.totalAmount} ${widget.asset.id.id}'), - Text('To: ${_preview!.to.first}'), + Text('Amount: ${_preview!.balanceChanges.netChange} ' + '${widget.asset.id.id}'), + Text('To: ${_preview!.to.join(', ')}'), _buildFeeDetails(_preview!.fee), if (_preview!.kmdRewards != null) ...[ const SizedBox(height: 8), diff --git a/packages/komodo_defi_sdk/lib/src/withdrawals/withdrawal_manager.dart b/packages/komodo_defi_sdk/lib/src/withdrawals/withdrawal_manager.dart index b32a457..a5d3524 100644 --- a/packages/komodo_defi_sdk/lib/src/withdrawals/withdrawal_manager.dart +++ b/packages/komodo_defi_sdk/lib/src/withdrawals/withdrawal_manager.dart @@ -36,7 +36,7 @@ class WithdrawalManager { .map(_mapStatusToProgress) .forEach(controller.add); - // Send the raw transaction to the network if + // Send the raw transaction to the network if successful if (lastProgress?.status == 'Ok' && lastProgress?.details is WithdrawResult) { final details = lastProgress!.details as WithdrawResult; @@ -51,12 +51,12 @@ class WithdrawalManager { message: 'Withdrawal complete', withdrawalResult: WithdrawalResult( txHash: response.txHash, - amount: Decimal.parse(details.totalAmount), + balanceChanges: details.balanceChanges, coin: parameters.asset, toAddress: parameters.toAddress, fee: details.fee, kmdRewardsEligible: details.kmdRewards != null && - (details.kmdRewards?.amount ?? '0') != '0', + Decimal.parse(details.kmdRewards!.amount) > Decimal.zero, ), ); } @@ -97,11 +97,12 @@ class WithdrawalManager { message: 'Withdrawal generated. Sending transaction...', withdrawalResult: WithdrawalResult( txHash: result.txHash, - amount: Decimal.parse(result.totalAmount), + balanceChanges: result.balanceChanges, coin: result.coin, toAddress: result.to.first, fee: result.fee, - kmdRewardsEligible: result.kmdRewards != null, + kmdRewardsEligible: result.kmdRewards != null && + Decimal.parse(result.kmdRewards!.amount) > Decimal.zero, ), ); } diff --git a/packages/komodo_defi_types/lib/src/transactions/balance_changes.dart b/packages/komodo_defi_types/lib/src/transactions/balance_changes.dart new file mode 100644 index 0000000..b718ec2 --- /dev/null +++ b/packages/komodo_defi_types/lib/src/transactions/balance_changes.dart @@ -0,0 +1,45 @@ +import 'package:decimal/decimal.dart'; +import 'package:equatable/equatable.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; + +/// Represents the effect a transaction has on wallet balances +class BalanceChanges extends Equatable { + const BalanceChanges({ + required this.netChange, + required this.receivedByMe, + required this.spentByMe, + required this.totalAmount, + }); + + factory BalanceChanges.fromJson(JsonMap json) => BalanceChanges( + netChange: Decimal.parse(json.value('my_balance_change')), + receivedByMe: Decimal.parse(json.value('received_by_me')), + spentByMe: Decimal.parse(json.value('spent_by_me')), + totalAmount: Decimal.parse(json.value('total_amount')), + ); + + /// The net change in the wallet's balance (positive for incoming, + /// negative for outgoing) + final Decimal netChange; + + /// Amount received by my addresses in this transaction + final Decimal receivedByMe; + + /// Amount spent from my addresses in this transaction + final Decimal spentByMe; + + /// The total amount of coins transferred + final Decimal totalAmount; + + bool get isIncoming => netChange > Decimal.zero; + + @override + List get props => [netChange, receivedByMe, spentByMe, totalAmount]; + + Map toJson() => { + 'my_balance_change': netChange.toString(), + 'received_by_me': receivedByMe.toString(), + 'spent_by_me': spentByMe.toString(), + 'total_amount': totalAmount.toString(), + }; +} diff --git a/packages/komodo_defi_types/lib/src/transactions/transaction.dart b/packages/komodo_defi_types/lib/src/transactions/transaction.dart index 8470458..333d331 100644 --- a/packages/komodo_defi_types/lib/src/transactions/transaction.dart +++ b/packages/komodo_defi_types/lib/src/transactions/transaction.dart @@ -1,5 +1,6 @@ import 'package:decimal/decimal.dart'; import 'package:equatable/equatable.dart'; +import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; /// Domain model for a transaction, decoupled from the API representation @@ -8,15 +9,15 @@ class Transaction extends Equatable { required this.id, required this.internalId, required this.assetId, - required this.amount, + required this.balanceChanges, required this.timestamp, required this.confirmations, required this.blockHeight, required this.from, required this.to, required this.txHash, - required this.fee, - required this.memo, + this.fee, + this.memo, }); factory Transaction.fromJson(Map json) => Transaction( @@ -26,7 +27,7 @@ class Transaction extends Equatable { json.value('asset_id'), knownIds: null, ), - amount: Decimal.parse(json.value('my_balance_change')), + balanceChanges: BalanceChanges.fromJson(json), timestamp: DateTime.parse(json.value('timestamp')), confirmations: json.value('confirmations'), blockHeight: json.value('block_height'), @@ -42,27 +43,28 @@ class Transaction extends Equatable { final String id; final String internalId; final AssetId assetId; - final Decimal amount; + final BalanceChanges balanceChanges; final DateTime timestamp; final int confirmations; final int blockHeight; final List from; final List to; - - // Null for cases such as SIA coin. TODO: Consider if there is a better way - // represent this property usin final String? txHash; final FeeInfo? fee; final String? memo; - bool get isIncoming => amount > Decimal.zero; + /// Convenience getter for the net balance change + Decimal get amount => balanceChanges.netChange; + + /// Convenience getter for whether transaction is incoming + bool get isIncoming => balanceChanges.isIncoming; @override List get props => [ id, internalId, assetId, - amount, + balanceChanges, timestamp, confirmations, blockHeight, @@ -77,14 +79,43 @@ class Transaction extends Equatable { 'id': id, 'internal_id': internalId, 'asset_id': assetId.toJson(), - 'my_balance_change': amount.toString(), + ...balanceChanges.toJson(), 'timestamp': timestamp.toIso8601String(), 'confirmations': confirmations, 'block_height': blockHeight, 'from': from, 'to': to, - 'tx_hash': txHash, - 'memo': memo, - 'fee': fee.toString(), + if (txHash != null) 'tx_hash': txHash, + if (fee != null) 'fee': fee!.toJson(), + if (memo != null) 'memo': memo, }; } + +extension TransactionInfoExtension on TransactionInfo { + Transaction asTransaction(AssetId assetId) => Transaction( + id: txHash, + internalId: internalId, + assetId: assetId, + balanceChanges: BalanceChanges( + netChange: Decimal.parse(myBalanceChange), + receivedByMe: receivedByMe != null + ? Decimal.parse(receivedByMe!) + : Decimal.zero, + spentByMe: + spentByMe != null ? Decimal.parse(spentByMe!) : Decimal.zero, + totalAmount: Decimal.parse( + // For historical transactions that don't have spent/received, + // use the absolute value of the balance change + receivedByMe ?? spentByMe ?? myBalanceChange.replaceAll('-', ''), + ), + ), + timestamp: DateTime.fromMillisecondsSinceEpoch(timestamp * 1000), + confirmations: confirmations, + blockHeight: blockHeight, + from: from, + to: to, + txHash: txHash, + fee: feeDetails, + memo: memo, + ); +} diff --git a/packages/komodo_defi_types/lib/src/withdrawal/withdrawal_types.dart b/packages/komodo_defi_types/lib/src/withdrawal/withdrawal_types.dart index f6b7eb8..88036cd 100644 --- a/packages/komodo_defi_types/lib/src/withdrawal/withdrawal_types.dart +++ b/packages/komodo_defi_types/lib/src/withdrawal/withdrawal_types.dart @@ -1,18 +1,15 @@ -// TODO: Split the classes into separate files - import 'package:decimal/decimal.dart'; +import 'package:equatable/equatable.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; +/// Raw API response for a withdrawal operation class WithdrawResult { WithdrawResult({ required this.txHex, required this.txHash, required this.from, required this.to, - required this.totalAmount, - required this.spentByMe, - required this.receivedByMe, - required this.myBalanceChange, + required this.balanceChanges, required this.blockHeight, required this.timestamp, required this.fee, @@ -28,10 +25,7 @@ class WithdrawResult { txHash: json.value('tx_hash'), from: List.from(json.value('from')), to: List.from(json.value('to')), - totalAmount: json.value('total_amount'), - spentByMe: json.value('spent_by_me'), - receivedByMe: json.value('received_by_me'), - myBalanceChange: json.value('my_balance_change'), + balanceChanges: BalanceChanges.fromJson(json), blockHeight: json.value('block_height'), timestamp: json.value('timestamp'), fee: FeeInfo.fromJson(json.value('fee_details')), @@ -48,10 +42,7 @@ class WithdrawResult { final String txHash; final List from; final List to; - final String totalAmount; - final String spentByMe; - final String receivedByMe; - final String myBalanceChange; + final BalanceChanges balanceChanges; final int blockHeight; final int timestamp; final FeeInfo fee; @@ -65,10 +56,7 @@ class WithdrawResult { 'tx_hash': txHash, 'from': from, 'to': to, - 'total_amount': totalAmount, - 'spent_by_me': spentByMe, - 'received_by_me': receivedByMe, - 'my_balance_change': myBalanceChange, + ...balanceChanges.toJson(), 'block_height': blockHeight, 'timestamp': timestamp, 'fee_details': fee.toJson(), @@ -79,8 +67,54 @@ class WithdrawResult { }; } -class WithdrawalProgress { - WithdrawalProgress({ +/// Domain model for a successful withdrawal operation +class WithdrawalResult extends Equatable { + const WithdrawalResult({ + required this.txHash, + required this.balanceChanges, + required this.coin, + required this.toAddress, + required this.fee, + this.kmdRewardsEligible = false, + }); + + /// Create a domain model from API response + factory WithdrawalResult.fromWithdrawResult(WithdrawResult result) { + return WithdrawalResult( + txHash: result.txHash, + balanceChanges: result.balanceChanges, + coin: result.coin, + toAddress: result.to.first, + fee: result.fee, + kmdRewardsEligible: result.kmdRewards != null && + Decimal.parse(result.kmdRewards!.amount) > Decimal.zero, + ); + } + + final String txHash; + final BalanceChanges balanceChanges; + final String coin; + final String toAddress; + final FeeInfo fee; + final bool kmdRewardsEligible; + + /// Convenience getter for the withdrawal amount (abs of net change) + Decimal get amount => balanceChanges.netChange.abs(); + + @override + List get props => [ + txHash, + balanceChanges, + coin, + toAddress, + fee, + kmdRewardsEligible, + ]; +} + +/// Progress tracking for withdrawal operations +class WithdrawalProgress extends Equatable { + const WithdrawalProgress({ required this.status, required this.message, this.withdrawalResult, @@ -95,55 +129,21 @@ class WithdrawalProgress { final WithdrawalErrorCode? errorCode; final String? errorMessage; final String? taskId; -} -class WithdrawalResult { - WithdrawalResult({ - required this.txHash, - required this.amount, - required this.coin, - required this.toAddress, - required this.fee, - this.kmdRewardsEligible = false, - }); - final String txHash; - final Decimal amount; - final String coin; - final String toAddress; - final FeeInfo fee; - final bool kmdRewardsEligible; + @override + List get props => [ + status, + message, + withdrawalResult, + errorCode, + errorMessage, + taskId, + ]; } -typedef WithdrawalPreview = WithdrawResult; - -// class FeeDetails { -// FeeDetails({ -// required this.type, -// required this.amount, -// this.coin, -// }); - -// factory FeeDetails.fromJson(Map json) { -// return FeeDetails( -// type: json.value('type'), -// amount: json.value('amount'), -// coin: json.valueOrNull('coin'), -// ); -// } - -// final String type; -// final String amount; -// final String? coin; - -// Map toJson() => { -// 'type': type, -// 'amount': amount, -// if (coin != null) 'coin': coin, -// }; -// } - -class WithdrawParameters { - WithdrawParameters({ +/// Parameters for initiating a withdrawal +class WithdrawParameters extends Equatable { + const WithdrawParameters({ required this.asset, required this.toAddress, required this.amount, @@ -176,10 +176,26 @@ class WithdrawParameters { if (memo != null) 'memo': memo, if (ibcTransfer != null) 'ibc_transfer': ibcTransfer, }; + + @override + List get props => [ + asset, + toAddress, + amount, + fee, + from, + memo, + ibcTransfer, + isMax, + ]; } -class WithdrawalSource { - WithdrawalSource._({ +/// Preview of a withdrawal operation, using same structure as API response +typedef WithdrawalPreview = WithdrawResult; + +/// Specifies the source of funds for a withdrawal +class WithdrawalSource extends Equatable { + const WithdrawalSource._({ required this.type, required this.params, }); @@ -197,6 +213,7 @@ class WithdrawalSource { 'address_id': addressId, }, ); + final WithdrawalSourceType type; final Map params; @@ -204,45 +221,10 @@ class WithdrawalSource { 'type': type.toString(), ...params, }; -} - -// class WithdrawFeeDetails { -// WithdrawFeeDetails({ -// required this.type, -// required this.amount, -// this.coin, -// this.gas, -// this.gasPrice, -// this.totalFee, -// }); - -// factory WithdrawFeeDetails.fromJson(Map json) { -// return WithdrawFeeDetails( -// type: json.value('type'), -// amount: json.value('amount'), -// coin: json.valueOrNull('coin'), -// gas: json.valueOrNull('gas'), -// gasPrice: json.valueOrNull('gas_price'), -// totalFee: json.valueOrNull('total_fee'), -// ); -// } -// final String type; -// final String amount; -// final String? coin; -// final int? gas; -// final String? gasPrice; -// final String? totalFee; - -// Map toJson() => { -// 'type': type, -// 'amount': amount, -// if (coin != null) 'coin': coin, -// if (gas != null) 'gas': gas, -// if (gasPrice != null) 'gas_price': gasPrice, -// if (totalFee != null) 'total_fee': totalFee, -// }; -// } + @override + List get props => [type, params]; +} class KmdRewards { KmdRewards({ diff --git a/packages/komodo_defi_types/lib/types.dart b/packages/komodo_defi_types/lib/types.dart index e82c172..71bf331 100644 --- a/packages/komodo_defi_types/lib/types.dart +++ b/packages/komodo_defi_types/lib/types.dart @@ -41,6 +41,7 @@ export 'src/public_key/pubkey.dart'; export 'src/public_key/pubkey_strategy.dart'; export 'src/public_key/token_balance_map.dart'; export 'src/public_key/wallet_balance.dart'; +export 'src/transactions/balance_changes.dart'; export 'src/transactions/transaction.dart'; export 'src/transactions/transaction_history_strategy.dart'; export 'src/transactions/transaction_pagination_strategy.dart';