Skip to content

Commit b8ebf0a

Browse files
committed
feat(filecoin): filecoin chain and rpc support
Updates the supported chains to support filecoin and filecoin calibration. Because of the JWT authentication of Glif the rpcFactory and evmClient builder were imported from the hypercerts-indexer. Test were created and updated accordingly
1 parent bcdb8da commit b8ebf0a

24 files changed

+1404
-400
lines changed

.github/workflows/ci-test-unit.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ jobs:
1919
ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }}
2020
DRPC_API_KEY: "test"
2121
INFURA_API_KEY: "test"
22+
FILECOIN_API_KEY: "test"
2223

2324
INDEXER_ENVIRONMENT: "test"
2425

eslint.config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,9 @@ import tseslint from "typescript-eslint";
44
export default tseslint.config(
55
eslint.configs.recommended,
66
...tseslint.configs.strict,
7+
{
8+
rules: {
9+
"@typescript-eslint/no-extraneous-class": "off",
10+
},
11+
},
712
);

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@
2828
"dependencies": {
2929
"@graphql-tools/merge": "^9.0.3",
3030
"@graphql-yoga/plugin-response-cache": "^3.5.0",
31-
"@hypercerts-org/contracts": "2.0.0-alpha.11",
31+
"@hypercerts-org/contracts": "2.0.0-alpha.12",
3232
"@hypercerts-org/marketplace-sdk": "0.3.37",
33-
"@hypercerts-org/sdk": "2.3.0",
33+
"@hypercerts-org/sdk": "2.4.0",
3434
"@ipld/car": "^5.2.5",
3535
"@openzeppelin/merkle-tree": "^1.0.5",
3636
"@safe-global/api-kit": "^2.5.4",

pnpm-lock.yaml

Lines changed: 452 additions & 225 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/client/chainFactory.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import {
2+
indexerEnvironment as environment,
3+
Environment,
4+
} from "../utils/constants.js";
5+
import { Chain } from "viem";
6+
import {
7+
arbitrum,
8+
arbitrumSepolia,
9+
base,
10+
baseSepolia,
11+
celo,
12+
filecoin,
13+
filecoinCalibration,
14+
optimism,
15+
sepolia,
16+
} from "viem/chains";
17+
18+
export class ChainFactory {
19+
static getChain(chainId: number): Chain {
20+
const chains: Record<number, Chain> = {
21+
10: optimism,
22+
314: filecoin,
23+
8453: base,
24+
42161: arbitrum,
25+
42220: celo,
26+
84532: baseSepolia,
27+
314159: filecoinCalibration,
28+
421614: arbitrumSepolia,
29+
11155111: sepolia,
30+
};
31+
32+
const chain = chains[chainId];
33+
if (!chain) throw new Error(`Unsupported chain ID: ${chainId}`);
34+
return chain;
35+
}
36+
37+
static getSupportedChains(): number[] {
38+
return environment === Environment.TEST
39+
? [84532, 314159, 421614, 11155111]
40+
: [10, 8453, 42220, 42161, 314];
41+
}
42+
}

src/client/evmClient.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import {
2+
alchemyApiKey,
3+
drpcApiPkey,
4+
infuraApiKey,
5+
} from "../utils/constants.js";
6+
import { PublicClient, createPublicClient, fallback } from "viem";
7+
import { ChainFactory } from "./chainFactory.js";
8+
import { RpcClientFactory } from "./rpcClientFactory.js";
9+
import { Eip1193Provider, JsonRpcProvider } from "ethers";
10+
11+
interface RpcProvider {
12+
getUrl(chainId: number): string | undefined;
13+
}
14+
15+
class AlchemyProvider implements RpcProvider {
16+
getUrl(chainId: number): string | undefined {
17+
const urls: Record<number, string> = {
18+
10: `https://opt-mainnet.g.alchemy.com/v2/${alchemyApiKey}`,
19+
8453: `https://base-mainnet.g.alchemy.com/v2/${alchemyApiKey}`,
20+
42161: `https://arb-mainnet.g.alchemy.com/v2/${alchemyApiKey}`,
21+
421614: `https://arb-sepolia.g.alchemy.com/v2/${alchemyApiKey}`,
22+
84532: `https://base-sepolia.g.alchemy.com/v2/${alchemyApiKey}`,
23+
11155111: `https://eth-sepolia.g.alchemy.com/v2/${alchemyApiKey}`,
24+
};
25+
return urls[chainId];
26+
}
27+
}
28+
29+
class InfuraProvider implements RpcProvider {
30+
getUrl(chainId: number): string | undefined {
31+
const urls: Record<number, string> = {
32+
10: `https://optimism-mainnet.infura.io/v3/${infuraApiKey}`,
33+
42220: `https://celo-mainnet.infura.io/v3/${infuraApiKey}`,
34+
42161: `https://arbitrum-mainnet.infura.io/v3/${infuraApiKey}`,
35+
421614: `https://arbitrum-sepolia.infura.io/v3/${infuraApiKey}`,
36+
};
37+
return urls[chainId];
38+
}
39+
}
40+
41+
class DrpcProvider implements RpcProvider {
42+
getUrl(chainId: number): string | undefined {
43+
const networks: Record<number, string> = {
44+
10: "optimism",
45+
8453: "base",
46+
42220: "celo",
47+
42161: "arbitrum",
48+
421614: "arbitrum-sepolia",
49+
};
50+
const network = networks[chainId];
51+
return network
52+
? `https://lb.drpc.org/ogrpc?network=${network}&dkey=${drpcApiPkey}`
53+
: undefined;
54+
}
55+
}
56+
57+
class GlifProvider implements RpcProvider {
58+
getUrl(chainId: number): string | undefined {
59+
const urls: Record<number, string> = {
60+
314: `https://node.glif.io/space07/lotus/rpc/v1`,
61+
314159: `https://calibration.node.glif.io/archive/lotus/rpc/v1`,
62+
};
63+
return urls[chainId];
64+
}
65+
}
66+
67+
export class EvmClientFactory {
68+
private static readonly providers: RpcProvider[] = [
69+
new AlchemyProvider(),
70+
new InfuraProvider(),
71+
new DrpcProvider(),
72+
new GlifProvider(),
73+
];
74+
75+
static createViemClient(chainId: number): PublicClient {
76+
const urls = EvmClientFactory.getAllAvailableUrls(chainId);
77+
if (urls.length === 0)
78+
throw new Error(`No RPC URL available for chain ${chainId}`);
79+
80+
const transports = urls.map((url) =>
81+
RpcClientFactory.createViemTransport(chainId, url),
82+
);
83+
84+
return createPublicClient({
85+
chain: ChainFactory.getChain(chainId),
86+
transport: fallback(transports),
87+
});
88+
}
89+
90+
static createEthersClient(chainId: number): JsonRpcProvider {
91+
const url = EvmClientFactory.getFirstAvailableUrl(chainId);
92+
if (!url) throw new Error(`No RPC URL available for chain ${chainId}`);
93+
return RpcClientFactory.createEthersJsonRpcProvider(chainId, url);
94+
}
95+
96+
static createEip1193Client(chainId: number): Eip1193Provider {
97+
const url = EvmClientFactory.getFirstAvailableUrl(chainId);
98+
if (!url) throw new Error(`No RPC URL available for chain ${chainId}`);
99+
return RpcClientFactory.createEip1193Provider(chainId, url);
100+
}
101+
102+
static getAllAvailableUrls(chainId: number): string[] {
103+
return EvmClientFactory.providers
104+
.map((provider) => provider.getUrl(chainId))
105+
.filter((url): url is string => url !== undefined);
106+
}
107+
108+
// Keep this for backward compatibility
109+
static getFirstAvailableUrl(chainId: number): string | undefined {
110+
return EvmClientFactory.getAllAvailableUrls(chainId)[0];
111+
}
112+
}
113+
114+
export const getRpcUrl = (chainId: number): string => {
115+
const url = EvmClientFactory.getFirstAvailableUrl(chainId);
116+
if (!url) throw new Error(`No RPC URL available for chain ${chainId}`);
117+
return url;
118+
};
119+
120+
// Public API
121+
export const getSupportedChains = ChainFactory.getSupportedChains;

src/client/rpcClientFactory.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { http, Transport } from "viem";
2+
import { CustomEthersJsonRpcProvider } from "../lib/rpcProviders/customEthersJsonRpcProvider.js";
3+
import { filecoinApiKey } from "../utils/constants.js";
4+
import { ChainFactory } from "./chainFactory.js";
5+
import { CustomEip1193Provider } from "../lib/rpcProviders/customEthers1193RpcProvider.js";
6+
7+
interface RpcConfig {
8+
url: string;
9+
headers?: Record<string, string>;
10+
timeout?: number;
11+
}
12+
13+
// Chain-specific RPC configuration factory
14+
class RpcConfigFactory {
15+
private static readonly DEFAULT_TIMEOUT = 20_000;
16+
17+
static getConfig(chainId: number, url: string): RpcConfig {
18+
const baseConfig: RpcConfig = {
19+
url,
20+
timeout: this.DEFAULT_TIMEOUT,
21+
};
22+
23+
// Chain-specific configurations
24+
switch (chainId) {
25+
case 314:
26+
case 314159:
27+
return {
28+
...baseConfig,
29+
headers: {
30+
Authorization: `Bearer ${filecoinApiKey}`,
31+
},
32+
};
33+
default:
34+
return baseConfig;
35+
}
36+
}
37+
}
38+
39+
// Unified client factory for both Viem and Chainsauce clients
40+
export class RpcClientFactory {
41+
// Creates a Viem transport
42+
static createViemTransport(chainId: number, url: string): Transport {
43+
const config = RpcConfigFactory.getConfig(chainId, url);
44+
45+
const httpConfig: Parameters<typeof http>[1] = {
46+
timeout: config.timeout,
47+
};
48+
49+
if (config.headers) {
50+
httpConfig.fetchOptions = {
51+
headers: config.headers,
52+
};
53+
}
54+
55+
return http(config.url, httpConfig);
56+
}
57+
58+
static createEthersJsonRpcProvider(chainId: number, url: string) {
59+
const config = RpcConfigFactory.getConfig(chainId, url);
60+
const chain = ChainFactory.getChain(chainId);
61+
const network = {
62+
chainId: chain.id,
63+
name: chain.name,
64+
ensAddress: chain.contracts?.ensRegistry?.address,
65+
};
66+
67+
return new CustomEthersJsonRpcProvider({
68+
url: config.url,
69+
config: { headers: config.headers },
70+
network,
71+
});
72+
}
73+
74+
static createEip1193Provider(chainId: number, url: string) {
75+
const config = RpcConfigFactory.getConfig(chainId, url);
76+
return new CustomEip1193Provider({
77+
url: config.url,
78+
config: { headers: config.headers },
79+
});
80+
}
81+
}

src/controllers/BlueprintController.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,19 @@ import {
99
SuccessResponse,
1010
Tags,
1111
} from "tsoa";
12+
import { isAddress } from "viem";
13+
import { z } from "zod";
14+
import { EvmClientFactory } from "../client/evmClient.js";
15+
import { SupabaseDataService } from "../services/SupabaseDataService.js";
1216
import type {
13-
BlueprintResponse,
17+
BaseResponse,
1418
BlueprintCreateRequest,
1519
BlueprintDeleteRequest,
1620
BlueprintQueueMintRequest,
17-
BaseResponse,
21+
BlueprintResponse,
1822
} from "../types/api.js";
19-
import { z } from "zod";
20-
import { SupabaseDataService } from "../services/SupabaseDataService.js";
21-
import { verifyAuthSignedData } from "../utils/verifyAuthSignedData.js";
22-
import { isAddress } from "viem";
2323
import { Json } from "../types/supabaseData.js";
24-
import { getEvmClient } from "../utils/getRpcUrl.js";
24+
import { verifyAuthSignedData } from "../utils/verifyAuthSignedData.js";
2525
import { waitForTxThenMintBlueprint } from "../utils/waitForTxThenMintBlueprint.js";
2626

2727
@Route("v1/blueprints")
@@ -367,7 +367,7 @@ export class BlueprintController extends Controller {
367367
};
368368
}
369369

370-
const client = getEvmClient(chain_id);
370+
const client = EvmClientFactory.createViemClient(chain_id);
371371
const transaction = await client.getTransaction({
372372
hash: tx_hash as `0x${string}`,
373373
});

src/controllers/MarketplaceController.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {
33
HypercertExchangeClient,
44
utils,
55
} from "@hypercerts-org/marketplace-sdk";
6-
import { ethers, verifyTypedData } from "ethers";
6+
import { verifyTypedData } from "ethers";
77
import {
88
Body,
99
Controller,
@@ -17,12 +17,12 @@ import {
1717
import { z } from "zod";
1818

1919
import { isAddress, verifyMessage } from "viem";
20+
import { EvmClientFactory } from "../client/evmClient.js";
2021
import { SupabaseDataService } from "../services/SupabaseDataService.js";
22+
import { BaseResponse } from "../types/api.js";
2123
import { getFractionsById } from "../utils/getFractionsById.js";
22-
import { getRpcUrl } from "../utils/getRpcUrl.js";
2324
import { isParsableToBigInt } from "../utils/isParsableToBigInt.js";
2425
import { getHypercertTokenId } from "../utils/tokenIds.js";
25-
import { BaseResponse } from "../types/api.js";
2626

2727
export interface CreateOrderRequest {
2828
signature: string;
@@ -148,7 +148,7 @@ export class MarketplaceController extends Controller {
148148
const hec = new HypercertExchangeClient(
149149
chainId,
150150
// @ts-expect-error Typing issue with provider
151-
new ethers.JsonRpcProvider(getRpcUrl(chainId)),
151+
EvmClientFactory.createEthersClient(chainId),
152152
);
153153
const typedData = hec.getTypedDataDomain();
154154

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Eip1193Provider, FetchRequest } from "ethers";
2+
3+
interface CustomEip1193Config {
4+
headers?: Record<string, string>;
5+
}
6+
7+
interface CustomEip1193ProviderOptions {
8+
url: string | FetchRequest;
9+
config?: CustomEip1193Config;
10+
}
11+
12+
export class CustomEip1193Provider implements Eip1193Provider {
13+
private fetchRequest: FetchRequest;
14+
15+
constructor({ url, config }: CustomEip1193ProviderOptions) {
16+
if (typeof url === "string") {
17+
url = new FetchRequest(url);
18+
}
19+
this.fetchRequest = url.clone();
20+
21+
if (config?.headers) {
22+
Object.entries(config.headers).forEach(([key, value]) => {
23+
this.fetchRequest.setHeader(key, value);
24+
});
25+
}
26+
}
27+
28+
async request(args: {
29+
method: string;
30+
params?: Array<unknown>;
31+
}): Promise<unknown> {
32+
this.fetchRequest.method = "POST";
33+
this.fetchRequest.body = JSON.stringify({
34+
jsonrpc: "2.0",
35+
id: 1,
36+
method: args.method,
37+
params: args.params || [],
38+
});
39+
40+
const response = await this.fetchRequest.send();
41+
42+
const result = JSON.parse(response.bodyText);
43+
if (result.error) {
44+
throw new Error(result.error.message || "RPC Error");
45+
}
46+
return result.result;
47+
}
48+
}

0 commit comments

Comments
 (0)