Define and subscribe to solidity events
This commit is contained in:
parent
21f98c4086
commit
ff5a35aac0
|
@ -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"
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
|
@ -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"
|
||||||
|
|
|
@ -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)
|
|
@ -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 =
|
||||||
|
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
Loading…
Reference in New Issue