Skip to content

Commit 028f072

Browse files
authored
Merge pull request #239 from hypercerts-org/develop
Push to PRD
2 parents c90b545 + 4d28567 commit 028f072

31 files changed

+1012
-586
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.5.0-beta.3",
33+
"@hypercerts-org/sdk": "2.5.0-beta.6",
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: 39 additions & 356 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: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
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 { 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 getAllAvailableUrls(chainId: number): string[] {
97+
return EvmClientFactory.providers
98+
.map((provider) => provider.getUrl(chainId))
99+
.filter((url): url is string => url !== undefined);
100+
}
101+
102+
static getRpcUrl(chainId: number): string {
103+
const url = EvmClientFactory.getFirstAvailableUrl(chainId);
104+
if (!url) throw new Error(`No RPC URL available for chain ${chainId}`);
105+
return url;
106+
}
107+
108+
static getPublicRpcUrl(chainId: number): string {
109+
const chain = ChainFactory.getChain(chainId);
110+
if (!chain.rpcUrls?.default?.http?.[0]) {
111+
throw new Error(`No public RPC URL available for chain ${chainId}`);
112+
}
113+
return chain.rpcUrls.default.http[0];
114+
}
115+
116+
// Keep this for backward compatibility
117+
static getFirstAvailableUrl(chainId: number): string | undefined {
118+
return EvmClientFactory.getAllAvailableUrls(chainId)[0];
119+
}
120+
}
121+
122+
// Public API
123+
export const getSupportedChains = ChainFactory.getSupportedChains;

src/client/rpcClientFactory.ts

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

src/commands/CommandFactory.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ export function getCommand(request: SignatureRequest): ISafeApiCommand {
1313
return new UserUpsertCommand(
1414
request.safe_address,
1515
request.message_hash,
16-
request.chain_id,
16+
// The type is lying. It's a string.
17+
Number(request.chain_id),
1718
);
1819
default:
1920
console.warn("Unrecognized purpose:", request.purpose);

src/commands/SafeApiCommand.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import SafeApiKit from "@safe-global/api-kit";
22

3+
import { SafeApiStrategyFactory } from "../lib/safe/SafeApiKitStrategy.js";
34
import { SupabaseDataService } from "../services/SupabaseDataService.js";
45
import { ISafeApiCommand } from "../types/safe-signatures.js";
56

@@ -15,7 +16,8 @@ export abstract class SafeApiCommand implements ISafeApiCommand {
1516
this.messageHash = messageHash;
1617
this.chainId = chainId;
1718
this.dataService = new SupabaseDataService();
18-
this.safeApiKit = new SafeApiKit.default({ chainId: BigInt(chainId) });
19+
this.safeApiKit =
20+
SafeApiStrategyFactory.getStrategy(chainId).createInstance();
1921
}
2022

2123
abstract execute(): Promise<void>;

src/commands/UserUpsertCommand.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
USER_UPDATE_MESSAGE_SCHEMA,
66
} from "../lib/users/schemas.js";
77
import { isTypedMessage } from "../utils/signatures.js";
8-
import UserUpsertSignatureVerifier from "../lib/safe-signature-verification/UserUpsertSignatureVerifier.js";
8+
import UserUpsertSignatureVerifier from "../lib/safe/signature-verification/UserUpsertSignatureVerifier.js";
99
import { Database } from "../types/supabaseData.js";
1010

1111
import { SafeApiCommand } from "./SafeApiCommand.js";

0 commit comments

Comments
 (0)