Compare commits

...

2 Commits
2.0.0 ... main

Author SHA1 Message Date
Jacek Sieka
965b8cd752
chore: bump eth (#124)
* recycle common transaction signature helpers
* bump versions
2025-12-13 07:32:36 +01:00
Arnaud
30871c7b1d
chore: add EIP-1559 implementation for gas price (#113)
* Add EIP-1559 implementation for gas price

* Improve logs

* Improve comment

* Rename maxFee and maxPriorityFee to use official EIP-1559 names

* Delete gas price when using EIP-1559

* Allow override maxFeePerGas

* Code style

* Remove useless specific EIP1559 test because Hardhart support it so all transactions are using EIP1559 by default

* Restore test to check legacy transaction

* Update after rebase

* Call eth_maxPriorityFeePerGas and returns a manual defined maxPriorityFeePerGas as a fallback

* Catch JsonRpcProviderError instead of ProviderError

* Improve readability

* Set none value for maxFeePerGas in case of non EIP-1559 transaction

* Assign none to maxPriorityFeePerGas for non EIP-1559 transaction to avoid potential side effect in wallet signing

* Remove upper bound version for stew and update contractabi
2025-05-28 16:14:01 +02:00
12 changed files with 84 additions and 44 deletions

View File

@ -1,18 +1,18 @@
version = "2.0.0" version = "2.1.0"
author = "Nim Ethers Authors" author = "Nim Ethers Authors"
description = "library for interacting with Ethereum" description = "library for interacting with Ethereum"
license = "MIT" license = "MIT"
requires "nim >= 2.0.14" requires "nim >= 2.0.14"
requires "chronicles >= 0.10.3 & < 0.11.0" requires "chronicles >= 0.10.3 & < 0.13.0"
requires "chronos >= 4.0.4 & < 4.1.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 "questionable >= 0.10.2 & < 0.11.0"
requires "json_rpc >= 0.5.0 & < 0.6.0" requires "json_rpc >= 0.5.0 & < 0.6.0"
requires "serde >= 1.2.1 & < 1.3.0" requires "serde >= 1.2.1 & < 1.3.0"
requires "stint >= 0.8.1 & < 0.9.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" requires "eth >= 0.6.0 & < 0.10.0"
task test, "Run the test suite": task test, "Run the test suite":
# exec "nimble install -d -y" # exec "nimble install -d -y"

View File

@ -6,8 +6,8 @@ type
nonce*: ?UInt256 nonce*: ?UInt256
chainId*: ?UInt256 chainId*: ?UInt256
gasPrice*: ?UInt256 gasPrice*: ?UInt256
maxFee*: ?UInt256 maxFeePerGas*: ?UInt256
maxPriorityFee*: ?UInt256 maxPriorityFeePerGas*: ?UInt256
gasLimit*: ?UInt256 gasLimit*: ?UInt256
CallOverrides* = ref object of TransactionOverrides CallOverrides* = ref object of TransactionOverrides
blockTag*: ?BlockTag blockTag*: ?BlockTag

View File

@ -22,8 +22,8 @@ proc createTransaction*(call: ContractCall): Transaction =
nonce: call.overrides.nonce, nonce: call.overrides.nonce,
chainId: call.overrides.chainId, chainId: call.overrides.chainId,
gasPrice: call.overrides.gasPrice, gasPrice: call.overrides.gasPrice,
maxFee: call.overrides.maxFee, maxFeePerGas: call.overrides.maxFeePerGas,
maxPriorityFee: call.overrides.maxPriorityFee, maxPriorityFeePerGas: call.overrides.maxPriorityFeePerGas,
gasLimit: call.overrides.gasLimit, gasLimit: call.overrides.gasLimit,
) )

View File

@ -63,6 +63,7 @@ type
number*: ?UInt256 number*: ?UInt256
timestamp*: UInt256 timestamp*: UInt256
hash*: ?BlockHash hash*: ?BlockHash
baseFeePerGas* : ?UInt256
PastTransaction* {.serialize.} = object PastTransaction* {.serialize.} = object
blockHash*: BlockHash blockHash*: BlockHash
blockNumber*: UInt256 blockNumber*: UInt256
@ -121,6 +122,11 @@ method getGasPrice*(
): Future[UInt256] {.base, async: (raises: [ProviderError, CancelledError]).} = ): Future[UInt256] {.base, async: (raises: [ProviderError, CancelledError]).} =
doAssert false, "not implemented" doAssert false, "not implemented"
method getMaxPriorityFeePerGas*(
provider: Provider
): Future[UInt256] {.base, async: (raises: [CancelledError]).} =
doAssert false, "not implemented"
method getTransactionCount*( method getTransactionCount*(
provider: Provider, address: Address, blockTag = BlockTag.latest provider: Provider, address: Address, blockTag = BlockTag.latest
): Future[UInt256] {.base, async: (raises: [ProviderError, CancelledError]).} = ): Future[UInt256] {.base, async: (raises: [ProviderError, CancelledError]).} =

View File

@ -28,6 +28,7 @@ type
JsonRpcProvider* = ref object of Provider JsonRpcProvider* = ref object of Provider
client: Future[RpcClient] client: Future[RpcClient]
subscriptions: Future[JsonRpcSubscriptions] subscriptions: Future[JsonRpcSubscriptions]
maxPriorityFeePerGas: UInt256
JsonRpcSubscription* = ref object of Subscription JsonRpcSubscription* = ref object of Subscription
subscriptions: JsonRpcSubscriptions subscriptions: JsonRpcSubscriptions
@ -43,6 +44,7 @@ type
const defaultUrl = "http://localhost:8545" const defaultUrl = "http://localhost:8545"
const defaultPollingInterval = 4.seconds const defaultPollingInterval = 4.seconds
const defaultMaxPriorityFeePerGas = 1_000_000_000.u256
proc jsonHeaders: seq[(string, string)] = proc jsonHeaders: seq[(string, string)] =
@[("Content-Type", "application/json")] @[("Content-Type", "application/json")]
@ -50,7 +52,8 @@ proc jsonHeaders: seq[(string, string)] =
proc new*( proc new*(
_: type JsonRpcProvider, _: type JsonRpcProvider,
url=defaultUrl, url=defaultUrl,
pollingInterval=defaultPollingInterval): JsonRpcProvider {.raises: [JsonRpcProviderError].} = pollingInterval=defaultPollingInterval,
maxPriorityFeePerGas=defaultMaxPriorityFeePerGas): JsonRpcProvider {.raises: [JsonRpcProviderError].} =
var initialized: Future[void] var initialized: Future[void]
var client: RpcClient var client: RpcClient
@ -87,7 +90,7 @@ proc new*(
return subscriptions return subscriptions
initialized = initialize() initialized = initialize()
return JsonRpcProvider(client: awaitClient(), subscriptions: awaitSubscriptions()) return JsonRpcProvider(client: awaitClient(), subscriptions: awaitSubscriptions(), maxPriorityFeePerGas: maxPriorityFeePerGas)
proc callImpl( proc callImpl(
client: RpcClient, call: string, args: JsonNode client: RpcClient, call: string, args: JsonNode
@ -151,6 +154,18 @@ method getGasPrice*(
let client = await provider.client let client = await provider.client
return await client.eth_gasPrice() return await client.eth_gasPrice()
method getMaxPriorityFeePerGas*(
provider: JsonRpcProvider
): Future[UInt256] {.async: (raises: [CancelledError]).} =
try:
convertError:
let client = await provider.client
return await client.eth_maxPriorityFeePerGas()
except JsonRpcProviderError:
# If the provider does not provide the implementation
# let's just remove the manual value
return provider.maxPriorityFeePerGas
method getTransactionCount*( method getTransactionCount*(
provider: JsonRpcProvider, address: Address, blockTag = BlockTag.latest provider: JsonRpcProvider, address: Address, blockTag = BlockTag.latest
): Future[UInt256] {.async: (raises: [ProviderError, CancelledError]).} = ): Future[UInt256] {.async: (raises: [ProviderError, CancelledError]).} =

View File

@ -21,3 +21,4 @@ proc eth_newBlockFilter(): JsonNode
proc eth_newFilter(filter: EventFilter): JsonNode proc eth_newFilter(filter: EventFilter): JsonNode
proc eth_getFilterChanges(id: JsonNode): JsonNode proc eth_getFilterChanges(id: JsonNode): JsonNode
proc eth_uninstallFilter(id: JsonNode): bool proc eth_uninstallFilter(id: JsonNode): bool
proc eth_maxPriorityFeePerGas(): UInt256

View File

@ -1,4 +1,5 @@
import pkg/questionable import pkg/questionable
import pkg/chronicles
import ./basics import ./basics
import ./errors import ./errors
import ./provider import ./provider
@ -55,6 +56,11 @@ method getGasPrice*(
.} = .} =
return await signer.provider.getGasPrice() return await signer.provider.getGasPrice()
method getMaxPriorityFeePerGas*(
signer: Signer
): Future[UInt256] {.async: (raises: [SignerError, CancelledError]).} =
return await signer.provider.getMaxPriorityFeePerGas()
method getTransactionCount*( method getTransactionCount*(
signer: Signer, blockTag = BlockTag.latest signer: Signer, blockTag = BlockTag.latest
): Future[UInt256] {. ): Future[UInt256] {.
@ -124,8 +130,26 @@ method populateTransaction*(
populated.sender = some(address) populated.sender = some(address)
if transaction.chainId.isNone: if transaction.chainId.isNone:
populated.chainId = some(await signer.getChainId()) 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:
let maxPriorityFeePerGas = transaction.maxPriorityFeePerGas |? (await signer.provider.getMaxPriorityFeePerGas())
populated.maxPriorityFeePerGas = some(maxPriorityFeePerGas)
# 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)
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()))
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: if transaction.nonce.isNone and transaction.gasLimit.isNone:
# when both nonce and gasLimit are not populated, we must ensure getNonce is # when both nonce and gasLimit are not populated, we must ensure getNonce is

View File

@ -1,6 +1,7 @@
import pkg/eth/keys import pkg/eth/keys
import pkg/eth/rlp import pkg/eth/rlp
import pkg/eth/common/transaction as eth import pkg/eth/common/transaction as eth
import pkg/eth/common/transaction_utils
import pkg/eth/common/eth_hash import pkg/eth/common/eth_hash
import ../../basics import ../../basics
import ../../transaction as ethers import ../../transaction as ethers
@ -25,18 +26,18 @@ func toSignableTransaction(transaction: Transaction): SignableTransaction =
raiseWalletError "missing gas limit" raiseWalletError "missing gas limit"
signable.nonce = nonce.truncate(uint64) signable.nonce = nonce.truncate(uint64)
signable.chainId = ChainId(chainId.truncate(uint64)) signable.chainId = chainId
signable.gasLimit = GasInt(gasLimit.truncate(uint64)) signable.gasLimit = GasInt(gasLimit.truncate(uint64))
signable.to = Opt.some(EthAddress(transaction.to)) signable.to = Opt.some(EthAddress(transaction.to))
signable.value = transaction.value signable.value = transaction.value
signable.payload = transaction.data signable.payload = transaction.data
if maxFee =? transaction.maxFee and if maxFeePerGas =? transaction.maxFeePerGas and
maxPriorityFee =? transaction.maxPriorityFee: maxPriorityFeePerGas =? transaction.maxPriorityFeePerGas:
signable.txType = TxEip1559 signable.txType = TxEip1559
signable.maxFeePerGas = GasInt(maxFee.truncate(uint64)) signable.maxFeePerGas = GasInt(maxFeePerGas.truncate(uint64))
signable.maxPriorityFeePerGas = GasInt(maxPriorityFee.truncate(uint64)) signable.maxPriorityFeePerGas = GasInt(maxPriorityFeePerGas.truncate(uint64))
elif gasPrice =? transaction.gasPrice: elif gasPrice =? transaction.gasPrice:
signable.txType = TxLegacy signable.txType = TxLegacy
signable.gasPrice = GasInt(gasPrice.truncate(uint64)) signable.gasPrice = GasInt(gasPrice.truncate(uint64))
@ -47,21 +48,7 @@ func toSignableTransaction(transaction: Transaction): SignableTransaction =
func sign(key: PrivateKey, transaction: SignableTransaction): seq[byte] = func sign(key: PrivateKey, transaction: SignableTransaction): seq[byte] =
var transaction = transaction var transaction = transaction
transaction.signature = transaction.sign(key, true)
# Temporary V value, used to signal to the hashing function
# that we'd like to use an EIP-155 signature
transaction.V = uint64(transaction.chainId) * 2 + 35
let hash = transaction.txHashNoSignature().data
let signature = key.sign(SkMessage(hash)).toRaw()
transaction.R = UInt256.fromBytesBE(signature[0..<32])
transaction.S = UInt256.fromBytesBE(signature[32..<64])
transaction.V = uint64(signature[64])
if transaction.txType == TxLegacy:
transaction.V += uint64(transaction.chainId) * 2 + 35
rlp.encode(transaction) rlp.encode(transaction)
func sign*(key: PrivateKey, transaction: Transaction): seq[byte] = func sign*(key: PrivateKey, transaction: Transaction): seq[byte] =

View File

@ -15,8 +15,8 @@ type
nonce*: ?UInt256 nonce*: ?UInt256
chainId*: ?UInt256 chainId*: ?UInt256
gasPrice*: ?UInt256 gasPrice*: ?UInt256
maxFee*: ?UInt256 maxPriorityFeePerGas*: ?UInt256
maxPriorityFee*: ?UInt256 maxFeePerGas*: ?UInt256
gasLimit*: ?UInt256 gasLimit*: ?UInt256
transactionType* {.serialize("type").}: ?TransactionType transactionType* {.serialize("type").}: ?TransactionType

View File

@ -55,20 +55,27 @@ suite "JsonRpcSigner":
let transaction = Transaction.example let transaction = Transaction.example
let populated = await signer.populateTransaction(transaction) let populated = await signer.populateTransaction(transaction)
check !populated.sender == await signer.getAddress() check !populated.sender == await signer.getAddress()
check !populated.gasPrice == await signer.getGasPrice()
check !populated.nonce == await signer.getTransactionCount(BlockTag.pending) check !populated.nonce == await signer.getTransactionCount(BlockTag.pending)
check !populated.gasLimit == await signer.estimateGas(transaction) check !populated.gasLimit == await signer.estimateGas(transaction)
check !populated.chainId == await signer.getChainId() 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": test "populate does not overwrite existing fields":
let signer = provider.getSigner() let signer = provider.getSigner()
var transaction = Transaction.example var transaction = Transaction.example
transaction.sender = some await signer.getAddress() transaction.sender = some await signer.getAddress()
transaction.nonce = some UInt256.example transaction.nonce = some UInt256.example
transaction.chainId = some await signer.getChainId() transaction.chainId = some await signer.getChainId()
transaction.gasPrice = some UInt256.example transaction.maxPriorityFeePerGas = some UInt256.example
transaction.gasLimit = some UInt256.example transaction.gasLimit = some UInt256.example
let populated = await signer.populateTransaction(transaction) 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 check populated == transaction
test "populate fails when sender does not match signer address": test "populate fails when sender does not match signer address":

View File

@ -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[1])) == 25.u256
check (await token.connect(provider).balanceOf(accounts[2])) == 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( let overrides = TransactionOverrides(
nonce: some 100.u256, nonce: some 100.u256,
gasPrice: some 200.u256, maxPriorityFeePerGas: some 200.u256,
gasLimit: some 300.u256 gasLimit: some 300.u256
) )
let signer = MockSigner.new(provider) let signer = MockSigner.new(provider)
discard await token.connect(signer).mint(accounts[0], 42.u256, overrides) discard await token.connect(signer).mint(accounts[0], 42.u256, overrides)
check signer.transactions.len == 1 check signer.transactions.len == 1
check signer.transactions[0].nonce == overrides.nonce 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 check signer.transactions[0].gasLimit == overrides.gasLimit
test "can call functions for different block heights": test "can call functions for different block heights":

View File

@ -80,8 +80,8 @@ suite "Wallet":
to: wallet.address, to: wallet.address,
nonce: some 0.u256, nonce: some 0.u256,
chainId: some 31337.u256, chainId: some 31337.u256,
maxFee: some 2_000_000_000.u256, maxFeePerGas: some 2_000_000_000.u256,
maxPriorityFee: some 1_000_000_000.u256, maxPriorityFeePerGas: some 1_000_000_000.u256,
gasLimit: some 21_000.u256 gasLimit: some 21_000.u256
) )
let signedTx = await wallet.signTransaction(tx) let signedTx = await wallet.signTransaction(tx)
@ -115,8 +115,8 @@ suite "Wallet":
let wallet = !Wallet.new(pk_with_funds, provider) let wallet = !Wallet.new(pk_with_funds, provider)
let overrides = TransactionOverrides( let overrides = TransactionOverrides(
nonce: some 0.u256, nonce: some 0.u256,
maxFee: some 1_000_000_000.u256, maxFeePerGas: some 1_000_000_000.u256,
maxPriorityFee: some 1_000_000_000.u256, maxPriorityFeePerGas: some 1_000_000_000.u256,
gasLimit: some 22_000.u256) gasLimit: some 22_000.u256)
let testToken = Erc20.new(wallet.address, wallet) let testToken = Erc20.new(wallet.address, wallet)
await testToken.transfer(wallet.address, 24.u256, overrides) await testToken.transfer(wallet.address, 24.u256, overrides)