From 52ea602bba6e299d62af4152cc221c28cc408ef1 Mon Sep 17 00:00:00 2001 From: Jip Stavenuiter Date: Tue, 16 Apr 2024 00:29:48 +0200 Subject: [PATCH] Feature/multi chain support (#1301) * (feat): add support for querying from multiple chains at the same time * (feat): refactor parseClaimId to standalone utility method, add sql update script for claimIds in supabase * (feat): update autotask scripts * (feat): add is claim on connected chain utility method * (feat): update tests and docs * (feat): set default indexer environment to all * (fix): revert auto-task updates --- pnpm-lock.yaml | 49 +++-- ...ate-claimids-in-supabase-to-multichain.sql | 10 + sdk/README.md | 24 +-- sdk/package.json | 8 +- sdk/src/client.ts | 6 + sdk/src/constants.ts | 10 +- sdk/src/indexer.ts | 201 ++++++++++++------ sdk/src/indexer/gql/fragment-masking.ts | 50 ++--- sdk/src/indexer/gql/gql.ts | 25 +-- sdk/src/indexer/gql/index.ts | 2 +- sdk/src/types/client.ts | 19 +- sdk/src/types/indexer.ts | 5 +- sdk/src/utils/chains.ts | 9 + sdk/src/utils/config.ts | 48 +---- sdk/src/utils/index.ts | 2 + sdk/src/utils/parsing.ts | 37 ++++ sdk/test/indexer.test.ts | 25 ++- sdk/test/indexer/queries.test.ts | 5 +- sdk/test/utils/config.test.ts | 9 +- 19 files changed, 331 insertions(+), 213 deletions(-) create mode 100644 scripts/update-claimids-in-supabase-to-multichain.sql create mode 100644 sdk/src/utils/chains.ts create mode 100644 sdk/src/utils/parsing.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 54e7a20f..d159c099 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -574,8 +574,8 @@ importers: specifier: ^1.0.5 version: 1.0.5 '@urql/core': - specifier: ^4.2.0 - version: 4.2.0(graphql@16.8.1) + specifier: ^4.3.0 + version: 4.3.0(graphql@16.8.1) '@whatwg-node/fetch': specifier: ^0.9.13 version: 0.9.14 @@ -591,6 +591,9 @@ importers: ethers: specifier: 5.7.2 version: 5.7.2 + fast-deep-equal: + specifier: ^3.1.3 + version: 3.1.3 graphql: specifier: ^16.8.1 version: 16.8.1 @@ -602,7 +605,10 @@ importers: version: 4.0.6(graphql@16.8.1)(react@18.2.0) viem: specifier: ^2.9.4 - version: 2.9.4(typescript@5.3.2) + version: 2.9.8(typescript@5.3.2) + wonka: + specifier: ^6.3.4 + version: 6.3.4 devDependencies: '@babel/core': specifier: ^7.23.5 @@ -7382,7 +7388,7 @@ packages: '@graphql-typed-document-node/core': 3.2.0(graphql@16.8.1) '@hypercerts-org/contracts': 1.1.2(typescript@5.1.6) '@openzeppelin/merkle-tree': 1.0.5 - '@urql/core': 4.2.0(graphql@16.8.1) + '@urql/core': 4.3.0(graphql@16.8.1) '@whatwg-node/fetch': 0.9.14 ajv: 8.12.0 axios: 1.6.2(debug@4.3.4) @@ -7963,7 +7969,7 @@ packages: eventemitter2: 6.4.7 extension-port-stream: 2.1.1 i18next: 22.5.1 - i18next-browser-languagedetector: 7.2.0 + i18next-browser-languagedetector: 7.2.1 obj-multiplex: 1.0.0 pump: 3.0.0 qrcode-terminal-nooctal: 0.12.1 @@ -12530,8 +12536,8 @@ packages: /@ungap/structured-clone@1.2.0: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - /@urql/core@4.2.0(graphql@16.8.1): - resolution: {integrity: sha512-GRkZ4kECR9UohWAjiSk2UYUetco6/PqSrvyC4AH6g16tyqEShA63M232cfbE1J9XJPaGNjia14Gi+oOqzp144w==} + /@urql/core@4.3.0(graphql@16.8.1): + resolution: {integrity: sha512-wT+FeL8DG4x5o6RfHEnONNFVDM3616ouzATMYUClB6CB+iIu2mwfBKd7xSUxYOZmwtxna5/hDRQdMl3nbQZlnw==} dependencies: '@0no-co/graphql.web': 1.0.4(graphql@16.8.1) wonka: 6.3.4 @@ -16562,7 +16568,6 @@ packages: /cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} - hasBin: true dev: false /cssnano-preset-advanced@5.3.10(postcss@8.4.32): @@ -21704,8 +21709,8 @@ packages: resolution: {integrity: sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==} dev: false - /i18next-browser-languagedetector@7.2.0: - resolution: {integrity: sha512-U00DbDtFIYD3wkWsr2aVGfXGAj2TgnELzOX9qv8bT0aJtvPV9CRO77h+vgmHFBMe7LAxdwvT/7VkCWGya6L3tA==} + /i18next-browser-languagedetector@7.2.1: + resolution: {integrity: sha512-h/pM34bcH6tbz8WgGXcmWauNpQupCGr25XPp9cZwZInR9XHSjIFDYp1SIok7zSPsTOMxdvuLyu86V+g2Kycnfw==} dependencies: '@babel/runtime': 7.23.2 dev: false @@ -23559,6 +23564,7 @@ packages: /json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true dependencies: minimist: 1.2.8 dev: true @@ -23814,7 +23820,7 @@ packages: node-forge: 1.3.1 pathe: 1.1.1 std-env: 3.6.0 - ufo: 1.3.1 + ufo: 1.3.2 untun: 0.1.2 uqr: 0.1.2 dev: false @@ -26859,7 +26865,7 @@ packages: dependencies: destr: 2.0.2 node-fetch-native: 1.4.1 - ufo: 1.3.1 + ufo: 1.3.2 dev: false /on-exit-leak-free@0.2.0: @@ -30183,7 +30189,6 @@ packages: /sha.js@2.4.11: resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==} - hasBin: true dependencies: inherits: 2.0.4 safe-buffer: 5.2.1 @@ -32472,7 +32477,7 @@ packages: mri: 1.2.0 node-fetch-native: 1.4.1 ofetch: 1.3.3 - ufo: 1.3.1 + ufo: 1.3.2 transitivePeerDependencies: - supports-color dev: false @@ -32602,7 +32607,7 @@ packages: peerDependencies: react: '>= 16.8.0' dependencies: - '@urql/core': 4.2.0(graphql@16.8.1) + '@urql/core': 4.3.0(graphql@16.8.1) react: 18.2.0 wonka: 6.3.4 transitivePeerDependencies: @@ -32920,8 +32925,8 @@ packages: - zod dev: false - /viem@2.9.4(typescript@5.3.2): - resolution: {integrity: sha512-j239PwRYc9WU7GOogfJ5Iu/5jwYaidpR85gLxKCwQmuXkNILKiRHntEM15EAtC9bcgaa9oklYwQ+/MlLU593/A==} + /viem@2.9.8(typescript@5.1.6)(zod@3.22.4): + resolution: {integrity: sha512-vetoTZ6UF2okS/1I0+1p/QYdC4yA6uf4PeWwTBp3kD5wC6eQcmeh7zP+unNdnYHGGC63x7BTGldK1ep2IFVKcQ==} peerDependencies: typescript: '>=5.0.4' peerDependenciesMeta: @@ -32933,9 +32938,9 @@ packages: '@noble/hashes': 1.3.2 '@scure/bip32': 1.3.2 '@scure/bip39': 1.2.1 - abitype: 1.0.0(typescript@5.3.2) + abitype: 1.0.0(typescript@5.1.6)(zod@3.22.4) isows: 1.0.3(ws@8.13.0) - typescript: 5.3.2 + typescript: 5.1.6 ws: 8.13.0 transitivePeerDependencies: - bufferutil @@ -32943,7 +32948,7 @@ packages: - zod dev: false - /viem@2.9.8(typescript@5.1.6)(zod@3.22.4): + /viem@2.9.8(typescript@5.3.2): resolution: {integrity: sha512-vetoTZ6UF2okS/1I0+1p/QYdC4yA6uf4PeWwTBp3kD5wC6eQcmeh7zP+unNdnYHGGC63x7BTGldK1ep2IFVKcQ==} peerDependencies: typescript: '>=5.0.4' @@ -32956,9 +32961,9 @@ packages: '@noble/hashes': 1.3.2 '@scure/bip32': 1.3.2 '@scure/bip39': 1.2.1 - abitype: 1.0.0(typescript@5.1.6)(zod@3.22.4) + abitype: 1.0.0(typescript@5.3.2) isows: 1.0.3(ws@8.13.0) - typescript: 5.1.6 + typescript: 5.3.2 ws: 8.13.0 transitivePeerDependencies: - bufferutil diff --git a/scripts/update-claimids-in-supabase-to-multichain.sql b/scripts/update-claimids-in-supabase-to-multichain.sql new file mode 100644 index 00000000..97c3b5f3 --- /dev/null +++ b/scripts/update-claimids-in-supabase-to-multichain.sql @@ -0,0 +1,10 @@ +update "allowlistCache-chainId" set "claimId" = concat("chainId", '-', "claimId"); +update "allowlistCache-optimism" set "claimId" = concat('10', '-', "claimId"); +update "allowlistCache-goerli" set "claimId" = concat('5', '-', "claimId"); +update "allowlistCache-sepolia" set "claimId" = concat('11155111', '-', "claimId"); + +update "claims-metadata-mapping" set "claimId" = concat("chainId", '-', "claimId"); + +update "collections" set "claimId" = concat("chainId", '-', "claimId"); + +update "zuzalu-community-hypercerts" set "claimId" = concat("chainId", '-', "claimId"); \ No newline at end of file diff --git a/sdk/README.md b/sdk/README.md index 729940a6..6b5f1f23 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -58,18 +58,18 @@ HypercertClientConfig is a configuration object used when initializing a new ins you to customize the client by setting your own providers or deployments. At it's simplest, you only need to provide `chain.id` to initalize the client in `readonly` mode. -| Field | Type | Description | -| --------------------------- | ------- | ---------------------------------------------------------------------------------------------- | -| `chain` | Object | Partial configuration for the blockchain network. | -| `contractAddress` | String | The address of the deployed contract. | -| `graphUrl` | String | The URL to the subgraph that indexes the contract events. Override for localized testing. | -| `graphName` | String | The name of the subgraph. | -| `easContractAddress` | String | The address of the EAS contract. | -| `publicClient` | Object | The PublicClient is inherently read-only and is used for reading data from the blockchain. | -| `walletClient` | Object | The WalletClient is used for signing and sending transactions. | -| `unsafeForceOverrideConfig` | Boolean | Boolean to force the use of overridden values. | -| `readOnly` | Boolean | Boolean to assert if the client is in read-only mode. | -| `readOnlyReason` | String | Reason for read-only mode. This is optional and can be used for logging or debugging purposes. | +| Field | Type | Description | +| --------------------------- | --------------------------------- | ---------------------------------------------------------------------------------------------- | ------------------------------- | +| `chain` | Object | Partial configuration for the blockchain network. | +| `contractAddress` | String | The address of the deployed contract. | +| `graphName` | String | The name of the subgraph. | +| `easContractAddress` | String | The address of the EAS contract. | +| `publicClient` | Object | The PublicClient is inherently read-only and is used for reading data from the blockchain. | +| `walletClient` | Object | The WalletClient is used for signing and sending transactions. | +| `unsafeForceOverrideConfig` | Boolean | Boolean to force the use of overridden values. | +| `readOnly` | Boolean | Boolean to assert if the client is in read-only mode. | +| `readOnlyReason` | String | Reason for read-only mode. This is optional and can be used for logging or debugging purposes. | +| `indexerEnvironment` | `'test' \| 'production' \| 'all'` | Determines which graphs should be read out when querying | The environment of the indexer. | ### Read-only mode diff --git a/sdk/package.json b/sdk/package.json index f03761f3..caef98df 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@hypercerts-org/sdk", - "version": "1.5.0", + "version": "2.0.0-alpha.5", "description": "SDK for hypercerts protocol", "repository": "git@github.com:hypercerts-org/hypercerts.git", "author": "Hypercerts team", @@ -26,16 +26,18 @@ "@graphql-typed-document-node/core": "^3.2.0", "@hypercerts-org/contracts": "1.1.2", "@openzeppelin/merkle-tree": "^1.0.5", - "@urql/core": "^4.2.0", + "@urql/core": "^4.3.0", "@whatwg-node/fetch": "^0.9.13", "ajv": "^8.11.2", "axios": "^1.6.2", "dotenv": "^16.0.3", "ethers": "5.7.2", + "fast-deep-equal": "^3.1.3", "graphql": "^16.8.1", "loglevel": "^1.8.1", "urql": "^4.0.6", - "viem": "^2.9.4" + "viem": "^2.9.4", + "wonka": "^6.3.4" }, "devDependencies": { "@babel/core": "^7.23.5", diff --git a/sdk/src/client.ts b/sdk/src/client.ts index ac677b92..edba7016 100644 --- a/sdk/src/client.ts +++ b/sdk/src/client.ts @@ -22,6 +22,7 @@ import { parseAllowListEntriesToMerkleTree } from "./utils/allowlist"; import { DEPLOYMENTS } from "./constants"; import { getClaimStoredDataFromTxHash } from "./utils"; import { ParserReturnType } from "./utils/txParser"; +import { isClaimOnChain } from "./utils/chains"; /** * The `HypercertClient` is a core class in the hypercerts SDK, providing a high-level interface to interact with the hypercerts system. @@ -75,6 +76,11 @@ export class HypercertClient implements HypercertClientInterface { } } + isClaimOrFractionOnConnectedChain = (claimOrFractionId: string) => { + const connectedChain = this._walletClient?.chain?.id; + return isClaimOnChain(claimOrFractionId, connectedChain); + }; + /** * Gets the config for the client. * @returns The client config. diff --git a/sdk/src/constants.ts b/sdk/src/constants.ts index b6956c20..bb155cd6 100644 --- a/sdk/src/constants.ts +++ b/sdk/src/constants.ts @@ -2,10 +2,11 @@ * Constants */ -import { Deployment, SupportedChainIds } from "./types"; +import { Deployment, IndexerEnvironment, SupportedChainIds } from "./types"; import { deployments } from "@hypercerts-org/contracts"; -const DEFAULT_GRAPH_BASE_URL = "https://api.thegraph.com/subgraphs/name/hypercerts-admin"; +const DEFAULT_GRAPH_BASE_URL = "https://api.thegraph.com/subgraphs/name/hypercerts-org"; +export const DEFAULT_INDEXER_ENVIRONMENT: IndexerEnvironment = "all"; // The APIs we expose @@ -20,26 +21,31 @@ const DEPLOYMENTS: { [key in SupportedChainIds]: Partial } = { addresses: deployments[10], graphName: "hypercerts-optimism-mainnet", graphUrl: `${DEFAULT_GRAPH_BASE_URL}/hypercerts-optimism-mainnet`, + isTestnet: false, } as const, 42220: { addresses: deployments[42220], graphName: "hypercerts-celo", graphUrl: `${DEFAULT_GRAPH_BASE_URL}/hypercerts-celo`, + isTestnet: false, }, 11155111: { addresses: deployments[11155111], graphName: "hypercerts-sepolia", graphUrl: `${DEFAULT_GRAPH_BASE_URL}/hypercerts-sepolia`, + isTestnet: true, } as const, 84532: { addresses: deployments[84532], graphName: "hypercerts-base-sepolia", graphUrl: `${DEFAULT_GRAPH_BASE_URL}/hypercerts-base-sepolia`, + isTestnet: true, } as const, 8453: { addresses: deployments[8453], graphName: "hypercerts-base-mainnet", graphUrl: `${DEFAULT_GRAPH_BASE_URL}/hypercerts-base-mainnet`, + isTestnet: false, } as const, }; diff --git a/sdk/src/indexer.ts b/sdk/src/indexer.ts index 7abaeb53..5c62f9bf 100644 --- a/sdk/src/indexer.ts +++ b/sdk/src/indexer.ts @@ -1,58 +1,137 @@ import { logger } from "./utils"; import { defaultQueryParams } from "./indexer/utils"; -import { HypercertClientConfig, HypercertIndexerInterface, QueryParams } from "./types"; -import { Client, cacheExchange, fetchExchange } from "@urql/core"; import { - ClaimsByOwnerDocument, - ClaimsByOwnerQueryVariables, + HypercertClientConfig, + HypercertIndexerInterface, + IndexerEnvironment, + QueryParams, + QueryParamsWithChainId, +} from "./types"; + +import { AnyVariables, cacheExchange, Client, fetchExchange } from "@urql/core"; +import { ClaimByIdDocument, ClaimByIdQueryVariables, - RecentClaimsDocument, - RecentClaimsQueryVariables, - ClaimTokensByOwnerDocument, - ClaimTokensByOwnerQueryVariables, - ClaimTokensByClaimDocument, - ClaimTokensByClaimQueryVariables, + ClaimsByOwnerDocument, + ClaimsByOwnerQueryVariables, ClaimTokenByIdDocument, ClaimTokenByIdQueryVariables, + ClaimTokensByClaimDocument, + ClaimTokensByClaimQueryVariables, + ClaimTokensByOwnerDocument, + ClaimTokensByOwnerQueryVariables, + RecentClaimsDocument, + RecentClaimsQueryVariables, } from "./indexer/gql/graphql"; +import { DEPLOYMENTS } from "./constants"; +import { TypedDocumentNode } from "@graphql-typed-document-node/core"; +import { parseClaimOrFractionId } from "./utils/parsing"; + /** * A class that provides indexing functionality for Hypercerts. * * This class implements the `HypercertIndexerInterface` and provides methods for retrieving claims by owner and by ID. It uses the Graph client for indexing. * Because of the autogenerated Graph client packed with the SDK, this class is not recommended for custom Graph deployments. * - * @property {GraphClient} _graphClient - The Graph client used by the indexer. - * * @example - * const indexer = new HypercertIndexer({ graphUrl: 'your-graph-url', graphName: 'your-graph-name' }); + * const indexer = new HypercertIndexer({ indexerEnvironment: 'production' }); * const claims = await indexer.claimsByOwner('your-address'); */ export class HypercertIndexer implements HypercertIndexerInterface { /** The Graph client used by the indexer. */ - private _graphName?: string; - private _graphUrl: string; + private environment: IndexerEnvironment; + + private graphClients: Map; /** * Creates a new instance of the `HypercertIndexer` class. * @param options The configuration options for the indexer. */ constructor(options: Partial) { - logger.info("Creating HypercertIndexer", "constructor", { name: options.graphName, url: options.graphUrl }); - if (!options.graphUrl) throw new Error("Missing graphUrl"); - this._graphName = options.graphName; - this._graphUrl = options.graphUrl; + logger.info("Creating HypercertIndexer", "constructor (write)", { + environment: options.indexerEnvironment, + }); + + if (!options.indexerEnvironment) { + throw new Error("Missing indexer environment"); + } + this.environment = options.indexerEnvironment; + + const environments = HypercertIndexer.getDeploymentsForEnvironment(this.environment); + logger.info("Creating Graph clients", "constructor (read)", { environments }); + + this.graphClients = new Map(); + for (const [chainId, deployment] of environments) { + if (!deployment.graphUrl) { + logger.info("Missing graphUrl for chain", "constructor (read)", { chainId }); + continue; + } + this.graphClients.set( + parseInt(chainId), + new Client({ + url: deployment.graphUrl, + exchanges: [cacheExchange, fetchExchange], + }), + ); + } + } + + static getDeploymentsForEnvironment(environment: IndexerEnvironment) { + logger.info("Indexer", "getDeploymentsForEnvironment", { environment }); + return Object.entries(DEPLOYMENTS).filter(([_, deployment]) => { + if (environment === "all") { + return true; + } + + if (environment === "test") { + return deployment.isTestnet; + } + + if (environment === "production") { + return !deployment.isTestnet; + } + + return false; + }); } + performQuery = async ( + query: TypedDocumentNode, + variables: Variables, + chainId?: number, + ) => { + const chains = chainId ? [chainId] : Array.from(this.graphClients.keys()); + return await Promise.all( + chains.map(async (c) => { + const client = this.graphClients.get(c); + if (!client) { + throw new Error(`No client found for chain ${chainId}`); + } + + return client + .query(query, variables) + .toPromise() + .then((res) => { + if (res.error) { + throw res.error; + } + + return res.data; + }); + }), + ); + }; + /** * Gets the Graph client used by the indexer. * @returns The Graph client. */ - get graphClient(): Client { - return new Client({ - url: this._graphUrl, - exchanges: [cacheExchange, fetchExchange], - }); + getGraphClient(chainId: number): Client { + const client = this.graphClients.get(chainId); + if (!client) { + throw new Error(`No client found for chain ${chainId}`); + } + return client; } /** @@ -61,57 +140,51 @@ export class HypercertIndexer implements HypercertIndexerInterface { * @param params The query parameters. * @returns A Promise that resolves to the claims. */ - claimsByOwner = async (owner: string, params: QueryParams = defaultQueryParams) => { + claimsByOwner = async (owner: string, { chainId, ...params }: QueryParamsWithChainId = defaultQueryParams) => { const query = ClaimsByOwnerDocument; const variables: ClaimsByOwnerQueryVariables = { owner, ...params, }; - const result = await this.graphClient.query(query, variables); - - if (result.error) { - throw result.error; - } - - return result.data; + const results = await this.performQuery(query, variables, chainId); + const claims = results.flatMap((result) => result?.claims || []); + return { + claims, + }; }; /** * Gets a claim by its ID. - * @param id The ID of the claim. + * @param claimId The ID of the claim. * @returns A Promise that resolves to the claim. */ - claimById = async (id: string) => { + claimById = async (claimId: string) => { const query = ClaimByIdDocument; + const { chainId } = parseClaimOrFractionId(claimId); const variables: ClaimByIdQueryVariables = { - id, + id: claimId, }; - const result = await this.graphClient.query(query, variables); + const results = await this.performQuery(query, variables, chainId); - if (result.error) { - throw result.error; - } - - return result.data; + return results[0]; }; /** * Gets the most recent claims. * @param params The query parameters. * @returns A Promise that resolves to the claims. */ - firstClaims = async (params: QueryParams = defaultQueryParams) => { + firstClaims = async ({ chainId, ...params }: QueryParamsWithChainId = defaultQueryParams) => { const query = RecentClaimsDocument; const variables: RecentClaimsQueryVariables = { ...params, }; - const result = await this.graphClient.query(query, variables); - - if (result.error) { - throw result.error; - } - return result.data; + const results = await this.performQuery(query, variables, chainId); + const claims = results.flatMap((result) => result?.claims || []); + return { + claims, + }; }; /** @@ -120,19 +193,18 @@ export class HypercertIndexer implements HypercertIndexerInterface { * @param params The query parameters. * @returns A Promise that resolves to the claim tokens. */ - fractionsByOwner = async (owner: string, params: QueryParams = defaultQueryParams) => { + fractionsByOwner = async (owner: string, { chainId, ...params }: QueryParamsWithChainId = defaultQueryParams) => { const query = ClaimTokensByOwnerDocument; const variables: ClaimTokensByOwnerQueryVariables = { owner, ...params, }; - const result = await this.graphClient.query(query, variables); - - if (result.error) { - throw result.error; - } - return result.data; + const results = await this.performQuery(query, variables, chainId); + const claimTokens = results.flatMap((result) => result?.claimTokens || []); + return { + claimTokens, + }; }; /** @@ -143,17 +215,14 @@ export class HypercertIndexer implements HypercertIndexerInterface { */ fractionsByClaim = async (claimId: string, params: QueryParams = defaultQueryParams) => { const query = ClaimTokensByClaimDocument; + const { chainId } = parseClaimOrFractionId(claimId); const variables: ClaimTokensByClaimQueryVariables = { claimId, ...params, }; - const result = await this.graphClient.query(query, variables); - - if (result.error) { - throw result.error; - } - return result.data; + const results = await this.performQuery(query, variables, chainId); + return results[0]; }; /** @@ -163,15 +232,13 @@ export class HypercertIndexer implements HypercertIndexerInterface { */ fractionById = async (fractionId: string) => { const query = ClaimTokenByIdDocument; + const { chainId } = parseClaimOrFractionId(fractionId); + const variables: ClaimTokenByIdQueryVariables = { claimTokenId: fractionId, }; - const result = await this.graphClient.query(query, variables); - - if (result.error) { - throw result.error; - } - return result.data; + const results = await this.performQuery(query, variables, chainId); + return results[0]; }; } diff --git a/sdk/src/indexer/gql/fragment-masking.ts b/sdk/src/indexer/gql/fragment-masking.ts index 71bfb909..2ba06f10 100644 --- a/sdk/src/indexer/gql/fragment-masking.ts +++ b/sdk/src/indexer/gql/fragment-masking.ts @@ -1,57 +1,57 @@ -import { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from "@graphql-typed-document-node/core"; -import { FragmentDefinitionNode } from "graphql"; -import { Incremental } from "./graphql"; +import { ResultOf, DocumentTypeDecoration, TypedDocumentNode } from '@graphql-typed-document-node/core'; +import { FragmentDefinitionNode } from 'graphql'; +import { Incremental } from './graphql'; -export type FragmentType> = - TDocumentType extends DocumentTypeDecoration - ? [TType] extends [{ " $fragmentName"?: infer TKey }] - ? TKey extends string - ? { " $fragmentRefs"?: { [key in TKey]: TType } } - : never + +export type FragmentType> = TDocumentType extends DocumentTypeDecoration< + infer TType, + any +> + ? [TType] extends [{ ' $fragmentName'?: infer TKey }] + ? TKey extends string + ? { ' $fragmentRefs'?: { [key in TKey]: TType } } : never - : never; + : never + : never; // return non-nullable if `fragmentType` is non-nullable export function useFragment( _documentNode: DocumentTypeDecoration, - fragmentType: FragmentType>, + fragmentType: FragmentType> ): TType; // return nullable if `fragmentType` is nullable export function useFragment( _documentNode: DocumentTypeDecoration, - fragmentType: FragmentType> | null | undefined, + fragmentType: FragmentType> | null | undefined ): TType | null | undefined; // return array of non-nullable if `fragmentType` is array of non-nullable export function useFragment( _documentNode: DocumentTypeDecoration, - fragmentType: ReadonlyArray>>, + fragmentType: ReadonlyArray>> ): ReadonlyArray; // return array of nullable if `fragmentType` is array of nullable export function useFragment( _documentNode: DocumentTypeDecoration, - fragmentType: ReadonlyArray>> | null | undefined, + fragmentType: ReadonlyArray>> | null | undefined ): ReadonlyArray | null | undefined; export function useFragment( _documentNode: DocumentTypeDecoration, - fragmentType: - | FragmentType> - | ReadonlyArray>> - | null - | undefined, + fragmentType: FragmentType> | ReadonlyArray>> | null | undefined ): TType | ReadonlyArray | null | undefined { return fragmentType as any; } -export function makeFragmentData, FT extends ResultOf>( - data: FT, - _fragment: F, -): FragmentType { + +export function makeFragmentData< + F extends DocumentTypeDecoration, + FT extends ResultOf +>(data: FT, _fragment: F): FragmentType { return data as FragmentType; } export function isFragmentReady( queryNode: DocumentTypeDecoration, fragmentNode: TypedDocumentNode, - data: FragmentType, any>> | null | undefined, + data: FragmentType, any>> | null | undefined ): data is FragmentType { const deferredFields = (queryNode as { __meta__?: { deferredFields: Record } }).__meta__ ?.deferredFields; @@ -62,5 +62,5 @@ export function isFragmentReady( const fragName = fragDef?.name?.value; const fields = (fragName && deferredFields[fragName]) || []; - return fields.length > 0 && fields.every((field) => data && field in data); + return fields.length > 0 && fields.every(field => data && field in data); } diff --git a/sdk/src/indexer/gql/gql.ts b/sdk/src/indexer/gql/gql.ts index 9a5d6c61..2d67d9d7 100644 --- a/sdk/src/indexer/gql/gql.ts +++ b/sdk/src/indexer/gql/gql.ts @@ -1,6 +1,6 @@ /* eslint-disable */ -import * as types from "./graphql"; -import { TypedDocumentNode as DocumentNode } from "@graphql-typed-document-node/core"; +import * as types from './graphql'; +import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; /** * Map of all GraphQL operations in the project. @@ -13,10 +13,8 @@ import { TypedDocumentNode as DocumentNode } from "@graphql-typed-document-node/ * Therefore it is highly recommended to use the babel or swc plugin for production. */ const documents = { - 'query ClaimsByOwner($owner: Bytes = "", $orderDirection: OrderDirection, $first: Int, $skip: Int) {\n claims(\n where: {owner: $owner}\n skip: $skip\n first: $first\n orderDirection: $orderDirection\n ) {\n contract\n tokenID\n creator\n id\n owner\n totalUnits\n uri\n }\n}\n\nquery RecentClaims($orderDirection: OrderDirection, $first: Int, $skip: Int) {\n claims(orderDirection: $orderDirection, orderBy: creation, first: $first) {\n contract\n tokenID\n creator\n id\n owner\n totalUnits\n uri\n }\n}\n\nquery ClaimById($id: ID!) {\n claim(id: $id) {\n contract\n tokenID\n creator\n id\n owner\n totalUnits\n uri\n }\n}': - types.ClaimsByOwnerDocument, - 'query ClaimTokensByOwner($owner: Bytes = "", $orderDirection: OrderDirection, $first: Int, $skip: Int) {\n claimTokens(\n where: {owner: $owner}\n skip: $skip\n first: $first\n orderDirection: $orderDirection\n ) {\n id\n owner\n tokenID\n units\n claim {\n id\n creation\n uri\n totalUnits\n }\n }\n}\n\nquery ClaimTokensByClaim($claimId: String!, $orderDirection: OrderDirection, $first: Int, $skip: Int) {\n claimTokens(\n where: {claim: $claimId}\n skip: $skip\n first: $first\n orderDirection: $orderDirection\n ) {\n id\n owner\n tokenID\n units\n }\n}\n\nquery ClaimTokenById($claimTokenId: ID!) {\n claimToken(id: $claimTokenId) {\n id\n owner\n tokenID\n units\n claim {\n id\n creation\n uri\n totalUnits\n }\n }\n}': - types.ClaimTokensByOwnerDocument, + "query ClaimsByOwner($owner: Bytes = \"\", $orderDirection: OrderDirection, $first: Int, $skip: Int) {\n claims(\n where: {owner: $owner}\n skip: $skip\n first: $first\n orderDirection: $orderDirection\n ) {\n contract\n tokenID\n creator\n id\n owner\n totalUnits\n uri\n }\n}\n\nquery RecentClaims($orderDirection: OrderDirection, $first: Int, $skip: Int) {\n claims(orderDirection: $orderDirection, orderBy: creation, first: $first) {\n contract\n tokenID\n creator\n id\n owner\n totalUnits\n uri\n }\n}\n\nquery ClaimById($id: ID!) {\n claim(id: $id) {\n contract\n tokenID\n creator\n id\n owner\n totalUnits\n uri\n }\n}": types.ClaimsByOwnerDocument, + "query ClaimTokensByOwner($owner: Bytes = \"\", $orderDirection: OrderDirection, $first: Int, $skip: Int) {\n claimTokens(\n where: {owner: $owner}\n skip: $skip\n first: $first\n orderDirection: $orderDirection\n ) {\n id\n owner\n tokenID\n units\n claim {\n id\n creation\n uri\n totalUnits\n }\n }\n}\n\nquery ClaimTokensByClaim($claimId: String!, $orderDirection: OrderDirection, $first: Int, $skip: Int) {\n claimTokens(\n where: {claim: $claimId}\n skip: $skip\n first: $first\n orderDirection: $orderDirection\n ) {\n id\n owner\n tokenID\n units\n }\n}\n\nquery ClaimTokenById($claimTokenId: ID!) {\n claimToken(id: $claimTokenId) {\n id\n owner\n tokenID\n units\n claim {\n id\n creation\n uri\n totalUnits\n }\n }\n}": types.ClaimTokensByOwnerDocument, }; /** @@ -36,23 +34,14 @@ export function graphql(source: string): unknown; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql( - source: 'query ClaimsByOwner($owner: Bytes = "", $orderDirection: OrderDirection, $first: Int, $skip: Int) {\n claims(\n where: {owner: $owner}\n skip: $skip\n first: $first\n orderDirection: $orderDirection\n ) {\n contract\n tokenID\n creator\n id\n owner\n totalUnits\n uri\n }\n}\n\nquery RecentClaims($orderDirection: OrderDirection, $first: Int, $skip: Int) {\n claims(orderDirection: $orderDirection, orderBy: creation, first: $first) {\n contract\n tokenID\n creator\n id\n owner\n totalUnits\n uri\n }\n}\n\nquery ClaimById($id: ID!) {\n claim(id: $id) {\n contract\n tokenID\n creator\n id\n owner\n totalUnits\n uri\n }\n}', -): (typeof documents)['query ClaimsByOwner($owner: Bytes = "", $orderDirection: OrderDirection, $first: Int, $skip: Int) {\n claims(\n where: {owner: $owner}\n skip: $skip\n first: $first\n orderDirection: $orderDirection\n ) {\n contract\n tokenID\n creator\n id\n owner\n totalUnits\n uri\n }\n}\n\nquery RecentClaims($orderDirection: OrderDirection, $first: Int, $skip: Int) {\n claims(orderDirection: $orderDirection, orderBy: creation, first: $first) {\n contract\n tokenID\n creator\n id\n owner\n totalUnits\n uri\n }\n}\n\nquery ClaimById($id: ID!) {\n claim(id: $id) {\n contract\n tokenID\n creator\n id\n owner\n totalUnits\n uri\n }\n}']; +export function graphql(source: "query ClaimsByOwner($owner: Bytes = \"\", $orderDirection: OrderDirection, $first: Int, $skip: Int) {\n claims(\n where: {owner: $owner}\n skip: $skip\n first: $first\n orderDirection: $orderDirection\n ) {\n contract\n tokenID\n creator\n id\n owner\n totalUnits\n uri\n }\n}\n\nquery RecentClaims($orderDirection: OrderDirection, $first: Int, $skip: Int) {\n claims(orderDirection: $orderDirection, orderBy: creation, first: $first) {\n contract\n tokenID\n creator\n id\n owner\n totalUnits\n uri\n }\n}\n\nquery ClaimById($id: ID!) {\n claim(id: $id) {\n contract\n tokenID\n creator\n id\n owner\n totalUnits\n uri\n }\n}"): (typeof documents)["query ClaimsByOwner($owner: Bytes = \"\", $orderDirection: OrderDirection, $first: Int, $skip: Int) {\n claims(\n where: {owner: $owner}\n skip: $skip\n first: $first\n orderDirection: $orderDirection\n ) {\n contract\n tokenID\n creator\n id\n owner\n totalUnits\n uri\n }\n}\n\nquery RecentClaims($orderDirection: OrderDirection, $first: Int, $skip: Int) {\n claims(orderDirection: $orderDirection, orderBy: creation, first: $first) {\n contract\n tokenID\n creator\n id\n owner\n totalUnits\n uri\n }\n}\n\nquery ClaimById($id: ID!) {\n claim(id: $id) {\n contract\n tokenID\n creator\n id\n owner\n totalUnits\n uri\n }\n}"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql( - source: 'query ClaimTokensByOwner($owner: Bytes = "", $orderDirection: OrderDirection, $first: Int, $skip: Int) {\n claimTokens(\n where: {owner: $owner}\n skip: $skip\n first: $first\n orderDirection: $orderDirection\n ) {\n id\n owner\n tokenID\n units\n claim {\n id\n creation\n uri\n totalUnits\n }\n }\n}\n\nquery ClaimTokensByClaim($claimId: String!, $orderDirection: OrderDirection, $first: Int, $skip: Int) {\n claimTokens(\n where: {claim: $claimId}\n skip: $skip\n first: $first\n orderDirection: $orderDirection\n ) {\n id\n owner\n tokenID\n units\n }\n}\n\nquery ClaimTokenById($claimTokenId: ID!) {\n claimToken(id: $claimTokenId) {\n id\n owner\n tokenID\n units\n claim {\n id\n creation\n uri\n totalUnits\n }\n }\n}', -): (typeof documents)['query ClaimTokensByOwner($owner: Bytes = "", $orderDirection: OrderDirection, $first: Int, $skip: Int) {\n claimTokens(\n where: {owner: $owner}\n skip: $skip\n first: $first\n orderDirection: $orderDirection\n ) {\n id\n owner\n tokenID\n units\n claim {\n id\n creation\n uri\n totalUnits\n }\n }\n}\n\nquery ClaimTokensByClaim($claimId: String!, $orderDirection: OrderDirection, $first: Int, $skip: Int) {\n claimTokens(\n where: {claim: $claimId}\n skip: $skip\n first: $first\n orderDirection: $orderDirection\n ) {\n id\n owner\n tokenID\n units\n }\n}\n\nquery ClaimTokenById($claimTokenId: ID!) {\n claimToken(id: $claimTokenId) {\n id\n owner\n tokenID\n units\n claim {\n id\n creation\n uri\n totalUnits\n }\n }\n}']; +export function graphql(source: "query ClaimTokensByOwner($owner: Bytes = \"\", $orderDirection: OrderDirection, $first: Int, $skip: Int) {\n claimTokens(\n where: {owner: $owner}\n skip: $skip\n first: $first\n orderDirection: $orderDirection\n ) {\n id\n owner\n tokenID\n units\n claim {\n id\n creation\n uri\n totalUnits\n }\n }\n}\n\nquery ClaimTokensByClaim($claimId: String!, $orderDirection: OrderDirection, $first: Int, $skip: Int) {\n claimTokens(\n where: {claim: $claimId}\n skip: $skip\n first: $first\n orderDirection: $orderDirection\n ) {\n id\n owner\n tokenID\n units\n }\n}\n\nquery ClaimTokenById($claimTokenId: ID!) {\n claimToken(id: $claimTokenId) {\n id\n owner\n tokenID\n units\n claim {\n id\n creation\n uri\n totalUnits\n }\n }\n}"): (typeof documents)["query ClaimTokensByOwner($owner: Bytes = \"\", $orderDirection: OrderDirection, $first: Int, $skip: Int) {\n claimTokens(\n where: {owner: $owner}\n skip: $skip\n first: $first\n orderDirection: $orderDirection\n ) {\n id\n owner\n tokenID\n units\n claim {\n id\n creation\n uri\n totalUnits\n }\n }\n}\n\nquery ClaimTokensByClaim($claimId: String!, $orderDirection: OrderDirection, $first: Int, $skip: Int) {\n claimTokens(\n where: {claim: $claimId}\n skip: $skip\n first: $first\n orderDirection: $orderDirection\n ) {\n id\n owner\n tokenID\n units\n }\n}\n\nquery ClaimTokenById($claimTokenId: ID!) {\n claimToken(id: $claimTokenId) {\n id\n owner\n tokenID\n units\n claim {\n id\n creation\n uri\n totalUnits\n }\n }\n}"]; export function graphql(source: string) { return (documents as any)[source] ?? {}; } -export type DocumentType> = TDocumentNode extends DocumentNode< - infer TType, - any -> - ? TType - : never; +export type DocumentType> = TDocumentNode extends DocumentNode< infer TType, any> ? TType : never; \ No newline at end of file diff --git a/sdk/src/indexer/gql/index.ts b/sdk/src/indexer/gql/index.ts index 0ea4a91c..f5159916 100644 --- a/sdk/src/indexer/gql/index.ts +++ b/sdk/src/indexer/gql/index.ts @@ -1,2 +1,2 @@ export * from "./fragment-masking"; -export * from "./gql"; +export * from "./gql"; \ No newline at end of file diff --git a/sdk/src/types/client.ts b/sdk/src/types/client.ts index 4a5b1b90..dcd02794 100644 --- a/sdk/src/types/client.ts +++ b/sdk/src/types/client.ts @@ -62,12 +62,13 @@ export type Deployment = { /** The url to the subgraph that indexes the contract events. Override for localized testing */ graphUrl: string; graphName: string; + isTestnet: boolean; }; /** * Configuration options for the Hypercert client. */ -export type HypercertClientConfig = Deployment & +export type HypercertClientConfig = Pick & HypercertStorageConfig & HypercertEvaluatorConfig & { /** The PublicClient is inherently read-only */ @@ -79,8 +80,17 @@ export type HypercertClientConfig = Deployment & readOnly: boolean; /** Reason for readOnly mode */ readOnlyReason?: string; + /** The environment to run the indexer in. This can be either production, test or all. Defaults to test */ + indexerEnvironment: IndexerEnvironment; }; +/** + * The environment to run the indexer in. + * Production will run against all mainnet chains, while test will run against testnet chains. + * All will run against both + */ +export type IndexerEnvironment = "production" | "test" | "all"; + /** * Configuration options for the Hypercert storage layer. * @note The API tokens are optional, but required for storing data on NFT.storage and Web3.storage. @@ -283,4 +293,11 @@ export interface HypercertClientMethods { units: bigint[], proofs: (Hex | ByteArray)[][], ) => Promise<`0x${string}` | undefined>; + + /** + * Check if a claim or fraction is on the chain that the Hypercertclient + * is currently connected to + * @param claimOrFractionId The ID of the claim or fraction to check. + */ + isClaimOrFractionOnConnectedChain: (claimOrFractionId: string) => boolean; } diff --git a/sdk/src/types/indexer.ts b/sdk/src/types/indexer.ts index 9417e95d..2374e377 100644 --- a/sdk/src/types/indexer.ts +++ b/sdk/src/types/indexer.ts @@ -7,6 +7,7 @@ import { ClaimTokensByClaimQuery, ClaimTokenByIdQuery, } from "../indexer/gql/graphql"; + export type QueryParams = { orderDirections: "asc" | "desc"; skip: number; @@ -14,8 +15,10 @@ export type QueryParams = { [key: string]: string | number | undefined; }; +export type QueryParamsWithChainId = QueryParams & { chainId?: number }; + export interface HypercertIndexerInterface { - graphClient: Client; + getGraphClient(chainId: number): Client; claimsByOwner: (owner: string, params?: QueryParams) => Promise; claimById: (id: string) => Promise; firstClaims: (params?: QueryParams) => Promise; diff --git a/sdk/src/utils/chains.ts b/sdk/src/utils/chains.ts new file mode 100644 index 00000000..9f5ccb0d --- /dev/null +++ b/sdk/src/utils/chains.ts @@ -0,0 +1,9 @@ +import { parseClaimOrFractionId } from "./parsing"; + +export const isClaimOnChain = (claimId: string, chainId: number | undefined) => { + if (chainId === undefined) { + return false; + } + const { chainId: parsedChainId } = parseClaimOrFractionId(claimId); + return parsedChainId === chainId; +}; diff --git a/sdk/src/utils/config.ts b/sdk/src/utils/config.ts index 7ac30a8e..d58e2cb7 100644 --- a/sdk/src/utils/config.ts +++ b/sdk/src/utils/config.ts @@ -1,6 +1,6 @@ import { sepolia, optimism, celo, Chain, baseSepolia, base } from "viem/chains"; -import { DEPLOYMENTS } from "../constants"; +import { DEFAULT_INDEXER_ENVIRONMENT } from "../constants"; import { ConfigurationError, Deployment, @@ -34,7 +34,7 @@ import { deployments } from "../../src"; * @throws {UnsupportedChainError} Will throw an `UnsupportedChainError` if the default configuration for the provided chain ID is missing. */ export const getConfig = (overrides: Partial): Partial => { - // Get the chainId, first from overrides, then environment variables, then the constant + // Get the chainId of the writing chain, first from overrides, then environment variables, then the constant const chain = getChainConfig(overrides); if (!chain) { logger.warn("[getConfig]: No default config for chain found"); @@ -43,22 +43,20 @@ export const getConfig = (overrides: Partial): Partial & { unsafeForceOverrideConfig?: boolean }) | undefined; if (overrides.unsafeForceOverrideConfig) { - if (!overrides.chain?.id || !overrides.graphUrl) { + if (!overrides.chain?.id) { throw new InvalidOrMissingError( `attempted to override with chainId=${overrides.chain?.id}, but requires chainName, graphUrl, and contractAddress to be set`, { chainID: overrides.chain?.id?.toString(), - graphUrl: overrides.graphUrl, }, ); } baseDeployment = { chain: { ...chain, id: overrides.chain?.id }, - graphUrl: overrides.graphUrl, unsafeForceOverrideConfig: overrides.unsafeForceOverrideConfig, }; } else { - //TODO doo many casts + //TODO do many casts baseDeployment = overrides.chain?.id ? (getDeployment(overrides.chain?.id as SupportedChainIds) as Partial & { unsafeForceOverrideConfig?: boolean; @@ -81,8 +79,8 @@ export const getConfig = (overrides: Partial): Partial { return deployments[chainId]; }; +const getIndexerEnvironment = (overrides: Partial) => { + return { indexerEnvironment: overrides.indexerEnvironment || DEFAULT_INDEXER_ENVIRONMENT }; +}; + const getChainConfig = (overrides: Partial) => { const chainId = overrides?.chain?.id ? overrides.chain?.id : undefined; @@ -120,38 +122,6 @@ const getChainConfig = (overrides: Partial) => { return chain; }; -const getGraphUrl = (overrides: Partial) => { - let graphUrl; - if (overrides.unsafeForceOverrideConfig) { - if (!overrides.graphUrl) { - throw new ConfigurationError("A graphUrl must be specified when overriding configuration"); - } - try { - new URL(overrides.graphUrl); - } catch (error) { - throw new ConfigurationError("Invalid graph URL", { graphUrl: overrides.graphUrl }); - } - graphUrl = overrides.graphUrl; - return { graphUrl }; - } - - const chain = getChainConfig(overrides); - - graphUrl = DEPLOYMENTS[chain?.id as keyof typeof DEPLOYMENTS].graphUrl ?? process.env.GRAPH_URL; - if (!graphUrl) { - throw new UnsupportedChainError(`No Graph URL found in deployments or env vars`, { - chainID: chain?.toString(), - }); - } - try { - new URL(graphUrl); - } catch (error) { - throw new ConfigurationError("Invalid graph URL", { graphUrl }); - } - - return { graphUrl }; -}; - const getWalletClient = (overrides: Partial) => { const walletClient = overrides.walletClient; diff --git a/sdk/src/utils/index.ts b/sdk/src/utils/index.ts index 37e88876..ffe0e6b4 100644 --- a/sdk/src/utils/index.ts +++ b/sdk/src/utils/index.ts @@ -7,6 +7,7 @@ import { logger } from "./logger"; import { handleSdkError, handleContractError } from "./errors"; import { uploadMetadata, uploadAllowlist } from "./apis"; import { getClaimStoredDataFromTxHash } from "./txParser"; +import { parseClaimOrFractionId } from "./parsing"; export { walletClientToSigner, @@ -21,4 +22,5 @@ export { uploadAllowlist, parseAllowListEntriesToMerkleTree, getClaimStoredDataFromTxHash, + parseClaimOrFractionId, }; diff --git a/sdk/src/utils/parsing.ts b/sdk/src/utils/parsing.ts new file mode 100644 index 00000000..d699b8b4 --- /dev/null +++ b/sdk/src/utils/parsing.ts @@ -0,0 +1,37 @@ +import { isAddress } from "viem"; + +export const parseClaimOrFractionId = (claimId: string) => { + const [chainId, contractAddress, id] = claimId.split("-"); + + if (!chainId || !contractAddress || !id || !isAddress(contractAddress)) { + console.log(`Invalid claimId format (claimId given: ${claimId}}. Expected "chainId-contractAddress-tokenId`); + throw new Error(`Invalid claimId format (claimId given: ${claimId}}. Expected "chainId-contractAddress-tokenId"`); + } + + let chainIdParsed: number | undefined; + try { + chainIdParsed = parseInt(chainId, 10); + } catch (error) { + console.log(`Invalid chainId while parsing: ${chainId}`); + throw new Error(`Invalid chainId while parsing: ${chainId}`); + } + + let idParsed: bigint | undefined; + try { + idParsed = BigInt(id); + } catch (error) { + console.log(`Invalid id while parsing: ${id}`); + throw new Error(`Invalid id while parsing: ${id}`); + } + + if (!chainIdParsed || !idParsed) { + console.log(`Invalid claimId format (claimId given: ${claimId}}. Expected "chainId-contractAddress-tokenId"`); + throw new Error(`Invalid claimId format (claimId given: ${claimId}}. Expected "chainId-contractAddress-tokenId"`); + } + + return { + chainId: chainIdParsed, + contractAddress, + id: idParsed, + }; +}; diff --git a/sdk/test/indexer.test.ts b/sdk/test/indexer.test.ts index cc5d29d3..784e2d54 100644 --- a/sdk/test/indexer.test.ts +++ b/sdk/test/indexer.test.ts @@ -1,22 +1,21 @@ -import { describe, it } from "vitest"; - -import { expect } from "chai"; +import { describe, it, expect } from "vitest"; import { HypercertIndexer } from "../src/indexer"; +import { DEPLOYMENTS } from "../src/constants"; describe("HypercertsIndexer", () => { - it("should be able to create a new instance without valid graphName", () => { - const indexer = new HypercertIndexer({ graphUrl: "https://api.thegraph.com/subgraphs/name/hypercerts-testnet" }); - - expect(indexer).to.be.an.instanceOf(HypercertIndexer); + it("should only initialize with test environments", async () => { + const environments = HypercertIndexer.getDeploymentsForEnvironment("test"); + expect(environments.every(([_, deployment]) => deployment.isTestnet)).toBe(true); }); - it("should be able to create a new instance with valid graphName and url", () => { - const indexer = new HypercertIndexer({ - graphName: "hypercerts-testnet", - graphUrl: "https://api.thegraph.com/subgraphs/name/hypercerts-testnet", - }); + it("should only initialize with production environments", async () => { + const environments = HypercertIndexer.getDeploymentsForEnvironment("production"); + expect(environments.every(([_, deployment]) => !deployment.isTestnet)).toBe(true); + }); - expect(indexer).to.be.an.instanceOf(HypercertIndexer); + it("should only initialize with all environments", async () => { + const environments = HypercertIndexer.getDeploymentsForEnvironment("all"); + expect(environments.length).toEqual(Object.keys(DEPLOYMENTS).length); }); }); diff --git a/sdk/test/indexer/queries.test.ts b/sdk/test/indexer/queries.test.ts index 27096d01..9c4793f1 100644 --- a/sdk/test/indexer/queries.test.ts +++ b/sdk/test/indexer/queries.test.ts @@ -7,10 +7,7 @@ describe("HypercertIndexer", () => { let indexer: HypercertIndexer; beforeEach(() => { - indexer = new HypercertIndexer({ - graphName: "hypercerts-testnet", - graphUrl: "https://api.thegraph.com/subgraphs/name/hypercerts-testnet", - }); + indexer = new HypercertIndexer({ indexerEnvironment: "test" }); }); afterEach(() => { diff --git a/sdk/test/utils/config.test.ts b/sdk/test/utils/config.test.ts index b1af9e36..f9d21fce 100644 --- a/sdk/test/utils/config.test.ts +++ b/sdk/test/utils/config.test.ts @@ -8,6 +8,7 @@ import { ConfigurationError, HypercertClientConfig, InvalidOrMissingError } from import { getConfig } from "../../src/utils/config"; import { reloadEnv } from "../../test/setup-env"; import { walletClient, publicClient } from "../helpers"; +import { DEFAULT_INDEXER_ENVIRONMENT } from "../../src/constants"; chai.use(chaiSubset); @@ -16,25 +17,23 @@ describe("Config: graphUrl", () => { reloadEnv(); }); - it("should return the default graphUrl when no overrides are specified", () => { + it("should return the default indexer environment when no overrides are specified", () => { const result = getConfig({ chain: { id: 11155111 } }); - expect(result.graphUrl).to.equal("https://api.thegraph.com/subgraphs/name/hypercerts-admin/hypercerts-sepolia"); + expect(result.indexerEnvironment).to.equal(DEFAULT_INDEXER_ENVIRONMENT); }); it("should return the config specified by overrides", () => { const overrides: Partial = { chain: { id: 11155111 }, - graphUrl: "https://api.example.com", unsafeForceOverrideConfig: true, }; const result = getConfig(overrides); - expect(result.graphUrl).to.equal(overrides.graphUrl); + expect(result.chain?.id).to.equal(overrides.chain?.id); }); it("should throw an error when the graph URL specified by overrides is invalid", () => { const overrides: Partial = { chain: { id: 11155111 }, - graphUrl: "incorrect-url", unsafeForceOverrideConfig: true, };