Skip to content

Commit

Permalink
refactor: Refactor resolveCompositeAbiFromBytecode to resolveComposit…
Browse files Browse the repository at this point in the history
…eAbi
  • Loading branch information
jnsdls committed Feb 10, 2024
1 parent e245ed8 commit f88bf2e
Show file tree
Hide file tree
Showing 5 changed files with 325 additions and 70 deletions.
172 changes: 103 additions & 69 deletions packages/thirdweb/src/contract/actions/resolve-abi.ts
Original file line number Diff line number Diff line change
@@ -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<ThirdwebContract<Abi>, Promise<Abi>>();
Expand Down Expand Up @@ -48,7 +47,7 @@ export function resolveContractAbi<abi extends Abi>(
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<any>);
}
})();
ABI_RESOLUTION_CACHE.set(contract, prom);
Expand Down Expand Up @@ -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
Expand All @@ -254,95 +254,127 @@ const DIAMOND_ABI = {
* const abi = await resolveCompositeAbiFromBytecode(myContract);
* ```
*/
export async function resolveCompositeAbiFromBytecode(
contract: ThirdwebContract<any>,
export async function resolveCompositeAbi(
contract: ThirdwebContract,
rootAbi?: Abi,
resolveSubAbi?: (contract: ThirdwebContract) => Promise<Abi>,
) {
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<string[]> {
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<string[]> {
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<string[]> {
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<any>;
plugins: string[];
resolveSubAbi?: (contract: ThirdwebContract) => Promise<Abi>;
};

async function getAbisForPlugins(
options: GetAbisForPluginsOptions,
): Promise<Abi[]> {
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);
}
}),
);
}

Expand All @@ -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);
}
2 changes: 1 addition & 1 deletion packages/thirdweb/src/contract/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export {
resolveContractAbi,
resolveAbiFromBytecode,
resolveAbiFromContractApi,
resolveCompositeAbiFromBytecode,
resolveCompositeAbi,
} from "./actions/resolve-abi.js";

export { formatCompilerMetadata } from "./actions/compiler-metadata.js";
Expand Down
6 changes: 6 additions & 0 deletions packages/thirdweb/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,9 @@ export {
getCreate2FactoryAddress,
getCreate2FactoryDeploymentInfo,
} from "./any-evm/create-2-factory.js";

//signatures
export {
resolveSignature,
resolveSignatures,
} from "./signatures/resolve-signature.js";
88 changes: 88 additions & 0 deletions packages/thirdweb/src/utils/signatures/resolve-signature.test.ts
Original file line number Diff line number Diff line change
@@ -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)",
],
}
`);
});
});
Loading

0 comments on commit f88bf2e

Please sign in to comment.