Skip to content

Commit a97a85c

Browse files
authored
fix: fetch chains from API to support newer chains (#384)
* fix: use chains DB package * support lowercase, chain ID in overrides
1 parent 1f5ed0f commit a97a85c

File tree

4 files changed

+187
-55
lines changed

4 files changed

+187
-55
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
"@sinclair/typebox": "^0.31.28",
4545
"@t3-oss/env-core": "^0.6.0",
4646
"@thirdweb-dev/auth": "^4.1.0-nightly-c238fde8-20231020022304",
47-
"@thirdweb-dev/chains": "^0.1.61-nightly-d2001ca4-20231209002129",
47+
"@thirdweb-dev/chains": "^0.1.65",
4848
"@thirdweb-dev/sdk": "^4.0.23",
4949
"@thirdweb-dev/service-utils": "^0.4.2",
5050
"@thirdweb-dev/wallets": "^2.1.5",

src/server/utils/chain.ts

Lines changed: 44 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,60 @@
11
import { Static } from "@sinclair/typebox";
2-
import { allChains, getChainByChainIdAsync } from "@thirdweb-dev/chains";
2+
import {
3+
getChainByChainIdAsync,
4+
getChainBySlugAsync,
5+
} from "@thirdweb-dev/chains";
36
import { getConfig } from "../../utils/cache/getConfig";
47
import { networkResponseSchema } from "../../utils/cache/getSdk";
8+
import { logger } from "../../utils/logger";
59

6-
const ChainNameToChainId = {
7-
...allChains.reduce((acc, chain) => {
8-
acc[chain.slug] = chain.chainId;
9-
return acc;
10-
}, {} as Record<string, number>),
11-
} as Record<string, number>;
12-
13-
// TODO: We should use a universal name - probably chain in the routes
14-
export const getChainIdFromChain = async (chain: string): Promise<number> => {
15-
let chainId: number | undefined = undefined;
16-
let chainData: Static<typeof networkResponseSchema> | undefined = undefined;
17-
18-
// If we detect a valid chain name, use the corresponding chain id
19-
if (chain in ChainNameToChainId) {
20-
chainId = ChainNameToChainId[chain as keyof typeof ChainNameToChainId];
21-
}
10+
/**
11+
* Given a valid chain name ('Polygon') or ID ('137'), return the numeric chain ID.
12+
*/
13+
export const getChainIdFromChain = async (input: string): Promise<number> => {
14+
const inputSlug = input.toLowerCase();
15+
// inputId may be NaN if a slug is provided ('Polygon').
16+
const inputId = parseInt(input);
2217

2318
const config = await getConfig();
2419

25-
// If we're passed a valid chain id directly, then use it
26-
if (!isNaN(parseInt(chain))) {
27-
const unknownChainId = parseInt(chain);
20+
// Check if the chain ID exists in chainOverrides first.
21+
if (config.chainOverrides) {
2822
try {
29-
const chain = await getChainByChainIdAsync(unknownChainId);
30-
// If the chain id is for a supported chain, use the chain id
31-
if (chain) {
32-
chainId = unknownChainId;
33-
}
34-
} catch {
35-
if (!chainId) {
36-
if (config?.chainOverrides) {
37-
const parsedChainOverrides = JSON.parse(config.chainOverrides);
38-
chainData = parsedChainOverrides.find(
39-
(chainData: Static<typeof networkResponseSchema>) =>
40-
chainData.chainId === unknownChainId,
41-
);
23+
const parsedChainOverrides: Static<typeof networkResponseSchema>[] =
24+
JSON.parse(config.chainOverrides);
25+
26+
for (const chainData of parsedChainOverrides) {
27+
if (
28+
inputSlug === chainData.slug.toLowerCase() ||
29+
inputId === chainData.chainId
30+
) {
31+
return chainData.chainId;
4232
}
4333
}
44-
// If the chain id is unsupported, getChainByChainIdAsync will throw
45-
}
46-
} else {
47-
if (config?.chainOverrides) {
48-
const parsedChainOverrides = JSON.parse(config.chainOverrides);
49-
chainData = parsedChainOverrides.find(
50-
(chainData: Static<typeof networkResponseSchema>) =>
51-
chainData.slug === chain,
52-
);
34+
} catch (e) {
35+
logger({
36+
service: "server",
37+
level: "error",
38+
message: `Failed parsing chainOverrides: ${e}`,
39+
});
5340
}
5441
}
5542

56-
if (chainData) {
57-
chainId = chainData.chainId;
58-
}
59-
60-
if (!chainId) {
61-
// TODO: Custom error messages
62-
throw new Error(`Invalid chain ${chain}, please use a different value.`);
43+
if (!isNaN(inputId)) {
44+
// Fetch by chain ID.
45+
const chainData = await getChainByChainIdAsync(inputId);
46+
if (chainData) {
47+
return chainData.chainId;
48+
}
49+
} else {
50+
// Fetch by chain name.
51+
const chainData = await getChainBySlugAsync(inputSlug);
52+
if (chainData) {
53+
return chainData.chainId;
54+
}
6355
}
6456

65-
return chainId;
57+
throw new Error(
58+
`Invalid chain. Please confirm this is a valid chain: https://thirdweb.com/${input}`,
59+
);
6660
};

src/tests/chain.test.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import {
2+
getChainByChainIdAsync,
3+
getChainBySlugAsync,
4+
} from "@thirdweb-dev/chains";
5+
import { getChainIdFromChain } from "../server/utils/chain";
6+
import { getConfig } from "../utils/cache/getConfig";
7+
8+
// Mock the external dependencies
9+
jest.mock("../utils/cache/getConfig");
10+
jest.mock("@thirdweb-dev/chains");
11+
12+
const mockGetConfig = getConfig as jest.MockedFunction<typeof getConfig>;
13+
14+
const mockGetChainByChainIdAsync =
15+
getChainByChainIdAsync as jest.MockedFunction<typeof getChainByChainIdAsync>;
16+
const mockGetChainBySlugAsync = getChainBySlugAsync as jest.MockedFunction<
17+
typeof getChainBySlugAsync
18+
>;
19+
20+
describe("getChainIdFromChain", () => {
21+
beforeEach(() => {
22+
// Clear all mock calls before each test
23+
jest.clearAllMocks();
24+
});
25+
26+
it("should return the chainId from chainOverrides if it exists by slug", async () => {
27+
// @ts-ignore
28+
mockGetConfig.mockResolvedValueOnce({
29+
chainOverrides: JSON.stringify([
30+
{
31+
slug: "Polygon",
32+
chainId: 137,
33+
},
34+
]),
35+
});
36+
37+
const result = await getChainIdFromChain("Polygon");
38+
39+
expect(result).toBe(137);
40+
expect(getChainByChainIdAsync).not.toHaveBeenCalled();
41+
expect(getChainBySlugAsync).not.toHaveBeenCalled();
42+
});
43+
44+
it("should return the chainId from chainOverrides if it exists by slug, case-insensitive", async () => {
45+
// @ts-ignore
46+
mockGetConfig.mockResolvedValueOnce({
47+
chainOverrides: JSON.stringify([
48+
{
49+
slug: "Polygon",
50+
chainId: 137,
51+
},
52+
]),
53+
});
54+
55+
const result = await getChainIdFromChain("polygon");
56+
57+
expect(result).toBe(137);
58+
expect(getChainByChainIdAsync).not.toHaveBeenCalled();
59+
expect(getChainBySlugAsync).not.toHaveBeenCalled();
60+
});
61+
62+
it("should return the chainId from chainOverrides if it exists by ID", async () => {
63+
// @ts-ignore
64+
mockGetConfig.mockResolvedValueOnce({
65+
chainOverrides: JSON.stringify([
66+
{
67+
slug: "Polygon",
68+
chainId: 137,
69+
},
70+
]),
71+
});
72+
73+
const result = await getChainIdFromChain("137");
74+
75+
expect(result).toBe(137);
76+
expect(getChainByChainIdAsync).not.toHaveBeenCalled();
77+
expect(getChainBySlugAsync).not.toHaveBeenCalled();
78+
});
79+
80+
it("should return the chainId from chainOverrides if it exists", async () => {
81+
// @ts-ignore
82+
mockGetConfig.mockResolvedValueOnce({
83+
chainOverrides: JSON.stringify([
84+
{
85+
slug: "Polygon",
86+
chainId: 137,
87+
},
88+
]),
89+
});
90+
91+
const result = await getChainIdFromChain("Polygon");
92+
93+
expect(result).toBe(137);
94+
expect(getChainByChainIdAsync).not.toHaveBeenCalled();
95+
expect(getChainBySlugAsync).not.toHaveBeenCalled();
96+
});
97+
98+
it("should return the chainId from getChainByChainIdAsync if chain is a valid numeric string", async () => {
99+
// @ts-ignore
100+
mockGetConfig.mockResolvedValueOnce({});
101+
// @ts-ignore
102+
mockGetChainByChainIdAsync.mockResolvedValueOnce({
103+
name: "Polygon",
104+
chainId: 137,
105+
});
106+
107+
const result = await getChainIdFromChain("137");
108+
109+
expect(result).toBe(137);
110+
expect(getChainByChainIdAsync).toHaveBeenCalledWith(137);
111+
expect(getChainBySlugAsync).not.toHaveBeenCalled();
112+
});
113+
114+
it("should return the chainId from getChainBySlugAsync if chain is a valid string", async () => {
115+
// @ts-ignore
116+
mockGetConfig.mockResolvedValueOnce({});
117+
// @ts-ignore
118+
mockGetChainBySlugAsync.mockResolvedValueOnce({
119+
name: "Polygon",
120+
chainId: 137,
121+
});
122+
123+
const result = await getChainIdFromChain("Polygon");
124+
125+
expect(result).toBe(137);
126+
expect(getChainBySlugAsync).toHaveBeenCalledWith("polygon");
127+
expect(getChainByChainIdAsync).not.toHaveBeenCalled();
128+
});
129+
130+
it("should throw an error for an invalid chain", async () => {
131+
// @ts-ignore
132+
mockGetConfig.mockResolvedValueOnce({});
133+
134+
await expect(getChainIdFromChain("InvalidChain")).rejects.toThrow(
135+
"Invalid chain. Please confirm this is a valid chain",
136+
);
137+
});
138+
});

yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3435,10 +3435,10 @@
34353435
resolved "https://registry.yarnpkg.com/@thirdweb-dev/chains/-/chains-0.1.62.tgz#d7a5c8bc71d3dac68c7da44b13b5ea7113a01151"
34363436
integrity sha512-BLJEub6SIDfwktqLhFkeurVQbJp/RZeJLhzjjwxSWE4Kb1K7rv/nNkwRFXd3Hhc46DPJvrHuG3oBwkZgWxWgQw==
34373437

3438-
"@thirdweb-dev/chains@^0.1.61-nightly-d2001ca4-20231209002129":
3439-
version "0.1.61-nightly-d2001ca4-20231209002129"
3440-
resolved "https://registry.yarnpkg.com/@thirdweb-dev/chains/-/chains-0.1.61-nightly-d2001ca4-20231209002129.tgz#366a284ae2fa2f81167b0e4941be1b3d74c05838"
3441-
integrity sha512-nQfCDMg9YY/7CS7S2X6Cu1U+RksS3lLUDoNQQyoo1S6pVBygYyzezgFNmX5TxB0S1VMwvM+rmSVk6ENbwr7B5Q==
3438+
"@thirdweb-dev/chains@^0.1.65":
3439+
version "0.1.65"
3440+
resolved "https://registry.yarnpkg.com/@thirdweb-dev/chains/-/chains-0.1.65.tgz#a298e39d1c16b787008ace29b616bccc9b96d253"
3441+
integrity sha512-csxBZnnjSEAISjlQrYJP/FV4buUlqHb7NF4vX/15hYc31J6Z4vJPRWcsd2WhzPj9YXR5sgSJcu6ro2C6s4jmOA==
34423442

34433443
"@thirdweb-dev/contracts-js@1.3.16":
34443444
version "1.3.16"

0 commit comments

Comments
 (0)