Allow calls to non-constant functions

This commit is contained in:
Mark Spanbroek 2022-01-25 17:17:43 +01:00
parent 82116d3b14
commit e4224a1241
4 changed files with 120 additions and 14 deletions

View File

@ -3,6 +3,7 @@ import pkg/chronos
import pkg/contractabi import pkg/contractabi
import ./basics import ./basics
import ./provider import ./provider
import ./signer
export basics export basics
export provider export provider
@ -10,9 +11,19 @@ export provider
type type
Contract* = ref object of RootObj Contract* = ref object of RootObj
provider: Provider provider: Provider
signer: ?Signer
address: Address address: Address
ContractError* = object of EthersError 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) = template raiseContractError(message: string) =
raise newException(ContractError, message) raise newException(ContractError, message)
@ -30,13 +41,32 @@ proc decodeResponse(T: type, bytes: seq[byte]): T =
raiseContractError "unable to decode return value as " & $T raiseContractError "unable to decode return value as " & $T
return decoded return decoded
proc call[ContractType: Contract, ResultType]( proc call[ContractType: Contract, ReturnType](
contract: ContractType, contract: ContractType,
function: string, function: string,
parameters: tuple):Future[ResultType] {.async.} = parameters: tuple): Future[ReturnType] {.async.} =
let transaction = createTx(contract, function, parameters) let transaction = createTx(contract, function, parameters)
let response = await contract.provider.call(transaction) 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 = func getParameterTuple(procedure: var NimNode): NimNode =
let parameters = procedure[3] let parameters = procedure[3]
@ -46,22 +76,46 @@ func getParameterTuple(procedure: var NimNode): NimNode =
tupl.add name tupl.add name
return tupl 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) = func addContractCall(procedure: var NimNode) =
let name = procedure[0] let name = procedure[0]
let function = if name.kind == nnkPostfix: $name[1] else: $name let function = if name.kind == nnkPostfix: $name[1] else: $name
let parameters = procedure[3] let parameters = procedure[3]
let contract = parameters[1][0] let contract = parameters[1][0]
let contracttype = parameters[1][1] let contracttype = parameters[1][1]
let resulttype = parameters[0] let returntype = parameters[0]
let tupl = getParameterTuple(procedure) let tupl = getParameterTuple(procedure)
if procedure.isConstant:
if returntype.kind == nnkEmpty:
procedure[6] = quote do: procedure[6] = quote do:
return await call[`contracttype`,`resulttype`](`contract`, `function`, `tupl`) 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) = func addFuture(procedure: var NimNode) =
let returntype = procedure[3][0] let returntype = procedure[3][0]
if returntype.kind == nnkEmpty: if returntype.kind != nnkEmpty:
procedure[3][0] = quote do: Future[void]
else:
procedure[3][0] = quote do: Future[`returntype`] procedure[3][0] = quote do: Future[`returntype`]
func addAsyncPragma(procedure: var NimNode) = func addAsyncPragma(procedure: var NimNode) =
@ -75,11 +129,30 @@ func new*(ContractType: type Contract,
provider: Provider): ContractType = provider: Provider): ContractType =
ContractType(provider: provider, address: address) 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 = macro contract*(procedure: untyped{nkProcDef|nkMethodDef}): untyped =
let parameters = procedure[3] let parameters = procedure[3]
let body = procedure[6] let body = procedure[6]
parameters.expectMinLen(2) parameters.expectMinLen(2)
body.expectKind(nnkEmpty) body.expectKind(nnkEmpty)
procedure.checkReturnType()
var contractcall = copyNimTree(procedure) var contractcall = copyNimTree(procedure)
contractcall.addContractCall() contractcall.addContractCall()
contractcall.addFuture() contractcall.addFuture()

View File

@ -101,3 +101,8 @@ method getAddress*(signer: JsonRpcSigner): Future[Address] {.async.} =
return accounts[0] return accounts[0]
raiseProviderError "no address found" raiseProviderError "no address found"
method sendTransaction*(signer: JsonRpcSigner,
transaction: Transaction) {.async.} =
let client = await signer.provider.client
discard await client.eth_sendTransaction(transaction)

View File

@ -15,6 +15,10 @@ method provider*(signer: Signer): Provider {.base.} =
method getAddress*(signer: Signer): Future[Address] {.base.} = method getAddress*(signer: Signer): Future[Address] {.base.} =
doAssert false, "not implemented" doAssert false, "not implemented"
method sendTransaction*(signer: Signer,
transaction: Transaction) {.base, async.} =
doAssert false, "not implemented"
method getGasPrice*(signer: Signer): Future[UInt256] {.base.} = method getGasPrice*(signer: Signer): Future[UInt256] {.base.} =
signer.provider.getGasPrice() signer.provider.getGasPrice()

View File

@ -8,27 +8,51 @@ type
Erc20* = ref object of Contract Erc20* = ref object of Contract
TestToken = ref object of Erc20 TestToken = ref object of Erc20
method totalSupply*(erc20: Erc20): UInt256 {.base, contract.} method totalSupply*(erc20: Erc20): UInt256 {.base, contract, view.}
method balanceOf*(erc20: Erc20, account: Address): UInt256 {.base, contract.} method balanceOf*(erc20: Erc20, account: Address): UInt256 {.base, contract, view.}
method allowance*(erc20: Erc20, owner, spender: Address): UInt256 {.base, contract.} method allowance*(erc20: Erc20, owner, spender: Address): UInt256 {.base, contract, view.}
method mint(token: TestToken, holder: Address, amount: UInt256) {.base, contract.}
suite "Contracts": suite "Contracts":
var token: TestToken var token: TestToken
var provider: JsonRpcProvider var provider: JsonRpcProvider
var snapshot: JsonNode var snapshot: JsonNode
var accounts: seq[Address]
setup: setup:
provider = JsonRpcProvider.new() provider = JsonRpcProvider.new()
snapshot = await provider.send("evm_snapshot") snapshot = await provider.send("evm_snapshot")
accounts = await provider.listAccounts()
let deployment = readDeployment() let deployment = readDeployment()
token = TestToken.new(!deployment.address(TestToken), provider) token = TestToken.new(!deployment.address(TestToken), provider)
teardown: teardown:
discard await provider.send("evm_revert", @[snapshot]) discard await provider.send("evm_revert", @[snapshot])
test "can call view methods": test "can call constant functions":
let accounts = await provider.listAccounts()
check (await token.totalSupply()) == 0.u256 check (await token.totalSupply()) == 0.u256
check (await token.balanceOf(accounts[0])) == 0.u256 check (await token.balanceOf(accounts[0])) == 0.u256
check (await token.allowance(accounts[0], accounts[1])) == 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