diff --git a/.changeset/blue-pillows-peel.md b/.changeset/blue-pillows-peel.md new file mode 100644 index 0000000000..5ccb5fd833 --- /dev/null +++ b/.changeset/blue-pillows-peel.md @@ -0,0 +1,5 @@ +--- +"viem": patch +--- + +Added `decodeEventLog`. diff --git a/site/.vitepress/sidebar.ts b/site/.vitepress/sidebar.ts index 2be348292f..0dcb879d30 100644 --- a/site/.vitepress/sidebar.ts +++ b/site/.vitepress/sidebar.ts @@ -435,8 +435,8 @@ export const sidebar: DefaultTheme.Sidebar = { link: '/docs/contract/decodeErrorResult', }, { - text: 'decodeEventTopics 🚧', - link: '/docs/contract/decodeEventTopics', + text: 'decodeEventLog', + link: '/docs/contract/decodeEventLog', }, { text: 'decodeFunctionData', diff --git a/site/docs/contract/decodeAbi.md b/site/docs/contract/decodeAbi.md index 07c5a3ddc4..3c527cdce0 100644 --- a/site/docs/contract/decodeAbi.md +++ b/site/docs/contract/decodeAbi.md @@ -2,7 +2,7 @@ Decodes ABI encoded data using the [ABI specification](https://solidity.readthedocs.io/en/latest/abi-spec.html), given a set of ABI parameters (`inputs`/`outputs`) and the encoded ABI data. -The `decodeAbi` function is used by the other contract decoding utilities (ie. `decodeFunctionData`, `decodeEventTopics`, etc). +The `decodeAbi` function is used by the other contract decoding utilities (ie. `decodeFunctionData`, `decodeEventLog`, etc). ## Install diff --git a/site/docs/contract/decodeEventLog.md b/site/docs/contract/decodeEventLog.md new file mode 100644 index 0000000000..0b43b70a38 --- /dev/null +++ b/site/docs/contract/decodeEventLog.md @@ -0,0 +1,158 @@ +# decodeEventLog + +Decodes ABI encoded event topics & data (from an [Event Log](/docs/glossary/terms#TODO)) into an event name and structured arguments (both indexed & non-indexed). + +## Install + +```ts +import { decodeEventLog } from 'viem/contract' +``` + +## Usage + +::: code-group + +```ts [example.ts] +import { decodeEventLog } from 'viem/contract' + +const topics = decodeEventLog({ + abi: wagmiAbi, + data: '0x0000000000000000000000000000000000000000000000000000000000000001', + topics: [ + '0x406dade31f7ae4b5dbc276258c28dde5ae6d5c2773c5745802c493a2360e55e0', + '0x00000000000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266', + '0x0000000000000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8' + ] +}) +/** + * { + * eventName: 'Transfer', + * args: { + * from: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', + * to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8' + * value: 1n + * } + * } + */ +``` + +```ts +export const wagmiAbi = [ + ... + { + inputs: [ + { + indexed: true, + name: 'from', + type: 'address', + }, + { indexed: true, name: 'to', type: 'address' }, + { + indexed: false, + name: 'value', + type: 'uint256', + }, + ], + name: 'Transfer', + type: 'event', + }, + ... +] as const; +``` + +```ts [client.ts] +import { createPublicClient, http } from 'viem' +import { mainnet } from 'viem/chains' + +export const publicClient = createPublicClient({ + chain: mainnet, + transport: http() +}) +``` + +::: + +## Return Value + +```ts +{ + eventName: string; + args: Inferred; +} +``` + +Decoded ABI event topics. + +## Parameters + +### abi + +- **Type:** [`Abi`](/docs/glossary/types#TODO) + +The contract's ABI. + +```ts +const topics = decodeEventLog({ + abi: wagmiAbi, // [!code focus] + data: '0x0000000000000000000000000000000000000000000000000000000000000001', + topics: [ + '0x406dade31f7ae4b5dbc276258c28dde5ae6d5c2773c5745802c493a2360e55e0', + '0x00000000000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266', + '0x0000000000000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8' + ] +}) +``` + +### topics + +- **Type:** `[Hex, ...(Hex | Hex[] | null)[]]` + +A set of topics (encoded indexed args) from the [Event Log](/docs/glossary/terms#TODO). + +```ts +const topics = decodeEventLog({ + abi: wagmiAbi, + data: '0x0000000000000000000000000000000000000000000000000000000000000001', + topics: [ // [!code focus:5] + '0x406dade31f7ae4b5dbc276258c28dde5ae6d5c2773c5745802c493a2360e55e0', + '0x00000000000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266', + '0x0000000000000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8' + ] +}) +``` + +### data (optional) + +- **Type:** `string` + +The data (encoded non-indexed args) from the [Event Log](/docs/glossary/terms#TODO). + +```ts +const topics = decodeEventLog({ + abi: wagmiAbi, + data: '0x0000000000000000000000000000000000000000000000000000000000000001', // [!code focus] + topics: [ + '0x406dade31f7ae4b5dbc276258c28dde5ae6d5c2773c5745802c493a2360e55e0', + '0x00000000000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266', + '0x0000000000000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8' + ] +}) +``` + +### eventName (optional) + +- **Type:** `string` + +An event name from the ABI. Provide an `eventName` to infer the return type of `decodeEventLog`. + +```ts +const topics = decodeEventLog({ + abi: wagmiAbi, + eventName: 'Transfer', // [!code focus] + topics: [ + '0x406dade31f7ae4b5dbc276258c28dde5ae6d5c2773c5745802c493a2360e55e0', + '0x00000000000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266', + '0x0000000000000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8' + ] +}) +``` \ No newline at end of file diff --git a/site/docs/contract/decodeEventTopics.md b/site/docs/contract/decodeEventTopics.md deleted file mode 100644 index d9b3e32d30..0000000000 --- a/site/docs/contract/decodeEventTopics.md +++ /dev/null @@ -1,3 +0,0 @@ -# decodeEventTopics - -TODO \ No newline at end of file diff --git a/site/docs/contract/encodeEventTopics.md b/site/docs/contract/encodeEventTopics.md index 2b773f7d52..97df005a33 100644 --- a/site/docs/contract/encodeEventTopics.md +++ b/site/docs/contract/encodeEventTopics.md @@ -76,10 +76,10 @@ import { encodeEventTopics } from 'viem/contract' const topics = encodeEventTopics({ abi: wagmiAbi, eventName: 'Transfer' - args: [{ + args: { from: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', to: '0x70997970c51812dc3a010c7d01b50e0d17dc79c8' - }] + } }) // ["0x406dade31f7ae4b5dbc276258c28dde5ae6d5c2773c5745802c493a2360e55e0", "0x00000000000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266", "0x0000000000000000000000000000000070997970c51812dc3a010c7d01b50e0d17dc79c8"] ``` diff --git a/src/actions/public/getFilterLogs.test.ts b/src/actions/public/getFilterLogs.test.ts index 0058741222..955964fb7f 100644 --- a/src/actions/public/getFilterLogs.test.ts +++ b/src/actions/public/getFilterLogs.test.ts @@ -90,6 +90,7 @@ describe('events', () => { await mine(testClient, { blocks: 1 }) let logs = await getFilterLogs(publicClient, { filter }) + console.log(logs[0]) assertType(logs) expect(logs.length).toBe(2) }) diff --git a/src/contract.ts b/src/contract.ts index effd6f3335..9a0c1cb1f5 100644 --- a/src/contract.ts +++ b/src/contract.ts @@ -37,6 +37,8 @@ export type { DecodeAbiArgs, DecodeErrorResultArgs, DecodeErrorResultResponse, + DecodeEventLogArgs, + DecodeEventLogResponse, DecodeFunctionDataArgs, DecodeFunctionResultArgs, DecodeFunctionResultResponse, @@ -51,6 +53,7 @@ export type { export { decodeAbi, decodeErrorResult, + decodeEventLog, decodeFunctionData, decodeFunctionResult, encodeAbi, diff --git a/src/errors/abi.ts b/src/errors/abi.ts index 67f24e6b0e..30fa8df533 100644 --- a/src/errors/abi.ts +++ b/src/errors/abi.ts @@ -121,7 +121,23 @@ export class AbiErrorSignatureNotFoundError extends BaseError { [ `Encoded error signature "${signature}" not found on ABI.`, 'Make sure you are using the correct ABI and that the error exists on it.', - `You can look up the signature "${signature}" here: https://sig.eth.samczsun.com/.`, + `You can look up the signature here: https://openchain.xyz/signatures?query=${signature}.`, + ].join('\n'), + { + docsPath, + }, + ) + } +} + +export class AbiEventSignatureNotFoundError extends BaseError { + name = 'AbiEventSignatureNotFoundError' + constructor(signature: Hex, { docsPath }: { docsPath: string }) { + super( + [ + `Encoded event signature "${signature}" not found on ABI.`, + 'Make sure you are using the correct ABI and that the event exists on it.', + `You can look up the signature here: https://openchain.xyz/signatures?query=${signature}.`, ].join('\n'), { docsPath, @@ -183,7 +199,7 @@ export class AbiFunctionSignatureNotFoundError extends BaseError { [ `Encoded function signature "${signature}" not found on ABI.`, 'Make sure you are using the correct ABI and that the function exists on it.', - `You can look up the signature "${signature}" here: https://sig.eth.samczsun.com/.`, + `You can look up the signature here: https://openchain.xyz/signatures?query=${signature}.`, ].join('\n'), { docsPath, diff --git a/src/errors/index.ts b/src/errors/index.ts index cabf2d24e9..2e7adfb943 100644 --- a/src/errors/index.ts +++ b/src/errors/index.ts @@ -8,6 +8,7 @@ export { AbiErrorInputsNotFoundError, AbiErrorNotFoundError, AbiErrorSignatureNotFoundError, + AbiEventSignatureNotFoundError, AbiEventNotFoundError, AbiFunctionNotFoundError, AbiFunctionOutputsNotFoundError, diff --git a/src/types/contract.ts b/src/types/contract.ts index 75ee32c4ce..60edbfe5df 100644 --- a/src/types/contract.ts +++ b/src/types/contract.ts @@ -17,49 +17,135 @@ import type { ExtractAbiFunctionNames, Narrow, } from 'abitype' -import type { Address } from './misc' +import type { Address, Hex, LogTopic } from './misc' import type { TransactionRequest } from './transaction' -import type { Trim } from './utils' +import type { NoUndefined, Prettify, Trim } from './utils' ////////////////////////////////////////////////////////////////////// // ABIs export type AbiItem = Abi[number] +type HashedEventTypes = 'string' | 'bytes' | 'tuple' | `${string}[${string}]` + +type EventTopicParam< + TPrimitiveType = Hex, + TTopic extends LogTopic = LogTopic, +> = NoUndefined< + | (TTopic extends Hex ? TPrimitiveType : undefined) + | (TTopic extends Hex[] ? TPrimitiveType[] : undefined) + | (TTopic extends null ? null : undefined) +> + +export type AbiEventParameterToPrimitiveType = + EventTopicParam> + export type AbiEventParametersToPrimitiveTypes< TAbiParameters extends readonly AbiParameter[], TBase = TAbiParameters[0] extends { name: string } ? {} : [], -> = TAbiParameters extends readonly [infer Head, ...infer Tail] - ? Head extends { indexed: true } - ? Head extends AbiParameter - ? Head extends { name: infer Name } - ? Name extends string - ? { - [name in Name]?: - | AbiParameterToPrimitiveType - | AbiParameterToPrimitiveType[] - | null - } & (Tail extends readonly [] - ? {} - : Tail extends readonly AbiParameter[] - ? AbiEventParametersToPrimitiveTypes - : {}) - : never - : [ - ( - | AbiParameterToPrimitiveType - | AbiParameterToPrimitiveType[] - | null - ), - ...(Tail extends readonly [] - ? [] - : Tail extends readonly AbiParameter[] - ? AbiEventParametersToPrimitiveTypes - : []), - ] +> = Prettify< + TAbiParameters extends readonly [infer Head, ...infer Tail] + ? Head extends { indexed: true } + ? Head extends AbiParameter + ? Head extends { name: infer Name } + ? Name extends string + ? { + [name in Name]?: AbiEventParameterToPrimitiveType + } & (Tail extends readonly [] + ? {} + : Tail extends readonly AbiParameter[] + ? AbiEventParametersToPrimitiveTypes + : {}) + : never + : [ + AbiEventParameterToPrimitiveType, + ...(Tail extends readonly [] + ? [] + : Tail extends readonly AbiParameter[] + ? AbiEventParametersToPrimitiveTypes + : []), + ] + : TBase : TBase : TBase - : TBase +> + +export type AbiEventTopicToPrimitiveType< + TParam extends AbiParameter, + TTopic extends LogTopic, + TPrimitiveType = TParam['type'] extends HashedEventTypes + ? TTopic + : AbiParameterToPrimitiveType, +> = EventTopicParam + +export type AbiEventTopicsToPrimitiveTypes< + TAbiParameters extends readonly AbiParameter[], + TTopics extends LogTopic[] | undefined = undefined, + TData extends Hex | undefined = undefined, + TBase = TAbiParameters[0] extends { name: string } ? {} : [], +> = Prettify< + TAbiParameters extends readonly [infer Head, ...infer Tail] + ? TTopics extends readonly [infer TopicHead, ...infer TopicTail] + ? Head extends { indexed: true } + ? Head extends AbiParameter + ? Head extends { name: infer Name } + ? Name extends string + ? { + [name in Name]: TopicHead extends LogTopic + ? AbiEventTopicToPrimitiveType + : never + } & (Tail extends readonly [] + ? {} + : Tail extends readonly AbiParameter[] + ? TopicTail extends LogTopic[] + ? AbiEventTopicsToPrimitiveTypes + : {} + : {}) + : never + : [ + TopicHead extends LogTopic + ? AbiEventTopicToPrimitiveType + : never, + ...(Tail extends readonly [] + ? [] + : Tail extends readonly AbiParameter[] + ? TopicTail extends LogTopic[] + ? AbiEventTopicsToPrimitiveTypes + : [] + : []), + ] + : TBase + : TBase + : TTopics extends readonly [] + ? TData extends Hex + ? Head extends AbiParameter + ? Head extends { indexed: true } + ? Tail extends readonly AbiParameter[] + ? AbiEventTopicsToPrimitiveTypes + : TBase + : Head extends { name: infer Name } + ? Name extends string + ? { + [name in Name]: AbiParameterToPrimitiveType + } & (Tail extends readonly [] + ? {} + : Tail extends readonly AbiParameter[] + ? AbiEventTopicsToPrimitiveTypes + : {}) + : never + : [ + AbiParameterToPrimitiveType, + ...(Tail extends readonly [] + ? [] + : Tail extends readonly AbiParameter[] + ? AbiEventTopicsToPrimitiveTypes + : []), + ] + : TBase + : TBase + : TBase + : undefined +> export type ExtractArgsFromAbi< TAbi extends Abi | readonly unknown[], @@ -165,6 +251,23 @@ export type ExtractEventArgsFromAbi< args?: TArgs } +export type ExtractEventArgsFromTopics< + TAbi extends Abi | readonly unknown[], + TEventName extends string, + TTopics extends LogTopic[], + TData extends Hex | undefined, + TAbiEvent extends AbiEvent & { type: 'event' } = TAbi extends Abi + ? ExtractAbiEvent + : AbiEvent & { type: 'event' }, + TArgs = AbiEventTopicsToPrimitiveTypes, +> = TTopics extends readonly [] + ? TData extends undefined + ? { args?: never } + : { args?: TArgs } + : { + args?: TArgs + } + export type ExtractErrorNameFromAbi< TAbi extends Abi | readonly unknown[] = Abi, TErrorName extends string = string, @@ -348,15 +451,16 @@ type ExtractArgsFromDefinition< : 'Error: Invalid definition was provided.' export type ExtractArgsFromEventDefinition< - TDef, - TConfig extends ExtractArgsFromDefinitionConfig = { indexedOnly: true }, -> = ExtractArgsFromDefinition extends [...args: any] - ? ExtractArgsFromDefinition | [] - : ExtractArgsFromDefinition - -export type ExtractArgsFromFunctionDefinition = ExtractArgsFromDefinition< - TDef, - { indexedOnly: false } + TDef extends EventDefinition | undefined, + TConfig extends ExtractArgsFromDefinitionConfig = { + indexedOnly: true + }, +> = Prettify< + TDef extends EventDefinition + ? ExtractArgsFromDefinition extends [...args: any] + ? ExtractArgsFromDefinition | [] + : ExtractArgsFromDefinition + : undefined > ////////////////////////////////////////////////////////////////////// diff --git a/src/types/index.ts b/src/types/index.ts index 0d43a0aa52..1326ef2ed2 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -15,11 +15,11 @@ export type { EventDefinition, ExtractArgsFromAbi, ExtractArgsFromEventDefinition, - ExtractArgsFromFunctionDefinition, ExtractConstructorArgsFromAbi, ExtractErrorArgsFromAbi, ExtractErrorNameFromAbi, ExtractEventArgsFromAbi, + ExtractEventArgsFromTopics, ExtractEventNameFromAbi, ExtractFunctionNameFromAbi, ExtractNameFromAbi, diff --git a/src/types/utils.ts b/src/types/utils.ts index 93ae6b7ee8..dc50457a9c 100644 --- a/src/types/utils.ts +++ b/src/types/utils.ts @@ -29,6 +29,15 @@ export type OptionalNullable = { [K in keyof T as T[K] extends NonNullable ? never : K]?: T[K] } +/** + * @description Constructs a type by excluding `undefined` from `T`. + * + * @example + * NoUndefined + * => string + */ +export type NoUndefined = T extends undefined ? never : T + /** * @description Creates a type that is a partial of T, but with the required keys K. * @@ -38,13 +47,6 @@ export type OptionalNullable = { */ export type PartialBy = Omit & Partial> -type TrimLeft = T extends `${Chars}${infer R}` - ? TrimLeft - : T -type TrimRight = T extends `${infer R}${Chars}` - ? TrimRight - : T - /** * @description Combines members of an intersection into a readable type. * @@ -57,6 +59,13 @@ export type Prettify = { [K in keyof T]: T[K] } & {} +type TrimLeft = T extends `${Chars}${infer R}` + ? TrimLeft + : T +type TrimRight = T extends `${infer R}${Chars}` + ? TrimRight + : T + /** * @description Trims empty space from type T. * diff --git a/src/utils/abi/decodeErrorResult.test.ts b/src/utils/abi/decodeErrorResult.test.ts index 499278e567..2d664bb37a 100644 --- a/src/utils/abi/decodeErrorResult.test.ts +++ b/src/utils/abi/decodeErrorResult.test.ts @@ -222,7 +222,7 @@ test("errors: error doesn't exist", () => { ).toThrowErrorMatchingInlineSnapshot(` "Encoded error signature \\"0xa3741467\\" not found on ABI. Make sure you are using the correct ABI and that the error exists on it. - You can look up the signature \\"0xa3741467\\" here: https://sig.eth.samczsun.com/. + You can look up the signature here: https://openchain.xyz/signatures?query=0xa3741467. Docs: https://viem.sh/docs/contract/decodeErrorResult Version: viem@1.0.2" diff --git a/src/utils/abi/decodeEventLog.test.ts b/src/utils/abi/decodeEventLog.test.ts new file mode 100644 index 0000000000..f12c2383c9 --- /dev/null +++ b/src/utils/abi/decodeEventLog.test.ts @@ -0,0 +1,542 @@ +import { assertType, expect, test } from 'vitest' +import { getAddress } from '../address' +import { decodeEventLog } from './decodeEventLog' + +test('Transfer()', () => { + const event = decodeEventLog({ + abi: [ + { + inputs: [], + name: 'Transfer', + type: 'event', + }, + { + inputs: [], + name: 'Approve', + type: 'event', + }, + ], + topics: [ + '0x406dade31f7ae4b5dbc276258c28dde5ae6d5c2773c5745802c493a2360e55e0', + ], + }) + assertType({ eventName: 'Transfer' }) + expect(event).toEqual({ + eventName: 'Transfer', + }) +}) + +test('no args: Transfer(address,address,uint256)', () => { + const event = decodeEventLog({ + abi: [ + { + inputs: [ + { + indexed: true, + name: 'from', + type: 'address', + }, + { + indexed: true, + name: 'to', + type: 'address', + }, + { + indexed: false, + name: 'tokenId', + type: 'uint256', + }, + ], + name: 'Transfer', + type: 'event', + }, + ], + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + ], + }) + assertType({ eventName: 'Transfer' }) + expect(event).toEqual({ + eventName: 'Transfer', + }) +}) + +test('named args: Transfer(address,address,uint256)', () => { + const event = decodeEventLog({ + abi: [ + { + inputs: [ + { + indexed: true, + name: 'from', + type: 'address', + }, + { + indexed: true, + name: 'to', + type: 'address', + }, + { + indexed: false, + name: 'tokenId', + type: 'uint256', + }, + ], + name: 'Transfer', + type: 'event', + }, + ], + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac', + ], + }) + assertType({ eventName: 'Transfer', args: { from: '0x' } }) + expect(event).toEqual({ + eventName: 'Transfer', + args: { + from: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', + }, + }) +}) + +test('named args: Transfer(address,address,uint256)', () => { + const event = decodeEventLog({ + abi: [ + { + inputs: [ + { + indexed: true, + name: 'from', + type: 'address', + }, + { + indexed: true, + name: 'to', + type: 'address', + }, + { + indexed: false, + name: 'tokenId', + type: 'uint256', + }, + ], + name: 'Transfer', + type: 'event', + }, + ], + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac', + [], + ], + }) + assertType({ + eventName: 'Transfer', + args: { from: '0x', to: [] }, + }) + expect(event).toEqual({ + eventName: 'Transfer', + args: { + from: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', + to: [], + }, + }) +}) + +test('named args: Transfer(address,address,uint256)', () => { + const event = decodeEventLog({ + abi: [ + { + inputs: [ + { + indexed: true, + name: 'from', + type: 'address', + }, + { + indexed: true, + name: 'to', + type: 'address', + }, + { + indexed: false, + name: 'tokenId', + type: 'uint256', + }, + ], + name: 'Transfer', + type: 'event', + }, + ], + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + null, + '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac', + ], + }) + assertType({ + eventName: 'Transfer', + args: { from: null, to: '0x' }, + }) + expect(event).toEqual({ + eventName: 'Transfer', + args: { + from: null, + to: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', + }, + }) +}) + +test('named args: Transfer(address,address,uint256)', () => { + const event = decodeEventLog({ + abi: [ + { + inputs: [ + { + indexed: true, + name: 'from', + type: 'address', + }, + { + indexed: true, + name: 'to', + type: 'address', + }, + { + indexed: false, + name: 'tokenId', + type: 'uint256', + }, + ], + name: 'Transfer', + type: 'event', + }, + ], + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + null, + [ + '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac', + '0x000000000000000000000000c961145a54c96e3ae9baa048c4f4d6b04c13916b', + ], + ], + }) + assertType({ + eventName: 'Transfer', + args: { from: null, to: ['0x', '0x'] }, + }) + expect(event).toEqual({ + args: { + from: null, + to: [ + '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', + '0xc961145a54C96E3aE9bAA048c4F4D6b04C13916b', + ], + }, + eventName: 'Transfer', + }) +}) + +test('unnamed args: Transfer(address,address,uint256)', () => { + const event = decodeEventLog({ + abi: [ + { + inputs: [ + { + indexed: true, + type: 'address', + }, + { + indexed: true, + type: 'address', + }, + { + indexed: false, + type: 'uint256', + }, + ], + name: 'Transfer', + type: 'event', + }, + ], + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + null, + '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac', + ], + }) + assertType({ eventName: 'Transfer', args: [null, '0x'] }) + expect(event).toEqual({ + args: [null, '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC'], + eventName: 'Transfer', + }) +}) + +test('unnamed args: Transfer(address,address,uint256)', () => { + const event = decodeEventLog({ + abi: [ + { + inputs: [ + { + indexed: true, + type: 'address', + }, + { + indexed: true, + type: 'address', + }, + { + indexed: false, + type: 'uint256', + }, + ], + name: 'Transfer', + type: 'event', + }, + ], + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + null, + [ + '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac', + '0x000000000000000000000000c961145a54c96e3ae9baa048c4f4d6b04c13916b', + ], + ], + }) + assertType({ + eventName: 'Transfer', + args: [null, ['0x', '0x']], + }) + expect(event).toEqual({ + args: [ + null, + [ + '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', + '0xc961145a54C96E3aE9bAA048c4F4D6b04C13916b', + ], + ], + eventName: 'Transfer', + }) +}) + +test('Foo(string)', () => { + const event = decodeEventLog({ + abi: [ + { + inputs: [ + { + indexed: true, + name: 'message', + type: 'string', + }, + ], + name: 'Foo', + type: 'event', + }, + ], + topics: [ + '0x9f0b7f1630bdb7d474466e2dfef0fb9dff65f7a50eec83935b68f77d0808f08a', + '0x1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8', + ], + }) + assertType({ + eventName: 'Foo', + args: { + message: + '0x1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8', + }, + }) + expect(event).toEqual({ + args: { + message: + '0x1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8', + }, + eventName: 'Foo', + }) +}) + +test('args: eventName', () => { + const event = decodeEventLog({ + abi: [ + { + inputs: [ + { + indexed: true, + type: 'address', + }, + { + indexed: true, + type: 'address', + }, + { + indexed: false, + type: 'uint256', + }, + ], + name: 'Transfer', + type: 'event', + }, + { + inputs: [ + { + indexed: true, + name: 'message', + type: 'string', + }, + ], + name: 'Foo', + type: 'event', + }, + ], + eventName: 'Foo', + topics: [ + '0x9f0b7f1630bdb7d474466e2dfef0fb9dff65f7a50eec83935b68f77d0808f08a', + '0x1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8', + ], + }) + assertType({ + eventName: 'Foo', + args: { + message: + '0x1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8', + }, + }) + expect(event).toEqual({ + args: { + message: + '0x1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8', + }, + eventName: 'Foo', + }) +}) + +test('args: data', () => { + const event = decodeEventLog({ + abi: [ + { + inputs: [ + { + indexed: true, + name: 'from', + type: 'address', + }, + { + indexed: true, + name: 'to', + type: 'address', + }, + { + indexed: false, + name: 'tokenId', + type: 'uint256', + }, + ], + name: 'Transfer', + type: 'event', + }, + ], + data: '0x0000000000000000000000000000000000000000000000000000000000000001', + eventName: 'Transfer', + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045', + '0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266', + ], + }) + assertType({ + eventName: 'Transfer', + args: { + from: '0xd8da6bf26964af9d7eed9e03e53415d37aa96045', + to: '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', + tokenId: 1n, + }, + }) + expect(event).toEqual({ + eventName: 'Transfer', + args: { + from: getAddress('0xd8da6bf26964af9d7eed9e03e53415d37aa96045'), + to: getAddress('0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266'), + tokenId: 1n, + }, + }) +}) + +test('args: data', () => { + const event = decodeEventLog({ + abi: [ + { + inputs: [ + { + indexed: true, + type: 'address', + }, + { + indexed: true, + type: 'address', + }, + { + indexed: false, + type: 'uint256', + }, + ], + name: 'Transfer', + type: 'event', + }, + ], + data: '0x0000000000000000000000000000000000000000000000000000000000000001', + eventName: 'Transfer', + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045', + '0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266', + ], + }) + assertType({ + eventName: 'Transfer', + args: [ + '0xd8da6bf26964af9d7eed9e03e53415d37aa96045', + '0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266', + 1n, + ], + }) + expect(event).toEqual({ + eventName: 'Transfer', + args: [ + getAddress('0xd8da6bf26964af9d7eed9e03e53415d37aa96045'), + getAddress('0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266'), + 1n, + ], + }) +}) + +test("errors: event doesn't exist", () => { + expect(() => + decodeEventLog({ + abi: [ + { + inputs: [ + { + indexed: true, + name: 'message', + type: 'string', + }, + ], + name: 'Bar', + type: 'event', + }, + ], + topics: [ + '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef', + '0x1c8aff950685c2ed4bc3174f3472287b56d9517b9c948127319a09a7a36deac8', + ], + }), + ).toThrowErrorMatchingInlineSnapshot(` + "Encoded event signature \\"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef\\" not found on ABI. + Make sure you are using the correct ABI and that the event exists on it. + You can look up the signature here: https://openchain.xyz/signatures?query=0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef. + + Docs: https://viem.sh/docs/contract/decodeEventLog + Version: viem@1.0.2" + `) +}) diff --git a/src/utils/abi/decodeEventLog.ts b/src/utils/abi/decodeEventLog.ts new file mode 100644 index 0000000000..ead843ce9c --- /dev/null +++ b/src/utils/abi/decodeEventLog.ts @@ -0,0 +1,107 @@ +import { Abi, AbiParameter, Narrow } from 'abitype' +import { AbiEventSignatureNotFoundError } from '../../errors' +import { + EventDefinition, + ExtractEventArgsFromTopics, + ExtractEventNameFromAbi, + Hex, + LogTopic, +} from '../../types' +import { getEventSignature } from '../hash' +import { decodeAbi } from './decodeAbi' +import { formatAbiItem } from './formatAbiItem' + +export type DecodeEventLogArgs< + TAbi extends Abi | readonly unknown[] = Abi, + TEventName extends string = string, + TTopics extends LogTopic[] = LogTopic[], + TData extends Hex | undefined = undefined, +> = { + abi: Narrow + data?: TData + eventName?: ExtractEventNameFromAbi + topics: [signature: Hex, ...args: TTopics] +} + +export type DecodeEventLogResponse< + TAbi extends Abi | readonly unknown[] = Abi, + TEventName extends string = string, + TTopics extends LogTopic[] = LogTopic[], + TData extends Hex | undefined = undefined, +> = { + eventName: TEventName +} & ExtractEventArgsFromTopics + +export function decodeEventLog< + TAbi extends Abi | readonly unknown[], + TEventName extends string, + TTopics extends LogTopic[], + TData extends Hex | undefined = undefined, +>({ + abi, + data, + topics, +}: DecodeEventLogArgs< + TAbi, + TEventName, + TTopics, + TData +>): DecodeEventLogResponse { + const [signature, ...argTopics] = topics + const abiItem = (abi as Abi).find( + (x) => signature === getEventSignature(formatAbiItem(x) as EventDefinition), + ) + if (!(abiItem && 'name' in abiItem)) + throw new AbiEventSignatureNotFoundError(signature, { + docsPath: '/docs/contract/decodeEventLog', + }) + + const { name, inputs } = abiItem + const isUnnamed = inputs?.some((x) => !('name' in x && x.name)) + + let args: any = isUnnamed ? [] : {} + + // Decode topics (indexed args). + for (let i = 0; i < inputs.length; i++) { + const param = inputs[i] + const topic = argTopics[i] + if (topic === null) args[param.name || i] = null + if (!topic) continue + if (Array.isArray(topic)) { + args[param.name || i] = topic.map((t) => decodeTopic({ param, value: t })) + } else { + args[param.name || i] = decodeTopic({ param, value: topic }) + } + } + + // Decode data (non-indexed args). + if (data) { + const params = inputs.filter((x) => !('indexed' in x && x.indexed)) + const decodedData = decodeAbi({ params, data }) + if (decodedData) { + if (isUnnamed) args = [...args, ...decodedData] + else { + for (let i = 0; i < params.length; i++) { + args[params[i].name!] = decodedData[i] + } + } + } + } + + return { + eventName: name, + args: Object.values(args).length > 0 ? args : undefined, + } as unknown as DecodeEventLogResponse +} + +function decodeTopic({ param, value }: { param: AbiParameter; value: Hex }) { + if ( + param.type === 'string' || + param.type === 'bytes' || + param.type === 'tuple' || + param.type.match(/^(.*)\[(\d+)?\]$/) + ) + return value + const decodedArg = decodeAbi({ params: [param], data: value }) || [] + return decodedArg[0] +} diff --git a/src/utils/abi/decodeFunctionData.test.ts b/src/utils/abi/decodeFunctionData.test.ts index c13cb0086a..90f23239ad 100644 --- a/src/utils/abi/decodeFunctionData.test.ts +++ b/src/utils/abi/decodeFunctionData.test.ts @@ -19,7 +19,6 @@ test('foo()', () => { ).toEqual({ args: undefined, functionName: 'foo' }) expect( decodeFunctionData({ - // @ts-expect-error abi: [ { name: 'foo', @@ -131,7 +130,7 @@ test("errors: function doesn't exist", () => { ).toThrowErrorMatchingInlineSnapshot(` "Encoded function signature \\"0xa3741467\\" not found on ABI. Make sure you are using the correct ABI and that the function exists on it. - You can look up the signature \\"0xa3741467\\" here: https://sig.eth.samczsun.com/. + You can look up the signature here: https://openchain.xyz/signatures?query=0xa3741467. Docs: https://viem.sh/docs/contract/decodeFunctionData Version: viem@1.0.2" diff --git a/src/utils/abi/decodeFunctionData.ts b/src/utils/abi/decodeFunctionData.ts index 22e27ac617..2be91db072 100644 --- a/src/utils/abi/decodeFunctionData.ts +++ b/src/utils/abi/decodeFunctionData.ts @@ -7,11 +7,14 @@ import { getFunctionSignature } from '../hash' import { decodeAbi } from './decodeAbi' import { formatAbiItem } from './formatAbiItem' -export type DecodeFunctionDataArgs = { abi: Abi; data: Hex } +export type DecodeFunctionDataArgs = { + abi: Abi | readonly unknown[] + data: Hex +} export function decodeFunctionData({ abi, data }: DecodeFunctionDataArgs) { const signature = slice(data, 0, 4) - const description = abi.find( + const description = (abi as Abi).find( (x) => signature === getFunctionSignature(formatAbiItem(x)), ) if (!description) diff --git a/src/utils/abi/index.test.ts b/src/utils/abi/index.test.ts index 7328122fd3..b1fad4c6c2 100644 --- a/src/utils/abi/index.test.ts +++ b/src/utils/abi/index.test.ts @@ -7,6 +7,7 @@ test('exports utils', () => { { "decodeAbi": [Function], "decodeErrorResult": [Function], + "decodeEventLog": [Function], "decodeFunctionData": [Function], "decodeFunctionResult": [Function], "encodeAbi": [Function], diff --git a/src/utils/abi/index.ts b/src/utils/abi/index.ts index ec5f5b4e29..939b079272 100644 --- a/src/utils/abi/index.ts +++ b/src/utils/abi/index.ts @@ -7,6 +7,12 @@ export type { } from './decodeErrorResult' export { decodeErrorResult } from './decodeErrorResult' +export type { + DecodeEventLogArgs, + DecodeEventLogResponse, +} from './decodeEventLog' +export { decodeEventLog } from './decodeEventLog' + export type { DecodeFunctionDataArgs } from './decodeFunctionData' export { decodeFunctionData } from './decodeFunctionData' diff --git a/src/utils/index.test.ts b/src/utils/index.test.ts index c05cf4cf5e..b049b8356f 100644 --- a/src/utils/index.test.ts +++ b/src/utils/index.test.ts @@ -16,6 +16,7 @@ test('exports utils', () => { "decodeAbi": [Function], "decodeBytes": [Function], "decodeErrorResult": [Function], + "decodeEventLog": [Function], "decodeFunctionData": [Function], "decodeFunctionResult": [Function], "decodeHex": [Function], diff --git a/src/utils/index.ts b/src/utils/index.ts index f0722a087e..bbf32b64e4 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -2,6 +2,8 @@ export type { DecodeAbiArgs, DecodeErrorResultArgs, DecodeErrorResultResponse, + DecodeEventLogArgs, + DecodeEventLogResponse, DecodeFunctionDataArgs, DecodeFunctionResultArgs, DecodeFunctionResultResponse, @@ -16,6 +18,7 @@ export type { export { decodeAbi, decodeErrorResult, + decodeEventLog, decodeFunctionData, decodeFunctionResult, encodeAbi,