Skip to content

Commit

Permalink
feat(vue): useWatchContractEvent (#4252)
Browse files Browse the repository at this point in the history
* feat(vue): useWatchContractEvent

* chore: snaps
  • Loading branch information
tmm authored Sep 9, 2024
1 parent 6aa2c4c commit 67defb5
Show file tree
Hide file tree
Showing 10 changed files with 554 additions and 2 deletions.
5 changes: 5 additions & 0 deletions .changeset/lazy-sloths-hunt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@wagmi/vue": patch
---

Added `useWatchContractEvent`.
2 changes: 1 addition & 1 deletion packages/react/src/hooks/useWatchContractEvent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import {
} from '@wagmi/core'
import type { UnionCompute, UnionExactPartial } from '@wagmi/core/internal'
import { useEffect } from 'react'

import type { Abi, ContractEventName } from 'viem'

import type { ConfigParameter, EnabledParameter } from '../types/properties.js'
import { useChainId } from './useChainId.js'
import { useConfig } from './useConfig.js'
Expand Down
126 changes: 126 additions & 0 deletions packages/vue/src/composables/useWatchContractEvent.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { http, createConfig, webSocket } from '@wagmi/core'
import { mainnet, optimism } from '@wagmi/core/chains'
import { abi } from '@wagmi/test'
import { expectTypeOf, test } from 'vitest'

import { useWatchContractEvent } from './useWatchContractEvent.js'

test('default', () => {
useWatchContractEvent({
address: '0x',
abi: abi.erc20,
eventName: 'Transfer',
poll: false,
args: {
from: '0x',
to: '0x',
},
onLogs(logs) {
expectTypeOf(logs[0]!.eventName).toEqualTypeOf<'Transfer'>()
expectTypeOf(logs[0]!.args).toEqualTypeOf<{
from?: `0x${string}` | undefined
to?: `0x${string}` | undefined
value?: bigint | undefined
}>()
},
})
})

test('behavior: no eventName', () => {
useWatchContractEvent({
address: '0x',
abi: abi.erc20,
args: {
// TODO: Figure out why this is not working
// @ts-ignore
from: '0x',
to: '0x',
},
onLogs(logs) {
expectTypeOf(logs[0]!.eventName).toEqualTypeOf<'Transfer' | 'Approval'>()
expectTypeOf(logs[0]!.args).toEqualTypeOf<
| Record<string, unknown>
| readonly unknown[]
| {
from?: `0x${string}` | undefined
to?: `0x${string}` | undefined
value?: bigint | undefined
}
| {
owner?: `0x${string}` | undefined
spender?: `0x${string}` | undefined
value?: bigint | undefined
}
>()
},
})
})

test('differing transports', () => {
const config = createConfig({
chains: [mainnet, optimism],
transports: {
[mainnet.id]: http(),
[optimism.id]: webSocket(),
},
})

// TODO: Fix inference for `poll` (`DeepMaybeRef` wrapping `UseWatchContractEventParameters` not working as expected)
// type Result = UseWatchContractEventParameters<
// typeof abi.erc20,
// 'Transfer' | 'Approval',
// true,
// typeof config,
// typeof mainnet.id | typeof optimism.id
// >
// expectTypeOf<Result['poll']>().toEqualTypeOf<boolean | undefined>()
useWatchContractEvent({
config,
poll: false,
address: '0x',
abi: abi.erc20,
onLogs() {},
})

// type Result2 = UseWatchContractEventParameters<
// typeof abi.erc20,
// 'Transfer' | 'Approval',
// true,
// typeof config,
// typeof mainnet.id
// >
// expectTypeOf<Result2['poll']>().toEqualTypeOf<true | undefined>()
useWatchContractEvent({
config,
chainId: mainnet.id,
poll: true,
address: '0x',
abi: abi.erc20,
onLogs() {},
})

// type Result3 = UseWatchContractEventParameters<
// typeof abi.erc20,
// 'Transfer' | 'Approval',
// true,
// typeof config,
// typeof optimism.id
// >
// expectTypeOf<Result3['poll']>().toEqualTypeOf<boolean | undefined>()
useWatchContractEvent({
config,
chainId: optimism.id,
poll: true,
address: '0x',
abi: abi.erc20,
onLogs() {},
})
useWatchContractEvent({
config,
chainId: optimism.id,
poll: false,
address: '0x',
abi: abi.erc20,
onLogs() {},
})
})
87 changes: 87 additions & 0 deletions packages/vue/src/composables/useWatchContractEvent.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { connect, disconnect, getBalance, writeContract } from '@wagmi/core'
import {
abi,
accounts,
address,
config,
testClient,
transactionHashRegex,
wait,
} from '@wagmi/test'
import { renderComposable } from '@wagmi/test/vue'
import { http, createWalletClient, parseEther } from 'viem'
import type { WatchEventOnLogsParameter } from 'viem/actions'
import { expect, test } from 'vitest'

import { useWatchContractEvent } from './useWatchContractEvent.js'

const connector = config.connectors[0]!

test('default', async () => {
const data = await connect(config, { connector })
const connectedAddress = data.accounts[0]

// impersonate usdc holder account and transfer usdc to connected account
await testClient.mainnet.impersonateAccount({ address: address.usdcHolder })
await testClient.mainnet.setBalance({
address: address.usdcHolder,
value: 10000000000000000000000n,
})
await createWalletClient({
account: address.usdcHolder,
chain: testClient.mainnet.chain,
transport: http(),
}).writeContract({
address: address.usdc,
abi: abi.erc20,
functionName: 'transfer',
args: [connectedAddress, parseEther('10', 'gwei')],
})
await testClient.mainnet.mine({ blocks: 1 })
await testClient.mainnet.stopImpersonatingAccount({
address: address.usdcHolder,
})

const balance = await getBalance(config, {
address: connectedAddress,
token: address.usdc,
})
expect(balance.value).toBeGreaterThan(0n)

// start watching transfer events
let logs: WatchEventOnLogsParameter = []
renderComposable(() =>
useWatchContractEvent({
address: address.usdc,
abi: abi.erc20,
eventName: 'Transfer',
onLogs(next) {
logs = logs.concat(next)
},
}),
)

await writeContract(config, {
address: address.usdc,
abi: abi.erc20,
functionName: 'transfer',
args: [accounts[1], parseEther('1', 'gwei')],
})

await writeContract(config, {
address: address.usdc,
abi: abi.erc20,
functionName: 'transfer',
args: [accounts[3], parseEther('1', 'gwei')],
})

await testClient.mainnet.mine({ blocks: 1 })
await wait(100)
await testClient.mainnet.mine({ blocks: 1 })
await wait(100)

expect(logs.length).toBe(2)
expect(logs[0]?.transactionHash).toMatch(transactionHashRegex)

await disconnect(config, { connector })
})
77 changes: 77 additions & 0 deletions packages/vue/src/composables/useWatchContractEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import {
type Config,
type ResolvedRegister,
type WatchContractEventParameters,
watchContractEvent,
} from '@wagmi/core'
import type { UnionCompute, UnionExactPartial } from '@wagmi/core/internal'
import type { Abi, ContractEventName } from 'viem'
import { computed, watchEffect } from 'vue'

import type { ConfigParameter, EnabledParameter } from '../types/properties.js'
import type { DeepMaybeRef } from '../types/ref.js'
import { deepUnref } from '../utils/cloneDeep.js'
import { useChainId } from './useChainId.js'
import { useConfig } from './useConfig.js'

export type UseWatchContractEventParameters<
abi extends Abi | readonly unknown[] = Abi,
eventName extends ContractEventName<abi> = ContractEventName<abi>,
strict extends boolean | undefined = undefined,
config extends Config = Config,
chainId extends
config['chains'][number]['id'] = config['chains'][number]['id'],
> = DeepMaybeRef<
UnionCompute<
UnionExactPartial<
WatchContractEventParameters<abi, eventName, strict, config, chainId>
> &
ConfigParameter<config> &
EnabledParameter
>
>

export type UseWatchContractEventReturnType = void

/** https://wagmi.sh/vue/api/composables/useWatchContractEvent */
export function useWatchContractEvent<
const abi extends Abi | readonly unknown[],
eventName extends ContractEventName<abi>,
strict extends boolean | undefined = undefined,
config extends Config = ResolvedRegister['config'],
chainId extends
config['chains'][number]['id'] = config['chains'][number]['id'],
>(
parameters: UseWatchContractEventParameters<
abi,
eventName,
strict,
config,
chainId
> = {} as any,
): UseWatchContractEventReturnType {
const parameters_ = computed(() => deepUnref(parameters))

const config = useConfig(parameters_)
const configChainId = useChainId({ config })

watchEffect((onCleanup) => {
const {
chainId = configChainId.value,
enabled = true,
onLogs,
config: _,
...rest
} = parameters_.value

if (!enabled) return
if (!onLogs) return

const unwatch = watchContractEvent(config, {
...(rest as any),
chainId,
onLogs,
})
onCleanup(unwatch)
})
}
1 change: 1 addition & 0 deletions packages/vue/src/exports/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ test('exports', () => {
"useTransaction",
"useTransactionReceipt",
"useWatchBlockNumber",
"useWatchContractEvent",
"useWaitForTransactionReceipt",
"useWriteContract",
"createConfig",
Expand Down
6 changes: 6 additions & 0 deletions packages/vue/src/exports/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,12 @@ export {
useWatchBlockNumber,
} from '../composables/useWatchBlockNumber.js'

export {
type UseWatchContractEventParameters,
type UseWatchContractEventReturnType,
useWatchContractEvent,
} from '../composables/useWatchContractEvent.js'

export {
type UseWaitForTransactionReceiptParameters,
type UseWaitForTransactionReceiptReturnType,
Expand Down
8 changes: 8 additions & 0 deletions site/.vitepress/sidebar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,14 @@ export function getSidebar() {
text: 'useWaitForTransactionReceipt',
link: '/vue/api/composables/useWaitForTransactionReceipt',
},
{
text: 'useWatchBlockNumber',
link: '/vue/api/composables/useWatchBlockNumber',
},
{
text: 'useWatchContractEvent',
link: '/vue/api/composables/useWatchContractEvent',
},
{
text: 'useWriteContract',
link: '/vue/api/composables/useWriteContract',
Expand Down
Loading

0 comments on commit 67defb5

Please sign in to comment.