Skip to content

Commit

Permalink
feat: support for document store v4 (#285)
Browse files Browse the repository at this point in the history
* refactor: fix existing awkward logic

* feat: support document store v4

* chore: remove contract dependencies

* chore: update vulnerable deps

* refactor: add document store package

* chore: jest config
  • Loading branch information
superical committed Apr 5, 2024
1 parent 26bf36f commit db5fd03
Show file tree
Hide file tree
Showing 9 changed files with 141 additions and 394 deletions.
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ module.exports = {
moduleNameMapper: {
axios: "axios/dist/node/axios.cjs", // Temporary workaround: Force Jest to import the CommonJS Axios build
},
transformIgnorePatterns: ["node_modules/(?!(@govtechsg/document-store-ethers-v5)/)"],
};
401 changes: 38 additions & 363 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,8 @@
"license": "Apache-2.0",
"dependencies": {
"@govtechsg/dnsprove": "^2.6.2",
"@govtechsg/document-store": "^2.2.3",
"@govtechsg/document-store-ethers-v5": "^4.0.0",
"@govtechsg/open-attestation": "^6.9.0",
"@govtechsg/token-registry": "^4.1.7",
"axios": "^1.6.2",
"debug": "^4.3.1",
"did-resolver": "^4.1.0",
Expand Down
29 changes: 29 additions & 0 deletions src/common/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ import {
getOpenAttestationHashFragment,
invalidArgument,
isDocumentStoreAddressOrTokenRegistryAddressInvalid,
isBatchableDocumentStore,
serverError,
unhandledError,
} from "./utils";
import { Contract } from "ethers";

const fragments: AllVerificationFragment[] = [
{
Expand Down Expand Up @@ -767,3 +769,30 @@ describe("unhandledError", () => {
expect(unhandledError([verificationFragment])).toStrictEqual(false);
});
});

describe("isBatchableDocumentStore", () => {
let mockDocumentStore: Contract;

beforeEach(() => {
mockDocumentStore = {
supportsInterface: jest.fn(),
} as unknown as Contract;
});

it("should call supportsInterface with the correct id", async () => {
mockDocumentStore.supportsInterface.mockResolvedValue(true);

const res = await isBatchableDocumentStore(mockDocumentStore);

expect(mockDocumentStore.supportsInterface).toHaveBeenCalledWith("0xdcfd0745");
expect(res).toBe(true);
});

it("should return false when supportsInterface has error", async () => {
mockDocumentStore.supportsInterface.mockRejectedValue(new Error("Call Exception"));

const res = await isBatchableDocumentStore(mockDocumentStore);

expect(res).toBe(false);
});
});
11 changes: 10 additions & 1 deletion src/common/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { providers } from "ethers";
import { Contract, providers } from "ethers";
import { INFURA_API_KEY } from "../config";
import {
ProviderDetails,
Expand Down Expand Up @@ -233,3 +233,12 @@ export const unhandledError = (fragments: VerificationFragment[]): boolean => {
tokenRegistryMintedFragment?.reason?.code === OpenAttestationEthereumDocumentStoreStatusCode.ETHERS_UNHANDLED_ERROR
);
};

export const isBatchableDocumentStore = async (contract: Contract): Promise<boolean> => {
try {
// Interface for DocumentStoreBatchable
return (await contract.supportsInterface("0xdcfd0745")) as boolean;
} catch {
return false;
}
};
Original file line number Diff line number Diff line change
@@ -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 { DocumentStoreFactory } from "@govtechsg/document-store";
import { VerificationFragmentType, Verifier, VerifierOptions } from "../../../types/core";
import { OpenAttestationEthereumDocumentStoreStatusCode, Reason } from "../../../types/error";
import { CodedError } from "../../../common/error";
Expand All @@ -14,6 +14,7 @@ import {
ValidDocumentStoreDataV3,
ValidDocumentStoreIssuanceStatusArray,
} from "./ethereumDocumentStoreStatus.type";
import { isBatchableDocumentStore } from "../../../common/utils";

const name = "OpenAttestationEthereumDocumentStoreStatus";
const type: VerificationFragmentType = "DOCUMENT_STATUS";
Expand All @@ -37,15 +38,27 @@ export const getIssuersDocumentStores = (document: WrappedDocument<v2.OpenAttest
export const isIssuedOnDocumentStore = async ({
documentStore,
merkleRoot,
targetHash,
proofs,
provider,
}: {
documentStore: string;
merkleRoot: string;
targetHash: string;
proofs: string[];
provider: providers.Provider;
}): Promise<DocumentStoreIssuanceStatus> => {
const documentStoreContract = DocumentStore__factory.connect(documentStore, provider);

try {
const documentStoreContract = await DocumentStoreFactory.connect(documentStore, provider);
const issued = await documentStoreContract.isIssued(merkleRoot);
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);
}
return issued
? {
issued: true,
Expand Down Expand Up @@ -115,7 +128,7 @@ const verifyV2 = async (
const proofs = document.signature.proof || [];
const issuanceStatuses = await Promise.all(
documentStores.map((documentStore) =>
isIssuedOnDocumentStore({ documentStore, merkleRoot, provider: options.provider })
isIssuedOnDocumentStore({ documentStore, merkleRoot, targetHash, proofs, provider: options.provider })
)
);
const notIssued = issuanceStatuses.find(InvalidDocumentStoreIssuanceStatus.guard);
Expand Down Expand Up @@ -189,7 +202,13 @@ const verifyV3 = async (
const merkleRoot = `0x${merkleRootRaw}`;
const { value: documentStore } = document.openAttestationMetadata.proof;

const issuance = await isIssuedOnDocumentStore({ documentStore, merkleRoot, provider: options.provider });
const issuance = await isIssuedOnDocumentStore({
documentStore,
merkleRoot,
targetHash,
proofs,
provider: options.provider,
});
const revocation = await isRevokedOnDocumentStore({
documentStore,
merkleRoot,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getData, utils, v2, v3, WrappedDocument } from "@govtechsg/open-attestation";
import { TradeTrustToken__factory } from "@govtechsg/token-registry/contracts";
import { TransferableDocumentStore__factory } from "@govtechsg/document-store-ethers-v5";
import { constants, errors, providers } from "ethers";
import { VerificationFragmentType, Verifier } from "../../../types/core";
import { OpenAttestationEthereumTokenRegistryStatusCode } from "../../../types/error";
Expand Down Expand Up @@ -71,7 +71,10 @@ const getMerkleRoot = (

const isNonExistentToken = (error: any) => {
const message: string | undefined = error.message;
if (!message) return false;
if (!message) {
// ERC721NonexistentToken error
return error.data && error.data.slice(0, 10) === "0x7e273289";
}
return message.includes("owner query for nonexistent token");
};
const isMissingTokenRegistry = (error: any) => {
Expand Down Expand Up @@ -116,8 +119,10 @@ export const isTokenMintedOnRegistry = async ({
provider: providers.Provider;
}): Promise<ValidTokenRegistryStatus | InvalidTokenRegistryStatus> => {
try {
const tokenRegistryContract = await TradeTrustToken__factory.connect(tokenRegistry, provider);
const minted = await tokenRegistryContract.ownerOf(merkleRoot).then((owner) => !(owner === constants.AddressZero));
const tokenRegistryContract = TransferableDocumentStore__factory.connect(tokenRegistry, provider);
const minted = await tokenRegistryContract
.ownerOf(merkleRoot)
.then((owner: string) => !(owner === constants.AddressZero));
return minted
? { minted, address: tokenRegistry }
: {
Expand Down
32 changes: 21 additions & 11 deletions src/verifiers/documentStatus/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { utils } from "@govtechsg/open-attestation";
import { DocumentStore } from "@govtechsg/document-store";
import { errors, providers } from "ethers";
import { DocumentStoreFactory } from "@govtechsg/document-store";
import { DocumentStore__factory } from "@govtechsg/document-store-ethers-v5";
import { Contract, errors, providers } from "ethers";
import { Hash } from "../../types/core";
import {
OpenAttestationEthereumDocumentStoreStatusCode,
Expand All @@ -11,6 +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";

export const getIntermediateHashes = (targetHash: Hash, proofs: Hash[] = []) => {
const hashes = [`0x${targetHash}`];
Expand Down Expand Up @@ -60,12 +60,12 @@ 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: DocumentStore, intermediateHashes: Hash[]) => {
export const isAnyHashRevoked = async (smartContract: Contract, intermediateHashes: Hash[]) => {
const revokedStatusDeferred = intermediateHashes.map((hash) =>
smartContract.isRevoked(hash).then((status) => (status ? hash : undefined))
smartContract["isRevoked(bytes32)"](hash).then((status: boolean) => status)
);
const revokedStatuses = await Promise.all(revokedStatusDeferred);
return revokedStatuses.find((hash) => hash);
return !revokedStatuses.every((status) => !status);
};

export const isRevokedOnDocumentStore = async ({
Expand All @@ -79,14 +79,24 @@ export const isRevokedOnDocumentStore = async ({
merkleRoot: string;
provider: providers.Provider;
targetHash: Hash;
proofs?: Hash[];
proofs: Hash[];
}): Promise<RevocationStatus> => {
try {
const documentStoreContract = await DocumentStoreFactory.connect(documentStore, provider);
const intermediateHashes = getIntermediateHashes(targetHash, proofs);
const revokedHash = await isAnyHashRevoked(documentStoreContract, intermediateHashes);
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);
}

return revokedHash
return revoked
? {
revoked: true,
address: documentStore,
Expand Down

0 comments on commit db5fd03

Please sign in to comment.