Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: multi providers retry #292

Draft
wants to merge 5 commits into
base: beta
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 56 additions & 10 deletions src/common/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Contract, providers } from "ethers";
import { Contract, errors, providers } from "ethers";
import { INFURA_API_KEY } from "../config";
import {
ProviderDetails,
Expand Down Expand Up @@ -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;
};

/**
Expand Down Expand Up @@ -234,11 +238,53 @@ export const unhandledError = (fragments: VerificationFragment[]): boolean => {
);
};

export const isBatchableDocumentStore = async (contract: Contract): Promise<boolean> => {
try {
// Interface for DocumentStoreBatchable
return (await contract.supportsInterface("0xdcfd0745")) as boolean;
} catch {
return false;
}
export const isBatchableDocumentStore = async (contract: Contract | Contract[]): Promise<boolean> => {
const contracts = Array.isArray(contract) ? contract : [contract];
// Interface for DocumentStoreBatchable
return queryContract(contracts, async (c) => {
try {
return (await c.supportsInterface("0xdcfd0745")) as boolean;
} catch {
return false;
}
});
};

/**
* Executes a given method on a contract and retries with a different provider if the execution fails due to a server error, timeout, or call exception.
*
* @param {T[]} contracts - An array of contracts to query, the underlying contract is the same, but the provider is different.
* @param {(contract: T) => Promise<R>} method - The method to execute on each contract.
* @return {Promise<R>} A promise that resolves to the result of the method execution.
*/
export async function queryContract<T extends Contract, R>(
contracts: T[],
method: (contract: T) => Promise<R>
): Promise<R> {
let tries = 0;
const initialContractIndex = Math.floor(Math.random() * contracts.length);
for (;;) {
// each attempt would be made with a different provider
const contractIndex = (initialContractIndex + tries) % contracts.length;
const contract = contracts[contractIndex];
console.debug("Attempt number ", tries, "Trying with provider index ", contractIndex, "out of ", contracts.length);

try {
return await method(contract);
} catch (error: unknown) {
if (error instanceof Error && "code" in error && typeof error.code === "string") {
if (
(error.code === errors.SERVER_ERROR ||
error.code === errors.TIMEOUT ||
error.code === errors.CALL_EXCEPTION) &&
tries < contracts.length
) {
tries++;
continue;
}
}

throw error;
}
}
}
4 changes: 2 additions & 2 deletions src/types/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Reason } from "./error";
export type PromiseCallback = (promises: Promise<VerificationFragment>[]) => void;

export interface VerificationBuilderOptionsWithProvider {
provider: providers.Provider;
provider: providers.Provider | providers.Provider[];
resolver?: Resolver;
}

Expand All @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
ValidDocumentStoreDataV3,
ValidDocumentStoreIssuanceStatusArray,
} from "./ethereumDocumentStoreStatus.type";
import { isBatchableDocumentStore } from "../../../common/utils";
import { isBatchableDocumentStore, queryContract } from "../../../common/utils";

const name = "OpenAttestationEthereumDocumentStoreStatus";
const type: VerificationFragmentType = "DOCUMENT_STATUS";
Expand Down Expand Up @@ -46,18 +46,21 @@ export const isIssuedOnDocumentStore = async ({
merkleRoot: string;
targetHash: string;
proofs: string[];
provider: providers.Provider;
provider: providers.Provider | providers.Provider[];
}): Promise<DocumentStoreIssuanceStatus> => {
const documentStoreContract = DocumentStore__factory.connect(documentStore, provider);
const providers = Array.isArray(provider) ? provider : [provider];
const contracts = providers.map((p) => DocumentStore__factory.connect(documentStore, p));

try {
const isBatchable = await isBatchableDocumentStore(documentStoreContract);
const isBatchable = await isBatchableDocumentStore(contracts);

let issued: boolean;
if (isBatchable) {
issued = await documentStoreContract["isIssued(bytes32,bytes32,bytes32[])"](merkleRoot, targetHash, proofs);
issued = await queryContract(contracts, (c) =>
c["isIssued(bytes32,bytes32,bytes32[])"](merkleRoot, targetHash, proofs)
);
} else {
issued = await documentStoreContract["isIssued(bytes32)"](merkleRoot);
issued = await queryContract(contracts, (c) => c["isIssued(bytes32)"](merkleRoot));
}
return issued
? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
42 changes: 26 additions & 16 deletions src/verifiers/documentStatus/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { CodedError } from "../../common/error";
import { OcspResponderRevocationReason, RevocationStatus } from "./revocation.types";
import axios from "axios";
import { ValidOcspResponse, ValidOcspResponseRevoked } from "./didSigned/didSignedDocumentStatus.type";
import { isBatchableDocumentStore } from "../../common/utils";
import { isBatchableDocumentStore, queryContract } from "../../common/utils";

export const getIntermediateHashes = (targetHash: Hash, proofs: Hash[] = []) => {
const hashes = [`0x${targetHash}`];
Expand Down Expand Up @@ -60,12 +60,22 @@ export const decodeError = (error: any) => {
/**
* Given a list of hashes, check against one smart contract if any of the hash has been revoked
* */
export const isAnyHashRevoked = async (smartContract: Contract, intermediateHashes: Hash[]) => {
const revokedStatusDeferred = intermediateHashes.map((hash) =>
smartContract["isRevoked(bytes32)"](hash).then((status: boolean) => status)
);
const revokedStatuses = await Promise.all(revokedStatusDeferred);
return !revokedStatuses.every((status) => !status);
export const isAnyHashRevoked = async (contracts: Contract[], intermediateHashes: Hash[], batchSize = 5) => {
for (let i = 0; i < intermediateHashes.length; i += batchSize) {
const batch = intermediateHashes.slice(i, i + batchSize);
const revokedStatusDeferred = batch.map((hash) =>
queryContract(contracts, async (c) => {
return c["isRevoked(bytes32)"](hash) as boolean;
})
);

const revokedStatuses = await Promise.all(revokedStatusDeferred);
if (!revokedStatuses.every((status) => !status)) {
return true;
}
}

return false;
};

export const isRevokedOnDocumentStore = async ({
Expand All @@ -77,23 +87,23 @@ export const isRevokedOnDocumentStore = async ({
}: {
documentStore: string;
merkleRoot: string;
provider: providers.Provider;
provider: providers.Provider | providers.Provider[];
targetHash: Hash;
proofs: Hash[];
}): Promise<RevocationStatus> => {
const providers = Array.isArray(provider) ? provider : [provider];
const contracts = providers.map((p) => DocumentStore__factory.connect(documentStore, p));

try {
const documentStoreContract = DocumentStore__factory.connect(documentStore, provider);
const isBatchable = await isBatchableDocumentStore(documentStoreContract);
const isBatchable = await isBatchableDocumentStore(contracts[0]);
let revoked: boolean;
if (isBatchable) {
revoked = (await documentStoreContract["isRevoked(bytes32,bytes32,bytes32[])"](
merkleRoot,
targetHash,
proofs
)) as boolean;
revoked = await queryContract(contracts, async (c) => {
return c["isRevoked(bytes32,bytes32,bytes32[])"](merkleRoot, targetHash, proofs) as Promise<boolean>;
});
} else {
const intermediateHashes = getIntermediateHashes(targetHash, proofs);
revoked = await isAnyHashRevoked(documentStoreContract, intermediateHashes);
revoked = await isAnyHashRevoked(contracts, intermediateHashes);
}

return revoked
Expand Down
3 changes: 2 additions & 1 deletion src/verifiers/issuerIdentity/dnsTxt/openAttestationDnsTxt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ const resolveIssuerIdentity = async (
smartContractAddress: string,
options: VerifierOptions
): Promise<DnsTxtVerificationStatus> => {
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) =>
Expand Down
Loading