Define and subscribe to solidity events

This commit is contained in:
Mark Spanbroek 2022-02-02 16:56:37 +01:00 committed by markspanbroek
parent 21f98c4086
commit ff5a35aac0
10 changed files with 163 additions and 8 deletions

View File

@ -4,7 +4,7 @@ description = "library for interacting with Ethereum"
license = "MIT" license = "MIT"
requires "chronos >= 3.0.0 & < 4.0.0" 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 "questionable >= 0.10.2 & < 0.11.0"
requires "upraises >= 0.1.0 & < 0.2.0" requires "upraises >= 0.1.0 & < 0.2.0"
requires "json_rpc" requires "json_rpc"

View File

@ -5,6 +5,7 @@ import ./basics
import ./provider import ./provider
import ./signer import ./signer
import ./events import ./events
import ./fields
export basics export basics
export provider export provider
@ -16,6 +17,7 @@ type
signer: ?Signer signer: ?Signer
address: Address address: Address
ContractError* = object of EthersError ContractError* = object of EthersError
EventHandler*[E: Event] = proc(event: E) {.gcsafe, upraises:[].}
func new*(ContractType: type Contract, func new*(ContractType: type Contract,
address: Address, address: Address,
@ -144,3 +146,17 @@ macro contract*(procedure: untyped{nkProcDef|nkMethodDef}): untyped =
template view* {.pragma.} template view* {.pragma.}
template pure* {.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)

View File

@ -1,10 +1,10 @@
import std/macros import std/macros
import pkg/contractabi import pkg/contractabi
import ./basics import ./basics
import ./provider
type type
Event* = object of RootObj Event* = object of RootObj
Topic* = array[32, byte]
ValueType = uint8 | uint16 | uint32 | uint64 | UInt256 | UInt128 | ValueType = uint8 | uint16 | uint32 | uint64 | UInt256 | UInt128 |
int8 | int16 | int32 | int64 | Int256 | Int128 | int8 | int16 | int32 | int64 | Int256 | Int128 |
bool | Address bool | Address

12
ethers/fields.nim Normal file
View File

@ -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)

View File

@ -10,6 +10,15 @@ push: {.upraises: [].}
type type
Provider* = ref object of RootObj 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.} = method getBlockNumber*(provider: Provider): Future[UInt256] {.base.} =
doAssert false, "not implemented" doAssert false, "not implemented"
@ -32,3 +41,12 @@ method estimateGas*(provider: Provider,
method getChainId*(provider: Provider): Future[UInt256] {.base.} = method getChainId*(provider: Provider): Future[UInt256] {.base.} =
doAssert false, "not implemented" 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"

View File

@ -1,8 +1,15 @@
import std/json import std/json
import pkg/json_rpc/jsonmarshal
import pkg/stew/byteutils import pkg/stew/byteutils
import ../../basics import ../basics
import ../../transaction import ../transaction
import ../../blocktag import ../blocktag
import ../provider
export jsonmarshal
func fromJson*(T: type, json: JsonNode, name = ""): T =
fromJson(json, name, result)
# byte sequence # byte sequence
@ -58,3 +65,12 @@ func `%`*(transaction: Transaction): JsonNode =
func `%`*(blockTag: BlockTag): JsonNode = func `%`*(blockTag: BlockTag): JsonNode =
%($blockTag) %($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)

View File

@ -1,9 +1,12 @@
import std/json
import std/tables
import std/uri import std/uri
import pkg/json_rpc/rpcclient import pkg/json_rpc/rpcclient
import ../basics import ../basics
import ../provider import ../provider
import ../signer import ../signer
import ./rpccalls import ./rpccalls
import ./conversions
export basics export basics
export provider export provider
@ -13,6 +16,10 @@ push: {.upraises: [].}
type type
JsonRpcProvider* = ref object of Provider JsonRpcProvider* = ref object of Provider
client: Future[RpcClient] client: Future[RpcClient]
subscriptions: Table[JsonNode, LogHandler]
JsonRpcSubscription = ref object of Subscription
provider: JsonRpcProvider
id: JsonNode
JsonRpcSigner* = ref object of Signer JsonRpcSigner* = ref object of Signer
provider: JsonRpcProvider provider: JsonRpcProvider
address: ?Address address: ?Address
@ -36,8 +43,12 @@ proc connect(_: type RpcClient, url: string): Future[RpcClient] {.async.} =
await client.connect(url) await client.connect(url)
return client return client
proc handleSubscriptions(provider: JsonRpcProvider) {.async.}
proc new*(_: type JsonRpcProvider, url=defaultUrl): JsonRpcProvider = 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, proc send*(provider: JsonRpcProvider,
call: string, call: string,
@ -87,6 +98,42 @@ method getChainId*(provider: JsonRpcProvider): Future[UInt256] {.async.} =
except CatchableError: except CatchableError:
return parse(await client.net_version(), UInt256) 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 # Signer
method provider*(signer: JsonRpcSigner): Provider = method provider*(signer: JsonRpcSigner): Provider =

View File

@ -3,7 +3,8 @@ import pkg/json_rpc/rpcclient
import ../basics import ../basics
import ../transaction import ../transaction
import ../blocktag import ../blocktag
import ./rpccalls/conversions import ../provider
import ./conversions
const file = currentSourcePath.parentDir / "rpccalls" / "signatures.nim" const file = currentSourcePath.parentDir / "rpccalls" / "signatures.nim"

View File

@ -8,3 +8,5 @@ proc eth_estimateGas(transaction: Transaction): UInt256
proc eth_chainId(): UInt256 proc eth_chainId(): UInt256
proc eth_sendTransaction(transaction: Transaction): array[32, byte] proc eth_sendTransaction(transaction: Transaction): array[32, byte]
proc eth_sign(account: Address, message: seq[byte]): seq[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

View File

@ -5,9 +5,15 @@ import pkg/ethers
import ./hardhat import ./hardhat
type type
Erc20* = ref object of Contract Erc20* = ref object of Contract
TestToken = ref object of Erc20 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 totalSupply*(erc20: Erc20): UInt256 {.base, contract, view.}
method balanceOf*(erc20: Erc20, account: Address): 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 allowance*(erc20: Erc20, owner, spender: Address): UInt256 {.base, contract, view.}
@ -23,7 +29,7 @@ suite "Contracts":
var accounts: seq[Address] var accounts: seq[Address]
setup: setup:
provider = JsonRpcProvider.new() provider = JsonRpcProvider.new("ws://localhost:8545")
snapshot = await provider.send("evm_snapshot") snapshot = await provider.send("evm_snapshot")
accounts = await provider.listAccounts() accounts = await provider.listAccounts()
let deployment = readDeployment() 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[0])) == 50.u256
check (await token.connect(provider).balanceOf(accounts[1])) == 25.u256 check (await token.connect(provider).balanceOf(accounts[1])) == 25.u256
check (await token.connect(provider).balanceOf(accounts[2])) == 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)]