diff --git a/eth/api_backend.go b/eth/api_backend.go index 3ae73e78af..cbcf428d13 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -486,3 +486,11 @@ func (b *EthAPIBackend) StateAtBlock(ctx context.Context, block *types.Block, re func (b *EthAPIBackend) StateAtTransaction(ctx context.Context, block *types.Block, txIndex int, reexec uint64) (*types.Transaction, vm.BlockContext, *state.StateDB, tracers.StateReleaseFunc, error) { return b.eth.stateAtTransaction(ctx, block, txIndex, reexec) } + +func (b *EthAPIBackend) RPCTxSyncDefaultTimeout() time.Duration { + return 2 * time.Second +} + +func (b *EthAPIBackend) RPCTxSyncMaxTimeout() time.Duration { + return 5 * time.Minute +} diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index a2cb28d3b2..8b28bb7092 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -1652,6 +1652,62 @@ func (api *TransactionAPI) SendRawTransaction(ctx context.Context, input hexutil return SubmitTransaction(ctx, api.b, tx) } +// SendRawTransactionSync will add the signed transaction to the transaction pool +// and wait for the transaction to be mined until the timeout (in milliseconds) is reached. +func (api *TransactionAPI) SendRawTransactionSync(ctx context.Context, input hexutil.Bytes, timeoutMs *hexutil.Uint64) (map[string]interface{}, error) { + tx := new(types.Transaction) + if err := tx.UnmarshalBinary(input); err != nil { + return nil, err + } + hash, err := SubmitTransaction(ctx, api.b, tx) + if err != nil { + return nil, err + } + + // compute effective timeout + max := api.b.RPCTxSyncMaxTimeout() + def := api.b.RPCTxSyncDefaultTimeout() + + eff := def + if timeoutMs != nil && *timeoutMs > 0 { + req := time.Duration(*timeoutMs) * time.Millisecond + if req > max { + eff = max + } else { + eff = req // allow shorter than default + } + } + + // Wait for receipt until timeout + receiptCtx, cancel := context.WithTimeout(ctx, eff) + defer cancel() + + ticker := time.NewTicker(time.Millisecond * 100) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + receipt, getErr := api.GetTransactionReceipt(receiptCtx, hash) + // If tx-index still building, keep polling + if getErr != nil && !errors.Is(getErr, NewTxIndexingError()) { + // transient or other error: just keep polling + } + if receipt != nil { + return receipt, nil + } + case <-receiptCtx.Done(): + if err := ctx.Err(); err != nil { + return nil, err // context canceled / deadline exceeded upstream + } + return nil, &txSyncTimeoutError{ + msg: fmt.Sprintf("The transaction was added to the transaction pool but wasn't processed in %v.", eff), + hash: hash, + } + } + } +} + // Sign calculates an ECDSA signature for: // keccak256("\x19Ethereum Signed Message:\n" + len(message) + message). // diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go index f709a1fcdc..af3d592b82 100644 --- a/internal/ethapi/backend.go +++ b/internal/ethapi/backend.go @@ -53,6 +53,8 @@ type Backend interface { RPCEVMTimeout() time.Duration // global timeout for eth_call over rpc: DoS protection RPCTxFeeCap() float64 // global tx fee cap for all transaction related APIs UnprotectedAllowed() bool // allows only for EIP155 transactions. + RPCTxSyncDefaultTimeout() time.Duration + RPCTxSyncMaxTimeout() time.Duration // Blockchain API SetHead(number uint64) diff --git a/internal/ethapi/errors.go b/internal/ethapi/errors.go index 154938fa0e..235f5b3fa8 100644 --- a/internal/ethapi/errors.go +++ b/internal/ethapi/errors.go @@ -21,6 +21,7 @@ import ( "fmt" "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/vm" @@ -33,6 +34,11 @@ type revertError struct { reason string // revert reason hex encoded } +type txSyncTimeoutError struct { + msg string + hash common.Hash +} + // ErrorCode returns the JSON error code for a revert. // See: https://ethereum.org/en/developers/docs/apis/json-rpc/#error-codes func (e *revertError) ErrorCode() int { @@ -108,6 +114,7 @@ const ( errCodeInvalidParams = -32602 errCodeReverted = -32000 errCodeVMError = -32015 + errCodeTxSyncTimeout = 4 ) func txValidationError(err error) *invalidTxError { @@ -168,3 +175,9 @@ type blockGasLimitReachedError struct{ message string } func (e *blockGasLimitReachedError) Error() string { return e.message } func (e *blockGasLimitReachedError) ErrorCode() int { return errCodeBlockGasLimitReached } + +func (e *txSyncTimeoutError) Error() string { return e.msg } +func (e *txSyncTimeoutError) ErrorCode() int { return errCodeTxSyncTimeout } + +// ErrorData should be JSON-safe; return the 0x-hex string. +func (e *txSyncTimeoutError) ErrorData() interface{} { return e.hash.Hex() }