From 45964f5d6c2fdd626d288d97065008e8a9af5807 Mon Sep 17 00:00:00 2001 From: Mark Spanbroek Date: Tue, 8 Apr 2025 17:19:25 +0200 Subject: [PATCH] Split contract module into several parts --- ethers.nim | 6 +- ethers/contract.nim | 408 ------------------------------ ethers/contracts.nim | 26 ++ ethers/contracts/confirmation.nim | 45 ++++ ethers/contracts/contract.nim | 36 +++ ethers/contracts/contractcall.nim | 35 +++ ethers/{ => contracts}/events.nim | 4 +- ethers/{ => contracts}/fields.nim | 0 ethers/contracts/filters.nim | 78 ++++++ ethers/contracts/function.nim | 86 +++++++ ethers/contracts/overrides.nim | 13 + ethers/contracts/syntax.nim | 76 ++++++ ethers/contracts/transactions.nim | 71 ++++++ ethers/errors.nim | 2 - 14 files changed, 471 insertions(+), 415 deletions(-) delete mode 100644 ethers/contract.nim create mode 100644 ethers/contracts.nim create mode 100644 ethers/contracts/confirmation.nim create mode 100644 ethers/contracts/contract.nim create mode 100644 ethers/contracts/contractcall.nim rename ethers/{ => contracts}/events.nim (97%) rename ethers/{ => contracts}/fields.nim (100%) create mode 100644 ethers/contracts/filters.nim create mode 100644 ethers/contracts/function.nim create mode 100644 ethers/contracts/overrides.nim create mode 100644 ethers/contracts/syntax.nim create mode 100644 ethers/contracts/transactions.nim diff --git a/ethers.nim b/ethers.nim index 4b77183..f24ddb6 100644 --- a/ethers.nim +++ b/ethers.nim @@ -1,11 +1,11 @@ import ./ethers/provider import ./ethers/signer import ./ethers/providers/jsonrpc -import ./ethers/contract +import ./ethers/contracts import ./ethers/wallet export provider export signer export jsonrpc -export contract -export wallet \ No newline at end of file +export contracts +export wallet diff --git a/ethers/contract.nim b/ethers/contract.nim deleted file mode 100644 index 68a1671..0000000 --- a/ethers/contract.nim +++ /dev/null @@ -1,408 +0,0 @@ -import pkg/serde -import std/macros -import std/sequtils -import pkg/chronicles -import pkg/chronos -import pkg/questionable -import pkg/contractabi -import ./basics -import ./provider -import ./signer -import ./events -import ./errors -import ./errors/conversion -import ./fields - -export basics -export provider -export events -export errors.SolidityError -export errors.errors - -{.push raises: [].} - -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 - Confirmable* = object - response*: ?TransactionResponse - convert*: ConvertCustomErrors - 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 {.raises: [SignerError].} = - ContractType(signer: some signer, provider: signer.provider, address: address) - -func connect*[T: Contract](contract: T, provider: Provider | Signer): T {.raises: [SignerError].} = - 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, bytes: seq[byte]): T {.raises: [ContractError].} = - without decoded =? AbiDecoder.decode(bytes, T): - raiseContractError "unable to decode return value as " & $T - return decoded - -proc call( - provider: Provider, transaction: Transaction, overrides: TransactionOverrides -): Future[seq[byte]] {.async: (raises: [ProviderError, CancelledError]).} = - if overrides of CallOverrides and blockTag =? CallOverrides(overrides).blockTag: - await provider.call(transaction, blockTag) - else: - await provider.call(transaction) - -proc call( - contract: Contract, - function: string, - parameters: tuple, - overrides = TransactionOverrides(), -) {.async: (raises: [ProviderError, SignerError, CancelledError]).} = - 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, - overrides = TransactionOverrides(), -): Future[ReturnType] {. - async: (raises: [ProviderError, SignerError, ContractError, CancelledError]) -.} = - 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, response) - -proc send( - contract: Contract, - function: string, - parameters: tuple, - overrides = TransactionOverrides() -): Future[?TransactionResponse] {.async: (raises: [SignerError, ProviderError, CancelledError]).} = - - if signer =? contract.signer: - withLock(signer): - let transaction = createTransaction(contract, function, parameters, overrides) - let populated = await signer.populateTransaction(transaction) - trace "sending contract transaction", function, params = $parameters - let txResp = await signer.sendTransaction(populated) - 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 getErrorTypes(procedure: NimNode): NimNode = - let pragmas = procedure[4] - var tupl = newNimNode(nnkTupleConstr) - for pragma in pragmas: - if pragma.kind == nnkExprColonExpr: - if pragma[0].eqIdent "errors": - pragma[1].expectKind(nnkBracket) - for error in pragma[1]: - tupl.add error - if tupl.len == 0: - quote do: tuple[] - else: - tupl - -func isGetter(procedure: NimNode): bool = - let pragmas = procedure[4] - for pragma in pragmas: - if pragma.eqIdent "getter": - return true - false - -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 - elif pragma.eqIdent "getter": - 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 isGetter = procedure.isGetter - - procedure.addOverrides() - let errors = getErrorTypes(procedure) - - func call: NimNode = - if returnType.kind == nnkEmpty: - quote: - await call(`contract`, `function`, `parameters`, overrides) - elif returnType.isMultipleReturn or isGetter: - quote: - return await call( - `contract`, `function`, `parameters`, `returnType`, overrides - ) - else: - quote: - # solidity functions return a tuple, so wrap return type in a tuple - let tupl = await call( - `contract`, `function`, `parameters`, (`returnType`,), overrides - ) - return tupl[0] - - 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.}, {.pure.} or {.getter.} ?" - .} - let response = await send(`contract`, `function`, `parameters`, overrides) - let convert = customErrorConversion(`errors`) - Confirmable(response: response, convert: convert) - - procedure[6] = - if procedure.isConstant: - call() - else: - send() - -func addErrorHandling(procedure: var NimNode) = - let body = procedure[6] - let errors = getErrorTypes(procedure) - procedure[6] = quote do: - try: - `body` - except ProviderError as error: - if data =? error.data: - let convert = customErrorConversion(`errors`) - raise convert(error) - else: - raise error - -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 nnkExprColonExpr.newTree( - newIdentNode("async"), - nnkTupleConstr.newTree( - nnkExprColonExpr.newTree( - newIdentNode("raises"), - nnkBracket.newTree( - newIdentNode("CancelledError"), - newIdentNode("ProviderError"), - newIdentNode("EthersError"), - ), - ) - ), - ) - -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.addErrorHandling() - contractcall.addFuture() - contractcall.addAsyncPragma() - contractcall - -template view* {.pragma.} -template pure* {.pragma.} -template getter* {.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(logResult: ?!Log) {.raises: [].} = - without log =? logResult, error: - handler(failure(E, error)) - return - - if event =? E.decode(log.data, log.topics): - handler(success(event)) - - contract.provider.subscribe(filter, logHandler) - -proc confirm(tx: Confirmable, confirmations, timeout: int): - Future[TransactionReceipt] {.async: (raises: [CancelledError, EthersError]).} = - - without response =? tx.response: - raise newException( - EthersError, - "Transaction hash required. Possibly was a call instead of a send?" - ) - - try: - return await response.confirm(confirmations, timeout) - except ProviderError as error: - let convert = tx.convert - raise convert(error) - -proc confirm*(tx: Future[Confirmable], - confirmations: int = EthersDefaultConfirmations, - timeout: int = EthersReceiptTimeoutBlks): - Future[TransactionReceipt] {.async: (raises: [CancelledError, EthersError]).} = - ## Convenience method that allows confirm to be chained to a contract - ## transaction, eg: - ## `await token.connect(signer0) - ## .mint(accounts[1], 100.u256) - ## .confirm(3)` - try: - return await (await tx).confirm(confirmations, timeout) - except CancelledError as e: - raise e - except EthersError as e: - raise e - except CatchableError as e: - raise newException( - EthersError, - "Error when trying to confirm the contract transaction: " & e.msg - ) - -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) diff --git a/ethers/contracts.nim b/ethers/contracts.nim new file mode 100644 index 0000000..2528d53 --- /dev/null +++ b/ethers/contracts.nim @@ -0,0 +1,26 @@ +import std/macros +import ./contracts/contract +import ./contracts/overrides +import ./contracts/confirmation +import ./contracts/events +import ./contracts/filters +import ./contracts/syntax +import ./contracts/function + +export contract +export overrides +export confirmation +export events +export filters +export syntax.view +export syntax.pure +export syntax.getter +export syntax.errors + +{.push raises: [].} + +macro contract*(procedure: untyped{nkProcDef | nkMethodDef}): untyped = + procedure.params.expectMinLen(2) # at least return type and contract instance + procedure.body.expectKind(nnkEmpty) + + createContractFunction(procedure) diff --git a/ethers/contracts/confirmation.nim b/ethers/contracts/confirmation.nim new file mode 100644 index 0000000..a2d2265 --- /dev/null +++ b/ethers/contracts/confirmation.nim @@ -0,0 +1,45 @@ +import ../provider +import ../errors/conversion + +{.push raises: [].} + +type Confirmable* = object + response*: ?TransactionResponse + convert*: ConvertCustomErrors + +proc confirm(tx: Confirmable, confirmations, timeout: int): + Future[TransactionReceipt] {.async: (raises: [CancelledError, EthersError]).} = + + without response =? tx.response: + raise newException( + EthersError, + "Transaction hash required. Possibly was a call instead of a send?" + ) + + try: + return await response.confirm(confirmations, timeout) + except ProviderError as error: + let convert = tx.convert + raise convert(error) + +proc confirm*(tx: Future[Confirmable], + confirmations: int = EthersDefaultConfirmations, + timeout: int = EthersReceiptTimeoutBlks): + Future[TransactionReceipt] {.async: (raises: [CancelledError, EthersError]).} = + ## Convenience method that allows confirm to be chained to a contract + ## transaction, eg: + ## `await token.connect(signer0) + ## .mint(accounts[1], 100.u256) + ## .confirm(3)` + try: + return await (await tx).confirm(confirmations, timeout) + except CancelledError as e: + raise e + except EthersError as e: + raise e + except CatchableError as e: + raise newException( + EthersError, + "Error when trying to confirm the contract transaction: " & e.msg + ) + diff --git a/ethers/contracts/contract.nim b/ethers/contracts/contract.nim new file mode 100644 index 0000000..ef556ab --- /dev/null +++ b/ethers/contracts/contract.nim @@ -0,0 +1,36 @@ +import ../basics +import ../provider +import ../signer + +{.push raises:[].} + +type Contract* = ref object of RootObj + provider: Provider + signer: ?Signer + address: Address + +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 {.raises: [SignerError].} = + ContractType(signer: some signer, provider: signer.provider, address: address) + +func connect*[C: Contract](contract: C, provider: Provider): C = + C.new(contract.address, provider) + +func connect*[C: Contract](contract: C, signer: Signer): C {.raises: [SignerError].} = + C.new(contract.address, signer) + +func provider*(contract: Contract): Provider = + contract.provider + +func signer*(contract: Contract): ?Signer = + contract.signer + +func address*(contract: Contract): Address = + contract.address + diff --git a/ethers/contracts/contractcall.nim b/ethers/contracts/contractcall.nim new file mode 100644 index 0000000..cbd67ac --- /dev/null +++ b/ethers/contracts/contractcall.nim @@ -0,0 +1,35 @@ +import ../basics +import ./contract +import ./overrides + +type ContractCall*[Arguments: tuple] = object + contract: Contract + function: string + arguments: Arguments + overrides: TransactionOverrides + +func init*[Arguments: tuple]( + _: type ContractCall, + contract: Contract, + function: string, + arguments: Arguments, + overrides: TransactionOverrides +): ContractCall[arguments] = + ContractCall[Arguments]( + contract: contract, + function: function, + arguments: arguments, + overrides: overrides + ) + +func contract*(call: ContractCall): Contract = + call.contract + +func function*(call: ContractCall): string = + call.function + +func arguments*(call: ContractCall): auto = + call.arguments + +func overrides*(call: ContractCall): TransactionOverrides = + call.overrides diff --git a/ethers/events.nim b/ethers/contracts/events.nim similarity index 97% rename from ethers/events.nim rename to ethers/contracts/events.nim index 356a1a6..3e6b17b 100644 --- a/ethers/events.nim +++ b/ethers/contracts/events.nim @@ -1,7 +1,7 @@ import std/macros import pkg/contractabi -import ./basics -import ./provider +import ../basics +import ../provider type Event* = object of RootObj diff --git a/ethers/fields.nim b/ethers/contracts/fields.nim similarity index 100% rename from ethers/fields.nim rename to ethers/contracts/fields.nim diff --git a/ethers/contracts/filters.nim b/ethers/contracts/filters.nim new file mode 100644 index 0000000..6e6c0cf --- /dev/null +++ b/ethers/contracts/filters.nim @@ -0,0 +1,78 @@ +import std/sequtils +import pkg/contractabi +import ../basics +import ../provider +import ./contract +import ./events +import ./fields + +type EventHandler*[E: Event] = proc(event: ?!E) {.gcsafe, raises:[].} + +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(logResult: ?!Log) {.raises: [].} = + without log =? logResult, error: + handler(failure(E, error)) + return + + if event =? E.decode(log.data, log.topics): + handler(success(event)) + + contract.provider.subscribe(filter, logHandler) + +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) diff --git a/ethers/contracts/function.nim b/ethers/contracts/function.nim new file mode 100644 index 0000000..3fc390f --- /dev/null +++ b/ethers/contracts/function.nim @@ -0,0 +1,86 @@ +import std/macros +import ../errors/conversion +import ./syntax +import ./transactions + +func getErrorTypes(procedure: NimNode): NimNode = + let pragmas = procedure[4] + var tupl = newNimNode(nnkTupleConstr) + for pragma in pragmas: + if pragma.kind == nnkExprColonExpr: + if pragma[0].eqIdent "errors": + pragma[1].expectKind(nnkBracket) + for error in pragma[1]: + tupl.add error + if tupl.len == 0: + quote do: tuple[] + else: + tupl + +func addContractCall(procedure: var NimNode) = + let contractCall = getContractCall(procedure) + let returnType = procedure.params[0] + let isGetter = procedure.isGetter + + let errors = getErrorTypes(procedure) + + func call: NimNode = + if returnType.kind == nnkEmpty: + quote: + await callTransaction(`contractCall`) + elif returnType.isMultipleReturn or isGetter: + quote: + return await callTransaction(`contractCall`, `returnType`) + else: + quote: + # solidity functions return a tuple, so wrap return type in a tuple + let tupl = await callTransaction(`contractCall`, (`returnType`,)) + return tupl[0] + + func send: NimNode = + if returnType.kind == nnkEmpty: + quote: + discard await sendTransaction(`contractCall`) + else: + quote: + when typeof(result) isnot Confirmable: + {.error: + "unexpected return type, " & + "missing {.view.}, {.pure.} or {.getter.} ?" + .} + let response = await sendTransaction(`contractCall`) + let convert = customErrorConversion(`errors`) + Confirmable(response: response, convert: convert) + + procedure.body = + if procedure.isConstant: + call() + else: + send() + +func addErrorHandling(procedure: var NimNode) = + let body = procedure[6] + let errors = getErrorTypes(procedure) + procedure.body = quote do: + try: + `body` + except ProviderError as error: + if data =? error.data: + let convert = customErrorConversion(`errors`) + raise convert(error) + else: + raise error + +func addFuture(procedure: var NimNode) = + let returntype = procedure[3][0] + if returntype.kind != nnkEmpty: + procedure[3][0] = quote: Future[`returntype`] + +func createContractFunction*(procedure: NimNode): NimNode = + result = copyNimTree(procedure) + result.addOverridesParameter() + result.addContractCall() + result.addErrorHandling() + result.addFuture() + result.addAsyncPragma() + diff --git a/ethers/contracts/overrides.nim b/ethers/contracts/overrides.nim new file mode 100644 index 0000000..6939706 --- /dev/null +++ b/ethers/contracts/overrides.nim @@ -0,0 +1,13 @@ +import ../basics +import ../blocktag + +type + TransactionOverrides* = ref object of RootObj + nonce*: ?UInt256 + chainId*: ?UInt256 + gasPrice*: ?UInt256 + maxFee*: ?UInt256 + maxPriorityFee*: ?UInt256 + gasLimit*: ?UInt256 + CallOverrides* = ref object of TransactionOverrides + blockTag*: ?BlockTag diff --git a/ethers/contracts/syntax.nim b/ethers/contracts/syntax.nim new file mode 100644 index 0000000..1af1dd2 --- /dev/null +++ b/ethers/contracts/syntax.nim @@ -0,0 +1,76 @@ +import std/macros +import ./contractcall + +template view* {.pragma.} +template pure* {.pragma.} +template getter* {.pragma.} +template errors*(types) {.pragma.} + +func isGetter*(procedure: NimNode): bool = + let pragmas = procedure[4] + for pragma in pragmas: + if pragma.eqIdent "getter": + return true + false + +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 + elif pragma.eqIdent "getter": + return true + false + +func isMultipleReturn*(returnType: NimNode): bool = + (returnType.kind == nnkPar and returnType.len > 1) or + (returnType.kind == nnkTupleConstr) or + (returnType.kind == nnkTupleTy) + +func getContract(procedure: NimNode): NimNode = + let firstArgument = procedure.params[1][0] + quote do: + Contract(`firstArgument`) + +func getFunctionName(procedure: NimNode): string = + $basename(procedure[0]) + +func getArgumentTuple(procedure: NimNode): NimNode = + let parameters = procedure.params + var arguments = newNimNode(nnkTupleConstr, parameters) + for parameter in parameters[2..^2]: + for name in parameter[0..^3]: + arguments.add name + return arguments + +func getOverrides(procedure: NimNode): NimNode = + procedure.params.last[^3] + +func getContractCall*(procedure: NimNode): NimNode = + let contract = getContract(procedure) + let function = getFunctionName(procedure) + let arguments = getArgumentTuple(procedure) + let overrides = getOverrides(procedure) + quote do: + ContractCall.init(`contract`, `function`, `arguments`, `overrides`) + +func addOverridesParameter*(procedure: var NimNode) = + let overrides = genSym(nskParam, "overrides") + procedure.params.add( + newIdentDefs( + overrides, + newEmptyNode(), + quote do: TransactionOverrides() + ) + ) + +func addAsyncPragma*(procedure: var NimNode) = + procedure.addPragma nnkExprColonExpr.newTree( + quote do: async, + quote do: (raises: [CancelledError, ProviderError, EthersError]) + ) + +func addUsedPragma*(procedure: var NimNode) = + procedure.addPragma(quote do: used) diff --git a/ethers/contracts/transactions.nim b/ethers/contracts/transactions.nim new file mode 100644 index 0000000..c538973 --- /dev/null +++ b/ethers/contracts/transactions.nim @@ -0,0 +1,71 @@ +import pkg/contractabi +import pkg/chronicles +import ../basics +import ../provider +import ../signer +import ../transaction +import ./contract +import ./contractcall +import ./overrides + +{.push raises: [].} + +logScope: + topics = "ethers contract" + +proc createTransaction*(call: ContractCall): Transaction = + let selector = selector(call.function, typeof call.arguments).toArray + let data = @selector & AbiEncoder.encode(call.arguments) + Transaction( + to: call.contract.address, + data: data, + nonce: call.overrides.nonce, + chainId: call.overrides.chainId, + gasPrice: call.overrides.gasPrice, + maxFee: call.overrides.maxFee, + maxPriorityFee: call.overrides.maxPriorityFee, + gasLimit: call.overrides.gasLimit, + ) + +proc decodeResponse(T: type, bytes: seq[byte]): T {.raises: [ContractError].} = + without decoded =? AbiDecoder.decode(bytes, T): + raise newException(ContractError, "unable to decode return value as " & $T) + return decoded + +proc call( + provider: Provider, transaction: Transaction, overrides: TransactionOverrides +): Future[seq[byte]] {.async: (raises: [ProviderError, CancelledError]).} = + if overrides of CallOverrides and blockTag =? CallOverrides(overrides).blockTag: + await provider.call(transaction, blockTag) + else: + await provider.call(transaction) + +proc callTransaction*(call: ContractCall) {.async: (raises: [ProviderError, SignerError, CancelledError]).} = + var transaction = createTransaction(call) + + if signer =? call.contract.signer and transaction.sender.isNone: + transaction.sender = some(await signer.getAddress()) + + discard await call.contract.provider.call(transaction, call.overrides) + +proc callTransaction*(call: ContractCall, ReturnType: type): Future[ReturnType] {.async: (raises: [ProviderError, SignerError, ContractError, CancelledError]).} = + var transaction = createTransaction(call) + + if signer =? call.contract.signer and transaction.sender.isNone: + transaction.sender = some(await signer.getAddress()) + + let response = await call.contract.provider.call(transaction, call.overrides) + return decodeResponse(ReturnType, response) + +proc sendTransaction*(call: ContractCall): Future[?TransactionResponse] {.async: (raises: [SignerError, ProviderError, CancelledError]).} = + if signer =? call.contract.signer: + withLock(signer): + let transaction = createTransaction(call) + let populated = await signer.populateTransaction(transaction) + trace "sending contract transaction", function = call.function, params = $call.arguments + let txResp = await signer.sendTransaction(populated) + return txResp.some + else: + await callTransaction(call) + return TransactionResponse.none + diff --git a/ethers/errors.nim b/ethers/errors.nim index 0151c93..5fece97 100644 --- a/ethers/errors.nim +++ b/ethers/errors.nim @@ -16,5 +16,3 @@ proc toErr*[E1: ref CatchableError, E2: EthersError]( msg: string = e1.msg): ref E2 = return newException(E2, msg, e1) - -template errors*(types) {.pragma.}