mirror of
https://github.com/status-im/nim-ethers.git
synced 2025-02-25 05:25:26 +00:00
Concurrent asynchronous population of transactions cause issues with nonces not being in sync with the transaction count for an account on chain. This was being mitigated by tracking a "last seen" nonce and locking inside of `populateTransaction` so that the nonce could be populated in a concurrent fashion. However, if there was an async cancellation before the transaction was sent, then the nonce would become out of sync. One solution was to decrease the nonce if a cancellation occurred. The other solution, in this commit, is simply to lock the populate and sendTransaction calls together, so that there will not be concurrent nonce discrepancies. This removes the need for "lastSeenNonce" and is overall more simple.
318 lines
9.6 KiB
Nim
318 lines
9.6 KiB
Nim
import std/json
|
|
import std/macros
|
|
import std/sequtils
|
|
import pkg/chronicles
|
|
import pkg/chronos
|
|
import pkg/contractabi
|
|
import ./basics
|
|
import ./provider
|
|
import ./signer
|
|
import ./events
|
|
import ./fields
|
|
|
|
export basics
|
|
export provider
|
|
export events
|
|
|
|
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
|
|
gasLimit*: ?UInt256
|
|
CallOverrides* = ref object of TransactionOverrides
|
|
blockTag*: ?BlockTag
|
|
|
|
ContractError* = object of EthersError
|
|
Confirmable* = ?TransactionResponse
|
|
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 =
|
|
ContractType(signer: some signer, provider: signer.provider, address: address)
|
|
|
|
func connect*[T: Contract](contract: T, provider: Provider | Signer): T =
|
|
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,
|
|
gasLimit: overrides.gasLimit,
|
|
)
|
|
|
|
proc decodeResponse(T: type, multiple: static bool, bytes: seq[byte]): T =
|
|
when multiple:
|
|
without decoded =? AbiDecoder.decode(bytes, T):
|
|
raiseContractError "unable to decode return value as " & $T
|
|
return decoded
|
|
else:
|
|
return decodeResponse((T,), true, bytes)[0]
|
|
|
|
proc call(provider: Provider,
|
|
transaction: Transaction,
|
|
overrides: TransactionOverrides): Future[seq[byte]] =
|
|
if overrides of CallOverrides and
|
|
blockTag =? CallOverrides(overrides).blockTag:
|
|
provider.call(transaction, blockTag)
|
|
else:
|
|
provider.call(transaction)
|
|
|
|
proc call(contract: Contract,
|
|
function: string,
|
|
parameters: tuple,
|
|
overrides = TransactionOverrides()) {.async.} =
|
|
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,
|
|
returnMultiple: static bool,
|
|
overrides = TransactionOverrides()): Future[ReturnType] {.async.} =
|
|
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, returnMultiple, response)
|
|
|
|
proc send(contract: Contract,
|
|
function: string,
|
|
parameters: tuple,
|
|
overrides = TransactionOverrides()):
|
|
Future[?TransactionResponse] {.async.} =
|
|
if signer =? contract.signer:
|
|
|
|
var params: seq[string] = @[]
|
|
for param in parameters.fields:
|
|
params.add $param
|
|
|
|
withLock(signer):
|
|
var transaction = createTransaction(contract, function, parameters, overrides)
|
|
transaction = await signer.populateTransaction(transaction)
|
|
trace "sending transaction", function, params
|
|
let txResp = await signer.sendTransaction(transaction)
|
|
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 isConstant(procedure: NimNode): bool =
|
|
let pragmas = procedure[4]
|
|
for pragma in pragmas:
|
|
if pragma.eqIdent "view":
|
|
return true
|
|
elif pragma.eqIdent "pure":
|
|
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 returnMultiple = returnType.isMultipleReturn.newLit
|
|
|
|
procedure.addOverrides()
|
|
|
|
func call: NimNode =
|
|
if returnType.kind == nnkEmpty:
|
|
quote:
|
|
await call(`contract`, `function`, `parameters`, overrides)
|
|
else:
|
|
quote:
|
|
return await call(
|
|
`contract`, `function`, `parameters`, `returnType`, `returnMultiple`, overrides)
|
|
|
|
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.} or {.pure.} ?".}
|
|
return await send(`contract`, `function`, `parameters`, overrides)
|
|
|
|
procedure[6] =
|
|
if procedure.isConstant:
|
|
call()
|
|
else:
|
|
send()
|
|
|
|
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 ident("async")
|
|
|
|
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.addFuture()
|
|
contractcall.addAsyncPragma()
|
|
contractcall
|
|
|
|
template view* {.pragma.}
|
|
template pure* {.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(log: Log) {.raises: [].} =
|
|
if event =? E.decode(log.data, log.topics):
|
|
handler(event)
|
|
|
|
contract.provider.subscribe(filter, logHandler)
|
|
|
|
proc confirm*(tx: Future[?TransactionResponse],
|
|
confirmations: int = EthersDefaultConfirmations,
|
|
timeout: int = EthersReceiptTimeoutBlks):
|
|
Future[TransactionReceipt] {.async.} =
|
|
## Convenience method that allows confirm to be chained to a contract
|
|
## transaction, eg:
|
|
## `await token.connect(signer0)
|
|
## .mint(accounts[1], 100.u256)
|
|
## .confirm(3)`
|
|
without response =? (await tx):
|
|
raise newException(
|
|
EthersError,
|
|
"Transaction hash required. Possibly was a call instead of a send?"
|
|
)
|
|
|
|
return await response.confirm(confirmations, timeout)
|
|
|
|
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)
|