Query past contract events (#51)

Based on ethers.js's queryFilter, allows querying of past contract events, by querying the logs for a contract's event topic.

* queryFilter to query past logs
* Allow querying of past block log events
* Can query by block number or block hash
This commit is contained in:
Eric 2023-07-20 15:51:28 +10:00 committed by GitHub
parent c49311fca2
commit 12d7a35203
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 143 additions and 11 deletions

View File

@ -1,4 +1,6 @@
import std/json
import std/macros import std/macros
import std/sequtils
import pkg/chronos import pkg/chronos
import pkg/contractabi import pkg/contractabi
import ./basics import ./basics
@ -227,7 +229,7 @@ proc subscribe*[E: Event](contract: Contract,
Future[Subscription] = Future[Subscription] =
let topic = topic($E, E.fieldTypes).toArray let topic = topic($E, E.fieldTypes).toArray
let filter = Filter(address: contract.address, topics: @[topic]) let filter = EventFilter(address: contract.address, topics: @[topic])
proc logHandler(log: Log) {.upraises: [].} = proc logHandler(log: Log) {.upraises: [].} =
if event =? E.decode(log.data, log.topics): if event =? E.decode(log.data, log.topics):
@ -252,3 +254,54 @@ proc confirm*(tx: Future[?TransactionResponse],
) )
return await response.confirm(confirmations, timeout) return await response.confirm(confirmations, timeout)
proc queryFilter[E: Event](contract: Contract,
_: type E,
filter: EventFilter):
Future[seq[E]] {.async.} =
var logs = await contract.provider.getLogs(filter)
logs.keepItIf(not it.removed)
var events: seq[E] = @[]
for log in logs:
if event =? E.decode(log.data, log.topics):
events.add event
return events
proc queryFilter*[E: Event](contract: Contract,
_: type E):
Future[seq[E]] =
let topic = topic($E, E.fieldTypes).toArray
let filter = EventFilter(address: contract.address,
topics: @[topic])
contract.queryFilter(E, filter)
proc queryFilter*[E: Event](contract: Contract,
_: type E,
blockHash: BlockHash):
Future[seq[E]] =
let topic = topic($E, E.fieldTypes).toArray
let filter = FilterByBlockHash(address: contract.address,
topics: @[topic],
blockHash: blockHash)
contract.queryFilter(E, filter)
proc queryFilter*[E: Event](contract: Contract,
_: type E,
fromBlock: BlockTag,
toBlock: BlockTag):
Future[seq[E]] =
let topic = topic($E, E.fieldTypes).toArray
let filter = Filter(address: contract.address,
topics: @[topic],
fromBlock: fromBlock,
toBlock: toBlock)
contract.queryFilter(E, filter)

View File

@ -12,11 +12,19 @@ type
Provider* = ref object of RootObj Provider* = ref object of RootObj
ProviderError* = object of EthersError ProviderError* = object of EthersError
Subscription* = ref object of RootObj Subscription* = ref object of RootObj
Filter* = object EventFilter* = ref object of RootObj
address*: Address address*: Address
topics*: seq[Topic] topics*: seq[Topic]
Filter* = ref object of EventFilter
fromBlock*: BlockTag
toBlock*: BlockTag
FilterByBlockHash* = ref object of EventFilter
blockHash*: BlockHash
Log* = object Log* = object
blockNumber*: UInt256
data*: seq[byte] data*: seq[byte]
logIndex*: UInt256
removed*: bool
topics*: seq[Topic] topics*: seq[Topic]
TransactionHash* = array[32, byte] TransactionHash* = array[32, byte]
BlockHash* = array[32, byte] BlockHash* = array[32, byte]
@ -81,6 +89,10 @@ method sendTransaction*(provider: Provider,
Future[TransactionResponse] {.base.} = Future[TransactionResponse] {.base.} =
doAssert false, "not implemented" doAssert false, "not implemented"
method getLogs*(provider: Provider,
filter: EventFilter): Future[seq[Log]] {.base.} =
doAssert false, "not implemented"
method estimateGas*(provider: Provider, method estimateGas*(provider: Provider,
transaction: Transaction): Future[UInt256] {.base.} = transaction: Transaction): Future[UInt256] {.base.} =
doAssert false, "not implemented" doAssert false, "not implemented"
@ -89,7 +101,7 @@ method getChainId*(provider: Provider): Future[UInt256] {.base.} =
doAssert false, "not implemented" doAssert false, "not implemented"
method subscribe*(provider: Provider, method subscribe*(provider: Provider,
filter: Filter, filter: EventFilter,
callback: LogHandler): callback: LogHandler):
Future[Subscription] {.base.} = Future[Subscription] {.base.} =
doAssert false, "not implemented" doAssert false, "not implemented"

View File

@ -144,6 +144,25 @@ method getTransactionReceipt*(provider: JsonRpcProvider,
let client = await provider.client let client = await provider.client
return await client.eth_getTransactionReceipt(txHash) return await client.eth_getTransactionReceipt(txHash)
method getLogs*(provider: JsonRpcProvider,
filter: EventFilter):
Future[seq[Log]] {.async.} =
convertError:
let client = await provider.client
let logsJson = if filter of Filter:
await client.eth_getLogs(Filter(filter))
elif filter of FilterByBlockHash:
await client.eth_getLogs(FilterByBlockHash(filter))
else:
await client.eth_getLogs(filter)
var logs: seq[Log] = @[]
for logJson in logsJson.getElems:
if log =? Log.fromJson(logJson).catch:
logs.add log
return logs
method estimateGas*(provider: JsonRpcProvider, method estimateGas*(provider: JsonRpcProvider,
transaction: Transaction): Future[UInt256] {.async.} = transaction: Transaction): Future[UInt256] {.async.} =
convertError: convertError:
@ -167,7 +186,7 @@ method sendTransaction*(provider: JsonRpcProvider, rawTransaction: seq[byte]): F
return TransactionResponse(hash: hash, provider: provider) return TransactionResponse(hash: hash, provider: provider)
method subscribe*(provider: JsonRpcProvider, method subscribe*(provider: JsonRpcProvider,
filter: Filter, filter: EventFilter,
onLog: LogHandler): onLog: LogHandler):
Future[Subscription] {.async.} = Future[Subscription] {.async.} =
convertError: convertError:

View File

@ -4,6 +4,7 @@ proc eth_blockNumber: UInt256
proc eth_call(transaction: Transaction, blockTag: BlockTag): seq[byte] proc eth_call(transaction: Transaction, blockTag: BlockTag): seq[byte]
proc eth_gasPrice(): UInt256 proc eth_gasPrice(): UInt256
proc eth_getBlockByNumber(blockTag: BlockTag, includeTransactions: bool): ?Block proc eth_getBlockByNumber(blockTag: BlockTag, includeTransactions: bool): ?Block
proc eth_getLogs(filter: EventFilter | Filter | FilterByBlockHash): JsonNode
proc eth_getBlockByHash(hash: BlockHash, includeTransactions: bool): ?Block proc eth_getBlockByHash(hash: BlockHash, includeTransactions: bool): ?Block
proc eth_getTransactionCount(address: Address, blockTag: BlockTag): UInt256 proc eth_getTransactionCount(address: Address, blockTag: BlockTag): UInt256
proc eth_estimateGas(transaction: Transaction): UInt256 proc eth_estimateGas(transaction: Transaction): UInt256
@ -12,10 +13,10 @@ proc eth_sendTransaction(transaction: Transaction): TransactionHash
proc eth_sendRawTransaction(data: seq[byte]): TransactionHash proc eth_sendRawTransaction(data: seq[byte]): TransactionHash
proc eth_getTransactionReceipt(hash: TransactionHash): ?TransactionReceipt proc eth_getTransactionReceipt(hash: TransactionHash): ?TransactionReceipt
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_subscribe(name: string, filter: EventFilter): JsonNode
proc eth_subscribe(name: string): JsonNode proc eth_subscribe(name: string): JsonNode
proc eth_unsubscribe(id: JsonNode): bool proc eth_unsubscribe(id: JsonNode): bool
proc eth_newBlockFilter(): JsonNode proc eth_newBlockFilter(): JsonNode
proc eth_newFilter(filter: Filter): JsonNode proc eth_newFilter(filter: EventFilter): JsonNode
proc eth_getFilterChanges(id: JsonNode): JsonNode proc eth_getFilterChanges(id: JsonNode): JsonNode
proc eth_uninstallFilter(id: JsonNode): bool proc eth_uninstallFilter(id: JsonNode): bool

View File

@ -21,7 +21,7 @@ method subscribeBlocks*(subscriptions: JsonRpcSubscriptions,
raiseAssert "not implemented" raiseAssert "not implemented"
method subscribeLogs*(subscriptions: JsonRpcSubscriptions, method subscribeLogs*(subscriptions: JsonRpcSubscriptions,
filter: Filter, filter: EventFilter,
onLog: LogHandler): onLog: LogHandler):
Future[JsonNode] Future[JsonNode]
{.async, base.} = {.async, base.} =
@ -74,7 +74,7 @@ method subscribeBlocks(subscriptions: WebSocketSubscriptions,
return id return id
method subscribeLogs(subscriptions: WebSocketSubscriptions, method subscribeLogs(subscriptions: WebSocketSubscriptions,
filter: Filter, filter: EventFilter,
onLog: LogHandler): onLog: LogHandler):
Future[JsonNode] Future[JsonNode]
{.async.} = {.async.} =
@ -148,7 +148,7 @@ method subscribeBlocks(subscriptions: PollingSubscriptions,
return id return id
method subscribeLogs(subscriptions: PollingSubscriptions, method subscribeLogs(subscriptions: PollingSubscriptions,
filter: Filter, filter: EventFilter,
onLog: LogHandler): onLog: LogHandler):
Future[JsonNode] Future[JsonNode]
{.async.} = {.async.} =

View File

@ -188,3 +188,50 @@ for url in ["ws://localhost:8545", "http://localhost:8545"]:
await provider.mineBlocks(2) # two additional blocks await provider.mineBlocks(2) # two additional blocks
let receipt = await confirming let receipt = await confirming
check receipt.blockNumber.isSome check receipt.blockNumber.isSome
test "can query last block event log":
let signer0 = provider.getSigner(accounts[0])
discard await token.connect(signer0).mint(accounts[0], 100.u256)
discard await token.connect(signer0).transfer(accounts[1], 50.u256)
let logs = await token.queryFilter(Transfer)
check eventually logs == @[
Transfer(sender: accounts[0], receiver: accounts[1], value: 50.u256)
]
test "can query past event logs by specifying from and to blocks":
let signer0 = provider.getSigner(accounts[0])
let signer1 = provider.getSigner(accounts[1])
discard await token.connect(signer0).mint(accounts[0], 100.u256)
discard await token.connect(signer0).transfer(accounts[1], 50.u256)
discard await token.connect(signer1).transfer(accounts[2], 25.u256)
let currentBlock = await provider.getBlockNumber()
let logs = await token.queryFilter(Transfer,
BlockTag.init(currentBlock - 1),
BlockTag.latest)
check logs == @[
Transfer(sender: accounts[0], receiver: accounts[1], value: 50.u256),
Transfer(sender: accounts[1], receiver: accounts[2], value: 25.u256)
]
test "can query past event logs by specifying a block hash":
let signer0 = provider.getSigner(accounts[0])
let receipt = await token.connect(signer0)
.mint(accounts[0], 100.u256)
.confirm(1)
discard await token.connect(signer0).transfer(accounts[1], 50.u256)
let logs = await token.queryFilter(Transfer, !receipt.blockHash)
check logs == @[
Transfer(receiver: accounts[0], value: 100.u256)
]