From f18adb71fc3bc9f346a5e8b76328d12a1202e5c2 Mon Sep 17 00:00:00 2001 From: ptisserand Date: Sat, 1 Mar 2025 10:35:29 +0100 Subject: [PATCH] fix: waitForAcceptance now chek on finality status (#479) * fix(starknet): waitForAcceptance now chek on finality status (#441) * starknet: use enum instead of string when waiting for transaction --- packages/starknet/lib/src/util.dart | 183 ++++++++++++++++++++++------ 1 file changed, 149 insertions(+), 34 deletions(-) diff --git a/packages/starknet/lib/src/util.dart b/packages/starknet/lib/src/util.dart index 817f1b1d..b768a9aa 100644 --- a/packages/starknet/lib/src/util.dart +++ b/packages/starknet/lib/src/util.dart @@ -15,40 +15,94 @@ const _defaultInterval = Duration(seconds: 5); /// Number of retry to wait for transaction to be declared as NOT_RECEIVED const _defaultMaxRetries = 60; -const _errorStates = ['REVERTED']; -/// Returns `true` when [transactionHash] status is in [states] -/// -/// The [provider] will be query with a period of [interval] -/// This function will try [maxRetries] query before setting transaction status to `NOT_RECEIVED` -/// An optional [debugLog] function could be use to display internal debug log -/// Return `false` in case of error -Future waitForState({ +class _Status { + final _ExecutionStatus execution; + final _FinalityStatus finality; + _Status(this.execution, this.finality); +} + +enum _ExecutionStatus { + pending('PENDING'), + succeeded('SUCCEEDED'), + reverted('REVERTED'), + unknown('UNKNOWN'); + + final String value; + const _ExecutionStatus(this.value); + + // Convert a string to the enum value + static _ExecutionStatus fromString(String value) { + return _ExecutionStatus.values.firstWhere( + (status) => status.value == value, + orElse: () => throw ArgumentError('Invalid status: $value'), + ); + } +} + +enum _FinalityStatus { + acceptedOnL1('ACCEPTED_ON_L1'), + acceptedOnL2('ACCEPTED_ON_L2'), + received('RECEIVED'), + rejected('REJECTED'), + pending('PENDING'), + unknown('UNKNOWN'); + + final String value; + const _FinalityStatus(this.value); + + // Convert a string to the enum value + static _FinalityStatus fromString(String value) { + return _FinalityStatus.values.firstWhere( + (status) => status.value == value, + orElse: () => throw ArgumentError('Invalid status: $value'), + ); + } +} + +Future _waitForTransactionStatus({ required String transactionHash, required Provider provider, - required List states, - Duration interval = _defaultInterval, - int maxRetries = _defaultMaxRetries, + required bool Function(_Status status) checkStatus, + required Duration interval, + required int maxRetries, void Function(dynamic message)? debugLog, }) async { var count = 0; var done = false; var succeed = false; - var status = 'UNKNOWN'; + var _status = _Status(_ExecutionStatus.unknown, _FinalityStatus.unknown); + final txHash = Felt.fromHexString(transactionHash); while (!done) { final receipt = await provider.getTransactionReceipt(txHash); receipt.when( result: (result) { result.map( - declareTxnReceipt: (receipt) => status = receipt.execution_status, - deployTxnReceipt: (receipt) => status = receipt.execution_status, - deployAccountTxnReceipt: (receipt) => - status = receipt.execution_status, - l1HandlerTxnReceipt: (receipt) => status = receipt.execution_status, - pendingDeployTxnReceipt: (receipt) => status = 'PENDING', - pendingCommonReceiptProperties: (receipt) => status = 'PENDING', - invokeTxnReceipt: (receipt) => status = receipt.execution_status, + invokeTxnReceipt: (receipt) => _status = _Status( + _ExecutionStatus.fromString(receipt.execution_status), + _FinalityStatus.fromString(receipt.finality_status), + ), + declareTxnReceipt: (receipt) => _status = _Status( + _ExecutionStatus.fromString(receipt.execution_status), + _FinalityStatus.fromString(receipt.finality_status), + ), + deployTxnReceipt: (receipt) => _status = _Status( + _ExecutionStatus.fromString(receipt.execution_status), + _FinalityStatus.fromString(receipt.finality_status), + ), + deployAccountTxnReceipt: (receipt) => _status = _Status( + _ExecutionStatus.fromString(receipt.execution_status), + _FinalityStatus.fromString(receipt.finality_status), + ), + l1HandlerTxnReceipt: (receipt) => _status = _Status( + _ExecutionStatus.fromString(receipt.execution_status), + _FinalityStatus.fromString(receipt.finality_status), + ), + pendingDeployTxnReceipt: (receipt) => _status = + _Status(_ExecutionStatus.pending, _FinalityStatus.pending), + pendingCommonReceiptProperties: (receipt) => _status = + _Status(_ExecutionStatus.pending, _FinalityStatus.pending), ); }, error: (error) { @@ -67,7 +121,7 @@ Future waitForState({ JsonRpcApiErrorCode.TXN_HASH_NOT_FOUND_PRE_0_4_0) && (count < maxRetries)) { count += 1; - status = 'UNKNOWN'; + _status = _Status(_ExecutionStatus.unknown, _FinalityStatus.unknown); debugLog?.call( 'Waiting for status of $transactionHash ($count / $maxRetries)', ); @@ -77,24 +131,86 @@ Future waitForState({ } }, ); - if (_errorStates.contains(status)) { + if ((_ExecutionStatus.reverted == _status.execution) || + (_FinalityStatus.rejected == _status.finality)) { succeed = false; break; } - if (states.contains(status)) { + if (checkStatus(_status)) { succeed = true; break; - } else { - await Future.delayed(interval); } + await Future.delayed(interval); } return succeed; } -/// Returns `true` when [transactionHash] status is in `{PENDING, ACCEPTED_ON_L2, ACCEPTED_ON_L2}` +Future _waitForExecutionStatus({ + required String transactionHash, + required Provider provider, + required List<_ExecutionStatus> statuses, + required Duration interval, + required int maxRetries, + void Function(dynamic message)? debugLog, +}) async { + return _waitForTransactionStatus( + transactionHash: transactionHash, + provider: provider, + checkStatus: (status) => statuses.contains(status.execution), + interval: interval, + maxRetries: maxRetries, + debugLog: debugLog, + ); +} + +Future _waitForFinalityStatus({ + required String transactionHash, + required Provider provider, + required List<_FinalityStatus> statuses, + required Duration interval, + required int maxRetries, + void Function(dynamic message)? debugLog, +}) async { + return _waitForTransactionStatus( + transactionHash: transactionHash, + provider: provider, + checkStatus: (status) => statuses.contains(status.finality), + interval: interval, + maxRetries: maxRetries, + debugLog: debugLog, + ); +} + +/// Returns `true` when [transactionHash] execution status is in [states] +/// +/// The [provider] will be query with a period of [interval] +/// This function will try [maxRetries] query before setting transaction status to `NOT_RECEIVED` +/// An optional [debugLog] function could be use to display internal debug log +/// Return `false` in case of error +Future waitForState({ + required String transactionHash, + required Provider provider, + required List states, + Duration interval = _defaultInterval, + int maxRetries = _defaultMaxRetries, + void Function(dynamic message)? debugLog, +}) async { + return _waitForExecutionStatus( + transactionHash: transactionHash, + provider: provider, + statuses: states.map(_ExecutionStatus.fromString).toList(), + interval: interval, + maxRetries: maxRetries, + debugLog: debugLog, + ); +} + +/// Returns `true` when [transactionHash] execution status is in `{PENDING, SUCCEEDED}` /// /// /// The [provider] will be query with a period of [interval] +/// This function will try [maxRetries] query before setting transaction status to `NOT_RECEIVED` +/// An optional [debugLog] function could be use to display internal debug log /// Return `false` in case of error Future waitForTransaction({ required String transactionHash, @@ -103,13 +219,10 @@ Future waitForTransaction({ int maxRetries = _defaultMaxRetries, void Function(dynamic message)? debugLog, }) async { - return waitForState( + return _waitForExecutionStatus( transactionHash: transactionHash, provider: provider, - states: [ - 'PENDING', - 'SUCCEEDED', - ], + statuses: [_ExecutionStatus.pending, _ExecutionStatus.succeeded], interval: interval, maxRetries: maxRetries, debugLog: debugLog, @@ -118,10 +231,12 @@ Future waitForTransaction({ /// Returns `true` if [transactionHash] is accepted /// -/// A transaction is **accepted** if its state is +/// A transaction is **accepted** if its finality status is /// `ACCEPTED_ON_L2` or `ACCEPTED_ON_L1` /// /// The [provider] will be query with a period of [interval] +/// This function will try [maxRetries] query before setting transaction status to `NOT_RECEIVED` +/// An optional [debugLog] function could be use to display internal debug log /// Return `false` in case of error Future waitForAcceptance({ required String transactionHash, @@ -130,10 +245,10 @@ Future waitForAcceptance({ int maxRetries = _defaultMaxRetries, void Function(dynamic message)? debugLog, }) async { - return waitForState( + return _waitForFinalityStatus( transactionHash: transactionHash, provider: provider, - states: ['SUCCEEDED'], + statuses: [_FinalityStatus.acceptedOnL1, _FinalityStatus.acceptedOnL2], interval: interval, maxRetries: maxRetries, debugLog: debugLog,