Skip to content

Commit

Permalink
Merge pull request #239 from hypercerts-org/develop
Browse files Browse the repository at this point in the history
Push to PRD
  • Loading branch information
bitbeckers authored Feb 3, 2025
2 parents c90b545 + 4d28567 commit 028f072
Show file tree
Hide file tree
Showing 31 changed files with 1,012 additions and 586 deletions.
1 change: 1 addition & 0 deletions .github/workflows/ci-test-unit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ jobs:
ALCHEMY_API_KEY: ${{ secrets.ALCHEMY_API_KEY }}
DRPC_API_KEY: "test"
INFURA_API_KEY: "test"
FILECOIN_API_KEY: "test"

INDEXER_ENVIRONMENT: "test"

Expand Down
5 changes: 5 additions & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@ import tseslint from "typescript-eslint";
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.strict,
{
rules: {
"@typescript-eslint/no-extraneous-class": "off",
},
},
);
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@
"dependencies": {
"@graphql-tools/merge": "^9.0.3",
"@graphql-yoga/plugin-response-cache": "^3.5.0",
"@hypercerts-org/contracts": "2.0.0-alpha.11",
"@hypercerts-org/contracts": "2.0.0-alpha.12",
"@hypercerts-org/marketplace-sdk": "0.3.37",
"@hypercerts-org/sdk": "2.5.0-beta.3",
"@hypercerts-org/sdk": "2.5.0-beta.6",
"@ipld/car": "^5.2.5",
"@openzeppelin/merkle-tree": "^1.0.5",
"@safe-global/api-kit": "^2.5.4",
Expand Down
395 changes: 39 additions & 356 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

42 changes: 42 additions & 0 deletions src/client/chainFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {
indexerEnvironment as environment,
Environment,
} from "../utils/constants.js";
import { Chain } from "viem";
import {
arbitrum,
arbitrumSepolia,
base,
baseSepolia,
celo,
filecoin,
filecoinCalibration,
optimism,
sepolia,
} from "viem/chains";

export class ChainFactory {
static getChain(chainId: number): Chain {
const chains: Record<number, Chain> = {
10: optimism,
314: filecoin,
8453: base,
42161: arbitrum,
42220: celo,
84532: baseSepolia,
314159: filecoinCalibration,
421614: arbitrumSepolia,
11155111: sepolia,
};

const chain = chains[chainId];
if (!chain) throw new Error(`Unsupported chain ID: ${chainId}`);
return chain;
}

static getSupportedChains(): number[] {
return environment === Environment.TEST
? [84532, 314159, 421614, 11155111]
: [10, 8453, 42220, 42161, 314];
}
}
123 changes: 123 additions & 0 deletions src/client/evmClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import {
alchemyApiKey,
drpcApiPkey,
infuraApiKey,
} from "../utils/constants.js";
import { PublicClient, createPublicClient, fallback } from "viem";
import { ChainFactory } from "./chainFactory.js";
import { RpcClientFactory } from "./rpcClientFactory.js";
import { JsonRpcProvider } from "ethers";

interface RpcProvider {
getUrl(chainId: number): string | undefined;
}

class AlchemyProvider implements RpcProvider {
getUrl(chainId: number): string | undefined {
const urls: Record<number, string> = {
10: `https://opt-mainnet.g.alchemy.com/v2/${alchemyApiKey}`,
8453: `https://base-mainnet.g.alchemy.com/v2/${alchemyApiKey}`,
42161: `https://arb-mainnet.g.alchemy.com/v2/${alchemyApiKey}`,
421614: `https://arb-sepolia.g.alchemy.com/v2/${alchemyApiKey}`,
84532: `https://base-sepolia.g.alchemy.com/v2/${alchemyApiKey}`,
11155111: `https://eth-sepolia.g.alchemy.com/v2/${alchemyApiKey}`,
};
return urls[chainId];
}
}

class InfuraProvider implements RpcProvider {
getUrl(chainId: number): string | undefined {
const urls: Record<number, string> = {
10: `https://optimism-mainnet.infura.io/v3/${infuraApiKey}`,
42220: `https://celo-mainnet.infura.io/v3/${infuraApiKey}`,
42161: `https://arbitrum-mainnet.infura.io/v3/${infuraApiKey}`,
421614: `https://arbitrum-sepolia.infura.io/v3/${infuraApiKey}`,
};
return urls[chainId];
}
}

class DrpcProvider implements RpcProvider {
getUrl(chainId: number): string | undefined {
const networks: Record<number, string> = {
10: "optimism",
8453: "base",
42220: "celo",
42161: "arbitrum",
421614: "arbitrum-sepolia",
};
const network = networks[chainId];
return network
? `https://lb.drpc.org/ogrpc?network=${network}&dkey=${drpcApiPkey}`
: undefined;
}
}

class GlifProvider implements RpcProvider {
getUrl(chainId: number): string | undefined {
const urls: Record<number, string> = {
314: `https://node.glif.io/space07/lotus/rpc/v1`,
314159: `https://calibration.node.glif.io/archive/lotus/rpc/v1`,
};
return urls[chainId];
}
}

export class EvmClientFactory {
private static readonly providers: RpcProvider[] = [
new AlchemyProvider(),
new InfuraProvider(),
new DrpcProvider(),
new GlifProvider(),
];

static createViemClient(chainId: number): PublicClient {
const urls = EvmClientFactory.getAllAvailableUrls(chainId);
if (urls.length === 0)
throw new Error(`No RPC URL available for chain ${chainId}`);

const transports = urls.map((url) =>
RpcClientFactory.createViemTransport(chainId, url),
);

return createPublicClient({
chain: ChainFactory.getChain(chainId),
transport: fallback(transports),
});
}

static createEthersClient(chainId: number): JsonRpcProvider {
const url = EvmClientFactory.getFirstAvailableUrl(chainId);
if (!url) throw new Error(`No RPC URL available for chain ${chainId}`);
return RpcClientFactory.createEthersJsonRpcProvider(chainId, url);
}

static getAllAvailableUrls(chainId: number): string[] {
return EvmClientFactory.providers
.map((provider) => provider.getUrl(chainId))
.filter((url): url is string => url !== undefined);
}

static getRpcUrl(chainId: number): string {
const url = EvmClientFactory.getFirstAvailableUrl(chainId);
if (!url) throw new Error(`No RPC URL available for chain ${chainId}`);
return url;
}

static getPublicRpcUrl(chainId: number): string {
const chain = ChainFactory.getChain(chainId);
if (!chain.rpcUrls?.default?.http?.[0]) {
throw new Error(`No public RPC URL available for chain ${chainId}`);
}
return chain.rpcUrls.default.http[0];
}

// Keep this for backward compatibility
static getFirstAvailableUrl(chainId: number): string | undefined {
return EvmClientFactory.getAllAvailableUrls(chainId)[0];
}
}

// Public API
export const getSupportedChains = ChainFactory.getSupportedChains;
72 changes: 72 additions & 0 deletions src/client/rpcClientFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { http, Transport } from "viem";
import { CustomEthersJsonRpcProvider } from "../lib/rpcProviders/customEthersJsonRpcProvider.js";
import { filecoinApiKey } from "../utils/constants.js";
import { ChainFactory } from "./chainFactory.js";

interface RpcConfig {
url: string;
headers?: Record<string, string>;
timeout?: number;
}

// Chain-specific RPC configuration factory
class RpcConfigFactory {
private static readonly DEFAULT_TIMEOUT = 20_000;

static getConfig(chainId: number, url: string): RpcConfig {
const baseConfig: RpcConfig = {
url,
timeout: this.DEFAULT_TIMEOUT,
};

// Chain-specific configurations
switch (chainId) {
case 314:
case 314159:
return {
...baseConfig,
headers: {
Authorization: `Bearer ${filecoinApiKey}`,
},
};
default:
return baseConfig;
}
}
}

// Unified client factory for both Viem and Chainsauce clients
export class RpcClientFactory {
// Creates a Viem transport
static createViemTransport(chainId: number, url: string): Transport {
const config = RpcConfigFactory.getConfig(chainId, url);

const httpConfig: Parameters<typeof http>[1] = {
timeout: config.timeout,
};

if (config.headers) {
httpConfig.fetchOptions = {
headers: config.headers,
};
}

return http(config.url, httpConfig);
}

static createEthersJsonRpcProvider(chainId: number, url: string) {
const config = RpcConfigFactory.getConfig(chainId, url);
const chain = ChainFactory.getChain(chainId);
const network = {
chainId: chain.id,
name: chain.name,
ensAddress: chain.contracts?.ensRegistry?.address,
};

return new CustomEthersJsonRpcProvider({
url: config.url,
config: { headers: config.headers },
network,
});
}
}
3 changes: 2 additions & 1 deletion src/commands/CommandFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export function getCommand(request: SignatureRequest): ISafeApiCommand {
return new UserUpsertCommand(
request.safe_address,
request.message_hash,
request.chain_id,
// The type is lying. It's a string.
Number(request.chain_id),
);
default:
console.warn("Unrecognized purpose:", request.purpose);
Expand Down
4 changes: 3 additions & 1 deletion src/commands/SafeApiCommand.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import SafeApiKit from "@safe-global/api-kit";

import { SafeApiStrategyFactory } from "../lib/safe/SafeApiKitStrategy.js";
import { SupabaseDataService } from "../services/SupabaseDataService.js";
import { ISafeApiCommand } from "../types/safe-signatures.js";

Expand All @@ -15,7 +16,8 @@ export abstract class SafeApiCommand implements ISafeApiCommand {
this.messageHash = messageHash;
this.chainId = chainId;
this.dataService = new SupabaseDataService();
this.safeApiKit = new SafeApiKit.default({ chainId: BigInt(chainId) });
this.safeApiKit =
SafeApiStrategyFactory.getStrategy(chainId).createInstance();
}

abstract execute(): Promise<void>;
Expand Down
2 changes: 1 addition & 1 deletion src/commands/UserUpsertCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
USER_UPDATE_MESSAGE_SCHEMA,
} from "../lib/users/schemas.js";
import { isTypedMessage } from "../utils/signatures.js";
import UserUpsertSignatureVerifier from "../lib/safe-signature-verification/UserUpsertSignatureVerifier.js";
import UserUpsertSignatureVerifier from "../lib/safe/signature-verification/UserUpsertSignatureVerifier.js";
import { Database } from "../types/supabaseData.js";

import { SafeApiCommand } from "./SafeApiCommand.js";
Expand Down
16 changes: 8 additions & 8 deletions src/controllers/BlueprintController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,19 @@ import {
SuccessResponse,
Tags,
} from "tsoa";
import { isAddress } from "viem";
import { z } from "zod";
import { EvmClientFactory } from "../client/evmClient.js";
import { SupabaseDataService } from "../services/SupabaseDataService.js";
import type {
BlueprintResponse,
BaseResponse,
BlueprintCreateRequest,
BlueprintDeleteRequest,
BlueprintQueueMintRequest,
BaseResponse,
BlueprintResponse,
} from "../types/api.js";
import { z } from "zod";
import { SupabaseDataService } from "../services/SupabaseDataService.js";
import { verifyAuthSignedData } from "../utils/verifyAuthSignedData.js";
import { isAddress } from "viem";
import { Json } from "../types/supabaseData.js";
import { getEvmClient } from "../utils/getRpcUrl.js";
import { verifyAuthSignedData } from "../utils/verifyAuthSignedData.js";
import { waitForTxThenMintBlueprint } from "../utils/waitForTxThenMintBlueprint.js";

@Route("v1/blueprints")
Expand Down Expand Up @@ -367,7 +367,7 @@ export class BlueprintController extends Controller {
};
}

const client = getEvmClient(chain_id);
const client = EvmClientFactory.createViemClient(chain_id);
const transaction = await client.getTransaction({
hash: tx_hash as `0x${string}`,
});
Expand Down
Loading

0 comments on commit 028f072

Please sign in to comment.