From ff5a35aac02c2a080c6adb37e4ed2508c5978b52 Mon Sep 17 00:00:00 2001 From: Mark Spanbroek Date: Wed, 2 Feb 2022 16:56:37 +0100 Subject: [PATCH] Define and subscribe to solidity events --- ethers.nimble | 2 +- ethers/contract.nim | 16 ++++++ ethers/events.nim | 2 +- ethers/fields.nim | 12 +++++ ethers/provider.nim | 18 +++++++ .../providers/{rpccalls => }/conversions.nim | 22 +++++++-- ethers/providers/jsonrpc.nim | 49 ++++++++++++++++++- ethers/providers/rpccalls.nim | 3 +- ethers/providers/rpccalls/signatures.nim | 2 + testmodule/testContracts.nim | 45 ++++++++++++++++- 10 files changed, 163 insertions(+), 8 deletions(-) create mode 100644 ethers/fields.nim rename ethers/providers/{rpccalls => }/conversions.nim (75%) diff --git a/ethers.nimble b/ethers.nimble index f7a0f3c..7657098 100644 --- a/ethers.nimble +++ b/ethers.nimble @@ -4,7 +4,7 @@ description = "library for interacting with Ethereum" license = "MIT" requires "chronos >= 3.0.0 & < 4.0.0" -requires "contractabi >= 0.4.1 & < 0.5.0" +requires "contractabi >= 0.4.2 & < 0.5.0" requires "questionable >= 0.10.2 & < 0.11.0" requires "upraises >= 0.1.0 & < 0.2.0" requires "json_rpc" diff --git a/ethers/contract.nim b/ethers/contract.nim index cec9054..6bf0ce6 100644 --- a/ethers/contract.nim +++ b/ethers/contract.nim @@ -5,6 +5,7 @@ import ./basics import ./provider import ./signer import ./events +import ./fields export basics export provider @@ -16,6 +17,7 @@ type signer: ?Signer address: Address ContractError* = object of EthersError + EventHandler*[E: Event] = proc(event: E) {.gcsafe, upraises:[].} func new*(ContractType: type Contract, address: Address, @@ -144,3 +146,17 @@ macro contract*(procedure: untyped{nkProcDef|nkMethodDef}): untyped = template view* {.pragma.} template pure* {.pragma.} + +proc subscribe*[E: Event](contract: Contract, + _: type E, + handler: EventHandler[E]): + Future[Subscription] = + + let topic = topic($E, E.fieldTypes).toArray + let filter = Filter(address: contract.address, topics: @[topic]) + + proc logHandler(log: Log) {.upraises: [].} = + if event =? E.decode(log.data, log.topics): + handler(event) + + contract.provider.subscribe(filter, logHandler) diff --git a/ethers/events.nim b/ethers/events.nim index c481701..7f89deb 100644 --- a/ethers/events.nim +++ b/ethers/events.nim @@ -1,10 +1,10 @@ import std/macros import pkg/contractabi import ./basics +import ./provider type Event* = object of RootObj - Topic* = array[32, byte] ValueType = uint8 | uint16 | uint32 | uint64 | UInt256 | UInt128 | int8 | int16 | int32 | int64 | Int256 | Int128 | bool | Address diff --git a/ethers/fields.nim b/ethers/fields.nim new file mode 100644 index 0000000..42b32ba --- /dev/null +++ b/ethers/fields.nim @@ -0,0 +1,12 @@ +import std/macros + +macro fieldValues*(obj: object): tuple = + result = newNimNode(nnkTupleConstr) + let typ = getTypeImpl(obj) + let fields = typ[2] + for field in fields: + let name = field[0] + result.add newDotExpr(obj, name) + +template fieldTypes*(T: type): type tuple = + typeof fieldValues(T.default) diff --git a/ethers/provider.nim b/ethers/provider.nim index 9487372..c285cdb 100644 --- a/ethers/provider.nim +++ b/ethers/provider.nim @@ -10,6 +10,15 @@ push: {.upraises: [].} type Provider* = ref object of RootObj + Subscription* = ref object of RootObj + Filter* = object + address*: Address + topics*: seq[Topic] + Log* = object + data*: seq[byte] + topics*: seq[Topic] + LogHandler* = proc(log: Log) {.gcsafe, upraises:[].} + Topic* = array[32, byte] method getBlockNumber*(provider: Provider): Future[UInt256] {.base.} = doAssert false, "not implemented" @@ -32,3 +41,12 @@ method estimateGas*(provider: Provider, method getChainId*(provider: Provider): Future[UInt256] {.base.} = doAssert false, "not implemented" + +method subscribe*(provider: Provider, + filter: Filter, + callback: LogHandler): + Future[Subscription] {.base.} = + doAssert false, "not implemented" + +method unsubscribe*(subscription: Subscription) {.base, async.} = + doAssert false, "not implemented" diff --git a/ethers/providers/rpccalls/conversions.nim b/ethers/providers/conversions.nim similarity index 75% rename from ethers/providers/rpccalls/conversions.nim rename to ethers/providers/conversions.nim index 493b061..1acd190 100644 --- a/ethers/providers/rpccalls/conversions.nim +++ b/ethers/providers/conversions.nim @@ -1,8 +1,15 @@ import std/json +import pkg/json_rpc/jsonmarshal import pkg/stew/byteutils -import ../../basics -import ../../transaction -import ../../blocktag +import ../basics +import ../transaction +import ../blocktag +import ../provider + +export jsonmarshal + +func fromJson*(T: type, json: JsonNode, name = ""): T = + fromJson(json, name, result) # byte sequence @@ -58,3 +65,12 @@ func `%`*(transaction: Transaction): JsonNode = func `%`*(blockTag: BlockTag): JsonNode = %($blockTag) + +# Log + +func fromJson*(json: JsonNode, name: string, result: var Log) = + var data: seq[byte] + var topics: seq[Topic] + fromJson(json["data"], "data", data) + fromJson(json["topics"], "topics", topics) + result = Log(data: data, topics: topics) diff --git a/ethers/providers/jsonrpc.nim b/ethers/providers/jsonrpc.nim index b44fea6..45169a3 100644 --- a/ethers/providers/jsonrpc.nim +++ b/ethers/providers/jsonrpc.nim @@ -1,9 +1,12 @@ +import std/json +import std/tables import std/uri import pkg/json_rpc/rpcclient import ../basics import ../provider import ../signer import ./rpccalls +import ./conversions export basics export provider @@ -13,6 +16,10 @@ push: {.upraises: [].} type JsonRpcProvider* = ref object of Provider client: Future[RpcClient] + subscriptions: Table[JsonNode, LogHandler] + JsonRpcSubscription = ref object of Subscription + provider: JsonRpcProvider + id: JsonNode JsonRpcSigner* = ref object of Signer provider: JsonRpcProvider address: ?Address @@ -36,8 +43,12 @@ proc connect(_: type RpcClient, url: string): Future[RpcClient] {.async.} = await client.connect(url) return client +proc handleSubscriptions(provider: JsonRpcProvider) {.async.} + proc new*(_: type JsonRpcProvider, url=defaultUrl): JsonRpcProvider = - JsonRpcProvider(client: RpcClient.connect(url)) + let provider = JsonRpcProvider(client: RpcClient.connect(url)) + asyncSpawn provider.handleSubscriptions() + provider proc send*(provider: JsonRpcProvider, call: string, @@ -87,6 +98,42 @@ method getChainId*(provider: JsonRpcProvider): Future[UInt256] {.async.} = except CatchableError: return parse(await client.net_version(), UInt256) +proc handleSubscriptions(provider: JsonRpcProvider) {.async.} = + + proc getLogHandler(id: JsonNode): ?LogHandler = + try: + if provider.subscriptions.hasKey(id): + provider.subscriptions[id].some + else: + LogHandler.none + except Exception: + LogHandler.none + + proc handleSubscription(arguments: JsonNode) {.upraises: [].} = + if id =? arguments["subscription"].catch and + handler =? getLogHandler(id) and + log =? Log.fromJson(arguments["result"]).catch: + handler(log) + + let client = await provider.client + client.setMethodHandler("eth_subscription", handleSubscription) + +method subscribe*(provider: JsonRpcProvider, + filter: Filter, + callback: LogHandler): + Future[Subscription] {.async.} = + let client = await provider.client + doAssert client of RpcWebSocketClient, "subscriptions require websockets" + let id = await client.eth_subscribe("logs", some filter) + provider.subscriptions[id] = callback + return JsonRpcSubscription(id: id, provider: provider) + +method unsubscribe*(subscription: JsonRpcSubscription) {.async.} = + let provider = subscription.provider + let client = await provider.client + discard await client.eth_unsubscribe(subscription.id) + provider.subscriptions.del(subscription.id) + # Signer method provider*(signer: JsonRpcSigner): Provider = diff --git a/ethers/providers/rpccalls.nim b/ethers/providers/rpccalls.nim index ea663cf..c2b4c0f 100644 --- a/ethers/providers/rpccalls.nim +++ b/ethers/providers/rpccalls.nim @@ -3,7 +3,8 @@ import pkg/json_rpc/rpcclient import ../basics import ../transaction import ../blocktag -import ./rpccalls/conversions +import ../provider +import ./conversions const file = currentSourcePath.parentDir / "rpccalls" / "signatures.nim" diff --git a/ethers/providers/rpccalls/signatures.nim b/ethers/providers/rpccalls/signatures.nim index 457e2ef..e882b40 100644 --- a/ethers/providers/rpccalls/signatures.nim +++ b/ethers/providers/rpccalls/signatures.nim @@ -8,3 +8,5 @@ proc eth_estimateGas(transaction: Transaction): UInt256 proc eth_chainId(): UInt256 proc eth_sendTransaction(transaction: Transaction): array[32, byte] proc eth_sign(account: Address, message: seq[byte]): seq[byte] +proc eth_subscribe(name: string, filter: ?Filter): JsonNode +proc eth_unsubscribe(id: JsonNode): bool diff --git a/testmodule/testContracts.nim b/testmodule/testContracts.nim index 6e46826..da2715c 100644 --- a/testmodule/testContracts.nim +++ b/testmodule/testContracts.nim @@ -5,9 +5,15 @@ import pkg/ethers import ./hardhat type + Erc20* = ref object of Contract TestToken = ref object of Erc20 + Transfer = object of Event + sender {.indexed.}: Address + receiver {.indexed.}: Address + value: UInt256 + 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.} @@ -23,7 +29,7 @@ suite "Contracts": var accounts: seq[Address] setup: - provider = JsonRpcProvider.new() + provider = JsonRpcProvider.new("ws://localhost:8545") snapshot = await provider.send("evm_snapshot") accounts = await provider.listAccounts() let deployment = readDeployment() @@ -77,3 +83,40 @@ suite "Contracts": check (await token.connect(provider).balanceOf(accounts[0])) == 50.u256 check (await token.connect(provider).balanceOf(accounts[1])) == 25.u256 check (await token.connect(provider).balanceOf(accounts[2])) == 25.u256 + + test "receives events when subscribed": + var transfers: seq[Transfer] + + proc handleTransfer(transfer: Transfer) = + transfers.add(transfer) + + let signer0 = provider.getSigner(accounts[0]) + let signer1 = provider.getSigner(accounts[1]) + + let subscription = await token.subscribe(Transfer, handleTransfer) + await token.connect(signer0).mint(accounts[0], 100.u256) + await token.connect(signer0).transfer(accounts[1], 50.u256) + await token.connect(signer1).transfer(accounts[2], 25.u256) + await subscription.unsubscribe() + + check transfers == @[ + Transfer(receiver: accounts[0], value: 100.u256), + Transfer(sender: accounts[0], receiver: accounts[1], value: 50.u256), + Transfer(sender: accounts[1], receiver: accounts[2], value: 25.u256) + ] + + test "stops receiving events when unsubscribed": + var transfers: seq[Transfer] + + proc handleTransfer(transfer: Transfer) = + transfers.add(transfer) + + let signer0 = provider.getSigner(accounts[0]) + + let subscription = await token.subscribe(Transfer, handleTransfer) + await token.connect(signer0).mint(accounts[0], 100.u256) + await subscription.unsubscribe() + + await token.connect(signer0).transfer(accounts[1], 50.u256) + + check transfers == @[Transfer(receiver: accounts[0], value: 100.u256)]