diff --git a/ethers/contract.nim b/ethers/contract.nim index 7e79f5c..382f6d4 100644 --- a/ethers/contract.nim +++ b/ethers/contract.nim @@ -3,6 +3,7 @@ import pkg/chronos import pkg/contractabi import ./basics import ./provider +import ./signer export basics export provider @@ -10,9 +11,19 @@ export provider type Contract* = ref object of RootObj provider: Provider + signer: ?Signer address: Address ContractError* = object of EthersError +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) @@ -30,13 +41,32 @@ proc decodeResponse(T: type, bytes: seq[byte]): T = raiseContractError "unable to decode return value as " & $T return decoded -proc call[ContractType: Contract, ResultType]( +proc call[ContractType: Contract, ReturnType]( contract: ContractType, function: string, - parameters: tuple):Future[ResultType] {.async.} = + parameters: tuple): Future[ReturnType] {.async.} = let transaction = createTx(contract, function, parameters) let response = await contract.provider.call(transaction) - return decodeResponse(ResultType, response) + return decodeResponse(ReturnType, response) + +proc callNoResult[ContractType: Contract]( + contract: ContractType, + function: string, + parameters: tuple) {.async.} = + let transaction = createTx(contract, function, parameters) + discard await contract.provider.call(transaction) + +proc send[ContractType: Contract]( + contract: ContractType, + function: string, + parameters: tuple) {.async.} = + + without signer =? contract.signer: + raiseContractError "trying to send transaction without a signer" + + let transaction = createTx(contract, function, parameters) + let populated = await signer.populateTransaction(transaction) + await signer.sendTransaction(populated) func getParameterTuple(procedure: var NimNode): NimNode = let parameters = procedure[3] @@ -46,22 +76,46 @@ func getParameterTuple(procedure: var NimNode): NimNode = 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 + elif pragma.eqIdent "constant": + return true + false + func addContractCall(procedure: var NimNode) = let name = procedure[0] let function = if name.kind == nnkPostfix: $name[1] else: $name let parameters = procedure[3] let contract = parameters[1][0] let contracttype = parameters[1][1] - let resulttype = parameters[0] + let returntype = parameters[0] let tupl = getParameterTuple(procedure) - procedure[6] = quote do: - return await call[`contracttype`,`resulttype`](`contract`, `function`, `tupl`) + if procedure.isConstant: + if returntype.kind == nnkEmpty: + procedure[6] = quote do: + await callNoResult[`contracttype`]( + `contract`, `function`, `tupl` + ) + else: + procedure[6] = quote do: + return await call[`contracttype`,`returntype`]( + `contract`, `function`, `tupl` + ) + else: + procedure[6] = quote do: + if `contract`.signer.isSome: + await send[`contracttype`](`contract`, `function`, `tupl`) + else: + await callNoResult[`contracttype`](`contract`, `function`, `tupl`) func addFuture(procedure: var NimNode) = let returntype = procedure[3][0] - if returntype.kind == nnkEmpty: - procedure[3][0] = quote do: Future[void] - else: + if returntype.kind != nnkEmpty: procedure[3][0] = quote do: Future[`returntype`] func addAsyncPragma(procedure: var NimNode) = @@ -75,11 +129,30 @@ func new*(ContractType: type Contract, 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) + +template view* {.pragma.} +template pure* {.pragma.} +template constant* {.pragma.} + +func checkReturnType(procedure: NimNode) = + let parameters = procedure[3] + let returntype = parameters[0] + if returntype.kind != nnkEmpty and not procedure.isConstant: + const message = + "only contract functions with {.constant.}, {.pure.} or {.view.} " & + "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) body.expectKind(nnkEmpty) + procedure.checkReturnType() var contractcall = copyNimTree(procedure) contractcall.addContractCall() contractcall.addFuture() diff --git a/ethers/providers/jsonrpc.nim b/ethers/providers/jsonrpc.nim index 9c95e29..cecd4d7 100644 --- a/ethers/providers/jsonrpc.nim +++ b/ethers/providers/jsonrpc.nim @@ -101,3 +101,8 @@ method getAddress*(signer: JsonRpcSigner): Future[Address] {.async.} = return accounts[0] raiseProviderError "no address found" + +method sendTransaction*(signer: JsonRpcSigner, + transaction: Transaction) {.async.} = + let client = await signer.provider.client + discard await client.eth_sendTransaction(transaction) diff --git a/ethers/signer.nim b/ethers/signer.nim index 200db89..2d6156e 100644 --- a/ethers/signer.nim +++ b/ethers/signer.nim @@ -15,6 +15,10 @@ method provider*(signer: Signer): Provider {.base.} = method getAddress*(signer: Signer): Future[Address] {.base.} = doAssert false, "not implemented" +method sendTransaction*(signer: Signer, + transaction: Transaction) {.base, async.} = + doAssert false, "not implemented" + method getGasPrice*(signer: Signer): Future[UInt256] {.base.} = signer.provider.getGasPrice() diff --git a/testmodule/testContracts.nim b/testmodule/testContracts.nim index bb655ec..09909ab 100644 --- a/testmodule/testContracts.nim +++ b/testmodule/testContracts.nim @@ -8,27 +8,51 @@ type Erc20* = ref object of Contract TestToken = ref object of Erc20 -method totalSupply*(erc20: Erc20): UInt256 {.base, contract.} -method balanceOf*(erc20: Erc20, account: Address): UInt256 {.base, contract.} -method allowance*(erc20: Erc20, owner, spender: Address): UInt256 {.base, contract.} +method totalSupply*(erc20: Erc20): UInt256 {.base, contract, view.} +method balanceOf*(erc20: Erc20, account: Address): UInt256 {.base, contract, view.} +method allowance*(erc20: Erc20, owner, spender: Address): UInt256 {.base, contract, view.} + +method mint(token: TestToken, holder: Address, amount: UInt256) {.base, contract.} suite "Contracts": var token: TestToken var provider: JsonRpcProvider var snapshot: JsonNode + var accounts: seq[Address] setup: provider = JsonRpcProvider.new() snapshot = await provider.send("evm_snapshot") + accounts = await provider.listAccounts() let deployment = readDeployment() token = TestToken.new(!deployment.address(TestToken), provider) teardown: discard await provider.send("evm_revert", @[snapshot]) - test "can call view methods": - let accounts = await provider.listAccounts() + test "can call constant functions": check (await token.totalSupply()) == 0.u256 check (await token.balanceOf(accounts[0])) == 0.u256 check (await token.allowance(accounts[0], accounts[1])) == 0.u256 + + test "can call non-constant functions": + token = TestToken.new(token.address, provider.getSigner()) + await token.mint(accounts[1], 100.u256) + check (await token.totalSupply()) == 100.u256 + check (await token.balanceOf(accounts[1])) == 100.u256 + + test "can call non-constant functions without a signer": + await token.mint(accounts[1], 100.u256) + check (await token.balanceOf(accounts[1])) == 0.u256 + + test "can call constant functions without a return type": + token = TestToken.new(token.address, provider.getSigner()) + proc mint(token: TestToken, holder: Address, amount: UInt256) {.contract, view.} + await mint(token, accounts[1], 100.u256) + check (await balanceOf(token, accounts[1])) == 0.u256 + + test "fails to compile when non-constant function has a return type": + let works = compiles: + proc foo(token: TestToken, bar: Address): UInt256 {.contract.} + check not works