From 39ccad8988b54f7cc25ad68a3170d03ec5a0fd3e Mon Sep 17 00:00:00 2001 From: Maxence Raballand Date: Sun, 7 Apr 2024 23:36:08 +0200 Subject: [PATCH] feat: add fromBlock param to watch event functions (#2082) * Added fromBlock param to watch event functions * grammar fix * Fix watchEvent and add coverage * Fix watchContractEvent and add coverage * chosre: changeset * fix changeset * Update gold-papayas-wait.md * chore: del bun file --------- Co-authored-by: jxom --- .changeset/gold-papayas-wait.md | 5 + bun | 0 bun.lockb | Bin 535084 -> 535084 bytes site/pages/docs/actions/public/watchEvent.md | 17 +++ .../pages/docs/contract/watchContractEvent.md | 15 +++ src/actions/public/watchContractEvent.test.ts | 127 ++++++++++++++++++ src/actions/public/watchContractEvent.ts | 7 + src/actions/public/watchEvent.test.ts | 99 ++++++++++++++ src/actions/public/watchEvent.ts | 7 + 9 files changed, 277 insertions(+) create mode 100644 .changeset/gold-papayas-wait.md delete mode 100644 bun diff --git a/.changeset/gold-papayas-wait.md b/.changeset/gold-papayas-wait.md new file mode 100644 index 0000000000..d3b66f39ba --- /dev/null +++ b/.changeset/gold-papayas-wait.md @@ -0,0 +1,5 @@ +--- +"viem": patch +--- + +Added `fromBlock` parameter to `watchEvent` and `watchContractEvent`. diff --git a/bun b/bun deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/bun.lockb b/bun.lockb index fda592d3a779d9ec37b42e26d1200c890c4c80b0..e6cf73600b52f58374b127f6e0143ca922a9040b 100755 GIT binary patch delta 9269 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 TcOUybboXI=`Q-Yr@0|a0i|gz^ delta 9268 zcmZA5cf9p0NGg9-$2L{N#|=lgllS0Dd4dB47&-#M@Q$IZE~bM9qV9Dmsr z$6xFof5WBI+2QG5x%{T{x)U$lcW8L>HD`VEg#8y**JL+dbn3O2pK(t3)o|~ZuiU$5 zXSs6k8M}nv5;d$4eOs{s<2zb0v|wJPxPdKLS1Tsafqji)3OzWpVg>`a*DB^Pf_I%_ z0TcMwE0!=r@Lk0U76`wmSi=g@_Z1s3exMaY3+AG@fh|}+R7{`)`v%1ndT?%3%wPcb zM~XR&;Qd&!fC>DcD3&loaFb#M3xum;4J$-HRcyq52gMY6aPFv>!2s@^6muBCyR%{e6Zm&gEMbPgR;*xw z@UDtAtPmZq*nn|2tr%J`Pf*;z7OWE$6X?LcyJ8AGIQLM@U;x)q%wYuYo{9xb;NMHJ zgc*W+D^{>Tcpt?YR*3Ga*nn|Atr%J`@2|LlEm*E%0v*^7P)wl*=YfhD4B$RUF^3Vn z2P+mZf&UQ25@rY2VoJT8W zFo63Q#T-WP9;;Zu1ir6W!VJOV6f0ODJV~*J6{5#0HeftKD~1-#Cn|1W3)Yhq6X?Kx zvSJE7IDujY1GrC7%wYuYsfqTc(P&*D@3O#HeftMD~1-#P;moW zu%4-yKnM1ziYfHqoTiw;0Pg9EIgH>vOR<0n{AVkcFhlSh#R?V(BgGn4h|W-Kz<91! z3@wkWztbYQ^ZD_9_WpJELwMCU3tV4SBFLks5n6*sU2D_2aQ1N(f%6nbzz zpqRk`?gtff7{U9HVgVEQ7buo6LvW#D1q*~9R;*!#s8DRcxJWC87R-wkH?RfkBZ>)h zV1HCGg&v$s6f+pW{g`47BY6823z)z!6-$^Q*io!tf$-ysHLMVQLa_nklUgyfV17z* z16!~@t(ZUu_Gc7R=)tKJGZ?`AtYQu$c%M@&U;_VA#S&%+KCf880^t`FYgi%rqGAKa zm$YJN!K@WGum$VOiV1XJ|AJx)JvhIpn85(m_lx~@_h1+6PelS9*ng&&LJ!WLD`qf&`xlBijNtvHVgVEQf2CN$3_-70!2;o5 zE7q_=^f!tP7=No3Lks5LDQ;j3*54~8(1Cr0VhTMt|Dc$`0Pdie!wB9#Di$z-|4)h~ z%n*E2v4RD{e^#tvh3H=t8!-M=D~1-#e^cDR7OYV*fe!3{S4^P?=RXuP7{L8c#T-WP z{!6id3H<+7EMbP=e-tZNApBp&8diuV#RiNkwPI+&{MNy@>Q(=DtL`2gxK+O`66nDG zj$#TuI9DlVFo1itVh$sC*C-Y+fj=vjFhg*yVg(C?*D2PpLUg@iv&;W~=N-Ot=Jk5# z#oyI=3+DH7`M=hp#}2@#SLu1x}{`xPdKLw^K}@1KUzep$F$U z#S8{;Z?Blc2;LnO3z)#aqhbj&1b0%bV1e+?iZ!ed-9@nhL$}vX3@w;<)l)XG1?zal z1Uj(qrkFwx&IyVc4B(!qn8OI(-4zR%z`uuL2{QzaVg(C?_f)K5h3H<24H);Ah*|3bwQW(Z!SSiu5etXRVe(Tf!uFkYe+Lks4aiW}I1^-{$IIm91ilGJbt%@7ig7r4V1Uj(au9!j(&N~z{7{Gm}Vh$sC?@}ya0{`8L zCCm_HiWMvnzDKc!6{7blHej5i6+;W=`xG~@1?ybJ1Uj(KQ%s=;=lzNq4B+O9IgH?) zuUNnY{s$CGm?8L}Vg(C?A5yGgh3Eps28;`}Vraqqu;K=`U=@l9bYNekm_iTE#fljW z;C@6ghY`GwDi$z-e~DrVGXx(~tYCp~zhVt5M5ST_#*S7DEtnry+`tyBPbenPf&EFv z6nbzzrI^70?xz)V7{U9DVgVEQm0}4q1fNx`V1e*+iZ!edU8>mZ@^fDaFV&xu`11!J z9IvJ_LmMmIQHg)e)r&#Z=81ACD)yH==mqr U`*t4(JZ$%2eC?$A@T<@L89q(t8~^|S diff --git a/site/pages/docs/actions/public/watchEvent.md b/site/pages/docs/actions/public/watchEvent.md index 607994c394..0fcc1b391c 100644 --- a/site/pages/docs/actions/public/watchEvent.md +++ b/site/pages/docs/actions/public/watchEvent.md @@ -388,6 +388,23 @@ const unwatch = publicClient.watchEvent( ) ``` +### fromBlock (optional) + +- **Type:** `bigint` + +The block number to start listening for logs from. + +```ts twoslash +// [!include ~/snippets/publicClient.ts] +// ---cut--- +const unwatch = publicClient.watchEvent( + { + fromBlock: 1n, // [!code focus] + onLogs: logs => console.log(logs), + } +) +``` + ## Live Example Check out the usage of `watchEvent` in the live [Event Logs Example](https://stackblitz.com/github/wevm/viem/tree/main/examples/logs_event-logs) below. diff --git a/site/pages/docs/contract/watchContractEvent.md b/site/pages/docs/contract/watchContractEvent.md index 94f6cddb6a..81e5f83ac4 100644 --- a/site/pages/docs/contract/watchContractEvent.md +++ b/site/pages/docs/contract/watchContractEvent.md @@ -317,6 +317,21 @@ const unwatch = publicClient.watchContractEvent({ }) ``` +### fromBlock (optional) + +- **Type:** `bigint` + +The block number to start listening for logs from. + +```ts +const unwatch = publicClient.watchContractEvent({ + address: '0xFBA3912Ca04dd458c843e2EE08967fC04f3579c2', + abi: wagmiAbi, + onLogs: logs => console.log(logs), + fromBlock: 1n // [!code focus] +}) +``` + ## JSON-RPC Methods **When poll `true` and RPC Provider supports `eth_newFilter`:** diff --git a/src/actions/public/watchContractEvent.test.ts b/src/actions/public/watchContractEvent.test.ts index c6dae76db8..16d1db876d 100644 --- a/src/actions/public/watchContractEvent.test.ts +++ b/src/actions/public/watchContractEvent.test.ts @@ -451,6 +451,67 @@ describe('poll', () => { expect(logs[0][1].eventName).toEqual('Transfer') }) + test('args: fromBlock', async () => { + const logs: WatchContractEventOnLogsParameter< + typeof usdcContractConfig.abi + >[] = [] + + await writeContract(walletClient, { + ...usdcContractConfig, + account: address.vitalik, + functionName: 'transfer', + args: [address.vitalik, 1n], + }) + + await mine(testClient, { blocks: 1 }) + await wait(200) + const startBlock = await getBlockNumber.getBlockNumber(publicClient) + + await writeContract(walletClient, { + ...usdcContractConfig, + account: address.vitalik, + functionName: 'transfer', + args: [address.vitalik, 1n], + }) + await mine(testClient, { blocks: 1 }) + await wait(200) + const unwatch = watchContractEvent(publicClient, { + abi: usdcContractConfig.abi, + onLogs: (logs_) => { + logs.push(logs_) + }, + poll: true, + fromBlock: startBlock + 1n, + }) + await wait(200) + await writeContract(walletClient, { + ...usdcContractConfig, + account: address.vitalik, + functionName: 'approve', + args: [address.vitalik, 1n], + }) + await mine(testClient, { blocks: 1 }) + await wait(200) + unwatch() + + expect(logs.length).toBe(2) + expect(logs[0].length).toBe(1) + expect(logs[1].length).toBe(1) + + expect(logs[0][0].args).toEqual({ + from: getAddress(address.vitalik), + to: getAddress(address.vitalik), + value: 1n, + }) + expect(logs[0][0].eventName).toEqual('Transfer') + expect(logs[1][0].args).toEqual({ + owner: getAddress(address.vitalik), + spender: getAddress(address.vitalik), + value: 1n, + }) + expect(logs[1][0].eventName).toEqual('Approval') + }) + describe('`getLogs` fallback', () => { test('falls back to `getLogs` if `createContractEventFilter` throws', async () => { // TODO: Something weird going on where the `getFilterChanges` spy is taking @@ -568,6 +629,72 @@ describe('poll', () => { expect(getFilterChangesSpy).toBeCalledTimes(0) expect(getLogsSpy).toBeCalled() }) + + test('args: fromBlock', async () => { + vi.spyOn( + createContractEventFilter, + 'createContractEventFilter', + ).mockRejectedValueOnce(new Error('foo')) + + const logs: WatchContractEventOnLogsParameter< + typeof usdcContractConfig.abi + >[] = [] + + await writeContract(walletClient, { + ...usdcContractConfig, + account: address.vitalik, + functionName: 'transfer', + args: [address.vitalik, 1n], + }) + + await mine(testClient, { blocks: 1 }) + await wait(200) + const startBlock = await getBlockNumber.getBlockNumber(publicClient) + + await writeContract(walletClient, { + ...usdcContractConfig, + account: address.vitalik, + functionName: 'transfer', + args: [address.vitalik, 1n], + }) + await mine(testClient, { blocks: 1 }) + await wait(200) + const unwatch = watchContractEvent(publicClient, { + abi: usdcContractConfig.abi, + onLogs: (logs_) => { + logs.push(logs_) + }, + poll: true, + fromBlock: startBlock + 1n, + }) + await wait(200) + await writeContract(walletClient, { + ...usdcContractConfig, + account: address.vitalik, + functionName: 'approve', + args: [address.vitalik, 1n], + }) + await mine(testClient, { blocks: 1 }) + await wait(200) + unwatch() + + expect(logs.length).toBe(2) + expect(logs[0].length).toBe(1) + expect(logs[1].length).toBe(1) + + expect(logs[0][0].args).toEqual({ + from: getAddress(address.vitalik), + to: getAddress(address.vitalik), + value: 1n, + }) + expect(logs[0][0].eventName).toEqual('Transfer') + expect(logs[1][0].args).toEqual({ + owner: getAddress(address.vitalik), + spender: getAddress(address.vitalik), + value: 1n, + }) + expect(logs[1][0].eventName).toEqual('Approval') + }) }) describe('errors', () => { diff --git a/src/actions/public/watchContractEvent.ts b/src/actions/public/watchContractEvent.ts index 4172b85ef4..b857d020cb 100644 --- a/src/actions/public/watchContractEvent.ts +++ b/src/actions/public/watchContractEvent.ts @@ -12,6 +12,7 @@ import { } from '../../errors/abi.js' import { InvalidInputRpcError } from '../../errors/rpc.js' import type { ErrorType } from '../../errors/utils.js' +import type { BlockNumber } from '../../types/block.js' import type { ContractEventArgs, ContractEventName, @@ -88,6 +89,8 @@ export type WatchContractEventParameters< * @default false */ strict?: strict | boolean | undefined + /** Block to start listening from. */ + fromBlock?: BlockNumber | undefined } & GetPollOptions export type WatchContractEventReturnType = () => void @@ -148,6 +151,7 @@ export function watchContractEvent< poll: poll_, pollingInterval = client.pollingInterval, strict: strict_, + fromBlock, } = parameters const enablePolling = @@ -164,10 +168,12 @@ export function watchContractEvent< eventName, pollingInterval, strict, + fromBlock, ]) return observe(observerId, { onLogs, onError }, (emit) => { let previousBlockNumber: bigint + if (fromBlock !== undefined) previousBlockNumber = fromBlock - 1n let filter: Filter<'event', abi, eventName> | undefined let initialized = false @@ -185,6 +191,7 @@ export function watchContractEvent< args: args as any, eventName: eventName as any, strict: strict as any, + fromBlock, })) as Filter<'event', abi, eventName> } catch {} initialized = true diff --git a/src/actions/public/watchEvent.test.ts b/src/actions/public/watchEvent.test.ts index d1bd12e69a..758572bb7f 100644 --- a/src/actions/public/watchEvent.test.ts +++ b/src/actions/public/watchEvent.test.ts @@ -358,6 +358,53 @@ describe('poll', () => { }) }) + test('args: fromBlock', async () => { + const logs: WatchEventOnLogsParameter< + undefined, + [typeof event.transfer, typeof event.approval] + >[] = [] + + await writeContract(walletClient, { + ...usdcContractConfig, + functionName: 'approve', + args: [accounts[1].address, 1n], + account: address.vitalik, + }) + + await mine(testClient, { blocks: 1 }) + await wait(200) + + const startBlock = await getBlockNumber.getBlockNumber(publicClient) + + await writeContract(walletClient, { + ...usdcContractConfig, + functionName: 'approve', + args: [accounts[1].address, 2n], + account: address.vitalik, + }) + await mine(testClient, { blocks: 1 }) + await wait(200) + + const unwatch = watchEvent(publicClient, { + address: usdcContractConfig.address, + events: [event.transfer, event.approval], + onLogs: (logs_) => logs.push(logs_), + fromBlock: startBlock + 1n, + }) + + await writeContract(walletClient, { + ...usdcContractConfig, + functionName: 'approve', + args: [accounts[1].address, 3n], + account: address.vitalik, + }) + await mine(testClient, { blocks: 1 }) + await wait(200) + unwatch() + + expect(logs.flat().length).toBe(2) + }) + test.todo('args: args') describe('`getLogs` fallback', () => { @@ -473,6 +520,58 @@ describe('poll', () => { expect(getFilterChangesSpy).toBeCalledTimes(0) expect(getLogsSpy).toBeCalled() }) + + test('args: fromBlock', async () => { + await wait(1) + vi.spyOn(createEventFilter, 'createEventFilter').mockRejectedValueOnce( + new Error('foo'), + ) + + const logs: WatchEventOnLogsParameter< + undefined, + [typeof event.transfer, typeof event.approval] + >[] = [] + + await writeContract(walletClient, { + ...usdcContractConfig, + functionName: 'approve', + args: [accounts[1].address, 1n], + account: address.vitalik, + }) + + await mine(testClient, { blocks: 1 }) + await wait(200) + + const startBlock = await getBlockNumber.getBlockNumber(publicClient) + + await writeContract(walletClient, { + ...usdcContractConfig, + functionName: 'approve', + args: [accounts[1].address, 2n], + account: address.vitalik, + }) + await mine(testClient, { blocks: 1 }) + await wait(200) + + const unwatch = watchEvent(publicClient, { + address: usdcContractConfig.address, + events: [event.transfer, event.approval], + onLogs: (logs_) => logs.push(logs_), + fromBlock: startBlock + 1n, + }) + + await writeContract(walletClient, { + ...usdcContractConfig, + functionName: 'approve', + args: [accounts[1].address, 3n], + account: address.vitalik, + }) + await mine(testClient, { blocks: 1 }) + await wait(200) + unwatch() + + expect(logs.flat().length).toBe(2) + }) }) describe('errors', () => { diff --git a/src/actions/public/watchEvent.ts b/src/actions/public/watchEvent.ts index e96f34693a..7e528591c6 100644 --- a/src/actions/public/watchEvent.ts +++ b/src/actions/public/watchEvent.ts @@ -22,6 +22,7 @@ import { } from '../../errors/abi.js' import { InvalidInputRpcError } from '../../errors/rpc.js' import type { ErrorType } from '../../errors/utils.js' +import type { BlockNumber } from '../../types/block.js' import { getAction } from '../../utils/getAction.js' import { decodeEventLog, @@ -74,6 +75,8 @@ export type WatchEventParameters< onError?: ((error: Error) => void) | undefined /** The callback to call when new event logs are received. */ onLogs: WatchEventOnLogsFn + /** Block to start listening from. */ + fromBlock?: BlockNumber | undefined } & GetPollOptions & ( | { @@ -166,6 +169,7 @@ export function watchEvent< poll: poll_, pollingInterval = client.pollingInterval, strict: strict_, + fromBlock, }: WatchEventParameters, ): WatchEventReturnType { const enablePolling = @@ -181,10 +185,12 @@ export function watchEvent< client.uid, event, pollingInterval, + fromBlock, ]) return observe(observerId, { onLogs, onError }, (emit) => { let previousBlockNumber: bigint + if (fromBlock !== undefined) previousBlockNumber = fromBlock - 1n let filter: Filter<'event', TAbiEvents, _EventName, any> let initialized = false @@ -202,6 +208,7 @@ export function watchEvent< event: event!, events, strict, + fromBlock, } as unknown as CreateEventFilterParameters)) as unknown as Filter< 'event', TAbiEvents,