diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/fee_management/fee_management_rpc_namespace.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/fee_management/fee_management_rpc_namespace.dart new file mode 100644 index 00000000..914beee0 --- /dev/null +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/fee_management/fee_management_rpc_namespace.dart @@ -0,0 +1,40 @@ +import 'package:komodo_defi_rpc_methods/src/internal_exports.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; + +class FeeManagementMethodsNamespace extends BaseRpcMethodNamespace { + FeeManagementMethodsNamespace(super.client); + + Future getEthEstimatedFeePerGas({ + required String coin, + required FeeEstimatorType estimatorType, + String? rpcPass, + }) => execute( + GetEthEstimatedFeePerGasRequest( + rpcPass: rpcPass ?? this.rpcPass ?? '', + coin: coin, + estimatorType: estimatorType, + ), + ); + + Future getSwapTransactionFeePolicy({ + required String coin, + String? rpcPass, + }) => execute( + GetSwapTransactionFeePolicyRequest( + rpcPass: rpcPass ?? this.rpcPass ?? '', + coin: coin, + ), + ); + + Future setSwapTransactionFeePolicy({ + required String coin, + required FeePolicy swapTxFeePolicy, + String? rpcPass, + }) => execute( + SetSwapTransactionFeePolicyRequest( + rpcPass: rpcPass ?? this.rpcPass ?? '', + coin: coin, + swapTxFeePolicy: swapTxFeePolicy, + ), + ); +} diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/fee_management/get_eth_estimated_fee_per_gas.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/fee_management/get_eth_estimated_fee_per_gas.dart new file mode 100644 index 00000000..b79bbb48 --- /dev/null +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/fee_management/get_eth_estimated_fee_per_gas.dart @@ -0,0 +1,44 @@ +import 'package:komodo_defi_rpc_methods/src/internal_exports.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; + +class GetEthEstimatedFeePerGasRequest + extends + BaseRequest { + GetEthEstimatedFeePerGasRequest({ + required super.rpcPass, + required this.coin, + required this.estimatorType, + }) : super(method: 'get_eth_estimated_fee_per_gas', mmrpc: '2.0'); + + final String coin; + final FeeEstimatorType estimatorType; + + @override + Map toJson() => { + ...super.toJson(), + 'params': {'coin': coin, 'estimator_type': estimatorType.toString()}, + }; + + @override + GetEthEstimatedFeePerGasResponse parse(Map json) => + GetEthEstimatedFeePerGasResponse.parse(json); +} + +class GetEthEstimatedFeePerGasResponse extends BaseResponse { + GetEthEstimatedFeePerGasResponse({ + required super.mmrpc, + required this.result, + }); + + factory GetEthEstimatedFeePerGasResponse.parse(Map json) => + GetEthEstimatedFeePerGasResponse( + mmrpc: json.value('mmrpc'), + result: EthEstimatedFeePerGas.fromJson(json.value('result')), + ); + + final EthEstimatedFeePerGas result; + + @override + Map toJson() => {'mmrpc': mmrpc, 'result': result.toJson()}; +} diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/fee_management/get_swap_transaction_fee_policy.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/fee_management/get_swap_transaction_fee_policy.dart new file mode 100644 index 00000000..29a1d3cf --- /dev/null +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/fee_management/get_swap_transaction_fee_policy.dart @@ -0,0 +1,46 @@ +import 'package:komodo_defi_rpc_methods/src/internal_exports.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; + +class GetSwapTransactionFeePolicyRequest + extends + BaseRequest { + GetSwapTransactionFeePolicyRequest({ + required super.rpcPass, + required this.coin, + }) : super(method: 'get_swap_transaction_fee_policy', mmrpc: '2.0'); + + final String coin; + + @override + Map toJson() => { + ...super.toJson(), + 'params': {'coin': coin}, + }; + + @override + GetSwapTransactionFeePolicyResponse parse(Map json) => + GetSwapTransactionFeePolicyResponse.parse(json); +} + +class GetSwapTransactionFeePolicyResponse extends BaseResponse { + GetSwapTransactionFeePolicyResponse({ + required super.mmrpc, + required this.result, + }); + + factory GetSwapTransactionFeePolicyResponse.parse( + Map json, + ) => GetSwapTransactionFeePolicyResponse( + mmrpc: json.value('mmrpc'), + result: FeePolicy.fromString(json.value('result')), + ); + + final FeePolicy result; + + @override + Map toJson() => { + 'mmrpc': mmrpc, + 'result': result.toString(), + }; +} diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/fee_management/set_swap_transaction_fee_policy.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/fee_management/set_swap_transaction_fee_policy.dart new file mode 100644 index 00000000..673424bf --- /dev/null +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods/fee_management/set_swap_transaction_fee_policy.dart @@ -0,0 +1,48 @@ +import 'package:komodo_defi_rpc_methods/src/internal_exports.dart'; +import 'package:komodo_defi_types/komodo_defi_type_utils.dart'; +import 'package:komodo_defi_types/komodo_defi_types.dart'; + +class SetSwapTransactionFeePolicyRequest + extends + BaseRequest { + SetSwapTransactionFeePolicyRequest({ + required super.rpcPass, + required this.coin, + required this.swapTxFeePolicy, + }) : super(method: 'set_swap_transaction_fee_policy', mmrpc: '2.0'); + + final String coin; + final FeePolicy swapTxFeePolicy; + + @override + Map toJson() => { + ...super.toJson(), + 'params': {'coin': coin, 'swap_tx_fee_policy': swapTxFeePolicy.toString()}, + }; + + @override + SetSwapTransactionFeePolicyResponse parse(Map json) => + SetSwapTransactionFeePolicyResponse.parse(json); +} + +class SetSwapTransactionFeePolicyResponse extends BaseResponse { + SetSwapTransactionFeePolicyResponse({ + required super.mmrpc, + required this.result, + }); + + factory SetSwapTransactionFeePolicyResponse.parse( + Map json, + ) => SetSwapTransactionFeePolicyResponse( + mmrpc: json.value('mmrpc'), + result: FeePolicy.fromString(json.value('result')), + ); + + final FeePolicy result; + + @override + Map toJson() => { + 'mmrpc': mmrpc, + 'result': result.toString(), + }; +} 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 f5ed1737..24c7525a 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 @@ -54,5 +54,9 @@ export 'wallet/my_balance.dart'; export 'withdrawal/send_raw_transaction_request.dart'; export 'withdrawal/withdraw_request.dart'; export 'withdrawal/withdrawal_rpc_namespace.dart'; +export 'fee_management/fee_management_rpc_namespace.dart'; +export 'fee_management/get_eth_estimated_fee_per_gas.dart'; +export 'fee_management/get_swap_transaction_fee_policy.dart'; +export 'fee_management/set_swap_transaction_fee_policy.dart'; export 'zhtlc/z_coin_tx_history.dart'; export 'zhtlc/zhtlc_rpc_namespace.dart'; diff --git a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods_library.dart b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods_library.dart index f41dd2e2..7b67eae8 100644 --- a/packages/komodo_defi_rpc_methods/lib/src/rpc_methods_library.dart +++ b/packages/komodo_defi_rpc_methods/lib/src/rpc_methods_library.dart @@ -49,6 +49,8 @@ class KomodoDefiRpcMethods { MessageSigningMethodsNamespace get messageSigning => MessageSigningMethodsNamespace(_client); UtilityMethods get utility => UtilityMethods(_client); + FeeManagementMethodsNamespace get feeManagement => + FeeManagementMethodsNamespace(_client); } class TaskMethods extends BaseRpcMethodNamespace { diff --git a/packages/komodo_defi_sdk/lib/src/_internal_exports.dart b/packages/komodo_defi_sdk/lib/src/_internal_exports.dart index 1420008b..4eba3ac1 100644 --- a/packages/komodo_defi_sdk/lib/src/_internal_exports.dart +++ b/packages/komodo_defi_sdk/lib/src/_internal_exports.dart @@ -6,3 +6,4 @@ library _internal_exports; export 'activation/_activation_index.dart'; export 'assets/_assets_index.dart'; export 'transaction_history/_transaction_history_index.dart'; +export 'fees/fee_manager.dart'; diff --git a/packages/komodo_defi_sdk/lib/src/bootstrap.dart b/packages/komodo_defi_sdk/lib/src/bootstrap.dart index d121e030..e8c0a39d 100644 --- a/packages/komodo_defi_sdk/lib/src/bootstrap.dart +++ b/packages/komodo_defi_sdk/lib/src/bootstrap.dart @@ -166,6 +166,11 @@ Future bootstrap({ return manager; }); + container.registerSingletonAsync(() async { + final client = await container.getAsync(); + return FeeManager(client); + }, dependsOn: [ApiClient]); + container.registerSingletonAsync( () async { final client = await container.getAsync(); @@ -194,8 +199,14 @@ Future bootstrap({ final client = await container.getAsync(); final assetProvider = await container.getAsync(); final activationManager = await container.getAsync(); - return WithdrawalManager(client, assetProvider, activationManager); - }, dependsOn: [ApiClient, AssetManager, ActivationManager]); + final feeManager = await container.getAsync(); + return WithdrawalManager( + client, + assetProvider, + activationManager, + feeManager, + ); + }, dependsOn: [ApiClient, AssetManager, ActivationManager, FeeManager]); // Wait for all async singletons to initialize await container.allReady(); diff --git a/packages/komodo_defi_sdk/lib/src/fees/fee_manager.dart b/packages/komodo_defi_sdk/lib/src/fees/fee_manager.dart new file mode 100644 index 00000000..b83944aa --- /dev/null +++ b/packages/komodo_defi_sdk/lib/src/fees/fee_manager.dart @@ -0,0 +1,159 @@ +import 'package:komodo_defi_types/komodo_defi_types.dart'; + +/// Manages cryptocurrency transaction fee operations and policies. +/// +/// The [FeeManager] provides functionality for: +/// - Retrieving estimated gas fees for Ethereum-based transactions +/// - Getting and setting fee policies for swap transactions +/// - Managing fee-related configuration for blockchain operations +/// +/// This manager abstracts away the complexity of fee estimation and management, +/// providing a simple interface for applications to work with transaction fees +/// across different blockchain protocols. +/// +/// Usage example: +/// ```dart +/// final feeManager = FeeManager(apiClient); +/// +/// // Get ETH gas fee estimates +/// final gasEstimates = await feeManager.getEthEstimatedFeePerGas('ETH'); +/// print('Slow fee: ${gasEstimates.slow.maxFeePerGas} gwei'); +/// print('Medium fee: ${gasEstimates.medium.maxFeePerGas} gwei'); +/// print('Fast fee: ${gasEstimates.fast.maxFeePerGas} gwei'); +/// +/// // Get current swap fee policy for a coin +/// final policy = await feeManager.getSwapTransactionFeePolicy('KMD'); +/// print('Current fee policy: ${policy.type}'); +/// +/// // Update fee policy if needed +/// if (policy.type != 'standard') { +/// final newPolicy = FeePolicy(type: 'standard', ...); +/// await feeManager.setSwapTransactionFeePolicy('KMD', newPolicy); +/// } +/// ``` +class FeeManager { + /// Creates a new [FeeManager] instance. + /// + /// Requires: + /// - [_client] - API client for making RPC calls to fee management endpoints + FeeManager(this._client); + + final ApiClient _client; + + /// Retrieves estimated fee per gas for Ethereum-based transactions. + /// + /// This method provides up-to-date gas fee estimates for Ethereum-compatible + /// chains with different speed options (slow, medium, fast). + /// + /// Parameters: + /// - [coin] - The ticker symbol of the coin (e.g., 'ETH', 'MATIC') + /// - [estimatorType] - The type of estimator to use (default: simple) + /// + /// Returns a [Future] containing gas fee estimates at + /// different priority levels: + /// - `slow` - Lower cost but potentially longer confirmation time + /// - `medium` - Balanced cost and confirmation time + /// - `fast` - Higher cost for faster confirmation + /// + /// Each estimate includes: + /// - `maxFeePerGas` - Maximum fee per gas unit + /// - `maxPriorityFeePerGas` - Maximum priority fee per gas unit + /// + /// Example: + /// ```dart + /// final estimates = await feeManager.getEthEstimatedFeePerGas('ETH'); + /// + /// // Choose a fee based on desired confirmation speed + /// final selectedFee = estimates.medium; + /// + /// print('Max fee: ${selectedFee.maxFeePerGas} gwei'); + /// print('Max priority fee: ${selectedFee.maxPriorityFeePerGas} gwei'); + /// ``` + Future getEthEstimatedFeePerGas( + String coin, { + FeeEstimatorType estimatorType = FeeEstimatorType.simple, + }) async { + final response = await _client.rpc.feeManagement.getEthEstimatedFeePerGas( + coin: coin, + estimatorType: estimatorType, + ); + return response.result; + } + + /// Retrieves the current fee policy for swap transactions of a specific coin. + /// + /// Fee policies determine how transaction fees are calculated and applied + /// for swap operations involving the specified coin. + /// + /// Parameters: + /// - [coin] - The ticker symbol of the coin (e.g., 'KMD', 'BTC') + /// + /// Returns a [Future] containing the current fee policy + /// configuration. + /// + /// Example: + /// ```dart + /// final policy = await feeManager.getSwapTransactionFeePolicy('KMD'); + /// + /// if (policy.type == 'utxo_per_kbyte') { + /// print('Fee rate: ${policy.feePerKbyte} sat/KB'); + /// } + /// ``` + Future getSwapTransactionFeePolicy(String coin) async { + final response = await _client.rpc.feeManagement + .getSwapTransactionFeePolicy(coin: coin); + return response.result; + } + + /// Sets a new fee policy for swap transactions of a specific coin. + /// + /// This method allows customizing how transaction fees are calculated and + /// applied for swap operations involving the specified coin. + /// + /// Parameters: + /// - [coin] - The ticker symbol of the coin (e.g., 'KMD', 'BTC') + /// - [policy] - The new fee policy to apply + /// + /// Returns a [Future] containing the updated fee policy + /// configuration. + /// + /// Example: + /// ```dart + /// // Create a new UTXO fee policy with a specific rate + /// final newPolicy = FeePolicy( + /// type: 'utxo_per_kbyte', + /// feePerKbyte: 1000, // 1000 satoshis per kilobyte + /// ); + /// + /// final updatedPolicy = await feeManager.setSwapTransactionFeePolicy( + /// 'BTC', + /// newPolicy, + /// ); + /// + /// print('Updated fee policy: ${updatedPolicy.type}'); + /// ``` + Future setSwapTransactionFeePolicy( + String coin, + FeePolicy policy, + ) async { + final response = await _client.rpc.feeManagement + .setSwapTransactionFeePolicy(coin: coin, swapTxFeePolicy: policy); + return response.result; + } + + /// Disposes of resources used by the FeeManager. + /// + /// This method is called when the FeeManager is no longer needed. + /// Currently, it doesn't perform any cleanup operations as the FeeManager + /// doesn't manage any resources that require explicit disposal. + /// + /// Example: + /// ```dart + /// // When done with the fee manager + /// await feeManager.dispose(); + /// ``` + Future dispose() { + // No resources to dispose. Return a future that completes immediately. + return Future.value(); + } +} diff --git a/packages/komodo_defi_sdk/lib/src/komodo_defi_sdk.dart b/packages/komodo_defi_sdk/lib/src/komodo_defi_sdk.dart index 7e5b775a..53b4b4a7 100644 --- a/packages/komodo_defi_sdk/lib/src/komodo_defi_sdk.dart +++ b/packages/komodo_defi_sdk/lib/src/komodo_defi_sdk.dart @@ -241,6 +241,9 @@ class KomodoDefiSdk with SecureRpcPasswordMixin { MarketDataManager get marketData => _assertSdkInitialized(_container()); + /// Provides access to fee management utilities. + FeeManager get fees => _assertSdkInitialized(_container()); + /// Gets a reference to the balance manager for checking asset balances. /// /// Provides functionality for checking and monitoring asset balances. @@ -333,7 +336,7 @@ class KomodoDefiSdk with SecureRpcPasswordMixin { /// cleanup of resources and background operations. /// /// NB! By default, this will terminate the KDF process. - /// + /// /// TODO: Consider future refactoring to separate KDF process disposal vs /// Dart object disposal. /// @@ -346,7 +349,7 @@ class KomodoDefiSdk with SecureRpcPasswordMixin { _isDisposed = true; if (!_isInitialized) return; - + _isInitialized = false; _initializationFuture = null; @@ -358,6 +361,7 @@ class KomodoDefiSdk with SecureRpcPasswordMixin { _disposeIfRegistered((m) => m.dispose()), _disposeIfRegistered((m) => m.dispose()), _disposeIfRegistered((m) => m.dispose()), + _disposeIfRegistered((m) => m.dispose()), _disposeIfRegistered((m) => m.dispose()), ]); 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 509dcafd..abbf60d7 100644 --- a/packages/komodo_defi_sdk/lib/src/withdrawals/withdrawal_manager.dart +++ b/packages/komodo_defi_sdk/lib/src/withdrawals/withdrawal_manager.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:developer' show log; import 'package:decimal/decimal.dart'; import 'package:komodo_defi_rpc_methods/komodo_defi_rpc_methods.dart'; @@ -6,21 +7,114 @@ import 'package:komodo_defi_sdk/src/_internal_exports.dart'; import 'package:komodo_defi_sdk/src/withdrawals/legacy_withdrawal_manager.dart'; import 'package:komodo_defi_types/komodo_defi_types.dart'; -/// Manages asset withdrawals using task-based API +/// Manages cryptocurrency asset withdrawals to external addresses. +/// +/// The [WithdrawalManager] provides functionality for: +/// - Creating withdrawal previews to check fees and expected results +/// - Executing withdrawals with progress tracking +/// - Managing and canceling active withdrawal operations +/// +/// It supports both task-based API operations for most chains and falls back to +/// legacy implementation for protocols that don't yet support tasks +/// (e.g., Tendermint). +/// +/// The manager ensures proper fee estimation when not provided explicitly +/// and handles the full lifecycle of a withdrawal transaction: +/// 1. Asset activation (if needed) +/// 2. Transaction creation +/// 3. Broadcasting to the network +/// 4. Status tracking +/// +/// Usage example: +/// ```dart +/// final manager = WithdrawalManager(...); +/// +/// // Preview a withdrawal +/// final preview = await manager.previewWithdrawal( +/// WithdrawParameters( +/// asset: 'BTC', +/// toAddress: '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa', +/// amount: Decimal.parse('0.001'), +/// ), +/// ); +/// +/// // Execute a withdrawal with progress tracking +/// final progressStream = manager.withdraw( +/// WithdrawParameters( +/// asset: 'BTC', +/// toAddress: '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa', +/// amount: Decimal.parse('0.001'), +/// ), +/// ); +/// +/// await for (final progress in progressStream) { +/// print('Status: ${progress.status}, Message: ${progress.message}'); +/// if (progress.withdrawalResult != null) { +/// print('Tx hash: ${progress.withdrawalResult!.txHash}'); +/// } +/// } +/// ``` class WithdrawalManager { - WithdrawalManager(this._client, this._assetProvider, this._activationManager); + /// Creates a new [WithdrawalManager] instance. + /// + /// Requires: + /// - [_client] - API client for making RPC calls + /// - [_assetProvider] - Provider for looking up asset information + /// - [_activationManager] - Manager for activating assets before withdrawal + /// - [_feeManager] - Manager for fee estimation and management + WithdrawalManager( + this._client, + this._assetProvider, + this._activationManager, + this._feeManager, + ); + + /// Default gas limit for basic ETH transactions. + /// + /// This is used when no specific gas limit is provided in the withdrawal + /// parameters. For standard ETH transfers, 21000 gas is the standard amount + /// required. + static const int _defaultEthGasLimit = 21000; final ApiClient _client; final IAssetProvider _assetProvider; final ActivationManager _activationManager; + final FeeManager _feeManager; final _activeWithdrawals = >{}; - /// Cancel an active withdrawal task + /// Cancels an active withdrawal task. + /// + /// This method attempts to cancel a withdrawal task that is currently in + /// progress. It's useful when a user wants to abort an ongoing withdrawal + /// operation. + /// + /// Parameters: + /// - [taskId] - The ID of the task to cancel + /// + /// Returns a [Future] that completes with: + /// - `true` if the cancellation was successful + /// - `false` if the cancellation failed + /// + /// The method will also clean up any resources associated with the task, + /// regardless of whether the cancellation was successful. + /// + /// Example: + /// ```dart + /// final success = await withdrawalManager.cancelWithdrawal(taskId); + /// if (success) { + /// print('Withdrawal canceled successfully'); + /// } else { + /// print('Failed to cancel withdrawal'); + /// } + /// ``` Future cancelWithdrawal(int taskId) async { try { final response = await _client.rpc.withdraw.cancel(taskId); return response.result == 'success'; - } catch (e) { + } catch (e, stackTrace) { + // Log the error and stack trace for debugging purposes + log('Error while canceling withdrawal: $e'); + log('Stack trace: $stackTrace'); return false; } finally { await _activeWithdrawals[taskId]?.close(); @@ -28,7 +122,18 @@ class WithdrawalManager { } } - /// Cleanup any active withdrawals + /// Cleans up all active withdrawals and releases resources. + /// + /// This method should be called when the manager is no longer needed, + /// typically when the application is shutting down or the user is + /// logging out. It attempts to cancel all active withdrawal tasks and + /// releases associated resources. + /// + /// Example: + /// ```dart + /// // When done with the withdrawal manager + /// await withdrawalManager.dispose(); + /// ``` Future dispose() async { final withdrawals = _activeWithdrawals.entries.toList(); _activeWithdrawals.clear(); @@ -39,6 +144,42 @@ class WithdrawalManager { } } + /// Creates a preview of a withdrawal operation without executing it. + /// + /// This method allows users to see what would happen if they executed the + /// withdrawal, including fees, balance changes, and other transaction + /// details, before committing to it. + /// + /// Parameters: + /// - [parameters] - The withdrawal parameters defining the asset, amount, + /// and destination + /// + /// Returns a [Future] containing the estimated transaction + /// details. + /// + /// Throws: + /// - [WithdrawalException] if the preview fails, with appropriate error code + /// + /// Note: For Tendermint-based assets, this method falls back to the legacy + /// implementation since task-based API is not yet supported for these assets. + /// + /// Example: + /// ```dart + /// try { + /// final preview = await withdrawalManager.previewWithdrawal( + /// WithdrawParameters( + /// asset: 'ETH', + /// toAddress: '0x1234...', + /// amount: Decimal.parse('0.1'), + /// ), + /// ); + /// + /// print('Estimated fee: ${preview.fee.totalFee}'); + /// print('Balance change: ${preview.balanceChanges.netChange}'); + /// } catch (e) { + /// print('Preview failed: $e'); + /// } + /// ``` Future previewWithdrawal( WithdrawParameters parameters, ) async { @@ -54,9 +195,11 @@ class WithdrawalManager { return await legacyManager.previewWithdrawal(parameters); } + final paramsWithFee = await _ensureFee(parameters, asset); + // Use task-based approach for non-Tendermint assets final stream = (await _client.rpc.withdraw.init( - parameters, + paramsWithFee, )).watch( getTaskStatus: (int taskId) => @@ -67,7 +210,7 @@ class WithdrawalManager { final lastStatus = await stream.last; - if (lastStatus.status.toLowerCase() == 'Error') { + if (lastStatus.status.toLowerCase() == 'error') { throw WithdrawalException( lastStatus.details as String, _mapErrorToCode(lastStatus.details as String), @@ -93,7 +236,54 @@ class WithdrawalManager { } } - /// Start a withdrawal operation and return a progress stream + /// Executes a withdrawal operation and provides a progress stream. + /// + /// This method performs the full withdrawal process: + /// 1. Ensures the asset is activated + /// 2. Creates the transaction + /// 3. Broadcasts it to the network + /// 4. Tracks and reports progress + /// + /// Parameters: + /// - [parameters] - The withdrawal parameters defining the asset, amount, + /// and destination + /// + /// Returns a [Stream] that emits progress updates + /// throughout the operation. The final event will either contain the + /// completed withdrawal result or an error. + /// + /// Error handling: + /// - Errors are emitted through the stream's error channel + /// - All errors are wrapped in [WithdrawalException] with appropriate + /// error codes + /// + /// Protocol handling: + /// - For Tendermint-based assets, this method uses a legacy implementation + /// - For other asset types, it uses the task-based API + /// + /// Example: + /// ```dart + /// final progressStream = withdrawalManager.withdraw( + /// WithdrawParameters( + /// asset: 'BTC', + /// toAddress: '1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa', + /// amount: Decimal.parse('0.001'), + /// ), + /// ); + /// + /// try { + /// await for (final progress in progressStream) { + /// if (progress.status == WithdrawalStatus.complete) { + /// final result = progress.withdrawalResult!; + /// print('Withdrawal complete! TX: ${result.txHash}'); + /// } else { + /// print('Progress: ${progress.message}'); + /// } + /// } + /// } catch (e) { + /// print('Withdrawal failed: $e'); + /// } + /// ``` Stream withdraw(WithdrawParameters parameters) async* { int? taskId; try { @@ -119,8 +309,10 @@ class WithdrawalManager { ); } + final paramsWithFee = await _ensureFee(parameters, asset); + // Initialize withdrawal task - final initResponse = await _client.rpc.withdraw.init(parameters); + final initResponse = await _client.rpc.withdraw.init(paramsWithFee); taskId = initResponse.taskId; WithdrawStatusResponse? lastProgress; @@ -173,7 +365,10 @@ class WithdrawalManager { Decimal.parse(details.kmdRewards!.amount) > Decimal.zero, ), ); - } catch (e) { + } catch (e, stackTrace) { + // Log the error and stack trace for debugging purposes + log('Error while broadcasting transaction: $e'); + log('Stack trace: $stackTrace'); yield* Stream.error( WithdrawalException( 'Failed to broadcast transaction: $e', @@ -182,7 +377,10 @@ class WithdrawalManager { ); } } - } catch (e) { + } catch (e, stackTrace) { + // Log the error and stack trace for debugging purposes + log('Error during withdrawal: $e'); + log('Stack trace: $stackTrace'); yield* Stream.error( WithdrawalException( 'Withdrawal failed: $e', @@ -195,7 +393,16 @@ class WithdrawalManager { } } - /// Maps error messages to withdrawal error codes + /// Maps error messages to withdrawal error codes. + /// + /// This helper method analyzes error messages from the API and maps them + /// to appropriate [WithdrawalErrorCode] values for consistent error + /// handling. + /// + /// Parameters: + /// - [error] - The error message to analyze + /// + /// Returns the appropriate [WithdrawalErrorCode] based on the error content. WithdrawalErrorCode _mapErrorToCode(String error) { final errorLower = error.toLowerCase(); @@ -215,7 +422,60 @@ class WithdrawalManager { return WithdrawalErrorCode.unknownError; } - /// Map API status response to domain progress model + /// Ensures fee parameters are set for the withdrawal. + /// + /// If fee parameters are already provided, returns the original parameters. + /// Otherwise, estimates appropriate fees for the asset and adds them to the + /// parameters. + /// + /// Parameters: + /// - [params] - The original withdrawal parameters + /// - [asset] - The asset being withdrawn + /// + /// Returns updated [WithdrawParameters] with fee information. + Future _ensureFee( + WithdrawParameters params, + Asset asset, + ) async { + if (params.fee != null) return params; + + try { + final estimation = await _feeManager.getEthEstimatedFeePerGas( + asset.id.id, + ); + final fee = FeeInfo.ethGas( + coin: asset.id.id, + gasPrice: estimation.medium.maxFeePerGas, + gas: _defaultEthGasLimit, + ); + return WithdrawParameters( + asset: params.asset, + toAddress: params.toAddress, + amount: params.amount, + fee: fee, + from: params.from, + memo: params.memo, + ibcTransfer: params.ibcTransfer, + ibcSourceChannel: params.ibcSourceChannel, + isMax: params.isMax, + ); + } catch (e, stackTrace) { + // Log the error and stack trace for debugging purposes + log('Error while estimating fee: $e'); + log('Stack trace: $stackTrace'); + return params; + } + } + + /// Maps API status response to domain progress model. + /// + /// Converts the raw API status response into a user-friendly progress object + /// that can be consumed by the application. + /// + /// Parameters: + /// - [status] - The API status response + /// + /// Returns a [WithdrawalProgress] object representing the current state. WithdrawalProgress _mapStatusToProgress(WithdrawStatusResponse status) { if (status.status == 'Ok') { final result = status.details as WithdrawResult; diff --git a/packages/komodo_defi_types/komodo_defi_constants.dart b/packages/komodo_defi_types/komodo_defi_constants.dart index 22fdaf9c..c21429bd 100644 --- a/packages/komodo_defi_types/komodo_defi_constants.dart +++ b/packages/komodo_defi_types/komodo_defi_constants.dart @@ -1,2 +1 @@ export 'package:komodo_defi_types/komodo_defi_types.dart' show kDefaultNetId; - diff --git a/packages/komodo_defi_types/lib/komodo_defi_types.dart b/packages/komodo_defi_types/lib/komodo_defi_types.dart index a4bfb637..50028202 100644 --- a/packages/komodo_defi_types/lib/komodo_defi_types.dart +++ b/packages/komodo_defi_types/lib/komodo_defi_types.dart @@ -21,6 +21,7 @@ export 'src/komodo_defi_types_base.dart'; export 'src/public_key/balance_strategy.dart'; export 'src/seed_node/seed_node.dart'; export 'src/types.dart'; +export 'src/fees/fee_management.dart'; // Export activation params types // export 'packages:komodo_defi_rpc_methods/lib/src/common_structures/activation/activation_params/activation_params_index.dart diff --git a/packages/komodo_defi_types/lib/src/fees/fee_management.dart b/packages/komodo_defi_types/lib/src/fees/fee_management.dart new file mode 100644 index 00000000..1943959e --- /dev/null +++ b/packages/komodo_defi_types/lib/src/fees/fee_management.dart @@ -0,0 +1,150 @@ +import 'package:decimal/decimal.dart'; +import 'package:equatable/equatable.dart'; + +/// Estimator type used when requesting fee data from the API. +enum FeeEstimatorType { + simple, + provider; + + @override + String toString() => switch (this) { + FeeEstimatorType.simple => 'Simple', + FeeEstimatorType.provider => 'Provider', + }; + + static FeeEstimatorType fromString(String value) { + switch (value.toLowerCase()) { + case 'provider': + return FeeEstimatorType.provider; + case 'simple': + default: + return FeeEstimatorType.simple; + } + } +} + +/// Fee policy used for swap transactions or general fee selection. +enum FeePolicy { + low, + medium, + high, + internal; + + @override + String toString() => switch (this) { + FeePolicy.low => 'Low', + FeePolicy.medium => 'Medium', + FeePolicy.high => 'High', + FeePolicy.internal => 'Internal', + }; + + static FeePolicy fromString(String value) { + switch (value.toLowerCase()) { + case 'low': + return FeePolicy.low; + case 'medium': + return FeePolicy.medium; + case 'high': + return FeePolicy.high; + case 'internal': + return FeePolicy.internal; + default: + throw ArgumentError('Invalid fee policy: $value'); + } + } +} + +/// Represents a single fee level returned by the API. +class EthFeeLevel extends Equatable { + const EthFeeLevel({ + required this.maxPriorityFeePerGas, + required this.maxFeePerGas, + this.minWaitTime, + this.maxWaitTime, + }); + + factory EthFeeLevel.fromJson(Map json) { + return EthFeeLevel( + maxPriorityFeePerGas: + Decimal.parse(json['max_priority_fee_per_gas'].toString()), + maxFeePerGas: Decimal.parse(json['max_fee_per_gas'].toString()), + minWaitTime: json['min_wait_time'] as int?, + maxWaitTime: json['max_wait_time'] as int?, + ); + } + + final Decimal maxPriorityFeePerGas; + final Decimal maxFeePerGas; + final int? minWaitTime; + final int? maxWaitTime; + + Map toJson() => { + 'max_priority_fee_per_gas': maxPriorityFeePerGas.toString(), + 'max_fee_per_gas': maxFeePerGas.toString(), + if (minWaitTime != null) 'min_wait_time': minWaitTime, + if (maxWaitTime != null) 'max_wait_time': maxWaitTime, + }; + + @override + List get props => + [maxPriorityFeePerGas, maxFeePerGas, minWaitTime, maxWaitTime]; +} + +/// Response object for [get_eth_estimated_fee_per_gas]. +class EthEstimatedFeePerGas extends Equatable { + const EthEstimatedFeePerGas({ + required this.baseFee, + required this.low, + required this.medium, + required this.high, + required this.source, + required this.units, + this.baseFeeTrend, + this.priorityFeeTrend, + }); + + factory EthEstimatedFeePerGas.fromJson(Map json) { + return EthEstimatedFeePerGas( + baseFee: Decimal.parse(json['base_fee'].toString()), + low: EthFeeLevel.fromJson(json['low'] as Map), + medium: EthFeeLevel.fromJson(json['medium'] as Map), + high: EthFeeLevel.fromJson(json['high'] as Map), + source: json['source'] as String, + baseFeeTrend: json['base_fee_trend'] as String?, + priorityFeeTrend: json['priority_fee_trend'] as String?, + units: json['units'] as String? ?? 'Gwei', + ); + } + + final Decimal baseFee; + final EthFeeLevel low; + final EthFeeLevel medium; + final EthFeeLevel high; + final String source; + final String units; + final String? baseFeeTrend; + final String? priorityFeeTrend; + + Map toJson() => { + 'base_fee': baseFee.toString(), + 'low': low.toJson(), + 'medium': medium.toJson(), + 'high': high.toJson(), + 'source': source, + if (baseFeeTrend != null) 'base_fee_trend': baseFeeTrend, + if (priorityFeeTrend != null) 'priority_fee_trend': priorityFeeTrend, + 'units': units, + }; + + @override + List get props => [ + baseFee, + low, + medium, + high, + source, + units, + baseFeeTrend, + priorityFeeTrend, + ]; +}