Skip to content

Commit 490159e

Browse files
karooolisfrolic
andauthored
feat(store-sync): system/world ABI from metadata (#3642)
Co-authored-by: Kevin Ingersoll <kingersoll@gmail.com>
1 parent 2048adf commit 490159e

14 files changed

Lines changed: 1286 additions & 85 deletions

.changeset/great-hairs-smoke.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@latticexyz/store-sync": patch
3+
---
4+
5+
`getWorldAbi` now returns a full world ABI (errors, parameter names, mutability, etc.) registered by the deployer using the metadata module.
6+
7+
Also added internal functions `getSystemAbi` and `getSystemAbis` to retrieve system-specific ABIs.

packages/store-sync/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"./recs": "./dist/recs/index.js",
2020
"./sqlite": "./dist/sqlite/index.js",
2121
"./trpc-indexer": "./dist/trpc-indexer/index.js",
22-
"./world": "./dist/world/index.js",
22+
"./world": "./dist/exports/world.js",
2323
"./zustand": "./dist/zustand/index.js"
2424
},
2525
"typesVersions": {
@@ -52,7 +52,7 @@
5252
"./dist/trpc-indexer/index.d.ts"
5353
],
5454
"world": [
55-
"./dist/world/index.d.ts"
55+
"./dist/exports/world.d.ts"
5656
],
5757
"zustand": [
5858
"./dist/zustand/index.d.ts"
@@ -84,6 +84,7 @@
8484
"@latticexyz/stash": "workspace:*",
8585
"@latticexyz/store": "workspace:*",
8686
"@latticexyz/world": "workspace:*",
87+
"@latticexyz/world-module-metadata": "workspace:*",
8788
"@trpc/client": "10.34.0",
8889
"@trpc/server": "10.34.0",
8990
"change-case": "^5.2.0",

packages/store-sync/src/exports/internal.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,7 @@ export * from "../stash/common";
66
export * from "../stash/createStorageAdapter";
77
export * from "../stash/createSyncAdapter";
88
export * from "../stash/syncToStash";
9+
10+
// World
11+
export * from "../world/getSystemAbi";
12+
export * from "../world/getSystemAbis";
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from "../world/getFunctions";
2+
export * from "../world/getWorldAbi";
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { createTestClient, http, parseAbi } from "viem";
2+
import { foundry } from "viem/chains";
3+
import { describe, expect, it, vi } from "vitest";
4+
import { mockError, mockMetadata, mockSystem1Fn, mockSystem1Id } from "./test/mocks";
5+
6+
vi.doMock("@latticexyz/store/internal", async (importOriginal) => {
7+
const actual = (await importOriginal()) as object;
8+
return {
9+
...actual,
10+
getRecord: vi.fn().mockResolvedValue({
11+
value: mockMetadata[0].value,
12+
}),
13+
};
14+
});
15+
16+
const { getSystemAbi } = await import("./getSystemAbi");
17+
18+
describe("System ABI", () => {
19+
it("should return the system ABI", async () => {
20+
const client = createTestClient({
21+
chain: foundry,
22+
mode: "anvil",
23+
transport: http(),
24+
});
25+
26+
const abi = await getSystemAbi({
27+
client,
28+
worldAddress: "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB",
29+
systemId: mockSystem1Id,
30+
});
31+
32+
expect(abi).toEqual(parseAbi([mockSystem1Fn, mockError]));
33+
});
34+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Client, Abi, Address, hexToString, Hex, stringToHex, parseAbi } from "viem";
2+
import metadataConfig from "@latticexyz/world-module-metadata/mud.config";
3+
import { getRecord } from "@latticexyz/store/internal";
4+
5+
export async function getSystemAbi({
6+
client,
7+
worldAddress,
8+
systemId,
9+
}: {
10+
readonly client: Client;
11+
readonly worldAddress: Address;
12+
readonly systemId: Hex;
13+
}): Promise<Abi> {
14+
const record = await getRecord(client, {
15+
address: worldAddress,
16+
table: metadataConfig.tables.metadata__ResourceTag,
17+
key: { resource: systemId, tag: stringToHex("abi", { size: 32 }) },
18+
});
19+
const abi = parseAbi(hexToString(record.value).split("\n"));
20+
21+
return abi;
22+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { createTestClient, http, parseAbi } from "viem";
2+
import { foundry } from "viem/chains";
3+
import { describe, expect, it, vi } from "vitest";
4+
import {
5+
mockError,
6+
mockMetadata,
7+
mockSystem1Fn,
8+
mockSystem1Id,
9+
mockSystem2Fn,
10+
mockSystem2Id,
11+
mockSystem3Id,
12+
} from "./test/mocks";
13+
14+
vi.doMock("../getRecords", () => ({
15+
getRecords: vi.fn().mockResolvedValue({
16+
records: mockMetadata,
17+
}),
18+
}));
19+
20+
const { getSystemAbis } = await import("./getSystemAbis");
21+
22+
describe("Systems ABIs", () => {
23+
it("should return the systems ABIs", async () => {
24+
const client = createTestClient({
25+
chain: foundry,
26+
mode: "anvil",
27+
transport: http(),
28+
});
29+
30+
const abi = await getSystemAbis({
31+
client,
32+
worldAddress: "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB",
33+
systemIds: [mockSystem1Id, mockSystem2Id, mockSystem3Id],
34+
fromBlock: 0n,
35+
toBlock: 0n,
36+
});
37+
38+
expect(abi).toEqual({
39+
[mockSystem1Id]: parseAbi([mockSystem1Fn, mockError]),
40+
[mockSystem2Id]: parseAbi([mockSystem2Fn, mockError]),
41+
[mockSystem3Id]: [],
42+
});
43+
});
44+
});
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { Client, Abi, Address, Hex, hexToString, parseAbi, stringToHex } from "viem";
2+
import metadataConfig from "@latticexyz/world-module-metadata/mud.config";
3+
import { getRecords } from "../getRecords";
4+
5+
export async function getSystemAbis({
6+
client,
7+
worldAddress,
8+
systemIds,
9+
fromBlock,
10+
toBlock,
11+
indexerUrl,
12+
chainId,
13+
}: {
14+
readonly client: Client;
15+
readonly worldAddress: Address;
16+
readonly systemIds: Hex[];
17+
readonly fromBlock?: bigint;
18+
readonly toBlock?: bigint;
19+
readonly indexerUrl?: string;
20+
readonly chainId?: number;
21+
}): Promise<{ readonly [systemId: Hex]: Abi }> {
22+
const { records } = await getRecords({
23+
table: metadataConfig.tables.metadata__ResourceTag,
24+
worldAddress,
25+
chainId,
26+
indexerUrl,
27+
client,
28+
fromBlock,
29+
toBlock,
30+
});
31+
32+
const abis = Object.fromEntries([
33+
...systemIds.map((id) => [id, [] as Abi] as const),
34+
...records
35+
.filter(({ resource, tag }) => tag === stringToHex("abi", { size: 32 }) && systemIds.includes(resource))
36+
.map(({ resource, value }) => {
37+
const abi = value === "0x" ? [] : parseAbi(hexToString(value).split("\n"));
38+
return [resource, abi] as const;
39+
}),
40+
]);
41+
42+
return abis;
43+
}
Lines changed: 11 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,18 @@
1-
import { describe, expect, it, vi } from "vitest";
2-
import { createTestClient, http } from "viem";
3-
import { getWorldAbi } from "./getWorldAbi";
1+
import { createTestClient, http, parseAbi } from "viem";
42
import { foundry } from "viem/chains";
3+
import { describe, expect, it, vi } from "vitest";
4+
import { mockError, mockMetadata, mockWorldFn } from "./test/mocks";
55

6-
vi.mock("./getFunctions", () => {
7-
const mockGetFunctionsResult = [{ signature: "setNumber(bool)" }, { signature: "batchCall((bytes32,bytes)[])" }];
8-
const getFunctions = vi.fn();
9-
getFunctions.mockResolvedValue(mockGetFunctionsResult);
6+
vi.doMock("../getRecords", () => ({
7+
getRecords: vi.fn().mockResolvedValue({
8+
records: mockMetadata,
9+
}),
10+
}));
1011

11-
return {
12-
getFunctions,
13-
};
14-
});
12+
const { getWorldAbi } = await import("./getWorldAbi");
1513

1614
describe("World ABI", () => {
17-
it("should concat base and world ABI", async () => {
15+
it("should return the world ABI", async () => {
1816
const client = createTestClient({
1917
chain: foundry,
2018
mode: "anvil",
@@ -28,24 +26,6 @@ describe("World ABI", () => {
2826
toBlock: 0n,
2927
});
3028

31-
expect(abi).toContainEqual({
32-
inputs: [
33-
{
34-
type: "bool",
35-
},
36-
],
37-
name: "setNumber",
38-
outputs: [],
39-
stateMutability: "nonpayable",
40-
type: "function",
41-
});
42-
43-
expect(abi).not.toContainEqual({
44-
name: "batchCall",
45-
type: "function",
46-
stateMutability: "nonpayable",
47-
inputs: [{ type: "tuple[]", components: [{ type: "bytes32" }, { type: "bytes" }] }],
48-
outputs: [],
49-
});
29+
expect(abi).toEqual(parseAbi([mockWorldFn, mockError]));
5030
});
5131
});
Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
1-
import { Client, Abi, AbiItem, AbiFunction, Address, getAddress, toFunctionSelector } from "viem";
2-
import IBaseWorldAbi from "@latticexyz/world/out/IBaseWorld.sol/IBaseWorld.abi.json" assert { type: "json" };
3-
import { functionSignatureToAbiItem } from "./functionSignatureToAbiItem";
4-
import { getFunctions } from "./getFunctions";
5-
import { isDefined } from "@latticexyz/common/utils";
6-
7-
function isAbiFunction(abiItem: AbiItem): abiItem is AbiFunction {
8-
return abiItem.type === "function";
9-
}
1+
import { Client, Abi, Address, hexToString, parseAbi, stringToHex } from "viem";
2+
import metadataConfig from "@latticexyz/world-module-metadata/mud.config";
3+
import { getRecords } from "../getRecords";
104

115
export async function getWorldAbi({
126
client,
@@ -23,26 +17,19 @@ export async function getWorldAbi({
2317
readonly indexerUrl?: string;
2418
readonly chainId?: number;
2519
}): Promise<Abi> {
26-
const worldFunctions = await getFunctions({
20+
const { records } = await getRecords({
21+
table: metadataConfig.tables.metadata__ResourceTag,
22+
worldAddress,
23+
chainId,
24+
indexerUrl,
2725
client,
28-
worldAddress: getAddress(worldAddress),
2926
fromBlock,
3027
toBlock,
31-
indexerUrl,
32-
chainId,
3328
});
34-
const baseFunctionSelectors = (IBaseWorldAbi as Abi).filter(isAbiFunction).map(toFunctionSelector);
35-
const worldFunctionsAbi = worldFunctions
36-
.map((func) => {
37-
try {
38-
return functionSignatureToAbiItem(func.signature);
39-
} catch (error) {
40-
console.error(error);
41-
}
42-
})
43-
.filter(isDefined)
44-
.filter((abiItem) => !baseFunctionSelectors.includes(toFunctionSelector(abiItem)));
45-
const abi = [...IBaseWorldAbi, ...worldFunctionsAbi];
4629

47-
return abi as never;
30+
const abi = records
31+
.filter(({ tag }) => tag === stringToHex("worldAbi", { size: 32 }))
32+
.flatMap(({ value }) => (value === "0x" ? [] : parseAbi(hexToString(value).split("\n"))));
33+
34+
return abi;
4835
}

0 commit comments

Comments
 (0)