Skip to content

Commit

Permalink
feat: add fromBlock param to watch event functions (#2082)
Browse files Browse the repository at this point in the history
* 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 <jakemoxey@gmail.com>
  • Loading branch information
maxencerb and jxom authored Apr 7, 2024
1 parent b07e1ae commit 39ccad8
Show file tree
Hide file tree
Showing 9 changed files with 277 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/gold-papayas-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"viem": patch
---

Added `fromBlock` parameter to `watchEvent` and `watchContractEvent`.
Empty file removed bun
Empty file.
Binary file modified bun.lockb
Binary file not shown.
17 changes: 17 additions & 0 deletions site/pages/docs/actions/public/watchEvent.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 15 additions & 0 deletions site/pages/docs/contract/watchContractEvent.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`:**
Expand Down
127 changes: 127 additions & 0 deletions src/actions/public/watchContractEvent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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', () => {
Expand Down
7 changes: 7 additions & 0 deletions src/actions/public/watchContractEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -88,6 +89,8 @@ export type WatchContractEventParameters<
* @default false
*/
strict?: strict | boolean | undefined
/** Block to start listening from. */
fromBlock?: BlockNumber<bigint> | undefined
} & GetPollOptions<transport>

export type WatchContractEventReturnType = () => void
Expand Down Expand Up @@ -148,6 +151,7 @@ export function watchContractEvent<
poll: poll_,
pollingInterval = client.pollingInterval,
strict: strict_,
fromBlock,
} = parameters

const enablePolling =
Expand All @@ -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

Expand All @@ -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
Expand Down
99 changes: 99 additions & 0 deletions src/actions/public/watchEvent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down
7 changes: 7 additions & 0 deletions src/actions/public/watchEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -74,6 +75,8 @@ export type WatchEventParameters<
onError?: ((error: Error) => void) | undefined
/** The callback to call when new event logs are received. */
onLogs: WatchEventOnLogsFn<TAbiEvent, TAbiEvents, TStrict, _EventName>
/** Block to start listening from. */
fromBlock?: BlockNumber<bigint> | undefined
} & GetPollOptions<TTransport> &
(
| {
Expand Down Expand Up @@ -166,6 +169,7 @@ export function watchEvent<
poll: poll_,
pollingInterval = client.pollingInterval,
strict: strict_,
fromBlock,
}: WatchEventParameters<TAbiEvent, TAbiEvents, TStrict, TTransport>,
): WatchEventReturnType {
const enablePolling =
Expand All @@ -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

Expand All @@ -202,6 +208,7 @@ export function watchEvent<
event: event!,
events,
strict,
fromBlock,
} as unknown as CreateEventFilterParameters)) as unknown as Filter<
'event',
TAbiEvents,
Expand Down

0 comments on commit 39ccad8

Please sign in to comment.