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"
description = "library for interacting with Ethereum"
license = "MIT"
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 "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 "eth >= 0.5.0 & < 0.6.0"
requires "stew >= 0.2.0"
requires "eth >= 0.6.0 & < 0.10.0"
task test, "Run the test suite":
# exec "nimble install -d -y"

View File

@ -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

View File

@ -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,
)

View File

@ -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: [CancelledError]).} =
doAssert false, "not implemented"
method getTransactionCount*(
provider: Provider, address: Address, blockTag = BlockTag.latest
): Future[UInt256] {.base, async: (raises: [ProviderError, CancelledError]).} =

View File

@ -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,18 @@ method getGasPrice*(
let client = await provider.client
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*(
provider: JsonRpcProvider, address: Address, blockTag = BlockTag.latest
): Future[UInt256] {.async: (raises: [ProviderError, CancelledError]).} =

View File

@ -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

View File

@ -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: [SignerError, CancelledError]).} =
return await signer.provider.getMaxPriorityFeePerGas()
method getTransactionCount*(
signer: Signer, blockTag = BlockTag.latest
): Future[UInt256] {.
@ -124,8 +130,26 @@ 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:
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:
# 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/rlp
import pkg/eth/common/transaction as eth
import pkg/eth/common/transaction_utils
import pkg/eth/common/eth_hash
import ../../basics
import ../../transaction as ethers
@ -25,18 +26,18 @@ func toSignableTransaction(transaction: Transaction): SignableTransaction =
raiseWalletError "missing gas limit"
signable.nonce = nonce.truncate(uint64)
signable.chainId = ChainId(chainId.truncate(uint64))
signable.chainId = chainId
signable.gasLimit = GasInt(gasLimit.truncate(uint64))
signable.to = Opt.some(EthAddress(transaction.to))
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))
@ -47,21 +48,7 @@ func toSignableTransaction(transaction: Transaction): SignableTransaction =
func sign(key: PrivateKey, transaction: SignableTransaction): seq[byte] =
var transaction = transaction
# 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
transaction.signature = transaction.sign(key, true)
rlp.encode(transaction)
func sign*(key: PrivateKey, transaction: Transaction): seq[byte] =

View File

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

View File

@ -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":

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[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":

View File

@ -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)
@ -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)