nim-ethers/ethers/contract.nim
Eric Mastro a3e888128c feat: Allow contract transactions to be waited on
Allow waiting for a specified number of confirmations for contract transactions.

This change only requires an optional TransactionResponse return type to be added to the contract function. This allows the transaction hash to be passed to `.wait`.

For example, previously the `mint` method looked like this without a return value:
```
method mint(token: TestToken, holder: Address, amount: UInt256) {.base, contract.}
```
it still works without a return value, but if we want to wait for a 3 confirmations, we can now define it like this:
```
method mint(token: TestToken, holder: Address, amount: UInt256): ?TransactionResponse {.base, contract.}
```
and use like this:
```
let receipt = await token.connect(signer0)
                    .mint(accounts[1], 100.u256)
                    .wait(3) # wait for 3 confirmations
```
2022-05-23 11:27:26 +10:00

192 lines
5.9 KiB
Nim

import std/macros
import pkg/chronos
import pkg/contractabi
import ./basics
import ./provider
import ./signer
import ./events
import ./fields
export basics
export provider
export events
type
Contract* = ref object of RootObj
provider: Provider
signer: ?Signer
address: Address
ContractError* = object of EthersError
EventHandler*[E: Event] = proc(event: E) {.gcsafe, upraises:[].}
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): Transaction =
let selector = selector(function, typeof parameters).toArray
let data = @selector & AbiEncoder.encode(parameters)
Transaction(to: contract.address, data: data)
proc decodeResponse(T: type, bytes: seq[byte]): T =
without decoded =? AbiDecoder.decode(bytes, T):
raiseContractError "unable to decode return value as " & $T
return decoded
proc call(contract: Contract,
function: string,
parameters: tuple,
blockTag = BlockTag.latest) {.async.} =
let transaction = createTransaction(contract, function, parameters)
discard await contract.provider.call(transaction, blockTag)
proc call(contract: Contract,
function: string,
parameters: tuple,
ReturnType: type,
blockTag = BlockTag.latest): Future[ReturnType] {.async.} =
let transaction = createTransaction(contract, function, parameters)
let response = await contract.provider.call(transaction, blockTag)
return decodeResponse(ReturnType, response)
proc send(contract: Contract, function: string, parameters: tuple):
Future[?TransactionResponse] {.async.} =
if signer =? contract.signer:
let transaction = createTransaction(contract, function, parameters)
let populated = await signer.populateTransaction(transaction)
let txResp = await signer.sendTransaction(populated)
return txResp.some
else:
await call(contract, function, parameters)
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 isTxResponse(returntype: NimNode): bool =
return returntype.kind == nnkPrefix and
returntype[0].kind == nnkIdent and
returntype[0].strVal == "?" and
returntype[1].kind == nnkIdent and
returntype[1].strVal == $TransactionResponse
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]
procedure[6] =
if procedure.isConstant:
if returntype.kind == nnkEmpty:
quote:
await call(`contract`, `function`, `parameters`)
else:
quote:
return await call(`contract`, `function`, `parameters`, `returntype`)
else:
# When ?TransactionResponse is specified as the return type of contract
# method, it allows for contract transactions to be awaited on
# confirmations
if returntype.isTxResponse:
quote:
return await send(`contract`, `function`, `parameters`)
else:
quote:
discard await send(`contract`, `function`, `parameters`)
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")
func checkReturnType(procedure: NimNode) =
let returntype = procedure[3][0]
if returntype.kind != nnkEmpty:
# Do not throw exception for methods that have a TransactionResponse
# return type as that is needed for .wait
if returntype.isTxResponse:
return
if not procedure.isConstant:
const message =
"only contract functions with {.view.} or {.pure.} " &
"can have a return type"
error(message, returntype)
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)
procedure.checkReturnType()
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 = Filter(address: contract.address, topics: @[topic])
proc logHandler(log: Log) {.upraises: [].} =
if event =? E.decode(log.data, log.topics):
handler(event)
contract.provider.subscribe(filter, logHandler)