Introduce Contract abstraction

This commit is contained in:
Mark Spanbroek 2022-01-20 12:56:18 +01:00
parent b965599a47
commit 04ff046553
12 changed files with 177 additions and 0 deletions

7
ethers.nim Normal file
View File

@ -0,0 +1,7 @@
import ./ethers/provider
import ./ethers/providers/jsonrpc
import ./ethers/contract
export provider
export jsonrpc
export contract

View File

@ -1,11 +1,13 @@
import pkg/chronos
import pkg/questionable
import pkg/questionable/results
import pkg/stint
import pkg/upraises
import pkg/contractabi/address
export chronos
export questionable
export results
export stint
export upraises
export address

86
ethers/contract.nim Normal file
View File

@ -0,0 +1,86 @@
import std/macros
import pkg/chronos
import pkg/contractabi
import ./basics
import ./provider
export basics
export provider
type
Contract* = ref object of RootObj
provider: Provider
address: Address
ContractError* = object of IOError
template raiseContractError(message: string) =
raise newException(ContractError, message)
proc createTxData(function: string, parameters: tuple): seq[byte] =
let selector = selector(function, typeof parameters).toArray
return @selector & AbiEncoder.encode(parameters)
proc createTx(contract: Contract,
function: string,
parameters: tuple): Transaction =
Transaction(to: contract.address, data: createTxData(function, parameters))
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[ContractType: Contract, ResultType](
contract: ContractType,
function: string,
parameters: tuple):Future[ResultType] {.async.} =
let transaction = createTx(contract, function, parameters)
let response = await contract.provider.call(transaction)
return decodeResponse(ResultType, response)
func getParameterTuple(procedure: var 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 addContractCall(procedure: var NimNode) =
let name = $procedure[0]
let parameters = procedure[3]
let contract = parameters[1][0]
let contracttype = parameters[1][1]
let resulttype = parameters[0]
let tupl = getParameterTuple(procedure)
procedure[6] = quote do:
result = await call[`contracttype`,`resulttype`](`contract`, `name`, `tupl`)
func addFuture(procedure: var NimNode) =
let returntype = procedure[3][0]
if returntype.kind == nnkEmpty:
procedure[3][0] = quote do: Future[void]
else:
procedure[3][0] = quote do: 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 new*(ContractType: type Contract,
address: Address,
provider: Provider): ContractType =
ContractType(provider: provider, address: address)
macro contract*(procedure: untyped{nkProcDef|nkMethodDef}): untyped =
let parameters = procedure[3]
let body = procedure[6]
parameters.expectMinLen(2)
body.expectKind(nnkEmpty)
var contractcall = copyNimTree(procedure)
contractcall.addContractCall()
contractcall.addFuture()
contractcall.addAsyncPragma()
contractcall

View File

@ -1,6 +1,8 @@
import ./basics
import ./transaction
export basics
export transaction
push: {.upraises: [].}
@ -9,3 +11,6 @@ type
method getBlockNumber*(provider: Provider): Future[UInt256] {.base.} =
doAssert false, "not implemented"
method call*(provider: Provider, tx: Transaction): Future[seq[byte]] {.base.} =
doAssert false, "not implemented"

View File

@ -41,3 +41,8 @@ proc listAccounts*(provider: JsonRpcProvider): Future[seq[Address]] {.async.} =
method getBlockNumber*(provider: JsonRpcProvider): Future[UInt256] {.async.} =
let client = await provider.client
return await client.eth_blockNumber()
method call*(provider: JsonRpcProvider,
tx: Transaction): Future[seq[byte]] {.async.} =
let client = await provider.client
return await client.eth_call(tx)

View File

@ -1,6 +1,7 @@
import std/os
import pkg/json_rpc/rpcclient
import ../basics
import ../transaction
import ./rpccalls/conversions
const file = currentSourcePath.parentDir / "rpccalls" / "signatures.nim"

View File

@ -1,5 +1,15 @@
import std/json
import pkg/stew/byteutils
import ../../basics
import ../../transaction
# byte sequence
func `%`*(bytes: seq[byte]): JsonNode =
%("0x" & bytes.toHex)
func fromJson*(json: JsonNode, name: string, result: var seq[byte]) =
result = hexToSeqByte(json.getStr())
# Address
@ -19,3 +29,8 @@ func `%`*(integer: UInt256): JsonNode =
func fromJson*(json: JsonNode, name: string, result: var UInt256) =
result = UInt256.fromHex(json.getStr())
# Transaction
func `%`*(tx: Transaction): JsonNode =
%{ "to": %tx.to, "data": %tx.data }

View File

@ -1,2 +1,3 @@
proc eth_accounts: seq[Address]
proc eth_blockNumber: UInt256
proc eth_call(tx: Transaction): seq[byte]

9
ethers/transaction.nim Normal file
View File

@ -0,0 +1,9 @@
import pkg/stew/byteutils
import ./basics
type Transaction* = object
to*: Address
data*: seq[byte]
func `$`*(transaction: Transaction): string =
"(to: " & $transaction.to & ", data: 0x" & $transaction.data.toHex & ")"

17
testmodule/hardhat.nim Normal file
View File

@ -0,0 +1,17 @@
import std/json
import pkg/ethers/basics
type Deployment* = object
json: JsonNode
const defaultFile = "../testnode/deployment.json"
## Reads deployment information from a json file. It expects a file that has
## been exported with Hardhat deploy. See also:
## https://github.com/wighawag/hardhat-deploy/tree/master#6-hardhat-export
proc readDeployment*(file = defaultFile): Deployment =
Deployment(json: parseFile(file))
proc address*(deployment: Deployment, contract: string|type): ?Address =
let address = deployment.json["contracts"][$contract]["address"].getStr()
Address.init(address)

View File

@ -1,3 +1,4 @@
import ./testJsonRpcProvider
import ./testContracts
{.warning[UnusedImport]:off.}

View File

@ -0,0 +1,28 @@
import pkg/asynctest
import pkg/stint
import pkg/ethers
import ./hardhat
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.}
suite "Contracts":
var token: TestToken
var provider: JsonRpcProvider
setup:
provider = JsonRpcProvider.new()
let deployment = readDeployment()
token = TestToken.new(!deployment.address(TestToken), provider)
test "can call view methods":
let accounts = await provider.listAccounts()
check (await token.totalSupply()) == 0.u256
check (await token.balanceOf(accounts[0])) == 0.u256
check (await token.allowance(accounts[0], accounts[1])) == 0.u256