From f88bf2ed9bbbe1f9e7512a599aa7ed03a423a2e0 Mon Sep 17 00:00:00 2001 From: Jonas Daniels Date: Fri, 9 Feb 2024 18:41:00 -0800 Subject: [PATCH] refactor: Refactor resolveCompositeAbiFromBytecode to resolveCompositeAbi --- .../src/contract/actions/resolve-abi.ts | 172 +++++++++++------- packages/thirdweb/src/contract/index.ts | 2 +- packages/thirdweb/src/utils/index.ts | 6 + .../signatures/resolve-signature.test.ts | 88 +++++++++ .../src/utils/signatures/resolve-signature.ts | 127 +++++++++++++ 5 files changed, 325 insertions(+), 70 deletions(-) create mode 100644 packages/thirdweb/src/utils/signatures/resolve-signature.test.ts create mode 100644 packages/thirdweb/src/utils/signatures/resolve-signature.ts diff --git a/packages/thirdweb/src/contract/actions/resolve-abi.ts b/packages/thirdweb/src/contract/actions/resolve-abi.ts index 278962e5c44..eacc5fb92fe 100644 --- a/packages/thirdweb/src/contract/actions/resolve-abi.ts +++ b/packages/thirdweb/src/contract/actions/resolve-abi.ts @@ -1,11 +1,10 @@ -import { formatAbi, type Abi, parseAbi } from "abitype"; +import { type Abi, parseAbi, formatAbi } from "abitype"; import type { ThirdwebContract } from "../index.js"; import { getChainIdFromChain } from "../../chain/index.js"; import { getClientFetch } from "../../utils/fetch.js"; import { getBytecode } from "./get-bytecode.js"; import { download } from "../../storage/download.js"; import { extractIPFSUri } from "../../utils/bytecode/extractIPFS.js"; -import { detectMethodInBytecode } from "../../utils/bytecode/detectExtension.js"; import { readContractRaw } from "../../transaction/actions/raw/raw-read.js"; const ABI_RESOLUTION_CACHE = new WeakMap, Promise>(); @@ -48,7 +47,7 @@ export function resolveContractAbi( return await resolveAbiFromContractApi(contract, contractApiBaseUrl); } catch (e) { // if that fails, try to resolve it from the bytecode - return await resolveCompositeAbiFromBytecode(contract); + return await resolveCompositeAbi(contract as ThirdwebContract); } })(); ABI_RESOLUTION_CACHE.set(contract, prom); @@ -240,6 +239,7 @@ const DIAMOND_ABI = { * If the contract follows the diamond pattern, it resolves the ABIs for the facets and merges them with the root ABI. * @param contract The contract for which to resolve the ABI. * @param rootAbi The root ABI to use for the contract. If not provided, it resolves the ABI from the contract's bytecode. + * @param resolveSubAbi A function to resolve the ABI for a sub-contract. If not provided, it uses the default ABI resolution logic. * @returns The resolved ABI for the contract. * @example * ```ts @@ -254,95 +254,127 @@ const DIAMOND_ABI = { * const abi = await resolveCompositeAbiFromBytecode(myContract); * ``` */ -export async function resolveCompositeAbiFromBytecode( - contract: ThirdwebContract, +export async function resolveCompositeAbi( + contract: ThirdwebContract, rootAbi?: Abi, + resolveSubAbi?: (contract: ThirdwebContract) => Promise, ) { - const [rootAbi_, bytecode] = await Promise.all([ + const [ + rootAbi_, + pluginPatternAddresses, + baseRouterAddresses, + diamondFacetAddresses, + ] = await Promise.all([ rootAbi ? rootAbi : resolveAbiFromBytecode(contract), - getBytecode(contract), + // check these all at the same time + resolvePluginPatternAddresses(contract), + resolveBaseRouterAddresses(contract), + resolveDiamondFacetAddresses(contract), ]); - // check if contract is plugin-pattern / dynamic - if (detectMethodInBytecode({ bytecode, method: PLUGINS_ABI })) { - try { - const pluginMap = await readContractRaw({ - contract, - method: PLUGINS_ABI, - }); - // if there are no plugins, return the root ABI - if (!pluginMap.length) { - return rootAbi_; - } - // get all the plugin addresses - const plugins = [...new Set(pluginMap.map((item) => item.pluginAddress))]; - // resolve all the plugin ABIs - const pluginAbis = await getAbisForPlugins({ contract, plugins }); - // return the merged ABI - return joinAbis({ pluginAbis, rootAbi: rootAbi_ }); - } catch (err) { - console.warn("[resolveCompositeAbi:dynamic] ", err); + const mergedPlugins = [ + ...new Set([ + ...pluginPatternAddresses, + ...baseRouterAddresses, + ...diamondFacetAddresses, + ]), + ]; + + // no plugins + if (!mergedPlugins.length) { + return rootAbi_; + } + // get all the abis for the plugins + const pluginAbis = await getAbisForPlugins({ + contract, + plugins: mergedPlugins, + resolveSubAbi, + }); + + // join them together + return joinAbis({ rootAbi: rootAbi_, pluginAbis }); +} + +async function resolvePluginPatternAddresses( + contract: ThirdwebContract, +): Promise { + try { + const pluginMap = await readContractRaw({ + contract, + method: PLUGINS_ABI, + }); + // if there are no plugins, return the root ABI + if (!pluginMap.length) { + return []; } + // get all the plugin addresses + return [...new Set(pluginMap.map((item) => item.pluginAddress))]; + } catch { + // no-op, expected because not everything supports this } + return []; +} - // check for "base router" pattern - if (detectMethodInBytecode({ bytecode, method: BASE_ROUTER_ABI })) { - try { - const pluginMap = await readContractRaw({ - contract, - method: BASE_ROUTER_ABI, - }); - // if there are no plugins, return the root ABI - if (!pluginMap.length) { - return rootAbi_; - } - // get all the plugin addresses - const plugins = [ - ...new Set(pluginMap.map((item) => item.metadata.implementation)), - ]; - // resolve all the plugin ABIs - const pluginAbis = await getAbisForPlugins({ contract, plugins }); - // return the merged ABI - return joinAbis({ pluginAbis, rootAbi: rootAbi_ }); - } catch (err) { - console.warn("[resolveCompositeAbi:base-router] ", err); +async function resolveBaseRouterAddresses( + contract: ThirdwebContract, +): Promise { + try { + const pluginMap = await readContractRaw({ + contract, + method: BASE_ROUTER_ABI, + }); + // if there are no plugins, return the root ABI + if (!pluginMap.length) { + return []; } + // get all the plugin addresses + return [...new Set(pluginMap.map((item) => item.metadata.implementation))]; + } catch { + // no-op, expected because not everything supports this } + return []; +} - // detect diamond pattern - if (detectMethodInBytecode({ bytecode, method: DIAMOND_ABI })) { - try { - const facets = await readContractRaw({ contract, method: DIAMOND_ABI }); - // if there are no facets, return the root ABI - if (!facets.length) { - return rootAbi_; - } - // get all the plugin addresses - const plugins = facets.map((item) => item.facetAddress); - const pluginAbis = await getAbisForPlugins({ contract, plugins }); - return joinAbis({ pluginAbis, rootAbi: rootAbi_ }); - } catch (err) { - console.warn("[resolveCompositeAbi:diamond] ", err); +async function resolveDiamondFacetAddresses( + contract: ThirdwebContract, +): Promise { + try { + const facets = await readContractRaw({ contract, method: DIAMOND_ABI }); + // if there are no facets, return the root ABI + if (!facets.length) { + return []; } + // get all the plugin addresses + return facets.map((item) => item.facetAddress); + } catch { + // no-op, expected because not everything supports this } - return rootAbi_; + return []; } type GetAbisForPluginsOptions = { contract: ThirdwebContract; plugins: string[]; + resolveSubAbi?: (contract: ThirdwebContract) => Promise; }; async function getAbisForPlugins( options: GetAbisForPluginsOptions, ): Promise { return Promise.all( - options.plugins.map((pluginAddress) => - resolveAbiFromBytecode({ + options.plugins.map((pluginAddress) => { + const newContract = { ...options.contract, address: pluginAddress, - }), - ), + }; + // if we have a method passed in that tells us how to resove the sub-api, use that + if (options.resolveSubAbi) { + return options.resolveSubAbi(newContract); + } else { + // otherwise default logic + return resolveAbiFromBytecode(newContract); + } + }), ); } @@ -352,17 +384,19 @@ type JoinAbisOptions = { }; function joinAbis(options: JoinAbisOptions): Abi { - const mergedPlugins = options.pluginAbis + let mergedPlugins = options.pluginAbis .flat() .filter((item) => item.type !== "constructor"); if (options.rootAbi) { - mergedPlugins.push(...options.rootAbi); + mergedPlugins = [...(options.rootAbi || []), ...mergedPlugins].filter( + Boolean, + ); } // unique by formatting every abi and then throwing them in a set // TODO: this may not be super efficient... - const humanReadableAbi = [...new Set(...formatAbi(mergedPlugins))]; + const humanReadableAbi = [...new Set(formatAbi(mergedPlugins))]; // finally parse it back out return parseAbi(humanReadableAbi); } diff --git a/packages/thirdweb/src/contract/index.ts b/packages/thirdweb/src/contract/index.ts index a5dee929c52..d98beeef2c3 100644 --- a/packages/thirdweb/src/contract/index.ts +++ b/packages/thirdweb/src/contract/index.ts @@ -8,7 +8,7 @@ export { resolveContractAbi, resolveAbiFromBytecode, resolveAbiFromContractApi, - resolveCompositeAbiFromBytecode, + resolveCompositeAbi, } from "./actions/resolve-abi.js"; export { formatCompilerMetadata } from "./actions/compiler-metadata.js"; diff --git a/packages/thirdweb/src/utils/index.ts b/packages/thirdweb/src/utils/index.ts index 7283dd48abd..0448f103e90 100644 --- a/packages/thirdweb/src/utils/index.ts +++ b/packages/thirdweb/src/utils/index.ts @@ -27,3 +27,9 @@ export { getCreate2FactoryAddress, getCreate2FactoryDeploymentInfo, } from "./any-evm/create-2-factory.js"; + +//signatures +export { + resolveSignature, + resolveSignatures, +} from "./signatures/resolve-signature.js"; diff --git a/packages/thirdweb/src/utils/signatures/resolve-signature.test.ts b/packages/thirdweb/src/utils/signatures/resolve-signature.test.ts new file mode 100644 index 00000000000..d6dfa51dd92 --- /dev/null +++ b/packages/thirdweb/src/utils/signatures/resolve-signature.test.ts @@ -0,0 +1,88 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { + resolveSignature, + clearCache, + resolveSignatures, +} from "./resolve-signature.js"; + +describe("resolveSignature", () => { + beforeEach(() => { + clearCache(); + }); + it("resolves a function signature", async () => { + const res = await resolveSignature("0x1f931c1c"); + expect(res.function).toMatchInlineSnapshot( + `"function diamondCut((address,uint8,bytes4[])[],address,bytes)"`, + ); + }); + + it("resolves an event signature", async () => { + const res = await resolveSignature("0x1f931c1c"); + expect(res.event).toMatchInlineSnapshot( + `"event DiamondCut((address,uint8,bytes4[])[],address,bytes)"`, + ); + }); +}); + +const TEST_SIGS = [ + "0x763b948a", + "0x42784f14", + "0x7b55f07f", + "0x882ffab2", + "0x55e2eb4b", + "0xbc5d2522", + "0x9ff2a527", + "0x39a209df", + "0x494ea092", + "0x722b6cd2", + "0xbf0806ba", + "0xb8f44a5f", + "0xecd32245", + "0x3e98dd6c", + "0x8047f2ce", + "0xd3606249", + "0xc78da66e", + "0xe0d20147", + "0x64e5b7a9", + "0x2eac7022", + "0xe9332638", + "0x6562e70e", +]; + +describe("resolveSignatures", () => { + beforeEach(() => { + clearCache(); + }); + it("resolves multiple signatures", async () => { + const res = await resolveSignatures(TEST_SIGS); + expect(res).toMatchInlineSnapshot(` + { + "events": [], + "functions": [ + "function balanceOfToken(address,uint256,uint256)", + "function batchCraftInstallations((uint16,uint16,uint40)[])", + "function claimInstallations(uint256[])", + "function craftInstallations(uint16[],uint40[])", + "function equipInstallation(address,uint256,uint256)", + "function getAltarLevel(uint256)", + "function getCraftQueue(address)", + "function getInstallationType(uint256)", + "function getInstallationTypes(uint256[])", + "function getInstallationUnequipType(uint256)", + "function getLodgeLevel(uint256)", + "function getReservoirCapacity(uint256)", + "function getReservoirStats(uint256)", + "function installationBalancesOfToken(address,uint256)", + "function installationBalancesOfTokenByIds(address,uint256,uint256[])", + "function installationBalancesOfTokenWithTypes(address,uint256)", + "function installationsBalances(address)", + "function installationsBalancesWithTypes(address)", + "function reduceCraftTime(uint256[],uint40[])", + "function spilloverRateAndRadiusOfId(uint256)", + "function unequipInstallation(address,uint256,uint256)", + "function upgradeComplete(uint256)", + ], + } + `); + }); +}); diff --git a/packages/thirdweb/src/utils/signatures/resolve-signature.ts b/packages/thirdweb/src/utils/signatures/resolve-signature.ts new file mode 100644 index 00000000000..5363bfab7b1 --- /dev/null +++ b/packages/thirdweb/src/utils/signatures/resolve-signature.ts @@ -0,0 +1,127 @@ +const function_cache = new Map(); +const event_cache = new Map(); + +type FunctionString = `function ${string}`; +type EventString = `event ${string}`; + +// TODO: investigate a better source for this +const SIGNATURE_API = "https://www.4byte.directory/api/v1"; + +async function resolveFunctionSignature( + hexSig: string, +): Promise { + if (function_cache.has(hexSig)) { + return function_cache.get(hexSig) as FunctionString; + } + const res = await fetch( + `${SIGNATURE_API}/signatures/?format=json&hex_signature=${hexSig}`, + ); + if (!res.ok) { + console.log(res.statusText); + return null; + } + const data = await res.json(); + if (data.count === 0) { + return null; + } + const signature = `function ${data.results[0].text_signature}` as const; + function_cache.set(hexSig, signature); + return signature; +} + +async function resolveEventSignature( + hexSig: string, +): Promise { + if (event_cache.has(hexSig)) { + return event_cache.get(hexSig) as EventString; + } + const res = await fetch( + `${SIGNATURE_API}/event-signatures/?format=json&hex_signature=${hexSig}`, + ); + if (!res.ok) { + console.log(res.statusText); + return null; + } + const data = await res.json(); + if (data.count === 0) { + return null; + } + + const signature = `event ${uppercaseFirstLetter( + data.results[0].text_signature, + )}` as const; + event_cache.set(hexSig, signature); + return signature; +} +// helper +function uppercaseFirstLetter(str: string) { + return str.charAt(0).toUpperCase() + str.slice(1); +} + +/** + * Resolves a signature by converting a hexadecimal string into a function or event signature. + * @param hexSig The hexadecimal signature to resolve. + * @returns A promise that resolves to an object containing the function and event signatures. + * @example + * ```ts + * import { resolveSignature } from "thirdweb/utils"; + * const res = await resolveSignature("0x1f931c1c"); + * console.log(res); + * ``` + */ +export async function resolveSignature(hexSig: string): Promise<{ + function: FunctionString | null; + event: EventString | null; +}> { + if (hexSig.startsWith("0x")) { + hexSig = hexSig.slice(2); + } + const all = await Promise.all([ + resolveFunctionSignature(hexSig), + resolveEventSignature(hexSig), + ]); + return { + function: all[0], + event: all[1], + }; +} + +/** + * Resolves the signatures of the given hexadecimal signatures. + * @param hexSigs An array of hexadecimal signatures. + * @returns A promise that resolves to an object containing the resolved functions and events. + * @example + * ```ts + * import { resolveSignatures } from "thirdweb/utils"; + * const res = await resolveSignatures(["0x1f931c1c", "0x1f931c1c"]); + * console.log(res); + * ``` + */ +export async function resolveSignatures(hexSigs: string[]): Promise<{ + functions: FunctionString[]; + events: EventString[]; +}> { + // dedupe hexSigs + hexSigs = Array.from(new Set(hexSigs)); + const all = await Promise.all( + hexSigs.map((hexSig) => resolveSignature(hexSig)), + ); + return { + functions: all + .map((x) => x.function) + .filter((x) => x !== null) + .sort() as FunctionString[], + events: all + .map((x) => x.event) + .filter((x) => x !== null) + .sort() as EventString[], + }; +} + +/** + * @internal + */ +export function clearCache() { + function_cache.clear(); + event_cache.clear(); +}