From fe7a5bc917b61ea078b27d10b6eff2f3a2d024ba Mon Sep 17 00:00:00 2001 From: Arnaud Date: Tue, 1 Apr 2025 20:26:42 +0200 Subject: [PATCH 01/16] Add EIP-1559 implementation for gas price --- ethers/contract.nim | 410 ++++++++++++++++++ ethers/provider.nim | 6 + ethers/providers/jsonrpc.nim | 13 +- ethers/signer.nim | 24 +- ethers/transaction.nim | 2 + .../providers/jsonrpc/testJsonRpcSigner.nim | 11 +- 6 files changed, 460 insertions(+), 6 deletions(-) create mode 100644 ethers/contract.nim diff --git a/ethers/contract.nim b/ethers/contract.nim new file mode 100644 index 00000000..affc4f90 --- /dev/null +++ b/ethers/contract.nim @@ -0,0 +1,410 @@ +import pkg/serde +import std/macros +import std/sequtils +import pkg/chronicles +import pkg/chronos +import pkg/questionable +import pkg/contractabi +import ./basics +import ./provider +import ./signer +import ./events +import ./errors +import ./errors/conversion +import ./fields + +export basics +export provider +export events +export errors.SolidityError +export errors.errors + +{.push raises: [].} + +logScope: + topics = "ethers contract" + +type + Contract* = ref object of RootObj + provider: Provider + signer: ?Signer + address: Address + TransactionOverrides* = ref object of RootObj + nonce*: ?UInt256 + chainId*: ?UInt256 + gasPrice*: ?UInt256 + maxFee*: ?UInt256 + maxPriorityFee*: ?UInt256 + maxPriorityFeePerGas*: ?UInt256 + gasLimit*: ?UInt256 + CallOverrides* = ref object of TransactionOverrides + blockTag*: ?BlockTag + Confirmable* = object + response*: ?TransactionResponse + convert*: ConvertCustomErrors + EventHandler*[E: Event] = proc(event: ?!E) {.gcsafe, raises:[].} + +func new*(ContractType: type Contract, + address: Address, + provider: Provider): ContractType = + ContractType(provider: provider, address: address) + +func new*(ContractType: type Contract, + address: Address, + signer: Signer): ContractType {.raises: [SignerError].} = + ContractType(signer: some signer, provider: signer.provider, address: address) + +func connect*[T: Contract](contract: T, provider: Provider | Signer): T {.raises: [SignerError].} = + T.new(contract.address, provider) + +func provider*(contract: Contract): Provider = + contract.provider + +func signer*(contract: Contract): ?Signer = + contract.signer + +func address*(contract: Contract): Address = + contract.address + +template raiseContractError(message: string) = + raise newException(ContractError, message) + +proc createTransaction(contract: Contract, + function: string, + parameters: tuple, + overrides = TransactionOverrides()): Transaction = + let selector = selector(function, typeof parameters).toArray + let data = @selector & AbiEncoder.encode(parameters) + Transaction( + to: contract.address, + data: data, + nonce: overrides.nonce, + chainId: overrides.chainId, + gasPrice: overrides.gasPrice, + maxFee: overrides.maxFee, + maxPriorityFee: overrides.maxPriorityFee, + maxPriorityFeePerGas: overrides.maxPriorityFeePerGas, + gasLimit: overrides.gasLimit, + ) + +proc decodeResponse(T: type, bytes: seq[byte]): T {.raises: [ContractError].} = + without decoded =? AbiDecoder.decode(bytes, T): + raiseContractError "unable to decode return value as " & $T + return decoded + +proc call( + provider: Provider, transaction: Transaction, overrides: TransactionOverrides +): Future[seq[byte]] {.async: (raises: [ProviderError, CancelledError]).} = + if overrides of CallOverrides and blockTag =? CallOverrides(overrides).blockTag: + await provider.call(transaction, blockTag) + else: + await provider.call(transaction) + +proc call( + contract: Contract, + function: string, + parameters: tuple, + overrides = TransactionOverrides(), +) {.async: (raises: [ProviderError, SignerError, CancelledError]).} = + var transaction = createTransaction(contract, function, parameters, overrides) + + if signer =? contract.signer and transaction.sender.isNone: + transaction.sender = some(await signer.getAddress()) + + discard await contract.provider.call(transaction, overrides) + +proc call( + contract: Contract, + function: string, + parameters: tuple, + ReturnType: type, + overrides = TransactionOverrides(), +): Future[ReturnType] {. + async: (raises: [ProviderError, SignerError, ContractError, CancelledError]) +.} = + var transaction = createTransaction(contract, function, parameters, overrides) + + if signer =? contract.signer and transaction.sender.isNone: + transaction.sender = some(await signer.getAddress()) + + let response = await contract.provider.call(transaction, overrides) + return decodeResponse(ReturnType, response) + +proc send( + contract: Contract, + function: string, + parameters: tuple, + overrides = TransactionOverrides() +): Future[?TransactionResponse] {.async: (raises: [SignerError, ProviderError, CancelledError]).} = + + if signer =? contract.signer: + withLock(signer): + let transaction = createTransaction(contract, function, parameters, overrides) + let populated = await signer.populateTransaction(transaction) + trace "sending contract transaction", function, params = $parameters + let txResp = await signer.sendTransaction(populated) + return txResp.some + else: + await call(contract, function, parameters, overrides) + return TransactionResponse.none + +func getParameterTuple(procedure: NimNode): NimNode = + let parameters = procedure[3] + var tupl = newNimNode(nnkTupleConstr, parameters) + for parameter in parameters[2..^1]: + for name in parameter[0..^3]: + tupl.add name + return tupl + +func getErrorTypes(procedure: NimNode): NimNode = + let pragmas = procedure[4] + var tupl = newNimNode(nnkTupleConstr) + for pragma in pragmas: + if pragma.kind == nnkExprColonExpr: + if pragma[0].eqIdent "errors": + pragma[1].expectKind(nnkBracket) + for error in pragma[1]: + tupl.add error + if tupl.len == 0: + quote do: tuple[] + else: + tupl + +func isGetter(procedure: NimNode): bool = + let pragmas = procedure[4] + for pragma in pragmas: + if pragma.eqIdent "getter": + return true + false + +func isConstant(procedure: NimNode): bool = + let pragmas = procedure[4] + for pragma in pragmas: + if pragma.eqIdent "view": + return true + elif pragma.eqIdent "pure": + return true + elif pragma.eqIdent "getter": + return true + false + +func isMultipleReturn(returnType: NimNode): bool = + (returnType.kind == nnkPar and returnType.len > 1) or + (returnType.kind == nnkTupleConstr) or + (returnType.kind == nnkTupleTy) + +func addOverrides(procedure: var NimNode) = + procedure[3].add( + newIdentDefs( + ident("overrides"), + newEmptyNode(), + quote do: TransactionOverrides() + ) + ) + +func addContractCall(procedure: var NimNode) = + let contract = procedure[3][1][0] + let function = $basename(procedure[0]) + let parameters = getParameterTuple(procedure) + let returnType = procedure[3][0] + let isGetter = procedure.isGetter + + procedure.addOverrides() + let errors = getErrorTypes(procedure) + + func call: NimNode = + if returnType.kind == nnkEmpty: + quote: + await call(`contract`, `function`, `parameters`, overrides) + elif returnType.isMultipleReturn or isGetter: + quote: + return await call( + `contract`, `function`, `parameters`, `returnType`, overrides + ) + else: + quote: + # solidity functions return a tuple, so wrap return type in a tuple + let tupl = await call( + `contract`, `function`, `parameters`, (`returnType`,), overrides + ) + return tupl[0] + + func send: NimNode = + if returnType.kind == nnkEmpty: + quote: + discard await send(`contract`, `function`, `parameters`, overrides) + else: + quote: + when typeof(result) isnot Confirmable: + {.error: + "unexpected return type, " & + "missing {.view.}, {.pure.} or {.getter.} ?" + .} + let response = await send(`contract`, `function`, `parameters`, overrides) + let convert = customErrorConversion(`errors`) + Confirmable(response: response, convert: convert) + + procedure[6] = + if procedure.isConstant: + call() + else: + send() + +func addErrorHandling(procedure: var NimNode) = + let body = procedure[6] + let errors = getErrorTypes(procedure) + procedure[6] = quote do: + try: + `body` + except ProviderError as error: + if data =? error.data: + let convert = customErrorConversion(`errors`) + raise convert(error) + else: + raise error + +func addFuture(procedure: var NimNode) = + let returntype = procedure[3][0] + if returntype.kind != nnkEmpty: + procedure[3][0] = quote: Future[`returntype`] + +func addAsyncPragma(procedure: var NimNode) = + let pragmas = procedure[4] + if pragmas.kind == nnkEmpty: + procedure[4] = newNimNode(nnkPragma) + procedure[4].add nnkExprColonExpr.newTree( + newIdentNode("async"), + nnkTupleConstr.newTree( + nnkExprColonExpr.newTree( + newIdentNode("raises"), + nnkBracket.newTree( + newIdentNode("CancelledError"), + newIdentNode("ProviderError"), + newIdentNode("EthersError"), + ), + ) + ), + ) + +macro contract*(procedure: untyped{nkProcDef | nkMethodDef}): untyped = + let parameters = procedure[3] + let body = procedure[6] + + parameters.expectMinLen(2) # at least return type and contract instance + body.expectKind(nnkEmpty) + + var contractcall = copyNimTree(procedure) + contractcall.addContractCall() + contractcall.addErrorHandling() + contractcall.addFuture() + contractcall.addAsyncPragma() + contractcall + +template view* {.pragma.} +template pure* {.pragma.} +template getter* {.pragma.} + +proc subscribe*[E: Event](contract: Contract, + _: type E, + handler: EventHandler[E]): + Future[Subscription] = + + let topic = topic($E, E.fieldTypes).toArray + let filter = EventFilter(address: contract.address, topics: @[topic]) + + proc logHandler(logResult: ?!Log) {.raises: [].} = + without log =? logResult, error: + handler(failure(E, error)) + return + + if event =? E.decode(log.data, log.topics): + handler(success(event)) + + contract.provider.subscribe(filter, logHandler) + +proc confirm(tx: Confirmable, confirmations, timeout: int): + Future[TransactionReceipt] {.async: (raises: [CancelledError, EthersError]).} = + + without response =? tx.response: + raise newException( + EthersError, + "Transaction hash required. Possibly was a call instead of a send?" + ) + + try: + return await response.confirm(confirmations, timeout) + except ProviderError as error: + let convert = tx.convert + raise convert(error) + +proc confirm*(tx: Future[Confirmable], + confirmations: int = EthersDefaultConfirmations, + timeout: int = EthersReceiptTimeoutBlks): + Future[TransactionReceipt] {.async: (raises: [CancelledError, EthersError]).} = + ## Convenience method that allows confirm to be chained to a contract + ## transaction, eg: + ## `await token.connect(signer0) + ## .mint(accounts[1], 100.u256) + ## .confirm(3)` + try: + return await (await tx).confirm(confirmations, timeout) + except CancelledError as e: + raise e + except EthersError as e: + raise e + except CatchableError as e: + raise newException( + EthersError, + "Error when trying to confirm the contract transaction: " & e.msg + ) + +proc queryFilter[E: Event](contract: Contract, + _: type E, + filter: EventFilter): + Future[seq[E]] {.async.} = + + var logs = await contract.provider.getLogs(filter) + logs.keepItIf(not it.removed) + + var events: seq[E] = @[] + for log in logs: + if event =? E.decode(log.data, log.topics): + events.add event + + return events + +proc queryFilter*[E: Event](contract: Contract, + _: type E): + Future[seq[E]] = + + let topic = topic($E, E.fieldTypes).toArray + let filter = EventFilter(address: contract.address, + topics: @[topic]) + + contract.queryFilter(E, filter) + +proc queryFilter*[E: Event](contract: Contract, + _: type E, + blockHash: BlockHash): + Future[seq[E]] = + + let topic = topic($E, E.fieldTypes).toArray + let filter = FilterByBlockHash(address: contract.address, + topics: @[topic], + blockHash: blockHash) + + contract.queryFilter(E, filter) + +proc queryFilter*[E: Event](contract: Contract, + _: type E, + fromBlock: BlockTag, + toBlock: BlockTag): + Future[seq[E]] = + + let topic = topic($E, E.fieldTypes).toArray + let filter = Filter(address: contract.address, + topics: @[topic], + fromBlock: fromBlock, + toBlock: toBlock) + + contract.queryFilter(E, filter) diff --git a/ethers/provider.nim b/ethers/provider.nim index 5a2153ec..c75df60b 100644 --- a/ethers/provider.nim +++ b/ethers/provider.nim @@ -63,6 +63,7 @@ type number*: ?UInt256 timestamp*: UInt256 hash*: ?BlockHash + baseFeePerGas* : ?UInt256 PastTransaction* {.serialize.} = object blockHash*: BlockHash blockNumber*: UInt256 @@ -121,6 +122,11 @@ method getGasPrice*( ): Future[UInt256] {.base, async: (raises: [ProviderError, CancelledError]).} = doAssert false, "not implemented" +method getMaxPriorityFeePerGas*( + provider: Provider +): Future[UInt256] {.base, async: (raises: [ProviderError, CancelledError]).} = + doAssert false, "not implemented" + method getTransactionCount*( provider: Provider, address: Address, blockTag = BlockTag.latest ): Future[UInt256] {.base, async: (raises: [ProviderError, CancelledError]).} = diff --git a/ethers/providers/jsonrpc.nim b/ethers/providers/jsonrpc.nim index 636193ca..f2ce53fb 100644 --- a/ethers/providers/jsonrpc.nim +++ b/ethers/providers/jsonrpc.nim @@ -28,6 +28,7 @@ type JsonRpcProvider* = ref object of Provider client: Future[RpcClient] subscriptions: Future[JsonRpcSubscriptions] + maxPriorityFeePerGas: UInt256 JsonRpcSubscription* = ref object of Subscription subscriptions: JsonRpcSubscriptions @@ -43,6 +44,7 @@ type const defaultUrl = "http://localhost:8545" const defaultPollingInterval = 4.seconds +const defaultMaxPriorityFeePerGas = 1_000_000_000.u256 proc jsonHeaders: seq[(string, string)] = @[("Content-Type", "application/json")] @@ -50,7 +52,8 @@ proc jsonHeaders: seq[(string, string)] = proc new*( _: type JsonRpcProvider, url=defaultUrl, - pollingInterval=defaultPollingInterval): JsonRpcProvider {.raises: [JsonRpcProviderError].} = + pollingInterval=defaultPollingInterval, + maxPriorityFeePerGas=defaultMaxPriorityFeePerGas): JsonRpcProvider {.raises: [JsonRpcProviderError].} = var initialized: Future[void] var client: RpcClient @@ -87,7 +90,7 @@ proc new*( return subscriptions initialized = initialize() - return JsonRpcProvider(client: awaitClient(), subscriptions: awaitSubscriptions()) + return JsonRpcProvider(client: awaitClient(), subscriptions: awaitSubscriptions(), maxPriorityFeePerGas: maxPriorityFeePerGas) proc callImpl( client: RpcClient, call: string, args: JsonNode @@ -151,6 +154,12 @@ method getGasPrice*( let client = await provider.client return await client.eth_gasPrice() +method getMaxPriorityFeePerGas*( + provider: JsonRpcProvider +): Future[UInt256] {.async: (raises: [ProviderError, CancelledError]).} = + convertError: + return provider.maxPriorityFeePerGas + method getTransactionCount*( provider: JsonRpcProvider, address: Address, blockTag = BlockTag.latest ): Future[UInt256] {.async: (raises: [ProviderError, CancelledError]).} = diff --git a/ethers/signer.nim b/ethers/signer.nim index a6aa82f6..dc6f2e5d 100644 --- a/ethers/signer.nim +++ b/ethers/signer.nim @@ -1,4 +1,5 @@ import pkg/questionable +import pkg/chronicles import ./basics import ./errors import ./provider @@ -55,6 +56,11 @@ method getGasPrice*( .} = return await signer.provider.getGasPrice() +method getMaxPriorityFeePerGas*( + signer: Signer +): Future[UInt256] {.async: (raises: [ProviderError, SignerError, CancelledError]).} = + return await signer.provider.getMaxPriorityFeePerGas() + method getTransactionCount*( signer: Signer, blockTag = BlockTag.latest ): Future[UInt256] {. @@ -124,8 +130,22 @@ method populateTransaction*( populated.sender = some(address) if transaction.chainId.isNone: populated.chainId = some(await signer.getChainId()) - if transaction.gasPrice.isNone and (transaction.maxFee.isNone or transaction.maxPriorityFee.isNone): - populated.gasPrice = some(await signer.getGasPrice()) + + let blk = await signer.provider.getBlock(BlockTag.latest) + + if baseFeePerGas =? blk.?baseFeePerGas: + trace "EIP-1559 is supported" + + let maxPriorityFeePerGas = transaction.maxPriorityFeePerGas |? (await signer.provider.getMaxPriorityFeePerGas()) + populated.maxPriorityFeePerGas = some(maxPriorityFeePerGas) + + # Multiply by 2 because during times of congestion, it can increase by 12.5% per block. + # https://github.com/ethers-io/ethers.js/discussions/3601#discussioncomment-4461273 + let maxFeePerGas = baseFeePerGas * 2 + maxPriorityFeePerGas + populated.maxFeePerGas = some(maxFeePerGas) + else: + trace "EIP-1559 is not supported" + populated.gasPrice = some(transaction.gasPrice |? (await signer.getGasPrice())) if transaction.nonce.isNone and transaction.gasLimit.isNone: # when both nonce and gasLimit are not populated, we must ensure getNonce is diff --git a/ethers/transaction.nim b/ethers/transaction.nim index 1ff2e1b2..6bd47f50 100644 --- a/ethers/transaction.nim +++ b/ethers/transaction.nim @@ -17,6 +17,8 @@ type gasPrice*: ?UInt256 maxFee*: ?UInt256 maxPriorityFee*: ?UInt256 + maxPriorityFeePerGas*: ?UInt256 + maxFeePerGas*: ?UInt256 gasLimit*: ?UInt256 transactionType* {.serialize("type").}: ?TransactionType diff --git a/testmodule/providers/jsonrpc/testJsonRpcSigner.nim b/testmodule/providers/jsonrpc/testJsonRpcSigner.nim index 64d05275..31d6df54 100644 --- a/testmodule/providers/jsonrpc/testJsonRpcSigner.nim +++ b/testmodule/providers/jsonrpc/testJsonRpcSigner.nim @@ -55,20 +55,27 @@ suite "JsonRpcSigner": let transaction = Transaction.example let populated = await signer.populateTransaction(transaction) check !populated.sender == await signer.getAddress() - check !populated.gasPrice == await signer.getGasPrice() check !populated.nonce == await signer.getTransactionCount(BlockTag.pending) check !populated.gasLimit == await signer.estimateGas(transaction) check !populated.chainId == await signer.getChainId() + let blk = !(await signer.provider.getBlock(BlockTag.latest)) + check !populated.maxPriorityFeePerGas == await signer.getMaxPriorityFeePerGas() + check !populated.maxFeePerGas == !blk.baseFeePerGas * 2.u256 + !populated.maxPriorityFeePerGas + test "populate does not overwrite existing fields": let signer = provider.getSigner() var transaction = Transaction.example transaction.sender = some await signer.getAddress() transaction.nonce = some UInt256.example transaction.chainId = some await signer.getChainId() - transaction.gasPrice = some UInt256.example + transaction.maxPriorityFeePerGas = some UInt256.example transaction.gasLimit = some UInt256.example let populated = await signer.populateTransaction(transaction) + + let blk = !(await signer.provider.getBlock(BlockTag.latest)) + transaction.maxFeePerGas = some(!blk.baseFeePerGas * 2.u256 + !populated.maxPriorityFeePerGas) + check populated == transaction test "populate fails when sender does not match signer address": From eb1cb8b7ee9f901acc7880b5984681daa2d60226 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Wed, 2 Apr 2025 09:18:26 +0200 Subject: [PATCH 02/16] Improve logs --- ethers/signer.nim | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ethers/signer.nim b/ethers/signer.nim index dc6f2e5d..35235998 100644 --- a/ethers/signer.nim +++ b/ethers/signer.nim @@ -134,8 +134,6 @@ method populateTransaction*( let blk = await signer.provider.getBlock(BlockTag.latest) if baseFeePerGas =? blk.?baseFeePerGas: - trace "EIP-1559 is supported" - let maxPriorityFeePerGas = transaction.maxPriorityFeePerGas |? (await signer.provider.getMaxPriorityFeePerGas()) populated.maxPriorityFeePerGas = some(maxPriorityFeePerGas) @@ -143,9 +141,11 @@ method populateTransaction*( # https://github.com/ethers-io/ethers.js/discussions/3601#discussioncomment-4461273 let maxFeePerGas = baseFeePerGas * 2 + maxPriorityFeePerGas populated.maxFeePerGas = some(maxFeePerGas) + + trace "EIP-1559 is supported", maxPriorityFeePerGas = maxPriorityFeePerGas, maxFeePerGas = maxFeePerGas else: - trace "EIP-1559 is not supported" populated.gasPrice = some(transaction.gasPrice |? (await signer.getGasPrice())) + trace "EIP-1559 is not supported", gasPrice = populated.gasPrice if transaction.nonce.isNone and transaction.gasLimit.isNone: # when both nonce and gasLimit are not populated, we must ensure getNonce is From f71f5bd11b05e72a5d9df87a9cafe4b9a218beab Mon Sep 17 00:00:00 2001 From: Arnaud Date: Wed, 2 Apr 2025 09:20:52 +0200 Subject: [PATCH 03/16] Improve comment --- ethers/signer.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ethers/signer.nim b/ethers/signer.nim index 35235998..e27aee65 100644 --- a/ethers/signer.nim +++ b/ethers/signer.nim @@ -137,7 +137,7 @@ method populateTransaction*( let maxPriorityFeePerGas = transaction.maxPriorityFeePerGas |? (await signer.provider.getMaxPriorityFeePerGas()) populated.maxPriorityFeePerGas = some(maxPriorityFeePerGas) - # Multiply by 2 because during times of congestion, it can increase by 12.5% per block. + # Multiply by 2 because during times of congestion, baseFeePerGas can increase by 12.5% per block. # https://github.com/ethers-io/ethers.js/discussions/3601#discussioncomment-4461273 let maxFeePerGas = baseFeePerGas * 2 + maxPriorityFeePerGas populated.maxFeePerGas = some(maxFeePerGas) From 8750c66a44b05ebb4182bbb41af4ebd5b753f80a Mon Sep 17 00:00:00 2001 From: Arnaud Date: Wed, 2 Apr 2025 09:43:16 +0200 Subject: [PATCH 04/16] Rename maxFee and maxPriorityFee to use official EIP-1559 names --- ethers/contract.nim | 6 ++---- ethers/signers/wallet/signing.nim | 8 ++++---- ethers/transaction.nim | 2 -- testmodule/testContracts.nim | 6 +++--- testmodule/testWallet.nim | 10 +++++----- 5 files changed, 14 insertions(+), 18 deletions(-) diff --git a/ethers/contract.nim b/ethers/contract.nim index affc4f90..f55b00e8 100644 --- a/ethers/contract.nim +++ b/ethers/contract.nim @@ -33,9 +33,8 @@ type nonce*: ?UInt256 chainId*: ?UInt256 gasPrice*: ?UInt256 - maxFee*: ?UInt256 - maxPriorityFee*: ?UInt256 maxPriorityFeePerGas*: ?UInt256 + maxFeePerGas*: ?UInt256 gasLimit*: ?UInt256 CallOverrides* = ref object of TransactionOverrides blockTag*: ?BlockTag @@ -81,8 +80,7 @@ proc createTransaction(contract: Contract, nonce: overrides.nonce, chainId: overrides.chainId, gasPrice: overrides.gasPrice, - maxFee: overrides.maxFee, - maxPriorityFee: overrides.maxPriorityFee, + maxFeePerGas: overrides.maxFeePerGas, maxPriorityFeePerGas: overrides.maxPriorityFeePerGas, gasLimit: overrides.gasLimit, ) diff --git a/ethers/signers/wallet/signing.nim b/ethers/signers/wallet/signing.nim index 18b73f34..7a05146a 100644 --- a/ethers/signers/wallet/signing.nim +++ b/ethers/signers/wallet/signing.nim @@ -32,11 +32,11 @@ func toSignableTransaction(transaction: Transaction): SignableTransaction = signable.value = transaction.value signable.payload = transaction.data - if maxFee =? transaction.maxFee and - maxPriorityFee =? transaction.maxPriorityFee: + if maxFeePerGas =? transaction.maxFeePerGas and + maxPriorityFeePerGas =? transaction.maxPriorityFeePerGas: signable.txType = TxEip1559 - signable.maxFeePerGas = GasInt(maxFee.truncate(uint64)) - signable.maxPriorityFeePerGas = GasInt(maxPriorityFee.truncate(uint64)) + signable.maxFeePerGas = GasInt(maxFeePerGas.truncate(uint64)) + signable.maxPriorityFeePerGas = GasInt(maxPriorityFeePerGas.truncate(uint64)) elif gasPrice =? transaction.gasPrice: signable.txType = TxLegacy signable.gasPrice = GasInt(gasPrice.truncate(uint64)) diff --git a/ethers/transaction.nim b/ethers/transaction.nim index 6bd47f50..3bf454f6 100644 --- a/ethers/transaction.nim +++ b/ethers/transaction.nim @@ -15,8 +15,6 @@ type nonce*: ?UInt256 chainId*: ?UInt256 gasPrice*: ?UInt256 - maxFee*: ?UInt256 - maxPriorityFee*: ?UInt256 maxPriorityFeePerGas*: ?UInt256 maxFeePerGas*: ?UInt256 gasLimit*: ?UInt256 diff --git a/testmodule/testContracts.nim b/testmodule/testContracts.nim index 023eb69f..f1e70d14 100644 --- a/testmodule/testContracts.nim +++ b/testmodule/testContracts.nim @@ -107,17 +107,17 @@ for url in ["ws://" & providerUrl, "http://" & providerUrl]: check (await token.connect(provider).balanceOf(accounts[1])) == 25.u256 check (await token.connect(provider).balanceOf(accounts[2])) == 25.u256 - test "takes custom values for nonce, gasprice and gaslimit": + test "takes custom values for nonce, gasprice and maxPriorityFeePerGas": let overrides = TransactionOverrides( nonce: some 100.u256, - gasPrice: some 200.u256, + maxPriorityFeePerGas: some 200.u256, gasLimit: some 300.u256 ) let signer = MockSigner.new(provider) discard await token.connect(signer).mint(accounts[0], 42.u256, overrides) check signer.transactions.len == 1 check signer.transactions[0].nonce == overrides.nonce - check signer.transactions[0].gasPrice == overrides.gasPrice + check signer.transactions[0].maxPriorityFeePerGas == overrides.maxPriorityFeePerGas check signer.transactions[0].gasLimit == overrides.gasLimit test "can call functions for different block heights": diff --git a/testmodule/testWallet.nim b/testmodule/testWallet.nim index 6f1e1053..39206ee9 100644 --- a/testmodule/testWallet.nim +++ b/testmodule/testWallet.nim @@ -80,8 +80,8 @@ suite "Wallet": to: wallet.address, nonce: some 0.u256, chainId: some 31337.u256, - maxFee: some 2_000_000_000.u256, - maxPriorityFee: some 1_000_000_000.u256, + maxFeePerGas: some 2_000_000_000.u256, + maxPriorityFeePerGas: some 1_000_000_000.u256, gasLimit: some 21_000.u256 ) let signedTx = await wallet.signTransaction(tx) @@ -105,7 +105,7 @@ suite "Wallet": let wallet = !Wallet.new(pk_with_funds, provider) let overrides = TransactionOverrides( nonce: some 0.u256, - gasPrice: some 1_000_000_000.u256, + maxPriorityFeePerGas: some 1_000_000_000.u256, gasLimit: some 22_000.u256) let testToken = Erc20.new(wallet.address, wallet) await testToken.transfer(wallet.address, 24.u256, overrides) @@ -115,8 +115,8 @@ suite "Wallet": let wallet = !Wallet.new(pk_with_funds, provider) let overrides = TransactionOverrides( nonce: some 0.u256, - maxFee: some 1_000_000_000.u256, - maxPriorityFee: some 1_000_000_000.u256, + maxFeePerGas: some 1_000_000_000.u256, + maxPriorityFeePerGas: some 1_000_000_000.u256, gasLimit: some 22_000.u256) let testToken = Erc20.new(wallet.address, wallet) await testToken.transfer(wallet.address, 24.u256, overrides) From 2510bec4251d310cb612ebc0cf67330566948f15 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Wed, 2 Apr 2025 09:43:34 +0200 Subject: [PATCH 05/16] Delete gas price when using EIP-1559 --- ethers/signer.nim | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ethers/signer.nim b/ethers/signer.nim index e27aee65..ac9cafe4 100644 --- a/ethers/signer.nim +++ b/ethers/signer.nim @@ -142,6 +142,8 @@ method populateTransaction*( let maxFeePerGas = baseFeePerGas * 2 + maxPriorityFeePerGas populated.maxFeePerGas = some(maxFeePerGas) + populated.gasPrice = none(UInt256) + trace "EIP-1559 is supported", maxPriorityFeePerGas = maxPriorityFeePerGas, maxFeePerGas = maxFeePerGas else: populated.gasPrice = some(transaction.gasPrice |? (await signer.getGasPrice())) From 23de34af237e06ab2ebc5917513edd48a3af2b6f Mon Sep 17 00:00:00 2001 From: Arnaud Date: Wed, 2 Apr 2025 09:45:15 +0200 Subject: [PATCH 06/16] Allow override maxFeePerGas --- ethers/signer.nim | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ethers/signer.nim b/ethers/signer.nim index ac9cafe4..5b8f5d32 100644 --- a/ethers/signer.nim +++ b/ethers/signer.nim @@ -139,12 +139,11 @@ method populateTransaction*( # Multiply by 2 because during times of congestion, baseFeePerGas can increase by 12.5% per block. # https://github.com/ethers-io/ethers.js/discussions/3601#discussioncomment-4461273 - let maxFeePerGas = baseFeePerGas * 2 + maxPriorityFeePerGas - populated.maxFeePerGas = some(maxFeePerGas) + populated.maxFeePerGas = some(transaction.maxFeePerGas |? baseFeePerGas * 2 + maxPriorityFeePerGas) populated.gasPrice = none(UInt256) - trace "EIP-1559 is supported", maxPriorityFeePerGas = maxPriorityFeePerGas, maxFeePerGas = maxFeePerGas + trace "EIP-1559 is supported", maxPriorityFeePerGas = maxPriorityFeePerGas, maxFeePerGas = populated.maxFeePerGas else: populated.gasPrice = some(transaction.gasPrice |? (await signer.getGasPrice())) trace "EIP-1559 is not supported", gasPrice = populated.gasPrice From 92f1932acbb2d02abcf0aee14d24a232bad83ca0 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Wed, 2 Apr 2025 09:46:10 +0200 Subject: [PATCH 07/16] Code style --- ethers/signer.nim | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ethers/signer.nim b/ethers/signer.nim index 5b8f5d32..d3b6b040 100644 --- a/ethers/signer.nim +++ b/ethers/signer.nim @@ -139,11 +139,12 @@ method populateTransaction*( # Multiply by 2 because during times of congestion, baseFeePerGas can increase by 12.5% per block. # https://github.com/ethers-io/ethers.js/discussions/3601#discussioncomment-4461273 - populated.maxFeePerGas = some(transaction.maxFeePerGas |? baseFeePerGas * 2 + maxPriorityFeePerGas) + let maxFeePerGas = transaction.maxFeePerGas |? baseFeePerGas * 2 + maxPriorityFeePerGas + populated.maxFeePerGas = some(maxFeePerGas) populated.gasPrice = none(UInt256) - trace "EIP-1559 is supported", maxPriorityFeePerGas = maxPriorityFeePerGas, maxFeePerGas = populated.maxFeePerGas + trace "EIP-1559 is supported", maxPriorityFeePerGas = maxPriorityFeePerGas, maxFeePerGas = maxFeePerGas else: populated.gasPrice = some(transaction.gasPrice |? (await signer.getGasPrice())) trace "EIP-1559 is not supported", gasPrice = populated.gasPrice From 09d3fff5023297e2f4590d97ecaeaa64024917d2 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Wed, 2 Apr 2025 09:49:21 +0200 Subject: [PATCH 08/16] Remove useless specific EIP1559 test because Hardhart support it so all transactions are using EIP1559 by default --- testmodule/testWallet.nim | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/testmodule/testWallet.nim b/testmodule/testWallet.nim index 39206ee9..67307038 100644 --- a/testmodule/testWallet.nim +++ b/testmodule/testWallet.nim @@ -101,16 +101,6 @@ suite "Wallet": check txHash.hash != TransactionHash.default test "Can call state-changing function automatically": - #TODO add actual token contract, not random address. Should work regardless - let wallet = !Wallet.new(pk_with_funds, provider) - let overrides = TransactionOverrides( - nonce: some 0.u256, - maxPriorityFeePerGas: some 1_000_000_000.u256, - gasLimit: some 22_000.u256) - let testToken = Erc20.new(wallet.address, wallet) - await testToken.transfer(wallet.address, 24.u256, overrides) - - test "Can call state-changing function automatically EIP1559": #TODO add actual token contract, not random address. Should work regardless let wallet = !Wallet.new(pk_with_funds, provider) let overrides = TransactionOverrides( From d4fcb373cf85d788023290a0d6be090135fa7d12 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Wed, 2 Apr 2025 09:58:23 +0200 Subject: [PATCH 09/16] Restore test to check legacy transaction --- testmodule/testWallet.nim | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/testmodule/testWallet.nim b/testmodule/testWallet.nim index 67307038..21b5018b 100644 --- a/testmodule/testWallet.nim +++ b/testmodule/testWallet.nim @@ -101,6 +101,16 @@ suite "Wallet": check txHash.hash != TransactionHash.default test "Can call state-changing function automatically": + #TODO add actual token contract, not random address. Should work regardless + let wallet = !Wallet.new(pk_with_funds, provider) + let overrides = TransactionOverrides( + nonce: some 0.u256, + gasPrice: some 1_000_000_000.u256, + gasLimit: some 22_000.u256) + let testToken = Erc20.new(wallet.address, wallet) + await testToken.transfer(wallet.address, 24.u256, overrides) + + test "Can call state-changing function automatically EIP1559": #TODO add actual token contract, not random address. Should work regardless let wallet = !Wallet.new(pk_with_funds, provider) let overrides = TransactionOverrides( From d8205e369de836f3ac58b0a66f076ee340136da1 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Mon, 19 May 2025 10:57:55 +0200 Subject: [PATCH 10/16] Update after rebase --- ethers/contract.nim | 408 ------------------------------ ethers/contracts/overrides.nim | 4 +- ethers/contracts/transactions.nim | 4 +- 3 files changed, 4 insertions(+), 412 deletions(-) delete mode 100644 ethers/contract.nim diff --git a/ethers/contract.nim b/ethers/contract.nim deleted file mode 100644 index f55b00e8..00000000 --- a/ethers/contract.nim +++ /dev/null @@ -1,408 +0,0 @@ -import pkg/serde -import std/macros -import std/sequtils -import pkg/chronicles -import pkg/chronos -import pkg/questionable -import pkg/contractabi -import ./basics -import ./provider -import ./signer -import ./events -import ./errors -import ./errors/conversion -import ./fields - -export basics -export provider -export events -export errors.SolidityError -export errors.errors - -{.push raises: [].} - -logScope: - topics = "ethers contract" - -type - Contract* = ref object of RootObj - provider: Provider - signer: ?Signer - address: Address - TransactionOverrides* = ref object of RootObj - nonce*: ?UInt256 - chainId*: ?UInt256 - gasPrice*: ?UInt256 - maxPriorityFeePerGas*: ?UInt256 - maxFeePerGas*: ?UInt256 - gasLimit*: ?UInt256 - CallOverrides* = ref object of TransactionOverrides - blockTag*: ?BlockTag - Confirmable* = object - response*: ?TransactionResponse - convert*: ConvertCustomErrors - EventHandler*[E: Event] = proc(event: ?!E) {.gcsafe, raises:[].} - -func new*(ContractType: type Contract, - address: Address, - provider: Provider): ContractType = - ContractType(provider: provider, address: address) - -func new*(ContractType: type Contract, - address: Address, - signer: Signer): ContractType {.raises: [SignerError].} = - ContractType(signer: some signer, provider: signer.provider, address: address) - -func connect*[T: Contract](contract: T, provider: Provider | Signer): T {.raises: [SignerError].} = - T.new(contract.address, provider) - -func provider*(contract: Contract): Provider = - contract.provider - -func signer*(contract: Contract): ?Signer = - contract.signer - -func address*(contract: Contract): Address = - contract.address - -template raiseContractError(message: string) = - raise newException(ContractError, message) - -proc createTransaction(contract: Contract, - function: string, - parameters: tuple, - overrides = TransactionOverrides()): Transaction = - let selector = selector(function, typeof parameters).toArray - let data = @selector & AbiEncoder.encode(parameters) - Transaction( - to: contract.address, - data: data, - nonce: overrides.nonce, - chainId: overrides.chainId, - gasPrice: overrides.gasPrice, - maxFeePerGas: overrides.maxFeePerGas, - maxPriorityFeePerGas: overrides.maxPriorityFeePerGas, - gasLimit: overrides.gasLimit, - ) - -proc decodeResponse(T: type, bytes: seq[byte]): T {.raises: [ContractError].} = - without decoded =? AbiDecoder.decode(bytes, T): - raiseContractError "unable to decode return value as " & $T - return decoded - -proc call( - provider: Provider, transaction: Transaction, overrides: TransactionOverrides -): Future[seq[byte]] {.async: (raises: [ProviderError, CancelledError]).} = - if overrides of CallOverrides and blockTag =? CallOverrides(overrides).blockTag: - await provider.call(transaction, blockTag) - else: - await provider.call(transaction) - -proc call( - contract: Contract, - function: string, - parameters: tuple, - overrides = TransactionOverrides(), -) {.async: (raises: [ProviderError, SignerError, CancelledError]).} = - var transaction = createTransaction(contract, function, parameters, overrides) - - if signer =? contract.signer and transaction.sender.isNone: - transaction.sender = some(await signer.getAddress()) - - discard await contract.provider.call(transaction, overrides) - -proc call( - contract: Contract, - function: string, - parameters: tuple, - ReturnType: type, - overrides = TransactionOverrides(), -): Future[ReturnType] {. - async: (raises: [ProviderError, SignerError, ContractError, CancelledError]) -.} = - var transaction = createTransaction(contract, function, parameters, overrides) - - if signer =? contract.signer and transaction.sender.isNone: - transaction.sender = some(await signer.getAddress()) - - let response = await contract.provider.call(transaction, overrides) - return decodeResponse(ReturnType, response) - -proc send( - contract: Contract, - function: string, - parameters: tuple, - overrides = TransactionOverrides() -): Future[?TransactionResponse] {.async: (raises: [SignerError, ProviderError, CancelledError]).} = - - if signer =? contract.signer: - withLock(signer): - let transaction = createTransaction(contract, function, parameters, overrides) - let populated = await signer.populateTransaction(transaction) - trace "sending contract transaction", function, params = $parameters - let txResp = await signer.sendTransaction(populated) - return txResp.some - else: - await call(contract, function, parameters, overrides) - return TransactionResponse.none - -func getParameterTuple(procedure: NimNode): NimNode = - let parameters = procedure[3] - var tupl = newNimNode(nnkTupleConstr, parameters) - for parameter in parameters[2..^1]: - for name in parameter[0..^3]: - tupl.add name - return tupl - -func getErrorTypes(procedure: NimNode): NimNode = - let pragmas = procedure[4] - var tupl = newNimNode(nnkTupleConstr) - for pragma in pragmas: - if pragma.kind == nnkExprColonExpr: - if pragma[0].eqIdent "errors": - pragma[1].expectKind(nnkBracket) - for error in pragma[1]: - tupl.add error - if tupl.len == 0: - quote do: tuple[] - else: - tupl - -func isGetter(procedure: NimNode): bool = - let pragmas = procedure[4] - for pragma in pragmas: - if pragma.eqIdent "getter": - return true - false - -func isConstant(procedure: NimNode): bool = - let pragmas = procedure[4] - for pragma in pragmas: - if pragma.eqIdent "view": - return true - elif pragma.eqIdent "pure": - return true - elif pragma.eqIdent "getter": - return true - false - -func isMultipleReturn(returnType: NimNode): bool = - (returnType.kind == nnkPar and returnType.len > 1) or - (returnType.kind == nnkTupleConstr) or - (returnType.kind == nnkTupleTy) - -func addOverrides(procedure: var NimNode) = - procedure[3].add( - newIdentDefs( - ident("overrides"), - newEmptyNode(), - quote do: TransactionOverrides() - ) - ) - -func addContractCall(procedure: var NimNode) = - let contract = procedure[3][1][0] - let function = $basename(procedure[0]) - let parameters = getParameterTuple(procedure) - let returnType = procedure[3][0] - let isGetter = procedure.isGetter - - procedure.addOverrides() - let errors = getErrorTypes(procedure) - - func call: NimNode = - if returnType.kind == nnkEmpty: - quote: - await call(`contract`, `function`, `parameters`, overrides) - elif returnType.isMultipleReturn or isGetter: - quote: - return await call( - `contract`, `function`, `parameters`, `returnType`, overrides - ) - else: - quote: - # solidity functions return a tuple, so wrap return type in a tuple - let tupl = await call( - `contract`, `function`, `parameters`, (`returnType`,), overrides - ) - return tupl[0] - - func send: NimNode = - if returnType.kind == nnkEmpty: - quote: - discard await send(`contract`, `function`, `parameters`, overrides) - else: - quote: - when typeof(result) isnot Confirmable: - {.error: - "unexpected return type, " & - "missing {.view.}, {.pure.} or {.getter.} ?" - .} - let response = await send(`contract`, `function`, `parameters`, overrides) - let convert = customErrorConversion(`errors`) - Confirmable(response: response, convert: convert) - - procedure[6] = - if procedure.isConstant: - call() - else: - send() - -func addErrorHandling(procedure: var NimNode) = - let body = procedure[6] - let errors = getErrorTypes(procedure) - procedure[6] = quote do: - try: - `body` - except ProviderError as error: - if data =? error.data: - let convert = customErrorConversion(`errors`) - raise convert(error) - else: - raise error - -func addFuture(procedure: var NimNode) = - let returntype = procedure[3][0] - if returntype.kind != nnkEmpty: - procedure[3][0] = quote: Future[`returntype`] - -func addAsyncPragma(procedure: var NimNode) = - let pragmas = procedure[4] - if pragmas.kind == nnkEmpty: - procedure[4] = newNimNode(nnkPragma) - procedure[4].add nnkExprColonExpr.newTree( - newIdentNode("async"), - nnkTupleConstr.newTree( - nnkExprColonExpr.newTree( - newIdentNode("raises"), - nnkBracket.newTree( - newIdentNode("CancelledError"), - newIdentNode("ProviderError"), - newIdentNode("EthersError"), - ), - ) - ), - ) - -macro contract*(procedure: untyped{nkProcDef | nkMethodDef}): untyped = - let parameters = procedure[3] - let body = procedure[6] - - parameters.expectMinLen(2) # at least return type and contract instance - body.expectKind(nnkEmpty) - - var contractcall = copyNimTree(procedure) - contractcall.addContractCall() - contractcall.addErrorHandling() - contractcall.addFuture() - contractcall.addAsyncPragma() - contractcall - -template view* {.pragma.} -template pure* {.pragma.} -template getter* {.pragma.} - -proc subscribe*[E: Event](contract: Contract, - _: type E, - handler: EventHandler[E]): - Future[Subscription] = - - let topic = topic($E, E.fieldTypes).toArray - let filter = EventFilter(address: contract.address, topics: @[topic]) - - proc logHandler(logResult: ?!Log) {.raises: [].} = - without log =? logResult, error: - handler(failure(E, error)) - return - - if event =? E.decode(log.data, log.topics): - handler(success(event)) - - contract.provider.subscribe(filter, logHandler) - -proc confirm(tx: Confirmable, confirmations, timeout: int): - Future[TransactionReceipt] {.async: (raises: [CancelledError, EthersError]).} = - - without response =? tx.response: - raise newException( - EthersError, - "Transaction hash required. Possibly was a call instead of a send?" - ) - - try: - return await response.confirm(confirmations, timeout) - except ProviderError as error: - let convert = tx.convert - raise convert(error) - -proc confirm*(tx: Future[Confirmable], - confirmations: int = EthersDefaultConfirmations, - timeout: int = EthersReceiptTimeoutBlks): - Future[TransactionReceipt] {.async: (raises: [CancelledError, EthersError]).} = - ## Convenience method that allows confirm to be chained to a contract - ## transaction, eg: - ## `await token.connect(signer0) - ## .mint(accounts[1], 100.u256) - ## .confirm(3)` - try: - return await (await tx).confirm(confirmations, timeout) - except CancelledError as e: - raise e - except EthersError as e: - raise e - except CatchableError as e: - raise newException( - EthersError, - "Error when trying to confirm the contract transaction: " & e.msg - ) - -proc queryFilter[E: Event](contract: Contract, - _: type E, - filter: EventFilter): - Future[seq[E]] {.async.} = - - var logs = await contract.provider.getLogs(filter) - logs.keepItIf(not it.removed) - - var events: seq[E] = @[] - for log in logs: - if event =? E.decode(log.data, log.topics): - events.add event - - return events - -proc queryFilter*[E: Event](contract: Contract, - _: type E): - Future[seq[E]] = - - let topic = topic($E, E.fieldTypes).toArray - let filter = EventFilter(address: contract.address, - topics: @[topic]) - - contract.queryFilter(E, filter) - -proc queryFilter*[E: Event](contract: Contract, - _: type E, - blockHash: BlockHash): - Future[seq[E]] = - - let topic = topic($E, E.fieldTypes).toArray - let filter = FilterByBlockHash(address: contract.address, - topics: @[topic], - blockHash: blockHash) - - contract.queryFilter(E, filter) - -proc queryFilter*[E: Event](contract: Contract, - _: type E, - fromBlock: BlockTag, - toBlock: BlockTag): - Future[seq[E]] = - - let topic = topic($E, E.fieldTypes).toArray - let filter = Filter(address: contract.address, - topics: @[topic], - fromBlock: fromBlock, - toBlock: toBlock) - - contract.queryFilter(E, filter) diff --git a/ethers/contracts/overrides.nim b/ethers/contracts/overrides.nim index 6939706d..241464de 100644 --- a/ethers/contracts/overrides.nim +++ b/ethers/contracts/overrides.nim @@ -6,8 +6,8 @@ type nonce*: ?UInt256 chainId*: ?UInt256 gasPrice*: ?UInt256 - maxFee*: ?UInt256 - maxPriorityFee*: ?UInt256 + maxFeePerGas*: ?UInt256 + maxPriorityFeePerGas*: ?UInt256 gasLimit*: ?UInt256 CallOverrides* = ref object of TransactionOverrides blockTag*: ?BlockTag diff --git a/ethers/contracts/transactions.nim b/ethers/contracts/transactions.nim index c538973e..814c5336 100644 --- a/ethers/contracts/transactions.nim +++ b/ethers/contracts/transactions.nim @@ -22,8 +22,8 @@ proc createTransaction*(call: ContractCall): Transaction = nonce: call.overrides.nonce, chainId: call.overrides.chainId, gasPrice: call.overrides.gasPrice, - maxFee: call.overrides.maxFee, - maxPriorityFee: call.overrides.maxPriorityFee, + maxFeePerGas: call.overrides.maxFeePerGas, + maxPriorityFeePerGas: call.overrides.maxPriorityFeePerGas, gasLimit: call.overrides.gasLimit, ) From 228d18efccca9211dab5d0568e23726fc66b02f4 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Mon, 19 May 2025 11:32:56 +0200 Subject: [PATCH 11/16] Call eth_maxPriorityFeePerGas and returns a manual defined maxPriorityFeePerGas as a fallback --- ethers/provider.nim | 2 +- ethers/providers/jsonrpc.nim | 12 +++++++++--- ethers/providers/jsonrpc/signatures.nim | 1 + ethers/signer.nim | 2 +- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/ethers/provider.nim b/ethers/provider.nim index c75df60b..55cc07c4 100644 --- a/ethers/provider.nim +++ b/ethers/provider.nim @@ -124,7 +124,7 @@ method getGasPrice*( method getMaxPriorityFeePerGas*( provider: Provider -): Future[UInt256] {.base, async: (raises: [ProviderError, CancelledError]).} = +): Future[UInt256] {.base, async: (raises: [CancelledError]).} = doAssert false, "not implemented" method getTransactionCount*( diff --git a/ethers/providers/jsonrpc.nim b/ethers/providers/jsonrpc.nim index f2ce53fb..f2abe850 100644 --- a/ethers/providers/jsonrpc.nim +++ b/ethers/providers/jsonrpc.nim @@ -156,9 +156,15 @@ method getGasPrice*( method getMaxPriorityFeePerGas*( provider: JsonRpcProvider -): Future[UInt256] {.async: (raises: [ProviderError, CancelledError]).} = - convertError: - return provider.maxPriorityFeePerGas +): Future[UInt256] {.async: (raises: [CancelledError]).} = + try: + convertError: + let client = await provider.client + return await client.eth_maxPriorityFeePerGas() + except ProviderError: + # If the provider does not provide the implementation + # let's just remove the manual value + return provider.maxPriorityFeePerGas method getTransactionCount*( provider: JsonRpcProvider, address: Address, blockTag = BlockTag.latest diff --git a/ethers/providers/jsonrpc/signatures.nim b/ethers/providers/jsonrpc/signatures.nim index 60aded45..9e78bc16 100644 --- a/ethers/providers/jsonrpc/signatures.nim +++ b/ethers/providers/jsonrpc/signatures.nim @@ -21,3 +21,4 @@ proc eth_newBlockFilter(): JsonNode proc eth_newFilter(filter: EventFilter): JsonNode proc eth_getFilterChanges(id: JsonNode): JsonNode proc eth_uninstallFilter(id: JsonNode): bool +proc eth_maxPriorityFeePerGas(): UInt256 diff --git a/ethers/signer.nim b/ethers/signer.nim index d3b6b040..9646a5c7 100644 --- a/ethers/signer.nim +++ b/ethers/signer.nim @@ -58,7 +58,7 @@ method getGasPrice*( method getMaxPriorityFeePerGas*( signer: Signer -): Future[UInt256] {.async: (raises: [ProviderError, SignerError, CancelledError]).} = +): Future[UInt256] {.async: (raises: [SignerError, CancelledError]).} = return await signer.provider.getMaxPriorityFeePerGas() method getTransactionCount*( From 2336db0ffb1d0e918d17f95d2eb326c96efc594f Mon Sep 17 00:00:00 2001 From: Arnaud Date: Tue, 20 May 2025 15:51:20 +0200 Subject: [PATCH 12/16] Catch JsonRpcProviderError instead of ProviderError --- ethers/providers/jsonrpc.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ethers/providers/jsonrpc.nim b/ethers/providers/jsonrpc.nim index f2abe850..65420a1c 100644 --- a/ethers/providers/jsonrpc.nim +++ b/ethers/providers/jsonrpc.nim @@ -161,7 +161,7 @@ method getMaxPriorityFeePerGas*( convertError: let client = await provider.client return await client.eth_maxPriorityFeePerGas() - except ProviderError: + except JsonRpcProviderError: # If the provider does not provide the implementation # let's just remove the manual value return provider.maxPriorityFeePerGas From fe19923051e992cd570ca63774a8dce07e355e2f Mon Sep 17 00:00:00 2001 From: Arnaud Date: Tue, 20 May 2025 15:51:52 +0200 Subject: [PATCH 13/16] Improve readability --- ethers/signer.nim | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ethers/signer.nim b/ethers/signer.nim index 9646a5c7..84d1c458 100644 --- a/ethers/signer.nim +++ b/ethers/signer.nim @@ -139,7 +139,7 @@ method populateTransaction*( # Multiply by 2 because during times of congestion, baseFeePerGas can increase by 12.5% per block. # https://github.com/ethers-io/ethers.js/discussions/3601#discussioncomment-4461273 - let maxFeePerGas = transaction.maxFeePerGas |? baseFeePerGas * 2 + maxPriorityFeePerGas + let maxFeePerGas = transaction.maxFeePerGas |? (baseFeePerGas * 2 + maxPriorityFeePerGas) populated.maxFeePerGas = some(maxFeePerGas) populated.gasPrice = none(UInt256) From d961425147807a3153c0ebf5220a72af5827d997 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Tue, 20 May 2025 15:52:26 +0200 Subject: [PATCH 14/16] Set none value for maxFeePerGas in case of non EIP-1559 transaction --- ethers/signer.nim | 1 + 1 file changed, 1 insertion(+) diff --git a/ethers/signer.nim b/ethers/signer.nim index 84d1c458..9f953e0f 100644 --- a/ethers/signer.nim +++ b/ethers/signer.nim @@ -147,6 +147,7 @@ method populateTransaction*( trace "EIP-1559 is supported", maxPriorityFeePerGas = maxPriorityFeePerGas, maxFeePerGas = maxFeePerGas else: populated.gasPrice = some(transaction.gasPrice |? (await signer.getGasPrice())) + populated.maxFeePerGas = none(UInt256) trace "EIP-1559 is not supported", gasPrice = populated.gasPrice if transaction.nonce.isNone and transaction.gasLimit.isNone: From 9ca0dac443f2564cdb4c0af5d23a0df6a1d7a5cf Mon Sep 17 00:00:00 2001 From: Arnaud Date: Wed, 21 May 2025 17:17:05 +0200 Subject: [PATCH 15/16] Assign none to maxPriorityFeePerGas for non EIP-1559 transaction to avoid potential side effect in wallet signing --- ethers/signer.nim | 1 + 1 file changed, 1 insertion(+) diff --git a/ethers/signer.nim b/ethers/signer.nim index 9f953e0f..e38509eb 100644 --- a/ethers/signer.nim +++ b/ethers/signer.nim @@ -148,6 +148,7 @@ method populateTransaction*( else: populated.gasPrice = some(transaction.gasPrice |? (await signer.getGasPrice())) populated.maxFeePerGas = none(UInt256) + populated.maxPriorityFeePerGas = none(UInt256) trace "EIP-1559 is not supported", gasPrice = populated.gasPrice if transaction.nonce.isNone and transaction.gasLimit.isNone: From b26ed90058131b7af292f9b4a44dd128312628f2 Mon Sep 17 00:00:00 2001 From: Arnaud Date: Wed, 28 May 2025 16:04:58 +0200 Subject: [PATCH 16/16] Remove upper bound version for stew and update contractabi --- ethers.nimble | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ethers.nimble b/ethers.nimble index 5df46682..d1489f8e 100644 --- a/ethers.nimble +++ b/ethers.nimble @@ -6,12 +6,12 @@ license = "MIT" requires "nim >= 2.0.14" requires "chronicles >= 0.10.3 & < 0.11.0" requires "chronos >= 4.0.4 & < 4.1.0" -requires "contractabi >= 0.7.0 & < 0.8.0" +requires "contractabi >= 0.7.2 & < 0.8.0" requires "questionable >= 0.10.2 & < 0.11.0" requires "json_rpc >= 0.5.0 & < 0.6.0" requires "serde >= 1.2.1 & < 1.3.0" requires "stint >= 0.8.1 & < 0.9.0" -requires "stew >= 0.2.0 & < 0.3.0" +requires "stew >= 0.2.0" requires "eth >= 0.5.0 & < 0.6.0" task test, "Run the test suite":