From ee4d256a50e4312614501b15c6b5f9b7b3220be3 Mon Sep 17 00:00:00 2001 From: "moxey.eth" Date: Sat, 28 Jan 2023 17:56:46 +1100 Subject: [PATCH] feat: encodeFunctionResult --- .changeset/violet-humans-press.md | 5 + site/.vitepress/sidebar.ts | 4 +- site/docs/contract/encodeFunctionResult.md | 190 +++++++++++++- src/index.test.ts | 1 + src/index.ts | 1 + src/types/index.ts | 1 + src/types/solidity.ts | 25 ++ src/utils/abi/encodeFunctionResult.test.ts | 284 +++++++++++++++++++++ src/utils/abi/encodeFunctionResult.ts | 32 +++ src/utils/abi/index.test.ts | 1 + src/utils/abi/index.ts | 2 + src/utils/index.test.ts | 1 + src/utils/index.ts | 1 + 13 files changed, 545 insertions(+), 3 deletions(-) create mode 100644 .changeset/violet-humans-press.md create mode 100644 src/utils/abi/encodeFunctionResult.test.ts create mode 100644 src/utils/abi/encodeFunctionResult.ts diff --git a/.changeset/violet-humans-press.md b/.changeset/violet-humans-press.md new file mode 100644 index 0000000000..76b4ca1f1a --- /dev/null +++ b/.changeset/violet-humans-press.md @@ -0,0 +1,5 @@ +--- +"viem": patch +--- + +Added `encodeFunctionResult`. diff --git a/site/.vitepress/sidebar.ts b/site/.vitepress/sidebar.ts index 199ba98cdc..96c29a2b1b 100644 --- a/site/.vitepress/sidebar.ts +++ b/site/.vitepress/sidebar.ts @@ -474,8 +474,8 @@ export const sidebar: DefaultTheme.Sidebar = { link: '/docs/contract/encodeFunctionData', }, { - text: 'encodeFunctionResult 🚧', - link: '/docs/contract/encodeFunctionData', + text: 'encodeFunctionResult', + link: '/docs/contract/encodeFunctionResult', }, ], }, diff --git a/site/docs/contract/encodeFunctionResult.md b/site/docs/contract/encodeFunctionResult.md index ffdc698c76..7b939cdf95 100644 --- a/site/docs/contract/encodeFunctionResult.md +++ b/site/docs/contract/encodeFunctionResult.md @@ -1,3 +1,191 @@ # encodeFunctionResult -TODO \ No newline at end of file +Encodes structured return data into ABI encoded data. It is the opposite of [`decodeFunctionResult`](/docs/contract/decodeFunctionResult). + +## Install + +```ts +import { encodeFunctionResult } from 'viem'; +``` + +## Usage + +Given an ABI (`abi`) and a function (`functionName`), pass through the values (`values`) to encode: + +::: code-group + +```ts [example.ts] +import { encodeFunctionResult } from 'viem'; + +const data = encodeFunctionResult({ + abi: wagmiAbi, + functionName: 'ownerOf', + value: ['0xa5cc3c03994db5b0d9a5eedd10cabab0813678ac'], +}); +// '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac' +``` + +```ts [abi.ts] +export const wagmiAbi = [ + ... + { + inputs: [{ name: 'tokenId', type: 'uint256' }], + name: 'ownerOf', + outputs: [{ name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, + ... +] as const; +``` + +```ts [client.ts] +import { createPublicClient, http } from 'viem'; +import { mainnet } from 'viem/chains'; + +export const publicClient = createPublicClient({ + chain: mainnet, + transport: http(), +}); +``` + +::: + +### A more complex example + +::: code-group + +```ts [example.ts] +import { decodeFunctionResult } from 'viem' + +const data = decodeFunctionResult({ + abi: wagmiAbi, + functionName: 'getInfo', + value: [ + { + foo: { + sender: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', + x: 69420n, + y: true + }, + sender: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', + z: 69 + } + ] +}) +// 0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac0000000000000000000000000000000000000000000000000000000000010f2c0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac0000000000000000000000000000000000000000000000000000000000000045 +``` + +```ts [abi.ts] +export const wagmiAbi = [ + ... + { + inputs: [], + name: 'getInfo', + outputs: [ + { + components: [ + { + components: [ + { + internalType: 'address', + name: 'sender', + type: 'address', + }, + { + internalType: 'uint256', + name: 'x', + type: 'uint256', + }, + { + internalType: 'bool', + name: 'y', + type: 'bool', + }, + ], + internalType: 'struct Example.Foo', + name: 'foo', + type: 'tuple', + }, + { + internalType: 'address', + name: 'sender', + type: 'address', + }, + { + internalType: 'uint32', + name: 'z', + type: 'uint32', + }, + ], + internalType: 'struct Example.Bar', + name: 'res', + type: 'tuple', + }, + ], + stateMutability: 'pure', + type: 'function', + }, + ... +] 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 + +The decoded data. Type is inferred from the ABI. + +## Parameters + +### abi + +- **Type:** [`Abi`](/docs/glossary/types#TODO) + +The contract's ABI. + +```ts +const data = encodeFunctionResult({ + abi: wagmiAbi, // [!code focus] + functionName: 'ownerOf', + value: ['0xa5cc3c03994db5b0d9a5eedd10cabab0813678ac'], +}); +``` + +### functionName + +- **Type:** `string` + +The function to encode from the ABI. + +```ts +const data = encodeFunctionResult({ + abi: wagmiAbi, + functionName: 'ownerOf', // [!code focus] + value: ['0xa5cc3c03994db5b0d9a5eedd10cabab0813678ac'], +}); +``` + +### values + +- **Type**: `Hex` + +Return values to encode. + +```ts +const data = encodeFunctionResult({ + abi: wagmiAbi, + functionName: 'ownerOf', + value: ['0xa5cc3c03994db5b0d9a5eedd10cabab0813678ac'], // [!code focus] +}); +``` diff --git a/src/index.test.ts b/src/index.test.ts index ef5b52c5e1..0b884998e6 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -74,6 +74,7 @@ test('exports actions', () => { "encodeAbi": [Function], "encodeBytes": [Function], "encodeFunctionData": [Function], + "encodeFunctionResult": [Function], "encodeHex": [Function], "encodeRlp": [Function], "estimateGas": [Function], diff --git a/src/index.ts b/src/index.ts index af2c99e0aa..ba2ce42b4c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -276,6 +276,7 @@ export { encodeAbi, encodeBytes, encodeFunctionData, + encodeFunctionResult, encodeHex, encodeRlp, getAddress, diff --git a/src/types/index.ts b/src/types/index.ts index cb24d62f58..784235fe4b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -40,6 +40,7 @@ export type { ExtractArgsFromAbi, ExtractArgsFromEventDefinition, ExtractArgsFromFunctionDefinition, + ExtractResultFromAbi, } from './solidity' export type { diff --git a/src/types/solidity.ts b/src/types/solidity.ts index e3802aa01b..92544a74ce 100644 --- a/src/types/solidity.ts +++ b/src/types/solidity.ts @@ -37,6 +37,31 @@ export type ExtractArgsFromAbi< /** Arguments to pass contract method */ args: TArgs } +export type ExtractResultFromAbi< + TAbi extends Abi | readonly unknown[], + TFunctionName extends string, + TAbiFunction extends AbiFunction & { type: 'function' } = TAbi extends Abi + ? ExtractAbiFunction + : AbiFunction & { type: 'function' }, + TArgs = AbiParametersToPrimitiveTypes, + FailedToParseArgs = + | ([TArgs] extends [never] ? true : false) + | (readonly unknown[] extends TArgs ? true : false), +> = true extends FailedToParseArgs + ? { + /** + * Arguments to pass contract method + * + * Use a [const assertion](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions) on {@link abi} for type inference. + */ + result?: readonly unknown[] + } + : TArgs extends readonly [] + ? { result?: never } + : { + /** Arguments to pass contract method */ result: TArgs + } + ////////////////////////////////////////////////////////////////////// // Event/Function Definitions diff --git a/src/utils/abi/encodeFunctionResult.test.ts b/src/utils/abi/encodeFunctionResult.test.ts new file mode 100644 index 0000000000..1ef075a318 --- /dev/null +++ b/src/utils/abi/encodeFunctionResult.test.ts @@ -0,0 +1,284 @@ +import { expect, test } from 'vitest' + +import { encodeFunctionResult } from './encodeFunctionResult' + +test('returns ()', () => { + expect( + encodeFunctionResult({ + abi: [ + { + inputs: [], + name: 'foo', + outputs: [], + stateMutability: 'pure', + type: 'function', + }, + ], + functionName: 'foo', + result: undefined, + }), + ).toEqual('0x') + expect( + encodeFunctionResult({ + abi: [ + { + inputs: [], + name: 'foo', + outputs: [], + stateMutability: 'pure', + type: 'function', + }, + ], + functionName: 'foo', + result: [undefined], + }), + ).toEqual('0x') + expect( + encodeFunctionResult({ + abi: [ + { + inputs: [], + name: 'foo', + outputs: [], + stateMutability: 'pure', + type: 'function', + }, + ], + functionName: 'foo', + result: [], + }), + ).toEqual('0x') +}) + +test('returns (address)', () => { + expect( + encodeFunctionResult({ + abi: [ + { + inputs: [], + name: 'foo', + outputs: [ + { + internalType: 'address', + name: 'sender', + type: 'address', + }, + ], + stateMutability: 'pure', + type: 'function', + }, + ] as const, + functionName: 'foo', + result: ['0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC'], + }), + ).toEqual( + '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac', + ) +}) + +test('returns (Bar)', () => { + expect( + encodeFunctionResult({ + abi: [ + { + inputs: [], + name: 'bar', + outputs: [ + { + components: [ + { + components: [ + { + internalType: 'address', + name: 'sender', + type: 'address', + }, + { + internalType: 'uint256', + name: 'x', + type: 'uint256', + }, + { + internalType: 'bool', + name: 'y', + type: 'bool', + }, + ], + internalType: 'struct Example.Foo', + name: 'foo', + type: 'tuple', + }, + { + internalType: 'address', + name: 'sender', + type: 'address', + }, + { + internalType: 'uint32', + name: 'z', + type: 'uint32', + }, + ], + internalType: 'struct Example.Bar', + name: 'res', + type: 'tuple', + }, + ], + stateMutability: 'pure', + type: 'function', + }, + ], + functionName: 'bar', + result: [ + { + foo: { + sender: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', + x: 69420n, + y: true, + }, + sender: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', + z: 69, + }, + ], + }), + ).toEqual( + '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac0000000000000000000000000000000000000000000000000000000000010f2c0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac0000000000000000000000000000000000000000000000000000000000000045', + ) +}) + +test('returns (Bar, string)', () => { + expect( + encodeFunctionResult({ + abi: [ + { + inputs: [], + name: 'baz', + outputs: [ + { + components: [ + { + components: [ + { + internalType: 'address', + name: 'sender', + type: 'address', + }, + { + internalType: 'uint256', + name: 'x', + type: 'uint256', + }, + { + internalType: 'bool', + name: 'y', + type: 'bool', + }, + ], + internalType: 'struct Example.Foo', + name: 'foo', + type: 'tuple', + }, + { + internalType: 'address', + name: 'sender', + type: 'address', + }, + { + internalType: 'uint32', + name: 'z', + type: 'uint32', + }, + ], + internalType: 'struct Example.Bar', + name: 'res', + type: 'tuple', + }, + { + internalType: 'string', + name: 'bob', + type: 'string', + }, + ], + stateMutability: 'pure', + type: 'function', + }, + ], + functionName: 'baz', + result: [ + { + foo: { + sender: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', + x: 69420n, + y: true, + }, + sender: '0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC', + z: 69, + }, + 'wagmi', + ], + }), + ).toEqual( + '0x000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac0000000000000000000000000000000000000000000000000000000000010f2c0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000a5cc3c03994db5b0d9a5eedd10cabab0813678ac000000000000000000000000000000000000000000000000000000000000004500000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000000057761676d69000000000000000000000000000000000000000000000000000000', + ) +}) + +test("error: function doesn't exist", () => { + expect(() => + encodeFunctionResult({ + abi: [ + { + inputs: [], + name: 'foo', + outputs: [ + { + internalType: 'address', + name: 'sender', + type: 'address', + }, + ], + stateMutability: 'pure', + type: 'function', + }, + ], + functionName: 'baz', + result: ['0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC'], + }), + ).toThrowErrorMatchingInlineSnapshot( + ` + "Function \\"baz\\" not found on ABI. + Make sure you are using the correct ABI and that the function exists on it. + + Docs: https://viem.sh/docs/contract/encodeFunctionData + + Version: viem@1.0.2" + `, + ) +}) + +test("error: function doesn't exist", () => { + expect(() => + encodeFunctionResult({ + abi: [ + // @ts-expect-error + { + inputs: [], + name: 'foo', + stateMutability: 'pure', + type: 'function', + }, + ], + functionName: 'foo', + result: ['0xa5cc3c03994DB5b0d9A5eEdD10CabaB0813678AC'], + }), + ).toThrowErrorMatchingInlineSnapshot( + ` + "Function \\"foo\\" does not contain any \`outputs\` on ABI. + Cannot decode function result without knowing what the parameter types are. + Make sure you are using the correct ABI and that the function exists on it. + + Docs: https://viem.sh/docs/contract/decodeFunctionResult + + Version: viem@1.0.2" + `, + ) +}) diff --git a/src/utils/abi/encodeFunctionResult.ts b/src/utils/abi/encodeFunctionResult.ts new file mode 100644 index 0000000000..8319180663 --- /dev/null +++ b/src/utils/abi/encodeFunctionResult.ts @@ -0,0 +1,32 @@ +import { Abi, ExtractAbiFunctionNames } from 'abitype' +import { + AbiFunctionNotFoundError, + AbiFunctionOutputsNotFoundError, +} from '../../errors' + +import { ExtractResultFromAbi } from '../../types' +import { encodeAbi } from './encodeAbi' + +export function encodeFunctionResult< + TAbi extends Abi = Abi, + TFunctionName extends ExtractAbiFunctionNames = any, +>({ + abi, + functionName, + result, +}: { abi: TAbi; functionName: TFunctionName } & ExtractResultFromAbi< + TAbi, + TFunctionName +>) { + const description = abi.find((x) => 'name' in x && x.name === functionName) + if (!description) throw new AbiFunctionNotFoundError(functionName) + if (!('outputs' in description)) + throw new AbiFunctionOutputsNotFoundError(functionName) + + let values = Array.isArray(result) ? result : [result] + if (description.outputs.length === 0 && !values[0]) values = [] + + const data = encodeAbi({ params: description.outputs, values }) + if (!data) return '0x' + return data +} diff --git a/src/utils/abi/index.test.ts b/src/utils/abi/index.test.ts index b1bf1486f8..5b2973f7bf 100644 --- a/src/utils/abi/index.test.ts +++ b/src/utils/abi/index.test.ts @@ -10,6 +10,7 @@ test('exports utils', () => { "decodeFunctionResult": [Function], "encodeAbi": [Function], "encodeFunctionData": [Function], + "encodeFunctionResult": [Function], } `) }) diff --git a/src/utils/abi/index.ts b/src/utils/abi/index.ts index ef5947e7ba..03ba7f325a 100644 --- a/src/utils/abi/index.ts +++ b/src/utils/abi/index.ts @@ -7,3 +7,5 @@ export { decodeFunctionResult } from './decodeFunctionResult' export { encodeAbi } from './encodeAbi' export { encodeFunctionData } from './encodeFunctionData' + +export { encodeFunctionResult } from './encodeFunctionResult' diff --git a/src/utils/index.test.ts b/src/utils/index.test.ts index 541baf8b9d..b3ca4eebed 100644 --- a/src/utils/index.test.ts +++ b/src/utils/index.test.ts @@ -22,6 +22,7 @@ test('exports utils', () => { "encodeAbi": [Function], "encodeBytes": [Function], "encodeFunctionData": [Function], + "encodeFunctionResult": [Function], "encodeHex": [Function], "encodeRlp": [Function], "extractFunctionName": [Function], diff --git a/src/utils/index.ts b/src/utils/index.ts index 202510b882..784ad32f5d 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -4,6 +4,7 @@ export { decodeFunctionResult, encodeAbi, encodeFunctionData, + encodeFunctionResult, } from './abi' export type {