diff --git a/ethers.nim b/ethers.nim new file mode 100644 index 0000000..7c5af0f --- /dev/null +++ b/ethers.nim @@ -0,0 +1,7 @@ +import ./ethers/provider +import ./ethers/providers/jsonrpc +import ./ethers/contract + +export provider +export jsonrpc +export contract diff --git a/ethers/basics.nim b/ethers/basics.nim index ddd07fd..2896a96 100644 --- a/ethers/basics.nim +++ b/ethers/basics.nim @@ -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 diff --git a/ethers/contract.nim b/ethers/contract.nim new file mode 100644 index 0000000..37a93e2 --- /dev/null +++ b/ethers/contract.nim @@ -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 diff --git a/ethers/provider.nim b/ethers/provider.nim index 6714e53..5d9436d 100644 --- a/ethers/provider.nim +++ b/ethers/provider.nim @@ -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" diff --git a/ethers/providers/jsonrpc.nim b/ethers/providers/jsonrpc.nim index cbc1ef0..2f6e294 100644 --- a/ethers/providers/jsonrpc.nim +++ b/ethers/providers/jsonrpc.nim @@ -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) diff --git a/ethers/providers/rpccalls.nim b/ethers/providers/rpccalls.nim index c97b77b..f198e1c 100644 --- a/ethers/providers/rpccalls.nim +++ b/ethers/providers/rpccalls.nim @@ -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" diff --git a/ethers/providers/rpccalls/conversions.nim b/ethers/providers/rpccalls/conversions.nim index 6f75b02..d320a1c 100644 --- a/ethers/providers/rpccalls/conversions.nim +++ b/ethers/providers/rpccalls/conversions.nim @@ -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 } diff --git a/ethers/providers/rpccalls/signatures.nim b/ethers/providers/rpccalls/signatures.nim index a439c3e..3fdc397 100644 --- a/ethers/providers/rpccalls/signatures.nim +++ b/ethers/providers/rpccalls/signatures.nim @@ -1,2 +1,3 @@ proc eth_accounts: seq[Address] proc eth_blockNumber: UInt256 +proc eth_call(tx: Transaction): seq[byte] diff --git a/ethers/transaction.nim b/ethers/transaction.nim new file mode 100644 index 0000000..fd2413a --- /dev/null +++ b/ethers/transaction.nim @@ -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 & ")" diff --git a/testmodule/hardhat.nim b/testmodule/hardhat.nim new file mode 100644 index 0000000..55bd79f --- /dev/null +++ b/testmodule/hardhat.nim @@ -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) diff --git a/testmodule/test.nim b/testmodule/test.nim index 8504cbf..0dff5da 100644 --- a/testmodule/test.nim +++ b/testmodule/test.nim @@ -1,3 +1,4 @@ import ./testJsonRpcProvider +import ./testContracts {.warning[UnusedImport]:off.} diff --git a/testmodule/testContracts.nim b/testmodule/testContracts.nim new file mode 100644 index 0000000..720db2b --- /dev/null +++ b/testmodule/testContracts.nim @@ -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