diff --git a/govtechsg-oa-verify-0.0.0-development.tgz b/govtechsg-oa-verify-0.0.0-development.tgz new file mode 100644 index 00000000..4c167a98 Binary files /dev/null and b/govtechsg-oa-verify-0.0.0-development.tgz differ diff --git a/src/common/utils.ts b/src/common/utils.ts index c6e9c7fe..186a14f8 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -39,8 +39,12 @@ export const getDefaultProvider = (options: VerificationBuilderOptionsWithNetwor }; // getProvider is a function to get an existing provider or to get a Default provider, when given the options -export const getProvider = (options: VerificationBuilderOptions): providers.Provider => { - return options.provider ?? getDefaultProvider(options); +export const getProvider = (options: VerificationBuilderOptions): providers.Provider[] => { + const providers = Array.isArray(options.provider) ? options.provider : options.provider ? [options.provider] : []; + if (!providers.length && !options.provider) { + providers.push(getDefaultProvider(options)); + } + return providers; }; /** diff --git a/src/types/core.ts b/src/types/core.ts index 5448f9d8..7ef7bf6e 100644 --- a/src/types/core.ts +++ b/src/types/core.ts @@ -9,7 +9,7 @@ import { Reason } from "./error"; export type PromiseCallback = (promises: Promise[]) => void; export interface VerificationBuilderOptionsWithProvider { - provider: providers.Provider; + provider: providers.Provider | providers.Provider[]; resolver?: Resolver; } @@ -22,7 +22,7 @@ export interface VerificationBuilderOptionsWithNetwork { export type VerificationBuilderOptions = VerificationBuilderOptionsWithProvider | VerificationBuilderOptionsWithNetwork; export interface VerifierOptions { - provider: providers.Provider; + provider: providers.Provider | providers.Provider[]; resolver?: Resolver; } diff --git a/src/verifiers/documentStatus/documentStore/ethereumDocumentStoreStatus.ts b/src/verifiers/documentStatus/documentStore/ethereumDocumentStoreStatus.ts index 092be54d..fe2e6899 100644 --- a/src/verifiers/documentStatus/documentStore/ethereumDocumentStoreStatus.ts +++ b/src/verifiers/documentStatus/documentStore/ethereumDocumentStoreStatus.ts @@ -1,6 +1,6 @@ import { getData, utils, v2, v3, WrappedDocument } from "@govtechsg/open-attestation"; import { DocumentStore__factory } from "@govtechsg/document-store-ethers-v5"; -import { providers } from "ethers"; +import { errors, providers } from "ethers"; import { VerificationFragmentType, Verifier, VerifierOptions } from "../../../types/core"; import { OpenAttestationEthereumDocumentStoreStatusCode, Reason } from "../../../types/error"; import { CodedError } from "../../../common/error"; @@ -46,51 +46,62 @@ export const isIssuedOnDocumentStore = async ({ merkleRoot: string; targetHash: string; proofs: string[]; - provider: providers.Provider; + provider: providers.Provider | providers.Provider[]; }): Promise => { - const documentStoreContract = DocumentStore__factory.connect(documentStore, provider); + const providers = Array.isArray(provider) ? provider : [provider]; + const queryProviderIndex = Math.floor(Math.random() * providers.length); + const documentStoreContractQueryProviders = providers.map((p) => DocumentStore__factory.connect(documentStore, p)); - try { - const isBatchable = await isBatchableDocumentStore(documentStoreContract); + let tries = 0; + for (;;) { + const documentStoreContract = + documentStoreContractQueryProviders[(queryProviderIndex + tries) % documentStoreContractQueryProviders.length]; + try { + const isBatchable = await isBatchableDocumentStore(documentStoreContract); - let issued: boolean; - if (isBatchable) { - issued = await documentStoreContract["isIssued(bytes32,bytes32,bytes32[])"](merkleRoot, targetHash, proofs); - } else { - issued = await documentStoreContract["isIssued(bytes32)"](merkleRoot); + let issued: boolean; + if (isBatchable) { + issued = await documentStoreContract["isIssued(bytes32,bytes32,bytes32[])"](merkleRoot, targetHash, proofs); + } else { + issued = await documentStoreContract["isIssued(bytes32)"](merkleRoot); + } + return issued + ? { + issued: true, + address: documentStore, + } + : { + issued: false, + address: documentStore, + reason: { + message: `Document ${merkleRoot} has not been issued under contract ${documentStore}`, + code: OpenAttestationEthereumDocumentStoreStatusCode.DOCUMENT_NOT_ISSUED, + codeString: + OpenAttestationEthereumDocumentStoreStatusCode[ + OpenAttestationEthereumDocumentStoreStatusCode.DOCUMENT_NOT_ISSUED + ], + }, + }; + } catch (error: any) { + if (error.code === errors.NETWORK_ERROR && tries < 3) { + tries++; + continue; + } + // If error can be decoded and it's because of document is not issued, we return false + // Else allow error to continue to bubble up + return { + issued: false, + address: documentStore, + reason: { + message: decodeError(error), + code: OpenAttestationEthereumDocumentStoreStatusCode.DOCUMENT_NOT_ISSUED, + codeString: + OpenAttestationEthereumDocumentStoreStatusCode[ + OpenAttestationEthereumDocumentStoreStatusCode.DOCUMENT_NOT_ISSUED + ], + }, + }; } - return issued - ? { - issued: true, - address: documentStore, - } - : { - issued: false, - address: documentStore, - reason: { - message: `Document ${merkleRoot} has not been issued under contract ${documentStore}`, - code: OpenAttestationEthereumDocumentStoreStatusCode.DOCUMENT_NOT_ISSUED, - codeString: - OpenAttestationEthereumDocumentStoreStatusCode[ - OpenAttestationEthereumDocumentStoreStatusCode.DOCUMENT_NOT_ISSUED - ], - }, - }; - } catch (error) { - // If error can be decoded and it's because of document is not issued, we return false - // Else allow error to continue to bubble up - return { - issued: false, - address: documentStore, - reason: { - message: decodeError(error), - code: OpenAttestationEthereumDocumentStoreStatusCode.DOCUMENT_NOT_ISSUED, - codeString: - OpenAttestationEthereumDocumentStoreStatusCode[ - OpenAttestationEthereumDocumentStoreStatusCode.DOCUMENT_NOT_ISSUED - ], - }, - }; } }; diff --git a/src/verifiers/documentStatus/tokenRegistry/ethereumTokenRegistryStatus.ts b/src/verifiers/documentStatus/tokenRegistry/ethereumTokenRegistryStatus.ts index aa84a092..4fe9535a 100644 --- a/src/verifiers/documentStatus/tokenRegistry/ethereumTokenRegistryStatus.ts +++ b/src/verifiers/documentStatus/tokenRegistry/ethereumTokenRegistryStatus.ts @@ -189,7 +189,11 @@ const verify: VerifierType["verify"] = async (document, options) => { ); const tokenRegistry = getTokenRegistry(document); const merkleRoot = getMerkleRoot(document); - const mintStatus = await isTokenMintedOnRegistry({ tokenRegistry, merkleRoot, provider: options.provider }); + const mintStatus = await isTokenMintedOnRegistry({ + tokenRegistry, + merkleRoot, + provider: Array.isArray(options.provider) ? options.provider[0] : options.provider, + }); if (ValidTokenRegistryStatus.guard(mintStatus)) { const fragment = { diff --git a/src/verifiers/documentStatus/utils.ts b/src/verifiers/documentStatus/utils.ts index cb2b6eb8..d630a45b 100644 --- a/src/verifiers/documentStatus/utils.ts +++ b/src/verifiers/documentStatus/utils.ts @@ -77,57 +77,69 @@ export const isRevokedOnDocumentStore = async ({ }: { documentStore: string; merkleRoot: string; - provider: providers.Provider; + provider: providers.Provider | providers.Provider[]; targetHash: Hash; proofs: Hash[]; }): Promise => { - try { - const documentStoreContract = DocumentStore__factory.connect(documentStore, provider); - const isBatchable = await isBatchableDocumentStore(documentStoreContract); - let revoked: boolean; - if (isBatchable) { - revoked = (await documentStoreContract["isRevoked(bytes32,bytes32,bytes32[])"]( - merkleRoot, - targetHash, - proofs - )) as boolean; - } else { - const intermediateHashes = getIntermediateHashes(targetHash, proofs); - revoked = await isAnyHashRevoked(documentStoreContract, intermediateHashes); - } + const providers = Array.isArray(provider) ? provider : [provider]; + const queryProviderIndex = Math.floor(Math.random() * providers.length); + const documentStoreContractQueryProviders = providers.map((p) => DocumentStore__factory.connect(documentStore, p)); + + let tries = 0; + for (;;) { + const documentStoreContract = + documentStoreContractQueryProviders[(queryProviderIndex + tries) % documentStoreContractQueryProviders.length]; + try { + const isBatchable = await isBatchableDocumentStore(documentStoreContract); + let revoked: boolean; + if (isBatchable) { + revoked = (await documentStoreContract["isRevoked(bytes32,bytes32,bytes32[])"]( + merkleRoot, + targetHash, + proofs + )) as boolean; + } else { + const intermediateHashes = getIntermediateHashes(targetHash, proofs); + revoked = await isAnyHashRevoked(documentStoreContract, intermediateHashes); + } - return revoked - ? { - revoked: true, - address: documentStore, - reason: { - message: `Document ${merkleRoot} has been revoked under contract ${documentStore}`, - code: OpenAttestationEthereumDocumentStoreStatusCode.DOCUMENT_REVOKED, - codeString: - OpenAttestationEthereumDocumentStoreStatusCode[ - OpenAttestationEthereumDocumentStoreStatusCode.DOCUMENT_REVOKED - ], - }, - } - : { - revoked: false, - address: documentStore, - }; - } catch (error) { - // If error can be decoded and it's because of document is not revoked, we return false - // Else allow error to continue to bubble up - return { - revoked: true, - address: documentStore, - reason: { - message: decodeError(error), - code: OpenAttestationEthereumDocumentStoreStatusCode.DOCUMENT_REVOKED, - codeString: - OpenAttestationEthereumDocumentStoreStatusCode[ - OpenAttestationEthereumDocumentStoreStatusCode.DOCUMENT_REVOKED - ], - }, - }; + return revoked + ? { + revoked: true, + address: documentStore, + reason: { + message: `Document ${merkleRoot} has been revoked under contract ${documentStore}`, + code: OpenAttestationEthereumDocumentStoreStatusCode.DOCUMENT_REVOKED, + codeString: + OpenAttestationEthereumDocumentStoreStatusCode[ + OpenAttestationEthereumDocumentStoreStatusCode.DOCUMENT_REVOKED + ], + }, + } + : { + revoked: false, + address: documentStore, + }; + } catch (error: any) { + if (error.code === errors.NETWORK_ERROR && tries < 3) { + tries++; + continue; + } + // If error can be decoded and it's because of document is not revoked, we return false + // Else allow error to continue to bubble up + return { + revoked: true, + address: documentStore, + reason: { + message: decodeError(error), + code: OpenAttestationEthereumDocumentStoreStatusCode.DOCUMENT_REVOKED, + codeString: + OpenAttestationEthereumDocumentStoreStatusCode[ + OpenAttestationEthereumDocumentStoreStatusCode.DOCUMENT_REVOKED + ], + }, + }; + } } }; diff --git a/src/verifiers/issuerIdentity/dnsTxt/openAttestationDnsTxt.ts b/src/verifiers/issuerIdentity/dnsTxt/openAttestationDnsTxt.ts index 4a5b68ba..a139f212 100644 --- a/src/verifiers/issuerIdentity/dnsTxt/openAttestationDnsTxt.ts +++ b/src/verifiers/issuerIdentity/dnsTxt/openAttestationDnsTxt.ts @@ -27,7 +27,8 @@ const resolveIssuerIdentity = async ( smartContractAddress: string, options: VerifierOptions ): Promise => { - const network = await options.provider.getNetwork(); + const provider = Array.isArray(options.provider) ? options.provider[0] : options.provider; + const network = await provider.getNetwork(); const records = await getDocumentStoreRecords(location); const matchingRecord = records.find( (record) =>