diff --git a/packages/komodo_defi_rpc_methods/lib/src/common_structures/activation/activation_params/sia_activation_params.dart b/packages/komodo_defi_rpc_methods/lib/src/common_structures/activation/activation_params/sia_activation_params.dart index 01c46ec81..a7d45306e 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/common_structures/activation/activation_params/sia_activation_params.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/common_structures/activation/activation_params/sia_activation_params.dart @@ -1,26 +1,33 @@ import 'package:komodo_defi_rpc_methods/src/common_structures/common_structures.dart'; import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; -import 'package:komodo_defi_types/komodo_defi_types.dart'; -/// SIA-specific activation parameters +/// Activation parameters specific to SIA protocol coins. /// -/// Supports: -/// - serverUrl: SiaScan-compatible wallet API base URL -/// - txHistory: whether to fetch transaction history on enable -/// - requiredConfirmations: confirmations to wait for swap steps +/// This extends the generic [ActivationParams] with: +/// - a required [serverUrl] pointing at a SiaScan-compatible wallet API +/// - an optional [password] for the SIA wallet daemon +/// - a [txHistory] flag that controls whether transaction history is enabled +/// +/// The shape produced by [toRpcParams] matches the KDF +/// `task::enable_sia::init` API, nesting values under `activation_params.client_conf`. class SiaActivationParams extends ActivationParams { const SiaActivationParams({ required this.serverUrl, + this.password, this.txHistory = true, - int? requiredConfirmations, - PrivateKeyPolicy privKeyPolicy = const PrivateKeyPolicy.contextPrivKey(), - }) : super( - requiredConfirmations: requiredConfirmations, - privKeyPolicy: privKeyPolicy, - ); + super.requiredConfirmations, + super.privKeyPolicy, + }); + /// Creates [SiaActivationParams] from a coins-config JSON entry. + /// + /// The SIA server URL is taken from: + /// - `server_url`, or + /// - the first `nodes[].url` entry as a fallback. + /// + /// Throws [ArgumentError] if no usable URL can be found. factory SiaActivationParams.fromConfigJson(JsonMap json) { - String? serverUrl = json.valueOrNull('server_url'); + var serverUrl = json.valueOrNull('server_url'); if (serverUrl == null && json.containsKey('nodes')) { final nodes = json.value>('nodes'); if (nodes.isNotEmpty) { @@ -45,15 +52,19 @@ class SiaActivationParams extends ActivationParams { ); } + /// Base URL of the SIA wallet API (e.g. `https://api.siascan.com/wallet/api`). final String serverUrl; + + /// Optional password to unlock or authenticate the SIA wallet daemon. + final String? password; + + /// Whether SIA transaction history should be enabled on activation. final bool txHistory; @override Map toRpcParams() => super.toRpcParams().deepMerge({ - 'client_conf': { - 'server_url': serverUrl, - }, - 'tx_history': txHistory, - }); + // SIA activation uses a nested client_conf object + 'client_conf': {'server_url': serverUrl, 'password': ?password}, + 'tx_history': txHistory, + }); } - diff --git a/packages/komodo_defi_rpc_methods/lib/src/common_structures/activation/strategies/utxo_activation_strategy.dart b/packages/komodo_defi_rpc_methods/lib/src/common_structures/activation/strategies/utxo_activation_strategy.dart deleted file mode 100644 index 8b1378917..000000000 --- a/packages/komodo_defi_rpc_methods/lib/src/common_structures/activation/strategies/utxo_activation_strategy.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/packages/komodo_defi_rpc_methods/lib/src/common_structures/common_structures.dart b/packages/komodo_defi_rpc_methods/lib/src/common_structures/common_structures.dart index db5bc1c83..f6faa0a0b 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/common_structures/common_structures.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/common_structures/common_structures.dart @@ -19,12 +19,10 @@ export 'activation/activation_params/utxo_activation_params.dart'; export 'activation/activation_params/zhtlc_activation_params.dart'; export 'activation/coin_protocol.dart'; export 'activation/evm_node.dart'; -export 'activation/strategies/utxo_activation_strategy.dart'; export 'activation/tokens_request.dart'; export 'activation/utxo_merge_params.dart'; export 'general/address_format.dart'; export 'general/balance_info.dart'; -export 'general/fee_info.dart'; export 'general/new_address_info.dart'; export 'general/scan_address_info.dart'; export 'general/sync_status.dart'; diff --git a/packages/komodo_defi_rpc_methods/lib/src/common_structures/general/fee_info.dart b/packages/komodo_defi_rpc_methods/lib/src/common_structures/general/fee_info.dart deleted file mode 100644 index 257b59920..000000000 --- a/packages/komodo_defi_rpc_methods/lib/src/common_structures/general/fee_info.dart +++ /dev/null @@ -1,2 +0,0 @@ -// Moved to package:komodo_defi_types/komodo_defi_types since this type is -// used in many contexts beyond RPC methods. 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 93e0e5950..8b0bfeea0 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 @@ -14,6 +14,7 @@ class TransactionInfo { required this.coin, required this.internalId, required this.memo, + this.txJson, this.spentByMe, this.receivedByMe, this.transactionFee, @@ -24,14 +25,14 @@ class TransactionInfo { txHash: json.value('tx_hash'), from: List.from(json.value('from')), to: List.from(json.value('to')), + txJson: json.valueOrNull('tx_json'), myBalanceChange: json.value('my_balance_change'), blockHeight: json.value('block_height'), confirmations: json.value('confirmations'), timestamp: json.value('timestamp'), - feeDetails: - json.containsKey('fee_details') - ? FeeInfo.fromJson(json.value('fee_details')) - : null, + feeDetails: json.containsKey('fee_details') + ? FeeInfo.fromJson(json.value('fee_details')) + : null, transactionFee: json.valueOrNull('transaction_fee'), coin: json.value('coin'), internalId: json.value('internal_id'), @@ -47,6 +48,9 @@ class TransactionInfo { final String myBalanceChange; final int blockHeight; final int confirmations; + + /// Raw transaction JSON (present for SIA protocol transactions). + final JsonMap? txJson; final int timestamp; final FeeInfo? feeDetails; final String? transactionFee; @@ -60,6 +64,7 @@ class TransactionInfo { 'tx_hash': txHash, 'from': from, 'to': to, + 'tx_json': ?txJson, 'my_balance_change': myBalanceChange, 'block_height': blockHeight, 'confirmations': confirmations, diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/rpc_methods.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/rpc_methods.dart index 5802c7c3f..5035f3e39 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/rpc_methods.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/rpc_methods.dart @@ -104,7 +104,6 @@ export 'wallet/get_wallet_names_response.dart'; export 'wallet/my_balance.dart'; export 'wallet/unban_pubkeys.dart'; export 'withdrawal/send_raw_transaction_request.dart'; -export 'withdrawal/sia_withdraw_request.dart'; export 'withdrawal/withdraw_request.dart'; export 'withdrawal/withdrawal_rpc_namespace.dart'; export 'zhtlc/z_coin_tx_history.dart'; diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/sia/enable_sia.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/sia/enable_sia.dart index 7095879a9..9a04995c2 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/sia/enable_sia.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/sia/enable_sia.dart @@ -1,13 +1,14 @@ import 'package:komodo_defi_rpc_methods/src/internal_exports.dart'; import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; +/// Request for the `task::enable_sia::init` RPC. +/// +/// Starts a task-managed activation flow for a SIA protocol coin and returns +/// a [NewTaskResponse] containing the activation task ID. class TaskEnableSiaInit extends BaseRequest { - TaskEnableSiaInit({ - required this.ticker, - required this.params, - super.rpcPass, - }) : super(method: 'task::enable_sia::init', mmrpc: RpcVersion.v2_0); + TaskEnableSiaInit({required this.ticker, required this.params, super.rpcPass}) + : super(method: 'task::enable_sia::init', mmrpc: RpcVersion.v2_0); final String ticker; @@ -17,15 +18,12 @@ class TaskEnableSiaInit @override Map toJson() => { - ...super.toJson(), - 'userpass': rpcPass, - 'mmrpc': mmrpc, - 'method': method, - 'params': { - 'ticker': ticker, - 'activation_params': params.toRpcParams(), - }, - }; + ...super.toJson(), + 'userpass': rpcPass, + 'mmrpc': mmrpc, + 'method': method, + 'params': {'ticker': ticker, 'activation_params': params.toRpcParams()}, + }; @override NewTaskResponse parse(Map json) { @@ -33,6 +31,9 @@ class TaskEnableSiaInit } } +/// Request for the `task::enable_sia::status` RPC. +/// +/// Polls the status of an ongoing SIA activation task. class TaskEnableSiaStatus extends BaseRequest { TaskEnableSiaStatus({ @@ -46,12 +47,12 @@ class TaskEnableSiaStatus @override Map toJson() => { - ...super.toJson(), - 'userpass': rpcPass, - 'mmrpc': mmrpc, - 'method': method, - 'params': {'task_id': taskId, 'forget_if_finished': forgetIfFinished}, - }; + ...super.toJson(), + 'userpass': rpcPass, + 'mmrpc': mmrpc, + 'method': method, + 'params': {'task_id': taskId, 'forget_if_finished': forgetIfFinished}, + }; @override TaskStatusResponse parse(Map json) { @@ -59,26 +60,86 @@ class TaskEnableSiaStatus } } +/// Request for the `task::enable_sia::cancel` RPC. +/// +/// Cancels an ongoing SIA activation task and returns a [SiaCancelResponse] +/// indicating whether the cancel operation was successful. class TaskEnableSiaCancel - extends BaseRequest { - TaskEnableSiaCancel({ + extends BaseRequest { + TaskEnableSiaCancel({required this.taskId, super.rpcPass}) + : super(method: 'task::enable_sia::cancel', mmrpc: RpcVersion.v2_0); + + final int taskId; + + @override + Map toJson() => { + ...super.toJson(), + 'userpass': rpcPass, + 'mmrpc': mmrpc, + 'method': method, + 'params': {'task_id': taskId}, + }; + + @override + SiaCancelResponse parse(Map json) => + SiaCancelResponse.parse(json); +} + +/// Request for the `task::enable_sia::user_action` RPC. +/// +/// Used when the SIA activation flow requires user interaction, such as +/// providing a hardware-wallet PIN or passphrase. +class TaskEnableSiaUserAction + extends BaseRequest { + TaskEnableSiaUserAction({ required this.taskId, + required this.actionType, + this.pin, + this.passphrase, super.rpcPass, - }) : super(method: 'task::enable_sia::cancel', mmrpc: RpcVersion.v2_0); + }) : super(method: 'task::enable_sia::user_action', mmrpc: RpcVersion.v2_0); final int taskId; + final String actionType; + final String? pin; + final String? passphrase; @override Map toJson() => { - ...super.toJson(), - 'userpass': rpcPass, - 'mmrpc': mmrpc, - 'method': method, - 'params': {'task_id': taskId}, - }; + ...super.toJson(), + 'userpass': rpcPass, + 'mmrpc': mmrpc, + 'method': method, + 'params': { + 'task_id': taskId, + 'user_action': { + 'action_type': actionType, + if (pin != null) 'pin': pin, + if (passphrase != null) 'passphrase': passphrase, + }, + }, + }; @override - TaskStatusResponse parse(Map json) => - TaskStatusResponse.parse(json); + UserActionResponse parse(Map json) => + UserActionResponse.parse(JsonMap.of(json)); } +/// Response returned by the `task::enable_sia::cancel` RPC. +/// +/// Wraps a simple [result] string, which is `"success"` on success. +class SiaCancelResponse extends BaseResponse { + SiaCancelResponse({required super.mmrpc, required this.result}); + + factory SiaCancelResponse.parse(Map json) { + return SiaCancelResponse( + mmrpc: json.value('mmrpc'), + result: json.value('result'), + ); + } + + final String result; + + @override + Map toJson() => {'mmrpc': mmrpc, 'result': result}; +} diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/sia/sia_rpc_namespace.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/sia/sia_rpc_namespace.dart index ef36b1f87..03eaac2fd 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/sia/sia_rpc_namespace.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/sia/sia_rpc_namespace.dart @@ -1,9 +1,14 @@ import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; -import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; +/// High-level namespace for SIA-specific RPC methods. +/// +/// Provides typed helpers over the raw `task::enable_sia::*` APIs. class SiaMethodsNamespace extends BaseRpcMethodNamespace { SiaMethodsNamespace(super.client); + /// Initialize SIA activation using `task::enable_sia::init`. + /// + /// Returns a [NewTaskResponse] with the activation task ID. Future enableSiaInit({ required String ticker, required SiaActivationParams params, @@ -17,6 +22,7 @@ class SiaMethodsNamespace extends BaseRpcMethodNamespace { ); } + /// Get activation status using `task::enable_sia::status`. Future enableSiaStatus( int taskId, { bool forgetIfFinished = true, @@ -30,10 +36,33 @@ class SiaMethodsNamespace extends BaseRpcMethodNamespace { ); } - Future enableSiaCancel({required int taskId}) { + /// Cancel an activation task using `task::enable_sia::cancel`. + /// + /// Returns a [SiaCancelResponse] that indicates success or failure. + Future enableSiaCancel({required int taskId}) { return execute( TaskEnableSiaCancel(taskId: taskId, rpcPass: rpcPass), ); } + + /// Provide user interaction for SIA activation via `task::enable_sia::user_action`. + /// + /// Typically used to pass Trezor PIN or passphrase when required. + Future enableSiaUserAction({ + required int taskId, + required String actionType, + String? pin, + String? passphrase, + }) { + return execute( + TaskEnableSiaUserAction( + taskId: taskId, + actionType: actionType, + pin: pin, + passphrase: passphrase, + rpcPass: rpcPass, + ), + ); + } } diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/withdrawal/send_raw_transaction_request.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/withdrawal/send_raw_transaction_request.dart index b8a95f8c9..d541aeaf0 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/withdrawal/send_raw_transaction_request.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/withdrawal/send_raw_transaction_request.dart @@ -1,7 +1,10 @@ import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; -/// Legacy send raw transaction request +/// Legacy `send_raw_transaction` request for UTXO/EVM-style coins. +/// +/// Sends a pre-built transaction hex ([txHex]) to the network for the given +/// [coin]. For SIA protocol coins, prefer [SiaSendRawTransactionRequest]. class SendRawTransactionLegacyRequest extends BaseRequest { SendRawTransactionLegacyRequest({ @@ -9,23 +12,51 @@ class SendRawTransactionLegacyRequest required this.coin, this.txHex, this.txJson, - }) : assert( - txHex != null || txJson != null, - 'Either txHex or txJson must be provided', - ), - super(method: 'send_raw_transaction', mmrpc: null); + }) : assert( + txHex != null || txJson != null, + 'Either txHex or txJson must be provided', + ), + super(method: 'send_raw_transaction', mmrpc: null); final String coin; final String? txHex; - final Map? txJson; + final JsonMap? txJson; @override Map toJson() => { - ...super.toJson(), - 'coin': coin, - if (txHex != null) 'tx_hex': txHex, - if (txJson != null) 'tx_json': txJson, - }; + ...super.toJson(), + 'coin': coin, + 'tx_hex': ?txHex, + 'tx_json': ?txJson, + }; + + @override + SendRawTransactionResponse parse(Map json) => + SendRawTransactionResponse.parse(json); +} + +/// SIA-specific legacy `send_raw_transaction` request using `tx_json`. +/// +/// For SIA protocol withdrawals, the KDF API expects the transaction details +/// as JSON ([txJson]) instead of a hex string. This request mirrors the SIA +/// examples in the KDF documentation. +class SiaSendRawTransactionRequest + extends BaseRequest { + SiaSendRawTransactionRequest({ + required super.rpcPass, + required this.coin, + required this.txJson, + }) : super(method: 'send_raw_transaction', mmrpc: null); + + final String coin; + final JsonMap txJson; + + @override + Map toJson() => { + ...super.toJson(), + 'coin': coin, + 'tx_json': txJson, + }; @override SendRawTransactionResponse parse(Map json) => diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/withdrawal/sia_withdraw_request.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/withdrawal/sia_withdraw_request.dart deleted file mode 100644 index bb60d9a55..000000000 --- a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/withdrawal/sia_withdraw_request.dart +++ /dev/null @@ -1,93 +0,0 @@ -import 'package:decimal/decimal.dart'; -import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; -import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; -import 'package:komodo_defi_types/komodo_defi_types.dart'; - -/// SIA-specific withdraw request -/// -/// Uses the same 'withdraw' RPC but parses SIA-flavored response payload -class SiaWithdrawRequest - extends BaseRequest { - SiaWithdrawRequest({ - required super.rpcPass, - required this.coin, - required this.to, - this.amount, - this.fee, - this.from, - this.max = false, - }) : assert(amount != null || max, 'Specify amount or set max=true'), - super(method: 'withdraw', mmrpc: RpcVersion.v2_0); - - final String coin; - final String to; - final Decimal? amount; - final FeeInfo? fee; - final WithdrawalSource? from; - final bool max; - - @override - Map toJson() => { - ...super.toJson(), - 'params': { - 'coin': coin, - 'to': to, - if (!max && amount != null) 'amount': amount!.toString(), - if (max) 'max': true, - if (fee != null) 'fee': fee!.toJson(), - if (from != null) 'from': from!.toRpcParams(), - }, - }; - - @override - SiaWithdrawResponse parse(Map json) => - SiaWithdrawResponse.parse(json); -} - -class SiaWithdrawResponse extends BaseResponse { - SiaWithdrawResponse({ - required super.mmrpc, - required this.status, - required this.spentByMe, - required this.receivedByMe, - required this.myBalanceChange, - this.feeDetails, - this.details, - }); - - factory SiaWithdrawResponse.parse(Map json) { - final result = json.value('result'); - return SiaWithdrawResponse( - mmrpc: json.value('mmrpc'), - status: result.value('status'), - spentByMe: result.value('spent_by_me'), - receivedByMe: result.value('received_by_me'), - myBalanceChange: result.value('my_balance_change'), - feeDetails: result.valueOrNull('fee_details') == null - ? null - : FeeInfo.fromJson(result.value('fee_details')), - details: result.valueOrNull('details'), - ); - } - - final String status; - final String spentByMe; - final String receivedByMe; - final String myBalanceChange; - final FeeInfo? feeDetails; - final dynamic details; - - @override - Map toJson() => { - 'mmrpc': mmrpc, - 'result': { - 'status': status, - 'spent_by_me': spentByMe, - 'received_by_me': receivedByMe, - 'my_balance_change': myBalanceChange, - if (feeDetails != null) 'fee_details': feeDetails!.toJson(), - if (details != null) 'details': details, - }, - }; -} - diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/withdrawal/withdrawal_rpc_namespace.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/withdrawal/withdrawal_rpc_namespace.dart index 10f939a86..a1accd71a 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/withdrawal/withdrawal_rpc_namespace.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/withdrawal/withdrawal_rpc_namespace.dart @@ -1,5 +1,5 @@ import 'package:komodo_defi_rpc_methods/src/internal_exports.dart'; -import 'package:decimal/decimal.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; class WithdrawMethodsNamespace extends BaseRpcMethodNamespace { @@ -24,28 +24,6 @@ class WithdrawMethodsNamespace extends BaseRpcMethodNamespace { ); } - /// Convenience wrapper for SIA withdrawals with SIA-specific response parsing - Future withdrawSia({ - required String coin, - required String to, - Decimal? amount, - FeeInfo? fee, - WithdrawalSource? from, - bool max = false, - }) { - return execute( - SiaWithdrawRequest( - rpcPass: rpcPass ?? '', - coin: coin, - to: to, - amount: amount, - fee: fee, - from: from, - max: max, - ), - ); - } - /// Initialize a new withdrawal task // TODO: Consider refactoring to use individual parameters instead of a single // object for the request parameters for the sake of consistency with other @@ -79,7 +57,7 @@ class WithdrawMethodsNamespace extends BaseRpcMethodNamespace { Future sendRawTransaction({ required String coin, String? txHex, - Map? txJson, + JsonMap? txJson, WithdrawalSource? from, }) { return execute( @@ -91,4 +69,18 @@ class WithdrawMethodsNamespace extends BaseRpcMethodNamespace { ), ); } + + /// SIA-specific `send_raw_transaction` using a `tx_json` payload. + Future sendRawTransactionSia({ + required String coin, + required JsonMap txJson, + }) { + return execute( + SiaSendRawTransactionRequest( + rpcPass: rpcPass ?? '', + coin: coin, + txJson: txJson, + ), + ); + } } diff --git a/packages/komodo_defi_rpc_methods/test/sia_rpc_methods_test.dart b/packages/komodo_defi_rpc_methods/test/sia_rpc_methods_test.dart index 0894efcd0..15c3d9b3a 100644 --- a/packages/komodo_defi_rpc_methods/test/sia_rpc_methods_test.dart +++ b/packages/komodo_defi_rpc_methods/test/sia_rpc_methods_test.dart @@ -1,6 +1,8 @@ +import 'package:decimal/decimal.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; void main() { group('SIA RPC', () { @@ -16,27 +18,43 @@ void main() { ); final json = req.toJson(); expect(json['method'], 'task::enable_sia::init'); - final p = (json['params'] as Map)['activation_params'] as Map; - final clientConf = p['client_conf'] as Map; + final activationParams = + (json['params'] as Map)['activation_params'] as Map; + final clientConf = activationParams['client_conf'] as Map; expect(clientConf['server_url'], 'https://api.siascan.com/wallet/api'); - expect(p['tx_history'], true); - expect(p['required_confirmations'], 1); + expect(activationParams['tx_history'], true); + expect(activationParams['required_confirmations'], 1); }); - test('SiaWithdrawResponse parses nullable fee_details safely', () { - final response = { - 'mmrpc': '2.0', - 'result': { - 'status': 'Ok', - 'spent_by_me': '0', - 'received_by_me': '100', - 'my_balance_change': '100', - // fee_details intentionally omitted + test('SiaWithdrawResponse parses full SIA withdraw shape', () { + final responseResult = { + 'tx_json': {'siacoinOutputs': []}, + 'tx_hash': 'hash', + 'from': ['from_addr'], + 'to': ['to_addr'], + 'total_amount': '10', + 'spent_by_me': '0', + 'received_by_me': '100', + 'my_balance_change': '100', + 'block_height': 1, + 'timestamp': 123456, + 'fee_details': { + 'type': 'Sia', + 'coin': 'SC', + 'policy': 'Fixed', + 'total_amount': '0.1', }, + 'coin': 'SC', + 'internal_id': '', + 'transaction_type': 'SiaV2Transaction', + 'memo': null, }; - final parsed = SiaWithdrawResponse.parse(JsonMap.of(response)); - expect(parsed.status, 'Ok'); - expect(parsed.feeDetails, isNull); + final parsed = WithdrawResult.fromJson(JsonMap.of(responseResult)); + expect(parsed.txHash, 'hash'); + expect(parsed.from, ['from_addr']); + expect(parsed.to, ['to_addr']); + expect(parsed.balanceChanges.totalAmount, Decimal.fromInt(10)); + expect(parsed.fee.coin, 'SC'); }); }); } diff --git a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/sia_activation_strategy.dart b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/sia_activation_strategy.dart index 3ece5c8bf..3cceb0af0 100644 --- a/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/sia_activation_strategy.dart +++ b/packages/komodo_defi_sdk/lib/src/activation/protocol_strategies/sia_activation_strategy.dart @@ -1,13 +1,21 @@ import 'dart:async'; +import 'dart:convert'; +import 'package:komodo_defi_framework/komodo_defi_framework.dart'; import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; import 'package:komodo_defi_sdk/src/activation/_activation.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; +import 'package:logging/logging.dart'; class SiaActivationStrategy extends ProtocolActivationStrategy { - SiaActivationStrategy(super.client); + SiaActivationStrategy( + super.client, { + this.pollingInterval = const Duration(milliseconds: 500), + }); - static const Duration kPollInterval = Duration(milliseconds: 500); + /// Delay added between activation task status RPC requests. Defaults to 500ms + final Duration pollingInterval; + static final Logger _log = Logger('SiaActivationStrategy'); @override Set get supportedProtocols => {CoinSubClass.sia}; @@ -32,25 +40,35 @@ class SiaActivationStrategy extends ProtocolActivationStrategy { requiredConfirmations: protocol.requiredConfirmations, ); - yield ActivationProgress( + yield const ActivationProgress( status: 'Starting SIA activation...', progressDetails: ActivationProgressDetails( currentStep: ActivationStep.initialization, stepCount: 3, - additionalInfo: { - 'assetType': 'platform', - 'protocol': 'SIA', - }, + additionalInfo: {'assetType': 'platform', 'protocol': 'SIA'}, ), ); try { - final init = await KomodoDefiRpcMethods(client).sia.enableSiaInit( - ticker: asset.id.id, - params: params, + // Debug logging for SIA activation + if (KdfLoggingConfig.verboseLogging) { + _log + ..info('[SIA] Activating SIA coin: ${asset.id.id}') + ..info( + '[SIA] Activation parameters: ${jsonEncode({'ticker': asset.id.id, 'server_url': serverUrl, 'required_confirmations': protocol.requiredConfirmations})}', ); + } + + final init = await KomodoDefiRpcMethods( + client, + ).sia.enableSiaInit(ticker: asset.id.id, params: params); final taskId = init.taskId; + + if (KdfLoggingConfig.verboseLogging) { + _log.info('[SIA] Task initiated for ${asset.id.id}, task_id: $taskId'); + } + yield ActivationProgress( status: 'SIA activation task started', progressDetails: ActivationProgressDetails( @@ -60,22 +78,18 @@ class SiaActivationStrategy extends ProtocolActivationStrategy { ), ); - while (true) { - final status = - await KomodoDefiRpcMethods(client).sia.enableSiaStatus(taskId); - - yield ActivationProgress( - status: 'SIA activation in progress', - progressDetails: ActivationProgressDetails( - currentStep: ActivationStep.processing, - stepCount: 3, - additionalInfo: {'status': status.status}, - ), - ); - - // Stop polling on any terminal state - if (status.status != 'InProgress') { + var isComplete = false; + while (!isComplete) { + final status = await KomodoDefiRpcMethods( + client, + ).sia.enableSiaStatus(taskId); + + if (status.isCompleted) { if (status.status == 'Ok') { + if (KdfLoggingConfig.verboseLogging) { + _log.info('[ELECTRUM] Activation completed for ${asset.id.id}'); + } + yield ActivationProgress( status: 'SIA activation complete', isComplete: true, @@ -86,6 +100,13 @@ class SiaActivationStrategy extends ProtocolActivationStrategy { ), ); } else { + if (KdfLoggingConfig.verboseLogging) { + _log.warning( + '[ELECTRUM] Activation failed for ${asset.id.id}: ' + '${status.status} - ${status.details}', + ); + } + yield ActivationProgress( status: 'SIA activation failed', isComplete: true, @@ -100,33 +121,35 @@ class SiaActivationStrategy extends ProtocolActivationStrategy { ), ); } + isComplete = true; + } else { yield ActivationProgress( - status: 'SIA activation concluded', - isComplete: true, + status: 'SIA activation in progress', progressDetails: ActivationProgressDetails( - currentStep: status.status == 'Ok' - ? ActivationStep.complete - : ActivationStep.error, + currentStep: ActivationStep.processing, stepCount: 3, + additionalInfo: {'status': status.status}, ), ); - break; + await Future.delayed(pollingInterval); } - - await Future.delayed(kPollInterval); } - } on Exception catch (e) { + } catch (e, s) { + _log.severe('[ELECTRUM] Activation exception for ${asset.id.id}', e, s); + yield ActivationProgress( status: 'SIA activation failed', isComplete: true, + errorMessage: e.toString(), progressDetails: ActivationProgressDetails( currentStep: ActivationStep.error, stepCount: 3, additionalInfo: {'error': e.toString()}, + errorDetails: e.toString(), + stackTrace: s.toString(), ), ); rethrow; } } } - 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 d0689ad42..3e78963f3 100644 --- a/packages/komodo_defi_sdk/lib/src/withdrawals/withdrawal_manager.dart +++ b/packages/komodo_defi_sdk/lib/src/withdrawals/withdrawal_manager.dart @@ -490,6 +490,9 @@ class WithdrawalManager { final asset = _assetProvider .findAssetsByConfigId(parameters.asset) .single; + + // TODO: refactor into strategy pattern or add a getter to the protocol + // class to check if the protocol requires the legacy withdrawals final isTendermintProtocol = asset.protocol is TendermintProtocol; final isSiaProtocol = asset.protocol is SiaProtocol; @@ -585,16 +588,23 @@ class WithdrawalManager { ) async* { try { final asset = _assetProvider.findAssetsByConfigId(assetId).single; + + // TODO: refactor into strategy pattern or add a getter to the protocol + // class to check if the protocol requires the legacy withdrawals final isTendermintProtocol = asset.protocol is TendermintProtocol; + final isSiaProtocol = asset.protocol is SiaProtocol; // Tendermint assets are not yet supported by the task-based API - if (isTendermintProtocol) { + // and require a legacy implementation + if (isTendermintProtocol || isSiaProtocol) { yield* _legacyManager.executeWithdrawal(preview, assetId); return; } // Ensure asset is activated before broadcasting - final activationResult = await _activationCoordinator.activateAsset(asset); + final activationResult = await _activationCoordinator.activateAsset( + asset, + ); if (activationResult.isFailure) { throw WithdrawalException( 'Failed to activate asset $assetId: ${activationResult.errorMessage ?? activationResult.toString()}',