nim-ethers/ethers/contract.nim

255 lines
7.7 KiB
Nim
Raw Normal View History

2022-01-20 11:56:18 +00:00
import std/macros
import pkg/chronos
import pkg/contractabi
import ./basics
import ./provider
2022-01-25 16:17:43 +00:00
import ./signer
2022-02-01 14:49:36 +00:00
import ./events
import ./fields
2022-01-20 11:56:18 +00:00
export basics
export provider
2022-02-01 14:49:36 +00:00
export events
2022-01-20 11:56:18 +00:00
type
Contract* = ref object of RootObj
provider: Provider
2022-01-25 16:17:43 +00:00
signer: ?Signer
2022-01-20 11:56:18 +00:00
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
2022-01-25 09:25:09 +00:00
ContractError* = object of EthersError
Confirmable* = ?TransactionResponse
EventHandler*[E: Event] = proc(event: E) {.gcsafe, upraises:[].}
2022-01-20 11:56:18 +00:00
2022-01-26 09:31:54 +00:00
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)
2022-01-26 11:23:30 +00:00
func connect*[T: Contract](contract: T, provider: Provider | Signer): T =
T.new(contract.address, provider)
2022-01-25 16:17:43 +00:00
func provider*(contract: Contract): Provider =
contract.provider
func signer*(contract: Contract): ?Signer =
contract.signer
func address*(contract: Contract): Address =
contract.address
2022-01-20 11:56:18 +00:00
template raiseContractError(message: string) =
raise newException(ContractError, message)
2022-01-26 09:31:54 +00:00
proc createTransaction(contract: Contract,
function: string,
parameters: tuple,
overrides = TransactionOverrides()): Transaction =
2022-01-20 11:56:18 +00:00
let selector = selector(function, typeof parameters).toArray
2022-01-26 09:31:54 +00:00
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,
)
2022-01-20 11:56:18 +00:00
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]
2022-01-20 11:56:18 +00:00
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)
2022-01-25 16:17:43 +00:00
2022-01-26 09:31:54 +00:00
proc call(contract: Contract,
2022-01-25 16:17:43 +00:00
function: string,
2022-01-26 09:31:54 +00:00
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)
2022-01-25 16:17:43 +00:00
proc send(contract: Contract,
function: string,
parameters: tuple,
overrides = TransactionOverrides()):
Future[?TransactionResponse] {.async.} =
2022-01-26 09:31:54 +00:00
if signer =? contract.signer:
let transaction = createTransaction(contract, function, parameters, overrides)
2022-01-26 09:31:54 +00:00
let populated = await signer.populateTransaction(transaction)
let txResp = await signer.sendTransaction(populated)
return txResp.some
2022-01-26 09:31:54 +00:00
else:
await call(contract, function, parameters)
return TransactionResponse.none
2022-01-20 11:56:18 +00:00
2022-01-26 09:31:54 +00:00
func getParameterTuple(procedure: NimNode): NimNode =
2022-01-20 11:56:18 +00:00
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
2022-01-25 16:17:43 +00:00
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()
)
)
2022-01-20 11:56:18 +00:00
func addContractCall(procedure: var NimNode) =
2022-01-26 09:31:54 +00:00
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)
2022-01-26 09:31:54 +00:00
procedure[6] =
if procedure.isConstant:
call()
2022-01-26 09:31:54 +00:00
else:
send()
2022-01-20 11:56:18 +00:00
func addFuture(procedure: var NimNode) =
let returntype = procedure[3][0]
2022-01-25 16:17:43 +00:00
if returntype.kind != nnkEmpty:
2022-01-26 09:31:54 +00:00
procedure[3][0] = quote: Future[`returntype`]
2022-01-20 11:56:18 +00:00
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 =
2022-01-26 09:31:54 +00:00
2022-01-20 11:56:18 +00:00
let parameters = procedure[3]
let body = procedure[6]
2022-01-26 09:31:54 +00:00
parameters.expectMinLen(2) # at least return type and contract instance
2022-01-20 11:56:18 +00:00
body.expectKind(nnkEmpty)
2022-01-26 09:31:54 +00:00
2022-01-20 11:56:18 +00:00
var contractcall = copyNimTree(procedure)
contractcall.addContractCall()
contractcall.addFuture()
contractcall.addAsyncPragma()
contractcall
2022-01-26 09:31:54 +00:00
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)
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)