diff --git a/package-lock.json b/package-lock.json index 3474c669..939965ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -660,9 +660,9 @@ } }, "@govtechsg/open-attestation": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@govtechsg/open-attestation/-/open-attestation-3.0.4.tgz", - "integrity": "sha512-QMPs7Y4CGq1yl2+gLh1LveY1blhgf2qbm0s0bP9gmmyKTI5uwstX5q2GsHiN7k0P7p5mqSIHbID2mhEM0cuc2A==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@govtechsg/open-attestation/-/open-attestation-3.1.0.tgz", + "integrity": "sha512-7QkanH/Q8CUor4gX5Be+jELh5uAdyiVtj5dxU0P8NMjwvtS+hEd/9Iz2do8Xwl8kDS0CYu9tjkLPIxq454dF4Q==", "requires": { "ajv": "6.10.2", "debug": "^4.1.1", @@ -694,9 +694,9 @@ } }, "uuid": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", - "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" } } }, @@ -1081,6 +1081,17 @@ "node-fetch": "^2.1.1", "universal-user-agent": "^2.0.0", "url-template": "^2.0.8" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } } }, "@octokit/types": { @@ -1559,6 +1570,12 @@ "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", "dev": true }, + "@types/debug": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", + "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==", + "dev": true + }, "@types/eslint-visitor-keys": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", @@ -3213,10 +3230,9 @@ "dev": true }, "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "requires": { "ms": "^2.1.1" } @@ -3946,9 +3962,9 @@ "dev": true }, "ethers": { - "version": "4.0.40", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-4.0.40.tgz", - "integrity": "sha512-MC9BtV7Hpq4dgFONEfanx9aU9GhhoWU270F+/wegHZXA7FR+2KXFdt36YIQYLmVY5ykUWswDxd+f9EVkIa7JOA==", + "version": "4.0.44", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-4.0.44.tgz", + "integrity": "sha512-kCkMPkpYjBkxzqjcuYUfDY7VHDbf5EXnfRPUOazdqdf59SvXaT+w5lgauxLlk1UjxnAiNfeNS87rkIXnsTaM7Q==", "requires": { "aes-js": "3.0.0", "bn.js": "^4.4.0", @@ -5447,6 +5463,17 @@ "requires": { "agent-base": "^4.1.0", "debug": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } } }, "human-signals": { diff --git a/package.json b/package.json index 551f268a..fd484b13 100644 --- a/package.json +++ b/package.json @@ -23,13 +23,15 @@ "dependencies": { "@govtechsg/dnsprove": "^2.0.5", "@govtechsg/open-attestation": "3.1.0", - "ethers": "^4.0.40" + "debug": "^4.1.1", + "ethers": "^4.0.44" }, "devDependencies": { "@commitlint/cli": "8.2.0", "@commitlint/config-conventional": "8.2.0", "@commitlint/prompt": "8.2.0", "@ls-age/commitlint-circle": "1.0.0", + "@types/debug": "^4.1.5", "@types/jest": "^24.0.23", "@typescript-eslint/eslint-plugin": "1.6.0", "@typescript-eslint/parser": "^2.11.0", diff --git a/src/common/logger.ts b/src/common/logger.ts new file mode 100644 index 00000000..ce6a4a64 --- /dev/null +++ b/src/common/logger.ts @@ -0,0 +1,19 @@ +import debug from "debug"; + +const logger = debug("oa-verify"); + +interface Logger { + trace: debug.Debugger; + debug: debug.Debugger; + info: debug.Debugger; + warn: debug.Debugger; + error: debug.Debugger; +} + +export const getLogger = (namespace: string): Logger => ({ + trace: logger.extend(`trace:${namespace}`), + debug: logger.extend(`debug:${namespace}`), + info: logger.extend(`info:${namespace}`), + warn: logger.extend(`warn:${namespace}`), + error: logger.extend(`error:${namespace}`) +}); diff --git a/src/common/smartContract/contractInstance.test.ts b/src/common/smartContract/contractInstance.test.ts deleted file mode 100644 index afc56cf3..00000000 --- a/src/common/smartContract/contractInstance.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as ethers from "ethers"; -import { contractInstance } from "./contractInstance"; - -jest.mock("../../config", () => ({ INFURA_API_KEY: "INFURA_API_KEY" })); -jest.mock("ethers"); - -it("creates a ethers.Contract instance with the right provider", () => { - contractInstance({ - abi: "ABI", - network: "NETWORK", - contractAddress: "0x0A" - }); - - // @ts-ignore - expect(ethers.providers.InfuraProvider.mock.calls[0]).toEqual(["NETWORK", "INFURA_API_KEY"]); - - // @ts-ignore - expect(ethers.Contract.mock.calls[0][0]).toEqual("0x0A"); - // @ts-ignore - expect(ethers.Contract.mock.calls[0][1]).toEqual("ABI"); -}); diff --git a/src/common/smartContract/contractInstance.ts b/src/common/smartContract/contractInstance.ts index bde9b381..7f531bc4 100644 --- a/src/common/smartContract/contractInstance.ts +++ b/src/common/smartContract/contractInstance.ts @@ -1,16 +1,26 @@ import * as ethers from "ethers"; import { INFURA_API_KEY } from "../../config"; import { Hash } from "../../types/core"; +import { getLogger } from "../logger"; +const logger = getLogger("contractInstance"); interface ContractInstance { abi: any; // type is any of json file in abi folder network: string; contractAddress: Hash; } + +export const getProvider = (options: { network: string }): ethers.providers.Provider => + new ethers.providers.InfuraProvider(options.network, INFURA_API_KEY); + export const contractInstance = (options: ContractInstance) => { - return new ethers.Contract( - options.contractAddress, - options.abi, - new ethers.providers.InfuraProvider(options.network, INFURA_API_KEY) - ); + const contract = new ethers.Contract(options.contractAddress, options.abi, getProvider(options)); + + // this is done to prevent uncaught exception to raise because an address is invalid + contract.addressPromise.catch(() => { + logger.trace( + `oa-verify caught an error from ethers when trying to resolve the address of the provided address ${options.contractAddress}` + ); + }); + return contract; }; diff --git a/src/common/smartContract/documentStoreContractInterface.ts b/src/common/smartContract/documentStoreContractInterface.ts new file mode 100644 index 00000000..97d391ea --- /dev/null +++ b/src/common/smartContract/documentStoreContractInterface.ts @@ -0,0 +1,31 @@ +import { Contract } from "ethers"; +import { getData, v2, v3, WrappedDocument } from "@govtechsg/open-attestation"; +import { Hash, isWrappedV2Document } from "../../types/core"; +import { contractInstance } from "./contractInstance"; +import documentStoreAbi from "./abi/documentStore.json"; + +export const getIssuersDocumentStore = ( + document: WrappedDocument | WrappedDocument +): string[] => { + if (isWrappedV2Document(document)) { + const data = getData(document); + return data.issuers.map(issuer => issuer.documentStore || issuer.certificateStore || ""); + } + return [getData(document).proof.value]; +}; + +export const createDocumentStoreContract = (address: string, { network }: { network: string }) => { + return contractInstance({ + contractAddress: address, + abi: documentStoreAbi, + network + }); +}; + +export const isIssuedOnDocumentStore = async (smartContract: Contract, hash: Hash): Promise => { + return smartContract.functions.isIssued(hash); +}; + +export const isRevokedOnDocumentStore = async (smartContract: Contract, hash: Hash): Promise => { + return smartContract.functions.isRevoked(hash); +}; diff --git a/src/common/smartContract/documentStoreErrors.ts b/src/common/smartContract/documentStoreErrors.ts new file mode 100644 index 00000000..423768fe --- /dev/null +++ b/src/common/smartContract/documentStoreErrors.ts @@ -0,0 +1,67 @@ +import { errors } from "ethers"; +import { + EthersError, + Hash, + OpenAttestationEthereumDocumentStoreIssuedCode, + OpenAttestationEthereumDocumentStoreRevokedCode, + Reason +} from "../.."; + +const contractNotFound = (address: Hash): Reason => { + return { + code: OpenAttestationEthereumDocumentStoreIssuedCode.CONTRACT_NOT_FOUND, + codeString: + OpenAttestationEthereumDocumentStoreIssuedCode[OpenAttestationEthereumDocumentStoreIssuedCode.CONTRACT_NOT_FOUND], + message: `Contract ${address} was not found` + }; +}; +const contractAddressInvalid = (address: Hash): Reason => { + return { + code: OpenAttestationEthereumDocumentStoreIssuedCode.CONTRACT_ADDRESS_INVALID, + codeString: + OpenAttestationEthereumDocumentStoreIssuedCode[ + OpenAttestationEthereumDocumentStoreIssuedCode.CONTRACT_ADDRESS_INVALID + ], + message: `Contract address ${address} is invalid` + }; +}; +export const contractNotIssued = (merkleRoot: Hash, address: string): Reason => { + return { + code: OpenAttestationEthereumDocumentStoreIssuedCode.DOCUMENT_NOT_ISSUED, + codeString: + OpenAttestationEthereumDocumentStoreIssuedCode[ + OpenAttestationEthereumDocumentStoreIssuedCode.DOCUMENT_NOT_ISSUED + ], + message: `Certificate ${merkleRoot} has not been issued under contract ${address}` + }; +}; + +export const contractRevoked = (merkleRoot: string, address: string): Reason => { + return { + code: OpenAttestationEthereumDocumentStoreRevokedCode.DOCUMENT_REVOKED, + codeString: + OpenAttestationEthereumDocumentStoreRevokedCode[OpenAttestationEthereumDocumentStoreRevokedCode.DOCUMENT_REVOKED], + message: `Certificate ${merkleRoot} has been revoked under contract ${address}` + }; +}; + +export const getErrorReason = (error: EthersError, address: string): Reason | null => { + const reason = error.reason && Array.isArray(error.reason) ? error.reason[0] : error.reason ?? ""; + if (reason.toLowerCase() === "contract not deployed".toLowerCase() && error.code === errors.UNSUPPORTED_OPERATION) { + return contractNotFound(address); + } else if ( + (reason.toLowerCase() === "ENS name not configured".toLowerCase() && error.code === errors.UNSUPPORTED_OPERATION) || + (reason.toLowerCase() === "bad address checksum".toLowerCase() && error.code === errors.INVALID_ARGUMENT) || + (reason.toLowerCase() === "invalid address".toLowerCase() && error.code === errors.INVALID_ARGUMENT) + ) { + return contractAddressInvalid(address); + } + return { + message: `Error with smart contract ${address}: ${error.reason}`, + code: OpenAttestationEthereumDocumentStoreIssuedCode.ETHERS_UNHANDLED_ERROR, + codeString: + OpenAttestationEthereumDocumentStoreIssuedCode[ + OpenAttestationEthereumDocumentStoreIssuedCode.ETHERS_UNHANDLED_ERROR + ] + }; +}; diff --git a/src/common/smartContract/documentToSmartContracts.ts b/src/common/smartContract/documentToSmartContracts.ts deleted file mode 100644 index 14aec2b3..00000000 --- a/src/common/smartContract/documentToSmartContracts.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { getData, v2, v3, WrappedDocument } from "@govtechsg/open-attestation"; -import { contractInstance } from "./contractInstance"; -import tokenRegistryAbi from "./abi/tokenRegistry.json"; -import documentStoreAbi from "./abi/documentStore.json"; -import { isWrappedV2Document, OpenAttestationContract } from "../../types/core"; - -// Given a raw document, return list of all token registry contracts -export const getTokenRegistrySmartContract = ( - document: WrappedDocument | WrappedDocument, - options: { network: string } -): OpenAttestationContract[] => { - if (isWrappedV2Document(document)) { - const documentData = getData(document); - const issuersWithTokenRegistry = documentData.issuers.filter(issuer => "tokenRegistry" in issuer).length; - if (issuersWithTokenRegistry > 1) { - throw new Error(`Only one token registry is allowed. Found ${issuersWithTokenRegistry}`); - } - return (documentData.issuers || []).map(issuer => { - if (!issuer.tokenRegistry) { - throw new Error(`No token registry for issuer "${issuer.name}"`); - } - return { - type: v3.Method.TokenRegistry, - address: issuer.tokenRegistry, - instance: contractInstance({ - contractAddress: issuer.tokenRegistry, - abi: tokenRegistryAbi, - network: options.network - }) - }; - }); - } - const documentData = getData(document); - return [ - { - type: documentData.proof.method, - address: documentData.proof.value, - instance: contractInstance({ - contractAddress: documentData.proof.value, - abi: tokenRegistryAbi, - network: options.network - }) - } - ]; -}; - -export const getDocumentStoreSmartContract = ( - document: WrappedDocument | WrappedDocument, - options: { network: string } -) => { - if (isWrappedV2Document(document)) { - const documentData = getData(document); - return (documentData.issuers || []).map(issuer => { - if (!issuer.certificateStore && !issuer.documentStore) { - throw new Error(`No document store for issuer "${issuer.name}"`); - } - const address = issuer.documentStore || issuer.certificateStore || ""; - return { - type: v3.Method.DocumentStore, - address, - instance: contractInstance({ - contractAddress: address, - abi: documentStoreAbi, - network: options.network - }) - }; - }); - } - const documentData = getData(document); - return [ - { - type: documentData.proof.method, - address: documentData.proof.value, - instance: contractInstance({ - contractAddress: documentData.proof.value, - abi: documentStoreAbi, - network: options.network - }) - } - ]; -}; diff --git a/src/common/smartContract/tokenRegistryContractInterface.ts b/src/common/smartContract/tokenRegistryContractInterface.ts new file mode 100644 index 00000000..cd8daf77 --- /dev/null +++ b/src/common/smartContract/tokenRegistryContractInterface.ts @@ -0,0 +1,27 @@ +import { constants, Contract } from "ethers"; +import { getData, v2, v3, WrappedDocument } from "@govtechsg/open-attestation"; +import { Hash, isWrappedV2Document } from "../../types/core"; +import { contractInstance } from "./contractInstance"; +import tokenRegistryAbi from "./abi/tokenRegistry.json"; + +export const getIssuersTokenRegistry = ( + document: WrappedDocument | WrappedDocument +): string[] => { + if (isWrappedV2Document(document)) { + const data = getData(document); + return data.issuers.map(issuer => issuer.tokenRegistry || ""); + } + return [getData(document).proof.value]; +}; + +export const createTokenRegistryContract = (address: string, { network }: { network: string }) => { + return contractInstance({ + contractAddress: address, + abi: tokenRegistryAbi, + network + }); +}; + +export const isMintedOnTokenRegistry = async (smartContract: Contract, hash: Hash): Promise => { + return smartContract.functions.ownerOf(hash).then(owner => !(owner === constants.AddressZero)); +}; diff --git a/src/index.ts b/src/index.ts index 8800090c..538553f9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,12 @@ import { v2, v3, WrappedDocument } from "@govtechsg/open-attestation"; import { verificationBuilder } from "./verifiers/verificationBuilder"; import { Verifier } from "./types/core"; -import { openAttestationHash } from "./verifiers/openAttestationHash"; -import { openAttestationDnsTxt } from "./verifiers/openAttestationDnsTxt"; +import { openAttestationHash } from "./verifiers/hash/openAttestationHash"; +import { openAttestationDnsTxt } from "./verifiers/dnsText/openAttestationDnsTxt"; import { openAttestationEthereumDocumentStoreIssued } from "./verifiers/documentStoreIssued/openAttestationEthereumDocumentStoreIssued"; -import { openAttestationEthereumTokenRegistryMinted } from "./verifiers/openAttestationEthereumTokenRegistryMinted"; import { openAttestationEthereumDocumentStoreRevoked } from "./verifiers/documentStoreRevoked/openAttestationEthereumDocumentStoreRevoked"; import { isValid } from "./validator"; +import { openAttestationEthereumTokenRegistryMinted } from "./verifiers/tokenRegistryMinted/openAttestationEthereumTokenRegistryMinted"; const openAttestationVerifiers: Verifier< WrappedDocument | WrappedDocument @@ -21,4 +21,5 @@ const openAttestationVerifiers: Verifier< const verify = verificationBuilder(openAttestationVerifiers); export * from "./types/core"; +export * from "./types/error"; export { verificationBuilder, openAttestationVerifiers, isValid, verify, Verifier }; diff --git a/src/types/core.ts b/src/types/core.ts index 3251c4b4..78f6c5e0 100644 --- a/src/types/core.ts +++ b/src/types/core.ts @@ -1,5 +1,6 @@ import { Contract } from "ethers"; import { v2, v3, WrappedDocument } from "@govtechsg/open-attestation"; +import { Reason } from "./error"; /** * - network on which to run the verification (if needed to connect to ethereum), For instance "ropste" or "homestead" @@ -25,15 +26,15 @@ export interface VerificationManagerOptions { * - return the name who can help to determine the verifier that created the result * * Additional fields might be populated - * - A message to provide further information about the error - * - Data to provide further information about the error + * - A reason to provide further information about the error/invalid/skipped state + * - Data to provide further information */ export interface VerificationFragment { name: string; type: VerificationFragmentType; - message?: string; data?: T; status: VerificationFragmentStatus; + reason?: Reason; } export type VerificationFragmentType = "DOCUMENT_INTEGRITY" | "DOCUMENT_STATUS" | "ISSUER_IDENTITY"; export type VerificationFragmentStatus = "ERROR" | "VALID" | "INVALID" | "SKIPPED"; @@ -46,7 +47,7 @@ export type VerificationFragmentStatus = "ERROR" | "VALID" | "INVALID" | "SKIPPE */ interface SkippedVerificationFragment extends VerificationFragment { status: "SKIPPED"; - message: string; + reason: Reason; } export interface Verifier< Document = WrappedDocument | WrappedDocument, diff --git a/src/types/error.ts b/src/types/error.ts new file mode 100644 index 00000000..a4680cc7 --- /dev/null +++ b/src/types/error.ts @@ -0,0 +1,50 @@ +// NEVER EVER REPLACE OR CHANGE A VALUE :) +// code for errors and invalid fragment +export enum OpenAttestationEthereumDocumentStoreIssuedCode { + UNEXPECTED_ERROR = 0, + DOCUMENT_NOT_ISSUED = 1, + CONTRACT_ADDRESS_INVALID = 2, + ETHERS_UNHANDLED_ERROR = 3, + SKIPPED = 4, + CONTRACT_NOT_FOUND = 404 +} +export enum OpenAttestationEthereumDocumentStoreRevokedCode { + UNEXPECTED_ERROR = 0, + DOCUMENT_REVOKED = 1, + CONTRACT_ADDRESS_INVALID = 2, + ETHERS_UNHANDLED_ERROR = 3, + SKIPPED = 4, + CONTRACT_NOT_FOUND = 404 +} +export enum OpenAttestationEthereumTokenRegistryMintedCode { + UNEXPECTED_ERROR = 0, + DOCUMENT_NOT_MINTED = 1, + CONTRACT_ADDRESS_INVALID = 2, + ETHERS_UNHANDLED_ERROR = 3, + SKIPPED = 4, + CONTRACT_NOT_FOUND = 404 +} +export enum OpenAttestationDnsTxtCode { + UNEXPECTED_ERROR = 0, + INVALID_IDENTITY = 1, + SKIPPED = 2 +} +export enum OpenAttestationHashCode { + DOCUMENT_TAMPERED = 0 +} + +export interface EthersError extends Error { + reason?: string | string[]; + code?: string; +} + +export interface Reason { + code: + | OpenAttestationEthereumDocumentStoreIssuedCode + | OpenAttestationEthereumDocumentStoreRevokedCode + | OpenAttestationEthereumTokenRegistryMintedCode + | OpenAttestationDnsTxtCode + | OpenAttestationHashCode; + codeString: string; + message: string; +} diff --git a/src/verifiers/openAttestationDnsTxt.ts b/src/verifiers/dnsText/openAttestationDnsTxt.ts similarity index 79% rename from src/verifiers/openAttestationDnsTxt.ts rename to src/verifiers/dnsText/openAttestationDnsTxt.ts index 98be7491..6a717973 100644 --- a/src/verifiers/openAttestationDnsTxt.ts +++ b/src/verifiers/dnsText/openAttestationDnsTxt.ts @@ -1,7 +1,8 @@ import { getData, v2, v3, WrappedDocument } from "@govtechsg/open-attestation"; import { getDocumentStoreRecords } from "@govtechsg/dnsprove"; import { utils } from "ethers"; -import { isWrappedV2Document, VerificationFragmentType, VerificationManagerOptions, Verifier } from "../types/core"; +import { isWrappedV2Document, VerificationFragmentType, VerificationManagerOptions, Verifier } from "../../types/core"; +import { OpenAttestationDnsTxtCode } from "../../types/error"; interface Identity { status: "VALID" | "INVALID" | "SKIPPED"; @@ -52,7 +53,11 @@ export const openAttestationDnsTxt: Verifier< status: "SKIPPED", type, name, - message: `Document issuers doesn't have "documentStore" / "tokenRegistry" property or doesn't use ${v3.IdentityProofType.DNSTxt} type` + reason: { + code: OpenAttestationDnsTxtCode.SKIPPED, + codeString: OpenAttestationDnsTxtCode[OpenAttestationDnsTxtCode.SKIPPED], + message: `Document issuers doesn't have "documentStore" / "tokenRegistry" property or doesn't use ${v3.IdentityProofType.DNSTxt} type` + } }); }, test: document => { @@ -96,7 +101,11 @@ export const openAttestationDnsTxt: Verifier< name, type, data: identities, - message: `Certificate issuer identity for ${smartContractAddress} is invalid`, + reason: { + code: OpenAttestationDnsTxtCode.INVALID_IDENTITY, + codeString: OpenAttestationDnsTxtCode[OpenAttestationDnsTxtCode.INVALID_IDENTITY], + message: `Certificate issuer identity for ${smartContractAddress} is invalid` + }, status: "INVALID" }; } @@ -115,7 +124,11 @@ export const openAttestationDnsTxt: Verifier< name, type, data: identity, - message: "Certificate issuer identity is invalid", + reason: { + code: OpenAttestationDnsTxtCode.INVALID_IDENTITY, + codeString: OpenAttestationDnsTxtCode[OpenAttestationDnsTxtCode.INVALID_IDENTITY], + message: "Certificate issuer identity is invalid" + }, status: "INVALID" }; } @@ -132,7 +145,11 @@ export const openAttestationDnsTxt: Verifier< name, type, data: e, - message: e.message, + reason: { + code: OpenAttestationDnsTxtCode.UNEXPECTED_ERROR, + codeString: OpenAttestationDnsTxtCode[OpenAttestationDnsTxtCode.UNEXPECTED_ERROR], + message: e.message + }, status: "ERROR" }; } diff --git a/src/verifiers/openAttestationDnsTxt.v2.test.ts b/src/verifiers/dnsText/openAttestationDnsTxt.v2.test.ts similarity index 87% rename from src/verifiers/openAttestationDnsTxt.v2.test.ts rename to src/verifiers/dnsText/openAttestationDnsTxt.v2.test.ts index e8f19ca0..f9545870 100644 --- a/src/verifiers/openAttestationDnsTxt.v2.test.ts +++ b/src/verifiers/dnsText/openAttestationDnsTxt.v2.test.ts @@ -1,6 +1,6 @@ import { openAttestationDnsTxt } from "./openAttestationDnsTxt"; -import { documentRopstenValidWithToken } from "../../test/fixtures/v2/documentRopstenValidWithToken"; -import { verificationBuilder } from "./verificationBuilder"; +import { documentRopstenValidWithToken } from "../../../test/fixtures/v2/documentRopstenValidWithToken"; +import { verificationBuilder } from "../verificationBuilder"; const verify = verificationBuilder([openAttestationDnsTxt]); describe("OpenAttestationDnsTxt v2 document", () => { @@ -87,7 +87,11 @@ describe("OpenAttestationDnsTxt v2 document", () => { value: "0xabcd" } ], - message: "Certificate issuer identity for 0xabcd is invalid", + reason: { + code: 1, + codeString: "INVALID_IDENTITY", + message: "Certificate issuer identity for 0xabcd is invalid" + }, status: "INVALID" } ]); @@ -117,7 +121,11 @@ describe("OpenAttestationDnsTxt v2 document", () => { type: "ISSUER_IDENTITY", name: "OpenAttestationDnsTxt", data: new Error("Location is missing"), - message: "Location is missing", + reason: { + code: 0, + codeString: "UNEXPECTED_ERROR", + message: "Location is missing" + }, status: "ERROR" } ]); @@ -142,8 +150,12 @@ describe("OpenAttestationDnsTxt v2 document", () => { }) ).toStrictEqual([ { - message: - 'Document issuers doesn\'t have "documentStore" / "tokenRegistry" property or doesn\'t use DNS-TXT type', + reason: { + code: 2, + codeString: "SKIPPED", + message: + 'Document issuers doesn\'t have "documentStore" / "tokenRegistry" property or doesn\'t use DNS-TXT type' + }, name: "OpenAttestationDnsTxt", status: "SKIPPED", type: "ISSUER_IDENTITY" @@ -170,8 +182,12 @@ describe("OpenAttestationDnsTxt v2 document", () => { }) ).toStrictEqual([ { - message: - 'Document issuers doesn\'t have "documentStore" / "tokenRegistry" property or doesn\'t use DNS-TXT type', + reason: { + code: 2, + codeString: "SKIPPED", + message: + 'Document issuers doesn\'t have "documentStore" / "tokenRegistry" property or doesn\'t use DNS-TXT type' + }, name: "OpenAttestationDnsTxt", status: "SKIPPED", type: "ISSUER_IDENTITY" @@ -201,8 +217,12 @@ describe("OpenAttestationDnsTxt v2 document", () => { }) ).toStrictEqual([ { - message: - 'Document issuers doesn\'t have "documentStore" / "tokenRegistry" property or doesn\'t use DNS-TXT type', + reason: { + code: 2, + codeString: "SKIPPED", + message: + 'Document issuers doesn\'t have "documentStore" / "tokenRegistry" property or doesn\'t use DNS-TXT type' + }, name: "OpenAttestationDnsTxt", status: "SKIPPED", type: "ISSUER_IDENTITY" @@ -232,8 +252,12 @@ describe("OpenAttestationDnsTxt v2 document", () => { }) ).toStrictEqual([ { - message: - 'Document issuers doesn\'t have "documentStore" / "tokenRegistry" property or doesn\'t use DNS-TXT type', + reason: { + code: 2, + codeString: "SKIPPED", + message: + 'Document issuers doesn\'t have "documentStore" / "tokenRegistry" property or doesn\'t use DNS-TXT type' + }, name: "OpenAttestationDnsTxt", status: "SKIPPED", type: "ISSUER_IDENTITY" @@ -330,7 +354,11 @@ describe("OpenAttestationDnsTxt v2 document", () => { value: "0xe59877ac86c0310e9ddaeb627f42fdee5f793fbe" } ], - message: "Certificate issuer identity for 0xabcd is invalid", + reason: { + code: 1, + codeString: "INVALID_IDENTITY", + message: "Certificate issuer identity for 0xabcd is invalid" + }, status: "INVALID" } ]); @@ -393,8 +421,12 @@ describe("OpenAttestationDnsTxt v2 document", () => { }) ).toStrictEqual([ { - message: - 'Document issuers doesn\'t have "documentStore" / "tokenRegistry" property or doesn\'t use DNS-TXT type', + reason: { + code: 2, + codeString: "SKIPPED", + message: + 'Document issuers doesn\'t have "documentStore" / "tokenRegistry" property or doesn\'t use DNS-TXT type' + }, name: "OpenAttestationDnsTxt", status: "SKIPPED", type: "ISSUER_IDENTITY" diff --git a/src/verifiers/openAttestationDnsTxt.v3.test.ts b/src/verifiers/dnsText/openAttestationDnsTxt.v3.test.ts similarity index 89% rename from src/verifiers/openAttestationDnsTxt.v3.test.ts rename to src/verifiers/dnsText/openAttestationDnsTxt.v3.test.ts index 7715c52f..f9406033 100644 --- a/src/verifiers/openAttestationDnsTxt.v3.test.ts +++ b/src/verifiers/dnsText/openAttestationDnsTxt.v3.test.ts @@ -1,5 +1,5 @@ import { openAttestationDnsTxt } from "./openAttestationDnsTxt"; -import { documentRopstenValidWithDocumentStore } from "../../test/fixtures/v3/documentRopstenValid"; +import { documentRopstenValidWithDocumentStore } from "../../../test/fixtures/v3/documentRopstenValid"; describe("OpenAttestationDnsTxt v3 document", () => { it("should return a valid fragment when document has valid identity", async () => { @@ -40,7 +40,12 @@ describe("OpenAttestationDnsTxt v3 document", () => { type: "ISSUER_IDENTITY", name: "OpenAttestationDnsTxt", data: { location: "some.io", value: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3", status: "INVALID" }, - message: "Certificate issuer identity is invalid", + + reason: { + code: 1, + codeString: "INVALID_IDENTITY", + message: "Certificate issuer identity is invalid" + }, status: "INVALID" }); }); @@ -66,7 +71,11 @@ describe("OpenAttestationDnsTxt v3 document", () => { type: "ISSUER_IDENTITY", name: "OpenAttestationDnsTxt", data: new Error("Identity type not supported"), - message: "Identity type not supported", + reason: { + code: 0, + codeString: "UNEXPECTED_ERROR", + message: "Identity type not supported" + }, status: "ERROR" }); }); @@ -92,7 +101,11 @@ describe("OpenAttestationDnsTxt v3 document", () => { type: "ISSUER_IDENTITY", name: "OpenAttestationDnsTxt", data: new Error("Location is missing"), - message: "Location is missing", + reason: { + code: 0, + codeString: "UNEXPECTED_ERROR", + message: "Location is missing" + }, status: "ERROR" }); }); diff --git a/src/verifiers/documentStoreIssued/contractInterface.integration.test.ts b/src/verifiers/documentStoreIssued/contractInterface.integration.test.ts deleted file mode 100644 index 46f7877f..00000000 --- a/src/verifiers/documentStoreIssued/contractInterface.integration.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { constants } from "ethers"; -import { isIssued, isIssuedOnDocumentStore, isIssuedOnTokenRegistry } from "./contractInterface"; -import { - getDocumentStoreSmartContract, - getTokenRegistrySmartContract -} from "../../common/smartContract/documentToSmartContracts"; -import { documentRopstenRevokedWithToken } from "../../../test/fixtures/v2/documentRopstenRevokedWithToken"; -import { documentRopstenValidWithCertificateStore } from "../../../test/fixtures/v2/documentRopstenValidWithCertificateStore"; -import { documentRopstenWithNonDeployedDocumentStore } from "../../../test/fixtures/v2/documentRopstenWithNonDeployedDocumentStore"; -import { documentRopstenValidWithToken } from "../../../test/fixtures/v2/documentRopstenValidWithToken"; - -describe("isIssuedOnTokenRegistry", () => { - it("returns true if token is created on tokenRegistry", async () => { - const smartContract = getTokenRegistrySmartContract(documentRopstenRevokedWithToken, { network: "ropsten" }); - const issued = await isIssuedOnTokenRegistry( - smartContract[0], - "0x30cc3db1f2b26e25d63a67b6f232c4cf2acd1402f632847a4857e73516a0762f" - ); - expect(issued).toBe(true); - }); - - it("allows error to bubble if token is nonexistent on tokenRegistry", async () => { - const smartContract = getTokenRegistrySmartContract(documentRopstenRevokedWithToken, { network: "ropsten" }); - await expect(isIssuedOnTokenRegistry(smartContract[0], constants.HashZero)).rejects.toThrow( - 'call revert exception (address="0x48399Fb88bcD031C556F53e93F690EEC07963Af3", args=["0x0000000000000000000000000000000000000000000000000000000000000000"], method="ownerOf(uint256)", errorSignature="Error(string)", errorArgs=[["ERC721: owner query for nonexistent token"]], reason=["ERC721: owner query for nonexistent token"], transaction={"to":{},"data":"0x6352211e0000000000000000000000000000000000000000000000000000000000000000"}, version=4.0.40)' - ); - }); -}); - -describe("isIssuedOnDocumentStore", () => { - it("returns true if document is issued on documentStore", async () => { - const smartContract = getDocumentStoreSmartContract(documentRopstenValidWithCertificateStore, { - network: "ropsten" - }); - const issued = await isIssuedOnDocumentStore( - smartContract[0], - "0x55609b30ae4182bc8621d398b5a8e50ec4dfca4dbce4719bef82f8041829bf23" - ); - expect(issued).toBe(true); - }); - - it("returns false if document is not issued on documentStore", async () => { - const smartContract = getDocumentStoreSmartContract(documentRopstenValidWithCertificateStore, { - network: "ropsten" - }); - const issued = await isIssuedOnDocumentStore(smartContract[0], constants.HashZero); - expect(issued).toBe(false); - }); - - it("allows error to bubble if documentStore is not deployed", async () => { - const smartContract = getDocumentStoreSmartContract(documentRopstenWithNonDeployedDocumentStore, { - network: "ropsten" - }); - await expect(isIssuedOnDocumentStore(smartContract[0], constants.HashZero)).rejects.toThrow( - 'contract not deployed (contractAddress="0x0000000000000000000000000000000000000000", operation="getDeployed", version=4.0.40)' - ); - }); -}); - -describe("isIssued", () => { - it("works for tokenRegistry", async () => { - const smartContract = getTokenRegistrySmartContract(documentRopstenValidWithToken, { network: "ropsten" }); - const issued = await isIssued( - smartContract[0], - "0x1b2c07f3d77078b44e65eae4c7f5d17fefaf0f73fb3f338fdb410912a8c4c4b7" - ); - expect(issued).toBe(true); - }); - - it("works for documentStore", async () => { - const smartContract = getDocumentStoreSmartContract(documentRopstenValidWithCertificateStore, { - network: "ropsten" - }); - const issued = await isIssued( - smartContract[0], - "0x55609b30ae4182bc8621d398b5a8e50ec4dfca4dbce4719bef82f8041829bf23" - ); - expect(issued).toBe(true); - }); - - it("throws for unsupported smart contract types", () => { - const smartContract = { type: "UNSUPPORTED_TYPE" }; - // @ts-ignore - expect(() => isIssued(smartContract, constants.HashZero)).toThrow("Smart contract type not supported"); - }); -}); diff --git a/src/verifiers/documentStoreIssued/contractInterface.ts b/src/verifiers/documentStoreIssued/contractInterface.ts deleted file mode 100644 index a5950d74..00000000 --- a/src/verifiers/documentStoreIssued/contractInterface.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { constants } from "ethers"; -import { v3 } from "@govtechsg/open-attestation"; -import { Hash, OpenAttestationContract } from "../../types/core"; - -// Return issued status given a smart contract instance (documentStore/tokenRegistry) -export const isIssuedOnTokenRegistry = async (smartContract: OpenAttestationContract, hash: Hash): Promise => { - const owner = await smartContract.instance.functions.ownerOf(hash); - return !(owner === constants.AddressZero); -}; - -export const isIssuedOnDocumentStore = async (smartContract: OpenAttestationContract, hash: Hash): Promise => { - return smartContract.instance.functions.isIssued(hash); -}; - -export const isIssued = (smartContract: OpenAttestationContract, hash: Hash) => { - switch (smartContract.type) { - case v3.Method.TokenRegistry: - return isIssuedOnTokenRegistry(smartContract, hash); - case v3.Method.DocumentStore: - return isIssuedOnDocumentStore(smartContract, hash); - default: - throw new Error("Smart contract type not supported"); - } -}; diff --git a/src/verifiers/documentStoreIssued/openAttestationEthereumDocumentStoreIssued.test.ts b/src/verifiers/documentStoreIssued/openAttestationEthereumDocumentStoreIssued.test.ts index 4f291b32..20f0807d 100644 --- a/src/verifiers/documentStoreIssued/openAttestationEthereumDocumentStoreIssued.test.ts +++ b/src/verifiers/documentStoreIssued/openAttestationEthereumDocumentStoreIssued.test.ts @@ -11,6 +11,7 @@ import { documentRopstenNotIssued } from "../../../test/fixtures/v3/documentRops import { documentRopstenNotIssuedWithTokenRegistry } from "../../../test/fixtures/v2/documentRopstenNotIssuedWithTokenRegistry"; describe("openAttestationEthereumDocumentStoreIssued", () => { + // TODO create a verifier and call it to test this => check dns verifier test describe("test", () => { it("should return true when v2 document has at least one certificate store", () => { const test = openAttestationEthereumDocumentStoreIssued.test(documentRopstenNotIssuedWithCertificateStore, { @@ -44,6 +45,180 @@ describe("openAttestationEthereumDocumentStoreIssued", () => { }); }); describe("v2", () => { + it("should return an invalid fragment when document has certificate store that does not exist", async () => { + const fragment = await openAttestationEthereumDocumentStoreIssued.verify( + { + ...documentRopstenNotIssuedWithCertificateStore, + data: { + ...documentRopstenNotIssuedWithCertificateStore.data, + issuers: [ + { + ...documentRopstenNotIssuedWithCertificateStore.data.issuers[0], + certificateStore: + "60a8bb36-ab89-4dec-be0e-575b5c59141c:string:0x0000000000000000000000000000000000000000" + } + ] + } + }, + { + network: "ropsten" + } + ); + expect(fragment).toStrictEqual({ + name: "OpenAttestationEthereumDocumentStoreIssued", + type: "DOCUMENT_STATUS", + data: { + details: [ + { + address: "0x0000000000000000000000000000000000000000", + issued: false, + reason: { + code: 404, + codeString: "CONTRACT_NOT_FOUND", + message: "Contract 0x0000000000000000000000000000000000000000 was not found" + } + } + ], + issuedOnAll: false + }, + reason: { + code: 404, + codeString: "CONTRACT_NOT_FOUND", + message: "Contract 0x0000000000000000000000000000000000000000 was not found" + }, + status: "INVALID" + }); + }); + it("should return an invalid fragment when document has invalid certificate store", async () => { + const fragment = await openAttestationEthereumDocumentStoreIssued.verify( + { + ...documentRopstenNotIssuedWithCertificateStore, + data: { + ...documentRopstenNotIssuedWithCertificateStore.data, + issuers: [ + { + ...documentRopstenNotIssuedWithCertificateStore.data.issuers[0], + certificateStore: "60a8bb36-ab89-4dec-be0e-575b5c59141c:string:0xabcd" + } + ] + } + }, + { + network: "ropsten" + } + ); + expect(fragment).toStrictEqual({ + name: "OpenAttestationEthereumDocumentStoreIssued", + type: "DOCUMENT_STATUS", + data: { + details: [ + { + address: "0xabcd", + issued: false, + reason: { + code: 2, + codeString: "CONTRACT_ADDRESS_INVALID", + message: "Contract address 0xabcd is invalid" + } + } + ], + issuedOnAll: false + }, + reason: { + code: 2, + codeString: "CONTRACT_ADDRESS_INVALID", + message: "Contract address 0xabcd is invalid" + }, + status: "INVALID" + }); + }); + it("should return an invalid fragment when document has invalid ens name certificate store", async () => { + const fragment = await openAttestationEthereumDocumentStoreIssued.verify( + { + ...documentRopstenNotIssuedWithCertificateStore, + data: { + ...documentRopstenNotIssuedWithCertificateStore.data, + issuers: [ + { + ...documentRopstenNotIssuedWithCertificateStore.data.issuers[0], + certificateStore: "60a8bb36-ab89-4dec-be0e-575b5c59141c:string:0xomgthisisnotgood" + } + ] + } + }, + { + network: "ropsten" + } + ); + expect(fragment).toStrictEqual({ + name: "OpenAttestationEthereumDocumentStoreIssued", + type: "DOCUMENT_STATUS", + data: { + details: [ + { + address: "0xomgthisisnotgood", + issued: false, + reason: { + code: 2, + codeString: "CONTRACT_ADDRESS_INVALID", + message: "Contract address 0xomgthisisnotgood is invalid" + } + } + ], + issuedOnAll: false + }, + reason: { + code: 2, + codeString: "CONTRACT_ADDRESS_INVALID", + message: "Contract address 0xomgthisisnotgood is invalid" + }, + status: "INVALID" + }); + }); + it("should return an invalid fragment when document has invalid certificate store with bad checksum", async () => { + const fragment = await openAttestationEthereumDocumentStoreIssued.verify( + { + ...documentRopstenNotIssuedWithCertificateStore, + data: { + ...documentRopstenNotIssuedWithCertificateStore.data, + issuers: [ + { + ...documentRopstenNotIssuedWithCertificateStore.data.issuers[0], + certificateStore: + "60a8bb36-ab89-4dec-be0e-575b5c59141c:string:0x8Fc57204c35fb9317D91285eF52D6b892EC08cd3" // replaced last D by d + } + ] + } + }, + { + network: "ropsten" + } + ); + expect(fragment).toStrictEqual({ + name: "OpenAttestationEthereumDocumentStoreIssued", + type: "DOCUMENT_STATUS", + data: { + details: [ + { + address: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cd3", + issued: false, + reason: { + code: 2, + codeString: "CONTRACT_ADDRESS_INVALID", + message: "Contract address 0x8Fc57204c35fb9317D91285eF52D6b892EC08cd3 is invalid" + } + } + ], + issuedOnAll: false + }, + reason: { + code: 2, + codeString: "CONTRACT_ADDRESS_INVALID", + message: "Contract address 0x8Fc57204c35fb9317D91285eF52D6b892EC08cd3 is invalid" + }, + status: "INVALID" + }); + }); it("should return an invalid fragment when document with certificate store has not been issued", async () => { const fragment = await openAttestationEthereumDocumentStoreIssued.verify( documentRopstenNotIssuedWithCertificateStore, @@ -58,12 +233,23 @@ describe("openAttestationEthereumDocumentStoreIssued", () => { details: [ { address: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3", - issued: false + issued: false, + reason: { + code: 1, + codeString: "DOCUMENT_NOT_ISSUED", + message: + "Certificate 0x2e97b28b1cb7ca50179af42f1f5581591251a2d93dd6dac75fafc8a69077f4ed has not been issued under contract 0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3" + } } ], issuedOnAll: false }, - message: "Certificate has not been issued", + reason: { + code: 1, + codeString: "DOCUMENT_NOT_ISSUED", + message: + "Certificate 0x2e97b28b1cb7ca50179af42f1f5581591251a2d93dd6dac75fafc8a69077f4ed has not been issued under contract 0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3" + }, status: "INVALID" }); }); @@ -81,12 +267,23 @@ describe("openAttestationEthereumDocumentStoreIssued", () => { details: [ { address: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3", - issued: false + issued: false, + reason: { + code: 1, + codeString: "DOCUMENT_NOT_ISSUED", + message: + "Certificate 0xda7a25d51e62bc50e1c7cfa17f7be0e5df3428b96f584e5d021f0cd8da97306d has not been issued under contract 0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3" + } } ], issuedOnAll: false }, - message: "Certificate has not been issued", + reason: { + code: 1, + codeString: "DOCUMENT_NOT_ISSUED", + message: + "Certificate 0xda7a25d51e62bc50e1c7cfa17f7be0e5df3428b96f584e5d021f0cd8da97306d has not been issued under contract 0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3" + }, status: "INVALID" }); }); @@ -134,7 +331,7 @@ describe("openAttestationEthereumDocumentStoreIssued", () => { status: "VALID" }); }); - it("should return an error fragment when document mixes document store and other verifier method", async () => { + it("should return an invalid fragment when document mixes document store and other verifier method", async () => { const fragment = await openAttestationEthereumDocumentStoreIssued.verify( { ...v2documentRopstenValidWithDocumentStore, @@ -156,13 +353,69 @@ describe("openAttestationEthereumDocumentStoreIssued", () => { expect(fragment).toStrictEqual({ name: "OpenAttestationEthereumDocumentStoreIssued", type: "DOCUMENT_STATUS", - data: new Error(`No document store for issuer "Other Issuer"`), - message: `No document store for issuer "Other Issuer"`, - status: "ERROR" + data: { + details: [ + { + address: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3", + issued: true + }, + { + address: "", + issued: false, + reason: { + code: 2, + codeString: "CONTRACT_ADDRESS_INVALID", + message: "Contract address is invalid" + } + } + ], + issuedOnAll: false + }, + reason: { code: 2, codeString: "CONTRACT_ADDRESS_INVALID", message: "Contract address is invalid" }, + status: "INVALID" }); }); }); describe("v3", () => { + it("should return an invalid fragment when document has document store that does not exist", async () => { + const fragment = await openAttestationEthereumDocumentStoreIssued.verify( + { + ...documentRopstenNotIssued, + data: { + ...documentRopstenNotIssued.data, + proof: { + ...documentRopstenNotIssued.data.proof, + value: "0b9bbe75-8421-4e70-a176-cba76843216d:string:0x0000000000000000000000000000000000000000" + } + } + }, + { + network: "ropsten" + } + ); + expect(fragment).toStrictEqual({ + name: "OpenAttestationEthereumDocumentStoreIssued", + type: "DOCUMENT_STATUS", + data: { + details: { + address: "0x0000000000000000000000000000000000000000", + issued: false, + reason: { + code: 404, + codeString: "CONTRACT_NOT_FOUND", + message: "Contract 0x0000000000000000000000000000000000000000 was not found" + } + }, + issuedOnAll: false + }, + reason: { + code: 404, + codeString: "CONTRACT_NOT_FOUND", + message: "Contract 0x0000000000000000000000000000000000000000 was not found" + }, + status: "INVALID" + }); + }); it("should return an invalid fragment when document with document store has not been issued", async () => { const fragment = await openAttestationEthereumDocumentStoreIssued.verify(documentRopstenNotIssued, { network: "ropsten" @@ -171,15 +424,24 @@ describe("openAttestationEthereumDocumentStoreIssued", () => { name: "OpenAttestationEthereumDocumentStoreIssued", type: "DOCUMENT_STATUS", data: { - details: [ - { - address: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3", - issued: false + details: { + address: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3", + issued: false, + reason: { + code: 1, + codeString: "DOCUMENT_NOT_ISSUED", + message: + "Certificate 0x76cb959f49db0cffc05107af4a3ecef14092fd445d9acb0c2e7e27908d262142 has not been issued under contract 0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3" } - ], + }, issuedOnAll: false }, - message: "Certificate has not been issued", + reason: { + code: 1, + codeString: "DOCUMENT_NOT_ISSUED", + message: + "Certificate 0x76cb959f49db0cffc05107af4a3ecef14092fd445d9acb0c2e7e27908d262142 has not been issued under contract 0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3" + }, status: "INVALID" }); }); @@ -191,12 +453,10 @@ describe("openAttestationEthereumDocumentStoreIssued", () => { name: "OpenAttestationEthereumDocumentStoreIssued", type: "DOCUMENT_STATUS", data: { - details: [ - { - address: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3", - issued: true - } - ], + details: { + address: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3", + issued: true + }, issuedOnAll: true }, status: "VALID" diff --git a/src/verifiers/documentStoreIssued/openAttestationEthereumDocumentStoreIssued.ts b/src/verifiers/documentStoreIssued/openAttestationEthereumDocumentStoreIssued.ts index 83105d14..39402d38 100644 --- a/src/verifiers/documentStoreIssued/openAttestationEthereumDocumentStoreIssued.ts +++ b/src/verifiers/documentStoreIssued/openAttestationEthereumDocumentStoreIssued.ts @@ -1,8 +1,18 @@ import { getData, v2, v3, WrappedDocument } from "@govtechsg/open-attestation"; import { isWrappedV3Document, VerificationFragmentType, Verifier } from "../../types/core"; -import { getDocumentStoreSmartContract } from "../../common/smartContract/documentToSmartContracts"; -import { verifyIssued } from "./verify"; +import { OpenAttestationEthereumDocumentStoreIssuedCode } from "../../types/error"; +import { + createDocumentStoreContract, + getIssuersDocumentStore, + isIssuedOnDocumentStore +} from "../../common/smartContract/documentStoreContractInterface"; +import { contractNotIssued, getErrorReason } from "../../common/smartContract/documentStoreErrors"; +interface Status { + issued: boolean; + address: string; + reason?: any; +} const name = "OpenAttestationEthereumDocumentStoreIssued"; const type: VerificationFragmentType = "DOCUMENT_STATUS"; export const openAttestationEthereumDocumentStoreIssued: Verifier< @@ -13,7 +23,12 @@ export const openAttestationEthereumDocumentStoreIssued: Verifier< status: "SKIPPED", type, name, - message: `Document issuers doesn't have "documentStore" or "certificateStore" property or ${v3.Method.DocumentStore} method` + reason: { + code: OpenAttestationEthereumDocumentStoreIssuedCode.SKIPPED, + codeString: + OpenAttestationEthereumDocumentStoreIssuedCode[OpenAttestationEthereumDocumentStoreIssuedCode.SKIPPED], + message: `Document issuers doesn't have "documentStore" or "certificateStore" property or ${v3.Method.DocumentStore} method` + } }); }, test: document => { @@ -26,21 +41,40 @@ export const openAttestationEthereumDocumentStoreIssued: Verifier< }, verify: async (document, options) => { try { - const smartContracts = getDocumentStoreSmartContract(document, options); - const status = await verifyIssued(document, smartContracts); - if (!status.issuedOnAll) { + const documentStores = getIssuersDocumentStore(document); + const merkleRoot = `0x${document.signature.merkleRoot}`; + const statuses: Status[] = await Promise.all( + documentStores.map(async documentStore => { + try { + const contract = createDocumentStoreContract(documentStore, options); + const issued = await isIssuedOnDocumentStore(contract, merkleRoot); + const status: Status = { + issued, + address: documentStore + }; + if (!issued) { + status.reason = contractNotIssued(merkleRoot, documentStore); + } + return status; + } catch (e) { + return { issued: false, address: documentStore, reason: getErrorReason(e, documentStore) }; + } + }) + ); + const notIssued = statuses.find(status => !status.issued); + if (notIssued) { return { name, type, - data: status, - message: "Certificate has not been issued", + data: { issuedOnAll: false, details: isWrappedV3Document(document) ? statuses[0] : statuses }, + reason: notIssued.reason, status: "INVALID" }; } return { name, type, - data: status, + data: { issuedOnAll: true, details: isWrappedV3Document(document) ? statuses[0] : statuses }, status: "VALID" }; } catch (e) { @@ -48,7 +82,14 @@ export const openAttestationEthereumDocumentStoreIssued: Verifier< name, type, data: e, - message: e.message, + reason: { + message: e.message, + code: OpenAttestationEthereumDocumentStoreIssuedCode.UNEXPECTED_ERROR, + codeString: + OpenAttestationEthereumDocumentStoreIssuedCode[ + OpenAttestationEthereumDocumentStoreIssuedCode.UNEXPECTED_ERROR + ] + }, status: "ERROR" }; } diff --git a/src/verifiers/documentStoreIssued/verify.test.ts b/src/verifiers/documentStoreIssued/verify.test.ts deleted file mode 100644 index eb2307c5..00000000 --- a/src/verifiers/documentStoreIssued/verify.test.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { Contract } from "ethers"; -import { v3 } from "@govtechsg/open-attestation"; -import { isIssuedOnAll, issuedStatusOnContracts, verifyIssued } from "./verify"; -import { isIssued } from "./contractInterface"; -import { OpenAttestationContract } from "../../types/core"; - -jest.mock("./contractInterface"); - -beforeEach(() => { - // @ts-ignore - isIssued.mockClear(); -}); - -// @ts-ignore force contract creation -const contract: Contract = {}; - -describe("issuedStatusOnContracts", () => { - it("returns issued status on all smart contracts provided", async () => { - // @ts-ignore - isIssued.mockResolvedValueOnce(true); - // @ts-ignore - isIssued.mockResolvedValueOnce(false); - const smartContracts: OpenAttestationContract[] = [ - { address: "0x0A", type: v3.Method.DocumentStore, instance: contract }, - { address: "0x0B", type: v3.Method.DocumentStore, instance: contract } - ]; - const issuedStatus = await issuedStatusOnContracts(smartContracts, "HASH"); - expect(issuedStatus).toEqual([ - { address: "0x0A", issued: true }, - { address: "0x0B", issued: false } - ]); - }); - - it("throws if any smart contract call failed", async () => { - // @ts-ignore - isIssued.mockResolvedValueOnce(true); - // @ts-ignore - isIssued.mockRejectedValueOnce(new Error("Some failure")); - const smartContracts: OpenAttestationContract[] = [ - { address: "0x0A", type: v3.Method.DocumentStore, instance: contract }, - { address: "0x0B", type: v3.Method.DocumentStore, instance: contract } - ]; - - const issuedStatus = await issuedStatusOnContracts(smartContracts, "HASH"); - expect(issuedStatus).toEqual([ - { address: "0x0A", issued: true }, - { address: "0x0B", issued: false, error: "Some failure" } - ]); - }); -}); - -describe("isIssuedOnAll", () => { - it("returns true if all the smart contract's issued status is true", () => { - const status = [ - { address: "0x0A", issued: true }, - { address: "0x0B", issued: true } - ]; - expect(isIssuedOnAll(status)).toBe(true); - }); - - it("returns false if no smart contract is present", () => { - expect(isIssuedOnAll([])).toBe(false); - }); - - it("returns false if any issued status is false", () => { - const contractWithStatus = [ - { address: "0x0A", issued: true }, - { address: "0x0B", issued: false } - ]; - expect(isIssuedOnAll(contractWithStatus)).toBe(false); - }); -}); - -describe("verifyIssued", () => { - it("returns valid summary of the status if document is issued on all smart contracts", async () => { - // @ts-ignore - isIssued.mockResolvedValueOnce(true); - // @ts-ignore - isIssued.mockResolvedValueOnce(true); - const smartContracts: OpenAttestationContract[] = [ - { address: "0x0A", type: v3.Method.DocumentStore, instance: contract }, - { address: "0x0B", type: v3.Method.DocumentStore, instance: contract } - ]; - const summary = await verifyIssued( - { - version: "version", - schema: "schema", - data: { - issuers: [] - }, - signature: { - merkleRoot: "MERKLE_ROOT", - type: "SHA3MerkleProof", - targetHash: "", - proof: [] - } - }, - smartContracts - ); - expect(summary).toEqual({ - issuedOnAll: true, - details: [ - { address: "0x0A", issued: true }, - { address: "0x0B", issued: true } - ] - }); - // @ts-ignore - expect(isIssued.mock.calls).toEqual([ - [{ address: "0x0A", type: v3.Method.DocumentStore, instance: contract }, "0xMERKLE_ROOT"], - [{ address: "0x0B", type: v3.Method.DocumentStore, instance: contract }, "0xMERKLE_ROOT"] - ]); - }); - - it("returns invalid summary of the status if document is not issued on all smart contracts", async () => { - // @ts-ignore - isIssued.mockResolvedValueOnce(true); - // @ts-ignore - isIssued.mockResolvedValueOnce(false); - const smartContracts: OpenAttestationContract[] = [ - { address: "0x0A", type: v3.Method.DocumentStore, instance: contract }, - { address: "0x0B", type: v3.Method.DocumentStore, instance: contract } - ]; - const summary = await verifyIssued( - { - version: "version", - schema: "schema", - data: { issuers: [] }, - signature: { - merkleRoot: "MERKLE_ROOT", - type: "SHA3MerkleProof", - targetHash: "", - proof: [] - } - }, - smartContracts - ); - expect(summary).toEqual({ - issuedOnAll: false, - details: [ - { address: "0x0A", issued: true }, - { address: "0x0B", issued: false } - ] - }); - // @ts-ignore - expect(isIssued.mock.calls).toEqual([ - [{ address: "0x0A", type: v3.Method.DocumentStore, instance: contract }, "0xMERKLE_ROOT"], - [{ address: "0x0B", type: v3.Method.DocumentStore, instance: contract }, "0xMERKLE_ROOT"] - ]); - }); -}); diff --git a/src/verifiers/documentStoreIssued/verify.ts b/src/verifiers/documentStoreIssued/verify.ts deleted file mode 100644 index d44d99b7..00000000 --- a/src/verifiers/documentStoreIssued/verify.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { WrappedDocument, v2, v3 } from "@govtechsg/open-attestation"; -import { isIssued } from "./contractInterface"; -import { Hash, OpenAttestationContract } from "../../types/core"; - -export const issuedStatusOnContracts = async (smartContracts: OpenAttestationContract[] = [], hash: Hash) => { - const issueStatusesDeferred = smartContracts.map(smartContract => - isIssued(smartContract, hash) - .then(issued => ({ - address: smartContract.address, - issued - })) - .catch(e => ({ - address: smartContract.address, - issued: false, - error: e.message || e - })) - ); - return Promise.all(issueStatusesDeferred); -}; - -export const isIssuedOnAll = (statuses: { address: Hash; issued: boolean }[]) => { - if (!statuses || statuses.length === 0) return false; - return statuses.every(status => status.issued); -}; - -export const verifyIssued = async ( - document: WrappedDocument, - smartContracts: OpenAttestationContract[] = [] -) => { - const hash = `0x${document.signature.merkleRoot}`; - const details = await issuedStatusOnContracts(smartContracts, hash); - const issuedOnAll = isIssuedOnAll(details); - - return { - issuedOnAll, - details - }; -}; diff --git a/src/verifiers/documentStoreRevoked/contractInterface.integration.test.ts b/src/verifiers/documentStoreRevoked/contractInterface.integration.test.ts deleted file mode 100644 index 045f7ec3..00000000 --- a/src/verifiers/documentStoreRevoked/contractInterface.integration.test.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { constants } from "ethers"; - -import { isRevoked, isRevokedOnDocumentStore, isRevokedOnTokenRegistry } from "./contractInterface"; -import { - getDocumentStoreSmartContract, - getTokenRegistrySmartContract -} from "../../common/smartContract/documentToSmartContracts"; -import { documentRopstenRevokedWithToken } from "../../../test/fixtures/v2/documentRopstenRevokedWithToken"; -import { documentRopstenRevokedWithDocumentStore } from "../../../test/fixtures/v2/documentRopstenRevokedWithDocumentStore"; -import { documentRopstenWithNonDeployedDocumentStore } from "../../../test/fixtures/v2/documentRopstenWithNonDeployedDocumentStore"; -import { documentRopstenValidWithToken } from "../../../test/fixtures/v2/documentRopstenValidWithToken"; -import { documentRopstenValidWithCertificateStore } from "../../../test/fixtures/v2/documentRopstenValidWithCertificateStore"; - -const TOKEN_WITH_OWNER = "0x30cc3db1f2b26e25d63a67b6f232c4cf2acd1402f632847a4857e73516a0762f"; -const TOKEN_WITH_SMART_CONTRACT = "0x30cc3db1f2b26e25d63a67b6f232c4cf2acd1402f632847a4857e73516a0762e"; -const TOKEN_UNMINTED = constants.AddressZero; - -describe("isRevokedOnTokenRegistry", () => { - it("returns false if token has valid owner", async () => { - const smartContract = getTokenRegistrySmartContract(documentRopstenRevokedWithToken, { network: "ropsten" }); - const issued = await isRevokedOnTokenRegistry(smartContract[0], TOKEN_WITH_OWNER); - expect(issued).toBe(false); - }); - - it("returns true if owner of token is the smart contract itself", async () => { - const smartContract = getTokenRegistrySmartContract(documentRopstenRevokedWithToken, { network: "ropsten" }); - const issued = await isRevokedOnTokenRegistry(smartContract[0], TOKEN_WITH_SMART_CONTRACT); - expect(issued).toBe(true); - }); - - it("allow errors to bubble if token is not minted", async () => { - const smartContract = getTokenRegistrySmartContract(documentRopstenRevokedWithToken, { network: "ropsten" }); - await expect(isRevokedOnTokenRegistry(smartContract[0], TOKEN_UNMINTED)).rejects.toThrow( - 'call revert exception (address="0x48399Fb88bcD031C556F53e93F690EEC07963Af3", args=["0x0000000000000000000000000000000000000000"], method="ownerOf(uint256)", errorSignature="Error(string)", errorArgs=[["ERC721: owner query for nonexistent token"]], reason=["ERC721: owner query for nonexistent token"], transaction={"to":{},"data":"0x6352211e0000000000000000000000000000000000000000000000000000000000000000"}, version=4.0.40)' - ); - }); -}); - -const DOCUMENT_REVOKED = "0x3d29524b18c3efe1cbad07e1ba9aa80c496cbf0b6255d6f331ca9b540e17e452"; -const DOCUMENT_UNREVOKED = "0x3d29524b18c3efe1cbad07e1ba9aa80c496cbf0b6255d6f331ca9b540e17e453"; - -describe("isRevokedOnDocumentStore", () => { - it("returns true if document is revoked on documentStore", async () => { - const smartContract = getDocumentStoreSmartContract(documentRopstenRevokedWithDocumentStore, { - network: "ropsten" - }); - const revoked = await isRevokedOnDocumentStore(smartContract[0], DOCUMENT_REVOKED); - expect(revoked).toBe(true); - }); - - it("returns false if document is not issued on documentStore", async () => { - const smartContract = getDocumentStoreSmartContract(documentRopstenRevokedWithDocumentStore, { - network: "ropsten" - }); - const issued = await isRevokedOnDocumentStore(smartContract[0], DOCUMENT_UNREVOKED); - expect(issued).toBe(false); - }); - - it("allows error to bubble up if documentStore is not deployed", async () => { - const smartContract = getDocumentStoreSmartContract(documentRopstenWithNonDeployedDocumentStore, { - network: "ropsten" - }); - await expect(isRevokedOnDocumentStore(smartContract[0], DOCUMENT_UNREVOKED)).rejects.toThrow( - 'contract not deployed (contractAddress="0x0000000000000000000000000000000000000000", operation="getDeployed", version=4.0.40)' - ); - }); -}); - -describe("isRevoked", () => { - it("works for tokenRegistry", async () => { - const smartContract = getTokenRegistrySmartContract(documentRopstenValidWithToken, { network: "ropsten" }); - const issued = await isRevoked( - smartContract[0], - "0x1b2c07f3d77078b44e65eae4c7f5d17fefaf0f73fb3f338fdb410912a8c4c4b7" - ); - expect(issued).toBe(false); - }); - - it("works for documentStore", async () => { - const smartContract = getDocumentStoreSmartContract(documentRopstenValidWithCertificateStore, { - network: "ropsten" - }); - const revoked = await isRevoked(smartContract[0], DOCUMENT_REVOKED); - expect(revoked).toBe(true); - }); -}); diff --git a/src/verifiers/documentStoreRevoked/contractInterface.ts b/src/verifiers/documentStoreRevoked/contractInterface.ts deleted file mode 100644 index 9ed8147a..00000000 --- a/src/verifiers/documentStoreRevoked/contractInterface.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { constants } from "ethers"; -import { v3 } from "@govtechsg/open-attestation"; -import { Hash, OpenAttestationContract } from "../../types/core"; - -export const isRevokedOnTokenRegistry = async ( - smartContract: OpenAttestationContract, - hash: Hash -): Promise => { - const owner = await smartContract.instance.functions.ownerOf(hash); - return owner === constants.AddressZero || owner === smartContract.address; -}; - -export const isRevokedOnDocumentStore = async ( - smartContract: OpenAttestationContract, - hash: Hash -): Promise => { - return smartContract.instance.functions.isRevoked(hash); -}; - -export const isRevoked = (smartContract: OpenAttestationContract, hash: Hash) => { - switch (smartContract.type) { - case v3.Method.TokenRegistry: - return isRevokedOnTokenRegistry(smartContract, hash); - case v3.Method.DocumentStore: - return isRevokedOnDocumentStore(smartContract, hash); - default: - throw new Error("Smart contract type not supported"); - } -}; diff --git a/src/verifiers/documentStoreRevoked/openAttestationEthereumDocumentStoreRevoked.test.ts b/src/verifiers/documentStoreRevoked/openAttestationEthereumDocumentStoreRevoked.test.ts index eee616ef..21cca19f 100644 --- a/src/verifiers/documentStoreRevoked/openAttestationEthereumDocumentStoreRevoked.test.ts +++ b/src/verifiers/documentStoreRevoked/openAttestationEthereumDocumentStoreRevoked.test.ts @@ -12,6 +12,7 @@ import { documentRopstenValidWithDocumentStore as v2documentRopstenValidWithDocu import { documentRopstenNotIssuedWithTokenRegistry } from "../../../test/fixtures/v2/documentRopstenNotIssuedWithTokenRegistry"; describe("openAttestationEthereumDocumentStoreRevoked", () => { + // TODO create a verifier and call it to test this => check dns verifier test describe("test", () => { it("should return true when v2 document has at least one certificate store", () => { const test = openAttestationEthereumDocumentStoreRevoked.test(documentRopstenNotIssuedWithCertificateStore, { @@ -45,6 +46,92 @@ describe("openAttestationEthereumDocumentStoreRevoked", () => { }); }); describe("v2", () => { + it("should return an invalid fragment when document store is invalid", async () => { + const fragment = await openAttestationEthereumDocumentStoreRevoked.verify( + { + ...documentRopstenRevokedWithDocumentStore, + data: { + ...documentRopstenRevokedWithDocumentStore.data, + issuers: [ + { + ...documentRopstenRevokedWithDocumentStore.data.issuers[0], + documentStore: "0c837c55-4948-4a5a-9ed3-801889db9ce3:string:0xabcd" + } + ] + } + }, + { + network: "ropsten" + } + ); + expect(fragment).toStrictEqual({ + name: "OpenAttestationEthereumDocumentStoreRevoked", + type: "DOCUMENT_STATUS", + data: { + details: [ + { + address: "0xabcd", + revoked: true, + reason: { + code: 2, + codeString: "CONTRACT_ADDRESS_INVALID", + message: "Contract address 0xabcd is invalid" + } + } + ], + revokedOnAny: true + }, + reason: { + code: 2, + codeString: "CONTRACT_ADDRESS_INVALID", + message: "Contract address 0xabcd is invalid" + }, + status: "INVALID" + }); + }); + it("should return an invalid fragment when document store does not exists", async () => { + const fragment = await openAttestationEthereumDocumentStoreRevoked.verify( + { + ...documentRopstenRevokedWithDocumentStore, + data: { + ...documentRopstenRevokedWithDocumentStore.data, + issuers: [ + { + ...documentRopstenRevokedWithDocumentStore.data.issuers[0], + documentStore: "0c837c55-4948-4a5a-9ed3-801889db9ce3:string:0x0000000000000000000000000000000000000000" + } + ] + } + }, + { + network: "ropsten" + } + ); + expect(fragment).toStrictEqual({ + name: "OpenAttestationEthereumDocumentStoreRevoked", + type: "DOCUMENT_STATUS", + data: { + details: [ + { + address: "0x0000000000000000000000000000000000000000", + revoked: true, + reason: { + code: 404, + codeString: "CONTRACT_NOT_FOUND", + message: "Contract 0x0000000000000000000000000000000000000000 was not found" + } + } + ], + revokedOnAny: true + }, + reason: { + code: 404, + codeString: "CONTRACT_NOT_FOUND", + message: "Contract 0x0000000000000000000000000000000000000000 was not found" + }, + status: "INVALID" + }); + }); it("should return an invalid fragment when document with document store has been revoked", async () => { const fragment = await openAttestationEthereumDocumentStoreRevoked.verify( documentRopstenRevokedWithDocumentStore, @@ -59,12 +146,23 @@ describe("openAttestationEthereumDocumentStoreRevoked", () => { details: [ { address: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3", - revoked: true + revoked: true, + reason: { + code: 1, + codeString: "DOCUMENT_REVOKED", + message: + "Certificate 0x3d29524b18c3efe1cbad07e1ba9aa80c496cbf0b6255d6f331ca9b540e17e452 has been revoked under contract 0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3" + } } ], revokedOnAny: true }, - message: "Certificate has been revoked", + reason: { + code: 1, + codeString: "DOCUMENT_REVOKED", + message: + "Certificate 0x3d29524b18c3efe1cbad07e1ba9aa80c496cbf0b6255d6f331ca9b540e17e452 has been revoked under contract 0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3" + }, status: "INVALID" }); }); @@ -82,12 +180,23 @@ describe("openAttestationEthereumDocumentStoreRevoked", () => { details: [ { address: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3", - revoked: true + revoked: true, + reason: { + code: 1, + codeString: "DOCUMENT_REVOKED", + message: + "Certificate 0xa874e4c79b27ddd3701984aaff9bc8bd30248f3214401d53ff238286900204a6 has been revoked under contract 0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3" + } } ], revokedOnAny: true }, - message: "Certificate has been revoked", + reason: { + code: 1, + codeString: "DOCUMENT_REVOKED", + message: + "Certificate 0xa874e4c79b27ddd3701984aaff9bc8bd30248f3214401d53ff238286900204a6 has been revoked under contract 0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3" + }, status: "INVALID" }); }); @@ -157,9 +266,31 @@ describe("openAttestationEthereumDocumentStoreRevoked", () => { expect(fragment).toStrictEqual({ name: "OpenAttestationEthereumDocumentStoreRevoked", type: "DOCUMENT_STATUS", - data: new Error(`No document store for issuer "Foo Issuer"`), - message: `No document store for issuer "Foo Issuer"`, - status: "ERROR" + data: { + details: [ + { + address: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3", + revoked: false + }, + { + address: "", + reason: { + code: 2, + codeString: "CONTRACT_ADDRESS_INVALID", + message: "Contract address is invalid" + }, + revoked: true + } + ], + revokedOnAny: true + }, + + reason: { + code: 2, + codeString: "CONTRACT_ADDRESS_INVALID", + message: "Contract address is invalid" + }, + status: "INVALID" }); }); }); @@ -172,15 +303,24 @@ describe("openAttestationEthereumDocumentStoreRevoked", () => { name: "OpenAttestationEthereumDocumentStoreRevoked", type: "DOCUMENT_STATUS", data: { - details: [ - { - address: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3", - revoked: true + details: { + address: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3", + revoked: true, + reason: { + code: 1, + codeString: "DOCUMENT_REVOKED", + message: + "Certificate 0xba106f273697b46862f5842fc805902fa65d1f41d50953e0aeb815e43e989fc1 has been revoked under contract 0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3" } - ], + }, revokedOnAny: true }, - message: "Certificate has been revoked", + reason: { + code: 1, + codeString: "DOCUMENT_REVOKED", + message: + "Certificate 0xba106f273697b46862f5842fc805902fa65d1f41d50953e0aeb815e43e989fc1 has been revoked under contract 0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3" + }, status: "INVALID" }); }); @@ -196,12 +336,10 @@ describe("openAttestationEthereumDocumentStoreRevoked", () => { name: "OpenAttestationEthereumDocumentStoreRevoked", type: "DOCUMENT_STATUS", data: { - details: [ - { - address: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3", - revoked: false - } - ], + details: { + address: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3", + revoked: false + }, revokedOnAny: false }, status: "VALID" diff --git a/src/verifiers/documentStoreRevoked/openAttestationEthereumDocumentStoreRevoked.ts b/src/verifiers/documentStoreRevoked/openAttestationEthereumDocumentStoreRevoked.ts index e8a3211e..cc123daa 100644 --- a/src/verifiers/documentStoreRevoked/openAttestationEthereumDocumentStoreRevoked.ts +++ b/src/verifiers/documentStoreRevoked/openAttestationEthereumDocumentStoreRevoked.ts @@ -1,7 +1,38 @@ -import { getData, v2, v3, WrappedDocument } from "@govtechsg/open-attestation"; -import { isWrappedV3Document, VerificationFragmentType, Verifier } from "../../types/core"; -import { getDocumentStoreSmartContract } from "../../common/smartContract/documentToSmartContracts"; -import { verifyRevoked } from "./verify"; +import { getData, utils, v2, v3, WrappedDocument } from "@govtechsg/open-attestation"; +import { Contract } from "ethers"; +import { Hash, isWrappedV3Document, VerificationFragmentType, Verifier } from "../../types/core"; +import { OpenAttestationEthereumDocumentStoreRevokedCode } from "../../types/error"; +import { + createDocumentStoreContract, + getIssuersDocumentStore, + isRevokedOnDocumentStore +} from "../../common/smartContract/documentStoreContractInterface"; +import { contractRevoked, getErrorReason } from "../../common/smartContract/documentStoreErrors"; + +interface Status { + revoked: boolean; + address: string; + reason?: any; +} + +const getIntermediateHashes = (targetHash: Hash, proofs: Hash[] = []) => { + const hashes = [`0x${targetHash}`]; + proofs.reduce((prev, curr) => { + const next = utils.combineHashString(prev, curr); + hashes.push(`0x${next}`); + return next; + }, targetHash); + return hashes; +}; + +// 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 => + isRevokedOnDocumentStore(smartContract, hash).then(status => (status ? hash : undefined)) + ); + const revokedStatuses = await Promise.all(revokedStatusDeferred); + return revokedStatuses.find(hash => hash); +}; const name = "OpenAttestationEthereumDocumentStoreRevoked"; const type: VerificationFragmentType = "DOCUMENT_STATUS"; @@ -13,7 +44,12 @@ export const openAttestationEthereumDocumentStoreRevoked: Verifier< status: "SKIPPED", type, name, - message: `Document issuers doesn't have "documentStore" or "certificateStore" property or ${v3.Method.DocumentStore} method` + reason: { + code: OpenAttestationEthereumDocumentStoreRevokedCode.SKIPPED, + codeString: + OpenAttestationEthereumDocumentStoreRevokedCode[OpenAttestationEthereumDocumentStoreRevokedCode.SKIPPED], + message: `Document issuers doesn't have "documentStore" or "certificateStore" property or ${v3.Method.DocumentStore} method` + } }); }, test: document => { @@ -26,21 +62,44 @@ export const openAttestationEthereumDocumentStoreRevoked: Verifier< }, verify: async (document, options) => { try { - const smartContracts = getDocumentStoreSmartContract(document, options); - const status = await verifyRevoked(document, smartContracts); - if (status.revokedOnAny) { + const documentStores = getIssuersDocumentStore(document); + const merkleRoot = `0x${document.signature.merkleRoot}`; + const { targetHash } = document.signature; + const proofs = document.signature.proof || []; + const statuses: Status[] = await Promise.all( + documentStores.map(async documentStore => { + try { + const contract = createDocumentStoreContract(documentStore, options); + const intermediateHashes = getIntermediateHashes(targetHash, proofs); + const revokedHash = await isAnyHashRevoked(contract, intermediateHashes); + + const status: Status = { + revoked: !!revokedHash, + address: documentStore + }; + if (revokedHash) { + status.reason = contractRevoked(merkleRoot, documentStore); + } + return status; + } catch (e) { + return { revoked: true, address: documentStore, reason: getErrorReason(e, documentStore) }; + } + }) + ); + const revoked = statuses.find(status => status.revoked); + if (revoked) { return { name, type, - data: status, - message: "Certificate has been revoked", + data: { revokedOnAny: true, details: isWrappedV3Document(document) ? statuses[0] : statuses }, + reason: revoked.reason, status: "INVALID" }; } return { name, type, - data: status, + data: { revokedOnAny: false, details: isWrappedV3Document(document) ? statuses[0] : statuses }, status: "VALID" }; } catch (e) { @@ -48,7 +107,14 @@ export const openAttestationEthereumDocumentStoreRevoked: Verifier< name, type, data: e, - message: e.message, + reason: { + message: e.message, + code: OpenAttestationEthereumDocumentStoreRevokedCode.UNEXPECTED_ERROR, + codeString: + OpenAttestationEthereumDocumentStoreRevokedCode[ + OpenAttestationEthereumDocumentStoreRevokedCode.UNEXPECTED_ERROR + ] + }, status: "ERROR" }; } diff --git a/src/verifiers/documentStoreRevoked/verify.test.ts b/src/verifiers/documentStoreRevoked/verify.test.ts deleted file mode 100644 index 073483aa..00000000 --- a/src/verifiers/documentStoreRevoked/verify.test.ts +++ /dev/null @@ -1,264 +0,0 @@ -import { Contract } from "ethers"; -import { v3 } from "@govtechsg/open-attestation"; -import { isRevoked } from "./contractInterface"; -import { - getIntermediateHashes, - isAnyHashRevokedOnStore, - isRevokedOnAny, - revokedStatusOnContracts, - verifyRevoked -} from "./verify"; -import { OpenAttestationContract } from "../../types/core"; - -jest.mock("./contractInterface"); - -beforeEach(() => { - // @ts-ignore - isRevoked.mockClear(); -}); - -// @ts-ignore force contract creation -const contract: Contract = "CONTRACT_INSTANCE"; - -const TOKEN_REGISTRY_CONTRACT = { - address: "0x0A", - type: v3.Method.TokenRegistry, - instance: contract -}; - -const DOCUMENT_STORE_CONTRACT = { - address: "0x0B", - type: v3.Method.DocumentStore, - instance: contract -}; - -const INTERMEDIATE_HASHES = ["0x0a", "0x0b", "0x0c"]; - -describe("isAnyHashRevokedOnStore", () => { - it("returns false if none of the hash are revoked", async () => { - // @ts-ignore - isRevoked.mockResolvedValue(false); - const revoked = await isAnyHashRevokedOnStore(TOKEN_REGISTRY_CONTRACT, INTERMEDIATE_HASHES); - expect(revoked).toBe(false); - }); - - it("returns true if any of the hashes is revoked", async () => { - // @ts-ignore - isRevoked.mockResolvedValueOnce(false); - // @ts-ignore - isRevoked.mockResolvedValueOnce(true); - // @ts-ignore - isRevoked.mockResolvedValueOnce(false); - const revoked = await isAnyHashRevokedOnStore(TOKEN_REGISTRY_CONTRACT, INTERMEDIATE_HASHES); - expect(revoked).toBe(true); - }); -}); - -describe("revokedStatusOnContracts", () => { - it("returns a mapping of smart contract to revoke status", async () => { - // @ts-ignore - isRevoked.mockResolvedValue(false); - const smartContracts = [TOKEN_REGISTRY_CONTRACT, DOCUMENT_STORE_CONTRACT]; - const revokedStatus = await revokedStatusOnContracts(smartContracts, INTERMEDIATE_HASHES); - expect(revokedStatus).toEqual([ - { address: "0x0A", revoked: false }, - { address: "0x0B", revoked: false } - ]); - // @ts-ignore - expect(isRevoked.mock.calls).toEqual([ - [ - { - address: "0x0A", - instance: "CONTRACT_INSTANCE", - type: "TOKEN_REGISTRY" - }, - "0x0a" - ], - [ - { - address: "0x0A", - instance: "CONTRACT_INSTANCE", - type: "TOKEN_REGISTRY" - }, - "0x0b" - ], - [ - { - address: "0x0A", - instance: "CONTRACT_INSTANCE", - type: "TOKEN_REGISTRY" - }, - "0x0c" - ], - [ - { - address: "0x0B", - instance: "CONTRACT_INSTANCE", - type: "DOCUMENT_STORE" - }, - "0x0a" - ], - [ - { - address: "0x0B", - instance: "CONTRACT_INSTANCE", - type: "DOCUMENT_STORE" - }, - "0x0b" - ], - [ - { - address: "0x0B", - instance: "CONTRACT_INSTANCE", - type: "DOCUMENT_STORE" - }, - "0x0c" - ] - ]); - }); - - it("should return empty array if no smart contract is provided", async () => { - const revokedStatus = await revokedStatusOnContracts([], INTERMEDIATE_HASHES); - expect(revokedStatus).toEqual([]); - }); - - it("results includes error if contract call fails", async () => { - // @ts-ignore - isRevoked.mockResolvedValue(false); - // @ts-ignore - isRevoked.mockRejectedValueOnce(new Error("Some error")); - const smartContracts = [TOKEN_REGISTRY_CONTRACT, DOCUMENT_STORE_CONTRACT]; - const revokedStatus = await revokedStatusOnContracts(smartContracts, INTERMEDIATE_HASHES); - expect(revokedStatus).toEqual([ - { address: "0x0A", revoked: true, error: "Some error" }, - { address: "0x0B", revoked: false } - ]); - }); -}); - -describe("isRevokedOnAny", () => { - it("returns false if all of the revoke status is false", () => { - const status = [ - { address: "0x0A", revoked: false }, - { address: "0x0B", revoked: false } - ]; - expect(isRevokedOnAny(status)).toBe(false); - }); - - it("returns true if any of the revoke status is true", () => { - const status = [ - { address: "0x0A", revoked: false }, - { address: "0x0B", revoked: true } - ]; - expect(isRevokedOnAny(status)).toBe(true); - }); -}); - -describe("getIntermediateHashes", () => { - it("returns array with single hash if no proof is present", () => { - const targetHash = "f7432b3219b2aa4122e289f44901830fa32f224ee9dfce28565677f1d279b2c7"; - const expected = ["0xf7432b3219b2aa4122e289f44901830fa32f224ee9dfce28565677f1d279b2c7"]; - - expect(getIntermediateHashes(targetHash)).toEqual(expected); - expect(getIntermediateHashes(targetHash, [])).toEqual(expected); - }); - - it("returns array of target hash, intermediate hashes up to merkle root when given target hash and proofs", () => { - const targetHash = "f7432b3219b2aa4122e289f44901830fa32f224ee9dfce28565677f1d279b2c7"; - const proofs = [ - "2bb9dd186994f38084ee68e06be848b9d43077c307684c300d81df343c7858cf", - "ed8bdba60a24af04bcdcd88b939251f3843e03839164fdd2dd502aaeef3bfb99" - ]; - const expected = [ - "0xf7432b3219b2aa4122e289f44901830fa32f224ee9dfce28565677f1d279b2c7", - "0xfe0958c4b90e768cecb50cea207f3af034580703e9ed74ef460c1a31dd1b4d6c", - "0xfcfce0e79adc002c1fd78a2a02c768c0fdc00e5b96f1da8ef80bed02876e18d1" - ]; - - expect(getIntermediateHashes(targetHash, proofs)).toEqual(expected); - }); -}); - -describe("verifyRevoked", () => { - it("returns valid summary of the status if document is not revoked on any smart contracts", async () => { - // @ts-ignore - isRevoked.mockResolvedValue(false); - const smartContracts: OpenAttestationContract[] = [ - { address: "0x0A", type: v3.Method.DocumentStore, instance: contract }, - { address: "0x0B", type: v3.Method.DocumentStore, instance: contract } - ]; - const summary = await verifyRevoked( - { - version: "version", - schema: "schema", - data: { - issuers: [] - }, - signature: { - merkleRoot: "MERKLE_ROOT", - type: "SHA3MerkleProof", - targetHash: "0d", - proof: ["0a"] - } - }, - smartContracts - ); - expect(summary).toEqual({ - revokedOnAny: false, - details: [ - { address: "0x0A", revoked: false }, - { address: "0x0B", revoked: false } - ] - }); - // @ts-ignore - expect(isRevoked.mock.calls).toEqual([ - [{ address: "0x0A", type: v3.Method.DocumentStore, instance: contract }, "0x0d"], - [ - { address: "0x0A", type: v3.Method.DocumentStore, instance: contract }, - "0x96851128a70d034965e58c4ef4681d4ffcf60ba27322aa9015cf340f2b242e3d" - ], - [{ address: "0x0B", type: v3.Method.DocumentStore, instance: contract }, "0x0d"], - [ - { address: "0x0B", type: v3.Method.DocumentStore, instance: contract }, - "0x96851128a70d034965e58c4ef4681d4ffcf60ba27322aa9015cf340f2b242e3d" - ] - ]); - }); - - it("returns invalid summary of the status if document is revoked on any smart contracts", async () => { - // @ts-ignore - isRevoked.mockResolvedValueOnce(false); - // @ts-ignore - isRevoked.mockResolvedValueOnce(true); - const smartContracts: OpenAttestationContract[] = [ - { address: "0x0A", type: v3.Method.DocumentStore, instance: contract }, - { address: "0x0B", type: v3.Method.DocumentStore, instance: contract } - ]; - const summary = await verifyRevoked( - { - version: "version", - schema: "schema", - data: { issuers: [] }, - signature: { - merkleRoot: "MERKLE_ROOT", - type: "SHA3MerkleProof", - targetHash: "0d", - proof: [] - } - }, - smartContracts - ); - expect(summary).toEqual({ - revokedOnAny: true, - details: [ - { address: "0x0A", revoked: false }, - { address: "0x0B", revoked: true } - ] - }); - // @ts-ignore - expect(isRevoked.mock.calls).toEqual([ - [{ address: "0x0A", type: v3.Method.DocumentStore, instance: contract }, "0x0d"], - [{ address: "0x0B", type: v3.Method.DocumentStore, instance: contract }, "0x0d"] - ]); - }); -}); diff --git a/src/verifiers/documentStoreRevoked/verify.ts b/src/verifiers/documentStoreRevoked/verify.ts deleted file mode 100644 index 6e6bc1ed..00000000 --- a/src/verifiers/documentStoreRevoked/verify.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { WrappedDocument, utils, v2, v3 } from "@govtechsg/open-attestation"; -import { Hash, OpenAttestationContract } from "../../types/core"; -import { isRevoked } from "./contractInterface"; - -// Given a list of hashes, check against one smart contract if any of the hash has been revoked -export const isAnyHashRevokedOnStore = async (smartContract: OpenAttestationContract, intermediateHashes: Hash[]) => { - const revokedStatusDeferred = intermediateHashes.map(hash => isRevoked(smartContract, hash)); - const revokedStatuses = await Promise.all(revokedStatusDeferred); - return revokedStatuses.some(status => status); -}; - -export const revokedStatusOnContracts = async ( - smartContracts: OpenAttestationContract[] = [], - intermediateHashes: Hash[] = [] -) => { - const revokeStatusesDefered = smartContracts.map(smartContract => - isAnyHashRevokedOnStore(smartContract, intermediateHashes) - .then(revoked => ({ - address: smartContract.address, - revoked - })) - .catch(e => ({ - address: smartContract.address, - revoked: true, - error: e.message || e - })) - ); - return Promise.all(revokeStatusesDefered); -}; - -export const isRevokedOnAny = (statuses: { address: Hash; revoked: boolean }[]) => { - if (!statuses || statuses.length === 0) return false; - return statuses.some(status => status.revoked); -}; - -export const getIntermediateHashes = (targetHash: Hash, proofs: Hash[] = []) => { - const hashes = [`0x${targetHash}`]; - proofs.reduce((prev, curr) => { - const next = utils.combineHashString(prev, curr); - hashes.push(`0x${next}`); - return next; - }, targetHash); - return hashes; -}; - -export const verifyRevoked = async ( - document: WrappedDocument, - smartContracts: OpenAttestationContract[] = [] -) => { - const { targetHash } = document.signature; - const proofs = document.signature.proof || []; - const intermediateHashes = getIntermediateHashes(targetHash, proofs); - const details = await revokedStatusOnContracts(smartContracts, intermediateHashes); - const revokedOnAny = isRevokedOnAny(details); - - return { - revokedOnAny, - details - }; -}; diff --git a/src/verifiers/openAttestationHash.test.ts b/src/verifiers/hash/openAttestationHash.test.ts similarity index 72% rename from src/verifiers/openAttestationHash.test.ts rename to src/verifiers/hash/openAttestationHash.test.ts index b2b5effd..06247460 100644 --- a/src/verifiers/openAttestationHash.test.ts +++ b/src/verifiers/hash/openAttestationHash.test.ts @@ -1,6 +1,6 @@ import { openAttestationHash } from "./openAttestationHash"; -import { tamperedDocumentWithCertificateStore } from "../../test/fixtures/v2/tamperedDocument"; -import { document } from "../../test/fixtures/v2/document"; +import { tamperedDocumentWithCertificateStore } from "../../../test/fixtures/v2/tamperedDocument"; +import { document } from "../../../test/fixtures/v2/document"; describe("OpenAttestationHash", () => { it("should return an invalid fragment when document has been tampered", async () => { @@ -9,7 +9,11 @@ describe("OpenAttestationHash", () => { name: "OpenAttestationHash", type: "DOCUMENT_INTEGRITY", data: false, - message: "Certificate has been tampered with", + reason: { + code: 0, + codeString: "DOCUMENT_TAMPERED", + message: "Certificate has been tampered with" + }, status: "INVALID" }); }); diff --git a/src/verifiers/openAttestationHash.ts b/src/verifiers/hash/openAttestationHash.ts similarity index 61% rename from src/verifiers/openAttestationHash.ts rename to src/verifiers/hash/openAttestationHash.ts index bec39869..a1f72b7c 100644 --- a/src/verifiers/openAttestationHash.ts +++ b/src/verifiers/hash/openAttestationHash.ts @@ -1,5 +1,6 @@ import { verifySignature } from "@govtechsg/open-attestation"; -import { VerificationFragmentType, Verifier } from "../types/core"; +import { VerificationFragmentType, Verifier } from "../../types/core"; +import { OpenAttestationHashCode } from "../../types/error"; const name = "OpenAttestationHash"; const type: VerificationFragmentType = "DOCUMENT_INTEGRITY"; @@ -15,7 +16,11 @@ export const openAttestationHash: Verifier = { type, name, data: hash, - message: "Certificate has been tampered with", + reason: { + code: OpenAttestationHashCode.DOCUMENT_TAMPERED, + codeString: OpenAttestationHashCode[OpenAttestationHashCode.DOCUMENT_TAMPERED], + message: "Certificate has been tampered with" + }, status: "INVALID" }; } diff --git a/src/verifiers/openAttestationEthereumTokenRegistryMinted.ts b/src/verifiers/openAttestationEthereumTokenRegistryMinted.ts deleted file mode 100644 index d7c9c871..00000000 --- a/src/verifiers/openAttestationEthereumTokenRegistryMinted.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { getData, v2, v3, WrappedDocument } from "@govtechsg/open-attestation"; -import { isWrappedV3Document, OpenAttestationContract, VerificationFragmentType, Verifier } from "../types/core"; -import { getTokenRegistrySmartContract } from "../common/smartContract/documentToSmartContracts"; -import { verifyIssued } from "./documentStoreIssued/verify"; - -const verifyMinted = async ( - document: WrappedDocument, - smartContracts: OpenAttestationContract[] = [] -) => { - const { details, issuedOnAll } = await verifyIssued(document, smartContracts); - return { - details: details.map(({ issued, ...rest }) => { - return { - ...rest, - minted: issued - }; - }), - mintedOnAll: issuedOnAll - }; -}; - -const name = "OpenAttestationEthereumTokenRegistryMinted"; -const type: VerificationFragmentType = "DOCUMENT_STATUS"; -export const openAttestationEthereumTokenRegistryMinted: Verifier< - WrappedDocument | WrappedDocument -> = { - skip: () => { - return Promise.resolve({ - status: "SKIPPED", - type, - name, - message: `Document issuers doesn't have "tokenRegistry" property or ${v3.Method.TokenRegistry} method` - }); - }, - test: document => { - if (isWrappedV3Document(document)) { - const documentData = getData(document); - return documentData.proof.method === v3.Method.TokenRegistry; - } - const documentData = getData(document); - return documentData.issuers.some(issuer => "tokenRegistry" in issuer); - }, - verify: async (document, options) => { - try { - const smartContracts = getTokenRegistrySmartContract(document, options); - const status = await verifyMinted(document, smartContracts); - if (!status.mintedOnAll) { - return { - name, - type, - data: status, - message: "Certificate has not been minted", - status: "INVALID" - }; - } - return { - name, - type, - data: status, - status: "VALID" - }; - } catch (e) { - return { - name, - type, - data: e, - message: e.message, - status: "ERROR" - }; - } - } -}; diff --git a/src/verifiers/tokenRegistryMinted/errors.ts b/src/verifiers/tokenRegistryMinted/errors.ts new file mode 100644 index 00000000..c551af88 --- /dev/null +++ b/src/verifiers/tokenRegistryMinted/errors.ts @@ -0,0 +1,58 @@ +import { errors } from "ethers"; +import { Hash } from "../../types/core"; +import { EthersError, OpenAttestationEthereumTokenRegistryMintedCode, Reason } from "../../types/error"; + +const contractNotFound = (address: Hash): Reason => { + return { + code: OpenAttestationEthereumTokenRegistryMintedCode.CONTRACT_NOT_FOUND, + codeString: + OpenAttestationEthereumTokenRegistryMintedCode[OpenAttestationEthereumTokenRegistryMintedCode.CONTRACT_NOT_FOUND], + message: `Contract ${address} was not found` + }; +}; +const contractAddressInvalid = (address: Hash): Reason => { + return { + code: OpenAttestationEthereumTokenRegistryMintedCode.CONTRACT_ADDRESS_INVALID, + codeString: + OpenAttestationEthereumTokenRegistryMintedCode[ + OpenAttestationEthereumTokenRegistryMintedCode.CONTRACT_ADDRESS_INVALID + ], + message: `Contract address ${address} is invalid` + }; +}; +export const contractNotMinted = (merkleRoot: Hash, address: string): Reason => { + return { + code: OpenAttestationEthereumTokenRegistryMintedCode.DOCUMENT_NOT_MINTED, + codeString: + OpenAttestationEthereumTokenRegistryMintedCode[ + OpenAttestationEthereumTokenRegistryMintedCode.DOCUMENT_NOT_MINTED + ], + message: `Certificate ${merkleRoot} has not been issued under contract ${address}` + }; +}; + +export const getErrorReason = (error: EthersError, address: string, hash: Hash): Reason => { + const reason = error.reason && Array.isArray(error.reason) ? error.reason[0] : error.reason ?? ""; + if (reason.toLowerCase() === "contract not deployed".toLowerCase() && error.code === errors.UNSUPPORTED_OPERATION) { + return contractNotFound(address); + } else if ( + (reason.toLowerCase() === "ENS name not configured".toLowerCase() && error.code === errors.UNSUPPORTED_OPERATION) || + (reason.toLowerCase() === "bad address checksum".toLowerCase() && error.code === errors.INVALID_ARGUMENT) || + (reason.toLowerCase() === "invalid address".toLowerCase() && error.code === errors.INVALID_ARGUMENT) + ) { + return contractAddressInvalid(address); + } else if ( + reason.toLowerCase() === "ERC721: owner query for nonexistent token".toLowerCase() && + error.code === errors.CALL_EXCEPTION + ) { + return contractNotMinted(hash, address); + } + return { + message: `Error with smart contract ${address}: ${error.reason}`, + code: OpenAttestationEthereumTokenRegistryMintedCode.ETHERS_UNHANDLED_ERROR, + codeString: + OpenAttestationEthereumTokenRegistryMintedCode[ + OpenAttestationEthereumTokenRegistryMintedCode.ETHERS_UNHANDLED_ERROR + ] + }; +}; diff --git a/src/verifiers/openAttestationEthereumTokenRegistryMinted.test.ts b/src/verifiers/tokenRegistryMinted/openAttestationEthereumTokenRegistryMinted.test.ts similarity index 55% rename from src/verifiers/openAttestationEthereumTokenRegistryMinted.test.ts rename to src/verifiers/tokenRegistryMinted/openAttestationEthereumTokenRegistryMinted.test.ts index e17f2980..bef15c02 100644 --- a/src/verifiers/openAttestationEthereumTokenRegistryMinted.test.ts +++ b/src/verifiers/tokenRegistryMinted/openAttestationEthereumTokenRegistryMinted.test.ts @@ -1,15 +1,16 @@ import { openAttestationEthereumTokenRegistryMinted } from "./openAttestationEthereumTokenRegistryMinted"; -import { documentRopstenNotIssuedWithTokenRegistry } from "../../test/fixtures/v2/documentRopstenNotIssuedWithTokenRegistry"; -import { documentRopstenValidWithToken } from "../../test/fixtures/v2/documentRopstenValidWithToken"; +import { documentRopstenNotIssuedWithTokenRegistry } from "../../../test/fixtures/v2/documentRopstenNotIssuedWithTokenRegistry"; +import { documentRopstenValidWithToken } from "../../../test/fixtures/v2/documentRopstenValidWithToken"; import { documentRopstenValidWithDocumentStore as v3documentRopstenValidWithDocumentStore, documentRopstenValidWithTokenRegistry as v3documentRopstenValidWithTokenRegistry -} from "../../test/fixtures/v3/documentRopstenValid"; -import { documentRopstenNotIssuedWithTokenRegistry as v3documentRopstenNotIssuedWithTokenRegistry } from "../../test/fixtures/v3/documentRopstenNotIssuedWithTokenRegistry"; -import { documentRopstenNotIssuedWithCertificateStore } from "../../test/fixtures/v2/documentRopstenNotIssuedWithCertificateStore"; -import { documentRopstenNotIssuedWithDocumentStore } from "../../test/fixtures/v2/documentRopstenNotIssuedWithDocumentStore"; +} from "../../../test/fixtures/v3/documentRopstenValid"; +import { documentRopstenNotIssuedWithTokenRegistry as v3documentRopstenNotIssuedWithTokenRegistry } from "../../../test/fixtures/v3/documentRopstenNotIssuedWithTokenRegistry"; +import { documentRopstenNotIssuedWithCertificateStore } from "../../../test/fixtures/v2/documentRopstenNotIssuedWithCertificateStore"; +import { documentRopstenNotIssuedWithDocumentStore } from "../../../test/fixtures/v2/documentRopstenNotIssuedWithDocumentStore"; describe("openAttestationEthereumTokenRegistryMinted", () => { + // TODO create a verifier and call it to test this => check dns verifier test describe("test", () => { it("should return false when v2 document uses certificate store", () => { const test = openAttestationEthereumTokenRegistryMinted.test(documentRopstenNotIssuedWithCertificateStore, { @@ -43,6 +44,92 @@ describe("openAttestationEthereumTokenRegistryMinted", () => { }); }); describe("v2", () => { + it("should return an invalid fragment when token registry is invalid", async () => { + const fragment = await openAttestationEthereumTokenRegistryMinted.verify( + { + ...documentRopstenNotIssuedWithTokenRegistry, + data: { + ...documentRopstenNotIssuedWithTokenRegistry.data, + issuers: [ + { + ...documentRopstenNotIssuedWithTokenRegistry.data.issuers[0], + tokenRegistry: "0fb5b63a-aaa5-4e6e-a6f4-391c0f6ba423:string:0xabcd" + } + ] + } + }, + { + network: "ropsten" + } + ); + expect(fragment).toStrictEqual({ + name: "OpenAttestationEthereumTokenRegistryMinted", + type: "DOCUMENT_STATUS", + data: { + details: [ + { + address: "0xabcd", + minted: false, + reason: { + code: 2, + codeString: "CONTRACT_ADDRESS_INVALID", + message: "Contract address 0xabcd is invalid" + } + } + ], + mintedOnAll: false + }, + reason: { + code: 2, + codeString: "CONTRACT_ADDRESS_INVALID", + message: "Contract address 0xabcd is invalid" + }, + status: "INVALID" + }); + }); + it("should return an invalid fragment when token registry does not exist", async () => { + const fragment = await openAttestationEthereumTokenRegistryMinted.verify( + { + ...documentRopstenNotIssuedWithTokenRegistry, + data: { + ...documentRopstenNotIssuedWithTokenRegistry.data, + issuers: [ + { + ...documentRopstenNotIssuedWithTokenRegistry.data.issuers[0], + tokenRegistry: "0fb5b63a-aaa5-4e6e-a6f4-391c0f6ba423:string:0x0000000000000000000000000000000000000000" + } + ] + } + }, + { + network: "ropsten" + } + ); + expect(fragment).toStrictEqual({ + name: "OpenAttestationEthereumTokenRegistryMinted", + type: "DOCUMENT_STATUS", + data: { + details: [ + { + address: "0x0000000000000000000000000000000000000000", + minted: false, + reason: { + code: 404, + codeString: "CONTRACT_NOT_FOUND", + message: "Contract 0x0000000000000000000000000000000000000000 was not found" + } + } + ], + mintedOnAll: false + }, + reason: { + code: 404, + codeString: "CONTRACT_NOT_FOUND", + message: "Contract 0x0000000000000000000000000000000000000000 was not found" + }, + status: "INVALID" + }); + }); it("should return an invalid fragment when document with token registry has not been minted", async () => { const fragment = await openAttestationEthereumTokenRegistryMinted.verify( documentRopstenNotIssuedWithTokenRegistry, @@ -58,13 +145,22 @@ describe("openAttestationEthereumTokenRegistryMinted", () => { { address: "0xb53499ee758352fAdDfCed863d9ac35C809E2F20", minted: false, - error: - 'call revert exception (address="0xb53499ee758352fAdDfCed863d9ac35C809E2F20", args=["0x693c86fbb8f75ac56f865f5b3100e545875f2154b3749bdcf448c874a1d67ef3"], method="ownerOf(uint256)", errorSignature="Error(string)", errorArgs=[["ERC721: owner query for nonexistent token"]], reason=["ERC721: owner query for nonexistent token"], transaction={"to":{},"data":"0x6352211e693c86fbb8f75ac56f865f5b3100e545875f2154b3749bdcf448c874a1d67ef3"}, version=4.0.40)' + reason: { + code: 1, + codeString: "DOCUMENT_NOT_MINTED", + message: + "Certificate 0x693c86fbb8f75ac56f865f5b3100e545875f2154b3749bdcf448c874a1d67ef3 has not been issued under contract 0xb53499ee758352fAdDfCed863d9ac35C809E2F20" + } } ], mintedOnAll: false }, - message: "Certificate has not been minted", + reason: { + code: 1, + codeString: "DOCUMENT_NOT_MINTED", + message: + "Certificate 0x693c86fbb8f75ac56f865f5b3100e545875f2154b3749bdcf448c874a1d67ef3 has not been issued under contract 0xb53499ee758352fAdDfCed863d9ac35C809E2F20" + }, status: "INVALID" }); }); @@ -104,7 +200,11 @@ describe("openAttestationEthereumTokenRegistryMinted", () => { name: "OpenAttestationEthereumTokenRegistryMinted", type: "DOCUMENT_STATUS", data: new Error("Only one token registry is allowed. Found 2"), - message: "Only one token registry is allowed. Found 2", + reason: { + code: 0, + codeString: "UNEXPECTED_ERROR", + message: "Only one token registry is allowed. Found 2" + }, status: "ERROR" }); }); @@ -130,8 +230,12 @@ describe("openAttestationEthereumTokenRegistryMinted", () => { expect(fragment).toStrictEqual({ name: "OpenAttestationEthereumTokenRegistryMinted", type: "DOCUMENT_STATUS", - data: new Error(`No token registry for issuer "Second Issuer"`), - message: `No token registry for issuer "Second Issuer"`, + data: new Error(`Only one token registry is allowed. Found 2`), + reason: { + code: 0, + codeString: "UNEXPECTED_ERROR", + message: "Only one token registry is allowed. Found 2" + }, status: "ERROR" }); }); @@ -148,17 +252,24 @@ describe("openAttestationEthereumTokenRegistryMinted", () => { name: "OpenAttestationEthereumTokenRegistryMinted", type: "DOCUMENT_STATUS", data: { - details: [ - { - address: "0xb53499ee758352fAdDfCed863d9ac35C809E2F20", - minted: false, - error: - 'call revert exception (address="0xb53499ee758352fAdDfCed863d9ac35C809E2F20", args=["0x7c56cf6bac41a744060e515cac8eb177c8f3d2d56f705a0a7df884906623bddc"], method="ownerOf(uint256)", errorSignature="Error(string)", errorArgs=[["ERC721: owner query for nonexistent token"]], reason=["ERC721: owner query for nonexistent token"], transaction={"to":{},"data":"0x6352211e7c56cf6bac41a744060e515cac8eb177c8f3d2d56f705a0a7df884906623bddc"}, version=4.0.40)' + details: { + address: "0xb53499ee758352fAdDfCed863d9ac35C809E2F20", + minted: false, + reason: { + code: 1, + codeString: "DOCUMENT_NOT_MINTED", + message: + "Certificate 0x7c56cf6bac41a744060e515cac8eb177c8f3d2d56f705a0a7df884906623bddc has not been issued under contract 0xb53499ee758352fAdDfCed863d9ac35C809E2F20" } - ], + }, mintedOnAll: false }, - message: "Certificate has not been minted", + reason: { + code: 1, + codeString: "DOCUMENT_NOT_MINTED", + message: + "Certificate 0x7c56cf6bac41a744060e515cac8eb177c8f3d2d56f705a0a7df884906623bddc has not been issued under contract 0xb53499ee758352fAdDfCed863d9ac35C809E2F20" + }, status: "INVALID" }); }); @@ -173,12 +284,10 @@ describe("openAttestationEthereumTokenRegistryMinted", () => { name: "OpenAttestationEthereumTokenRegistryMinted", type: "DOCUMENT_STATUS", data: { - details: [ - { - address: "0xb53499ee758352fAdDfCed863d9ac35C809E2F20", - minted: true - } - ], + details: { + address: "0xb53499ee758352fAdDfCed863d9ac35C809E2F20", + minted: true + }, mintedOnAll: true }, status: "VALID" diff --git a/src/verifiers/tokenRegistryMinted/openAttestationEthereumTokenRegistryMinted.ts b/src/verifiers/tokenRegistryMinted/openAttestationEthereumTokenRegistryMinted.ts new file mode 100644 index 00000000..68447bcc --- /dev/null +++ b/src/verifiers/tokenRegistryMinted/openAttestationEthereumTokenRegistryMinted.ts @@ -0,0 +1,100 @@ +import { getData, v2, v3, WrappedDocument } from "@govtechsg/open-attestation"; +import { isWrappedV3Document, VerificationFragmentType, Verifier } from "../../types/core"; +import { OpenAttestationEthereumTokenRegistryMintedCode } from "../../types/error"; +import { + createTokenRegistryContract, + getIssuersTokenRegistry, + isMintedOnTokenRegistry +} from "../../common/smartContract/tokenRegistryContractInterface"; +import { contractNotMinted, getErrorReason } from "./errors"; + +interface Status { + minted: boolean; + address: string; + reason?: any; +} +const name = "OpenAttestationEthereumTokenRegistryMinted"; +const type: VerificationFragmentType = "DOCUMENT_STATUS"; +export const openAttestationEthereumTokenRegistryMinted: Verifier< + WrappedDocument | WrappedDocument +> = { + skip: () => { + return Promise.resolve({ + status: "SKIPPED", + type, + name, + reason: { + code: OpenAttestationEthereumTokenRegistryMintedCode.SKIPPED, + codeString: + OpenAttestationEthereumTokenRegistryMintedCode[OpenAttestationEthereumTokenRegistryMintedCode.SKIPPED], + message: `Document issuers doesn't have "tokenRegistry" property or ${v3.Method.TokenRegistry} method` + } + }); + }, + test: document => { + if (isWrappedV3Document(document)) { + const documentData = getData(document); + return documentData.proof.method === v3.Method.TokenRegistry; + } + const documentData = getData(document); + return documentData.issuers.some(issuer => "tokenRegistry" in issuer); + }, + verify: async (document, options) => { + try { + const tokenRegistries = getIssuersTokenRegistry(document); + if (tokenRegistries.length > 1) { + throw new Error(`Only one token registry is allowed. Found ${tokenRegistries.length}`); + } + const merkleRoot = `0x${document.signature.merkleRoot}`; + const statuses: Status[] = await Promise.all( + tokenRegistries.map(async tokenRegistry => { + try { + const contract = createTokenRegistryContract(tokenRegistry, options); + const minted = await isMintedOnTokenRegistry(contract, merkleRoot); + const status: Status = { + minted, + address: tokenRegistry + }; + if (!minted) { + status.reason = contractNotMinted(merkleRoot, tokenRegistry); + } + return status; + } catch (e) { + return { minted: false, address: tokenRegistry, reason: getErrorReason(e, tokenRegistry, merkleRoot) }; + } + }) + ); + const notMinted = statuses.find(status => !status.minted); + if (notMinted) { + return { + name, + type, + data: { mintedOnAll: false, details: isWrappedV3Document(document) ? statuses[0] : statuses }, + reason: notMinted.reason, + status: "INVALID" + }; + } + return { + name, + type, + data: { mintedOnAll: true, details: isWrappedV3Document(document) ? statuses[0] : statuses }, + status: "VALID" + }; + } catch (e) { + return { + name, + type, + data: e, + reason: { + message: e.message, + code: OpenAttestationEthereumTokenRegistryMintedCode.UNEXPECTED_ERROR, + codeString: + OpenAttestationEthereumTokenRegistryMintedCode[ + OpenAttestationEthereumTokenRegistryMintedCode.UNEXPECTED_ERROR + ] + }, + status: "ERROR" + }; + } + } +}; diff --git a/src/verify.v2.integration.test.ts b/src/verify.v2.integration.test.ts index 2288bfbf..9ede2f6a 100644 --- a/src/verify.v2.integration.test.ts +++ b/src/verify.v2.integration.test.ts @@ -4,13 +4,16 @@ import { isValid, verify } from "./index"; import { documentMainnetValidWithCertificateStore } from "../test/fixtures/v2/documentMainnetValidWithCertificateStore"; -import { tamperedDocumentWithCertificateStore } from "../test/fixtures/v2/tamperedDocument"; +import { + tamperedDocumentWithCertificateStore, + tamperedDocumentWithInvalidCertificateStore +} from "../test/fixtures/v2/tamperedDocument"; import { documentRopstenValidWithCertificateStore } from "../test/fixtures/v2/documentRopstenValidWithCertificateStore"; import { documentRopstenValidWithToken } from "../test/fixtures/v2/documentRopstenValidWithToken"; import { documentRopstenRevokedWithToken } from "../test/fixtures/v2/documentRopstenRevokedWithToken"; describe("verify(integration)", () => { - it("should fail for OpenAttestationHash and OpenAttestationEthereumDocumentStoreIssued when document's hash is invalid and was not issued", async () => { + it("should fail for everything when document's hash is invalid and certificate store is invalid", async () => { const results = await verify(tamperedDocumentWithCertificateStore, { network: "ropsten" }); @@ -18,7 +21,11 @@ describe("verify(integration)", () => { expect(results).toStrictEqual([ { data: false, - message: "Certificate has been tampered with", + reason: { + code: 0, + codeString: "DOCUMENT_TAMPERED", + message: "Certificate has been tampered with" + }, status: "INVALID", name: "OpenAttestationHash", type: "DOCUMENT_INTEGRITY" @@ -28,20 +35,31 @@ describe("verify(integration)", () => { details: [ { address: "0x20bc9C354A18C8178A713B9BcCFFaC2152b53990", - error: - 'call exception (address="0x20bc9C354A18C8178A713B9BcCFFaC2152b53990", method="isIssued(bytes32)", args=["0x85df2b4e905a82cf10c317df8f4b659b5cf38cc12bd5fbaffba5fc901ef0011b"], version=4.0.40)', + reason: { + code: 3, + codeString: "ETHERS_UNHANDLED_ERROR", + message: "Error with smart contract 0x20bc9C354A18C8178A713B9BcCFFaC2152b53990: call exception" + }, issued: false } ], issuedOnAll: false }, - message: "Certificate has not been issued", + reason: { + code: 3, + codeString: "ETHERS_UNHANDLED_ERROR", + message: "Error with smart contract 0x20bc9C354A18C8178A713B9BcCFFaC2152b53990: call exception" + }, status: "INVALID", name: "OpenAttestationEthereumDocumentStoreIssued", type: "DOCUMENT_STATUS" }, { - message: 'Document issuers doesn\'t have "tokenRegistry" property or TOKEN_REGISTRY method', + reason: { + code: 4, + codeString: "SKIPPED", + message: 'Document issuers doesn\'t have "tokenRegistry" property or TOKEN_REGISTRY method' + }, name: "OpenAttestationEthereumTokenRegistryMinted", status: "SKIPPED", type: "DOCUMENT_STATUS" @@ -61,7 +79,101 @@ describe("verify(integration)", () => { type: "DOCUMENT_STATUS" }, { - message: `Document issuers doesn't have "documentStore" / "tokenRegistry" property or doesn't use DNS-TXT type`, + reason: { + code: 2, + codeString: "SKIPPED", + message: `Document issuers doesn't have "documentStore" / "tokenRegistry" property or doesn't use DNS-TXT type` + }, + status: "SKIPPED", + name: "OpenAttestationDnsTxt", + type: "ISSUER_IDENTITY" + } + ]); + expect(isValid(results)).toStrictEqual(false); + expect(isValid(results, ["DOCUMENT_INTEGRITY"])).toStrictEqual(false); + }); + + it("should fail for OpenAttestationHash and OpenAttestationEthereumDocumentStoreIssued when document's hash is invalid and was not issued", async () => { + const results = await verify(tamperedDocumentWithInvalidCertificateStore, { + network: "ropsten" + }); + + expect(results).toStrictEqual([ + { + data: false, + reason: { + code: 0, + codeString: "DOCUMENT_TAMPERED", + message: "Certificate has been tampered with" + }, + status: "INVALID", + name: "OpenAttestationHash", + type: "DOCUMENT_INTEGRITY" + }, + { + data: { + details: [ + { + address: "0x20bc9C354A18C8178A713B9BcCFFaC2152b53991", + reason: { + code: 2, + codeString: "CONTRACT_ADDRESS_INVALID", + message: "Contract address 0x20bc9C354A18C8178A713B9BcCFFaC2152b53991 is invalid" + }, + issued: false + } + ], + issuedOnAll: false + }, + reason: { + code: 2, + codeString: "CONTRACT_ADDRESS_INVALID", + message: "Contract address 0x20bc9C354A18C8178A713B9BcCFFaC2152b53991 is invalid" + }, + status: "INVALID", + name: "OpenAttestationEthereumDocumentStoreIssued", + type: "DOCUMENT_STATUS" + }, + { + reason: { + code: 4, + codeString: "SKIPPED", + message: 'Document issuers doesn\'t have "tokenRegistry" property or TOKEN_REGISTRY method' + }, + name: "OpenAttestationEthereumTokenRegistryMinted", + status: "SKIPPED", + type: "DOCUMENT_STATUS" + }, + { + data: { + details: [ + { + address: "0x20bc9C354A18C8178A713B9BcCFFaC2152b53991", + reason: { + code: 2, + codeString: "CONTRACT_ADDRESS_INVALID", + message: "Contract address 0x20bc9C354A18C8178A713B9BcCFFaC2152b53991 is invalid" + }, + revoked: true + } + ], + revokedOnAny: true + }, + reason: { + code: 2, + codeString: "CONTRACT_ADDRESS_INVALID", + message: "Contract address 0x20bc9C354A18C8178A713B9BcCFFaC2152b53991 is invalid" + }, + status: "INVALID", + name: "OpenAttestationEthereumDocumentStoreRevoked", + type: "DOCUMENT_STATUS" + }, + { + reason: { + code: 2, + codeString: "SKIPPED", + message: `Document issuers doesn't have "documentStore" / "tokenRegistry" property or doesn't use DNS-TXT type` + }, status: "SKIPPED", name: "OpenAttestationDnsTxt", type: "ISSUER_IDENTITY" @@ -98,7 +210,11 @@ describe("verify(integration)", () => { type: "DOCUMENT_STATUS" }, { - message: 'Document issuers doesn\'t have "tokenRegistry" property or TOKEN_REGISTRY method', + reason: { + code: 4, + codeString: "SKIPPED", + message: 'Document issuers doesn\'t have "tokenRegistry" property or TOKEN_REGISTRY method' + }, name: "OpenAttestationEthereumTokenRegistryMinted", status: "SKIPPED", type: "DOCUMENT_STATUS" @@ -118,7 +234,11 @@ describe("verify(integration)", () => { type: "DOCUMENT_STATUS" }, { - message: `Document issuers doesn't have "documentStore" / "tokenRegistry" property or doesn't use DNS-TXT type`, + reason: { + code: 2, + codeString: "SKIPPED", + message: `Document issuers doesn't have "documentStore" / "tokenRegistry" property or doesn't use DNS-TXT type` + }, status: "SKIPPED", name: "OpenAttestationDnsTxt", type: "ISSUER_IDENTITY" @@ -156,7 +276,11 @@ describe("verify(integration)", () => { type: "DOCUMENT_STATUS" }, { - message: 'Document issuers doesn\'t have "tokenRegistry" property or TOKEN_REGISTRY method', + reason: { + code: 4, + codeString: "SKIPPED", + message: 'Document issuers doesn\'t have "tokenRegistry" property or TOKEN_REGISTRY method' + }, name: "OpenAttestationEthereumTokenRegistryMinted", status: "SKIPPED", type: "DOCUMENT_STATUS" @@ -176,7 +300,11 @@ describe("verify(integration)", () => { type: "DOCUMENT_STATUS" }, { - message: `Document issuers doesn't have "documentStore" / "tokenRegistry" property or doesn't use DNS-TXT type`, + reason: { + code: 2, + codeString: "SKIPPED", + message: `Document issuers doesn't have "documentStore" / "tokenRegistry" property or doesn't use DNS-TXT type` + }, status: "SKIPPED", name: "OpenAttestationDnsTxt", type: "ISSUER_IDENTITY" @@ -200,8 +328,12 @@ describe("verify(integration)", () => { type: "DOCUMENT_INTEGRITY" }, { - message: - 'Document issuers doesn\'t have "documentStore" or "certificateStore" property or DOCUMENT_STORE method', + reason: { + code: 4, + codeString: "SKIPPED", + message: + 'Document issuers doesn\'t have "documentStore" or "certificateStore" property or DOCUMENT_STORE method' + }, name: "OpenAttestationEthereumDocumentStoreIssued", status: "SKIPPED", type: "DOCUMENT_STATUS" @@ -221,8 +353,12 @@ describe("verify(integration)", () => { type: "DOCUMENT_STATUS" }, { - message: - 'Document issuers doesn\'t have "documentStore" or "certificateStore" property or DOCUMENT_STORE method', + reason: { + code: 4, + codeString: "SKIPPED", + message: + 'Document issuers doesn\'t have "documentStore" or "certificateStore" property or DOCUMENT_STORE method' + }, name: "OpenAttestationEthereumDocumentStoreRevoked", status: "SKIPPED", type: "DOCUMENT_STATUS" @@ -256,8 +392,12 @@ describe("verify(integration)", () => { type: "DOCUMENT_INTEGRITY" }, { - message: - 'Document issuers doesn\'t have "documentStore" or "certificateStore" property or DOCUMENT_STORE method', + reason: { + code: 4, + codeString: "SKIPPED", + message: + 'Document issuers doesn\'t have "documentStore" or "certificateStore" property or DOCUMENT_STORE method' + }, name: "OpenAttestationEthereumDocumentStoreIssued", status: "SKIPPED", type: "DOCUMENT_STATUS" @@ -267,21 +407,34 @@ describe("verify(integration)", () => { details: [ { address: "0x48399Fb88bcD031C556F53e93F690EEC07963Af3", - error: - 'call revert exception (address="0x48399Fb88bcD031C556F53e93F690EEC07963Af3", args=["0x1e63c39cdd668da652484fd781f8c0812caadad0f6ebf71bf68bf3670242d1ef"], method="ownerOf(uint256)", errorSignature="Error(string)", errorArgs=[["ERC721: owner query for nonexistent token"]], reason=["ERC721: owner query for nonexistent token"], transaction={"to":{},"data":"0x6352211e1e63c39cdd668da652484fd781f8c0812caadad0f6ebf71bf68bf3670242d1ef"}, version=4.0.40)', + reason: { + code: 1, + codeString: "DOCUMENT_NOT_MINTED", + message: + "Certificate 0x1e63c39cdd668da652484fd781f8c0812caadad0f6ebf71bf68bf3670242d1ef has not been issued under contract 0x48399Fb88bcD031C556F53e93F690EEC07963Af3" + }, minted: false } ], mintedOnAll: false }, - message: "Certificate has not been minted", + reason: { + code: 1, + codeString: "DOCUMENT_NOT_MINTED", + message: + "Certificate 0x1e63c39cdd668da652484fd781f8c0812caadad0f6ebf71bf68bf3670242d1ef has not been issued under contract 0x48399Fb88bcD031C556F53e93F690EEC07963Af3" + }, status: "INVALID", name: "OpenAttestationEthereumTokenRegistryMinted", type: "DOCUMENT_STATUS" }, { - message: - 'Document issuers doesn\'t have "documentStore" or "certificateStore" property or DOCUMENT_STORE method', + reason: { + code: 4, + codeString: "SKIPPED", + message: + 'Document issuers doesn\'t have "documentStore" or "certificateStore" property or DOCUMENT_STORE method' + }, name: "OpenAttestationEthereumDocumentStoreRevoked", status: "SKIPPED", type: "DOCUMENT_STATUS" diff --git a/src/verify.v3.integration.test.ts b/src/verify.v3.integration.test.ts index af049adc..7e589ec2 100644 --- a/src/verify.v3.integration.test.ts +++ b/src/verify.v3.integration.test.ts @@ -14,19 +14,21 @@ describe("verify v3(integration)", () => { expect(results).toStrictEqual([ { data: false, - message: "Certificate has been tampered with", + reason: { + code: 0, + codeString: "DOCUMENT_TAMPERED", + message: "Certificate has been tampered with" + }, status: "INVALID", name: "OpenAttestationHash", type: "DOCUMENT_INTEGRITY" }, { data: { - details: [ - { - address: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3", - issued: true - } - ], + details: { + address: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3", + issued: true + }, issuedOnAll: true }, status: "VALID", @@ -34,19 +36,21 @@ describe("verify v3(integration)", () => { type: "DOCUMENT_STATUS" }, { - message: 'Document issuers doesn\'t have "tokenRegistry" property or TOKEN_REGISTRY method', + reason: { + code: 4, + codeString: "SKIPPED", + message: 'Document issuers doesn\'t have "tokenRegistry" property or TOKEN_REGISTRY method' + }, name: "OpenAttestationEthereumTokenRegistryMinted", status: "SKIPPED", type: "DOCUMENT_STATUS" }, { data: { - details: [ - { - address: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3", - revoked: false - } - ], + details: { + address: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3", + revoked: false + }, revokedOnAny: false }, status: "VALID", @@ -59,7 +63,11 @@ describe("verify v3(integration)", () => { status: "INVALID", value: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3" }, - message: "Certificate issuer identity is invalid", + reason: { + code: 1, + codeString: "INVALID_IDENTITY", + message: "Certificate issuer identity is invalid" + }, name: "OpenAttestationDnsTxt", status: "INVALID", type: "ISSUER_IDENTITY" @@ -80,33 +88,44 @@ describe("verify v3(integration)", () => { }, { data: { - details: [ - { - address: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3", - issued: false + details: { + address: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3", + issued: false, + reason: { + code: 1, + codeString: "DOCUMENT_NOT_ISSUED", + message: + "Certificate 0x76cb959f49db0cffc05107af4a3ecef14092fd445d9acb0c2e7e27908d262142 has not been issued under contract 0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3" } - ], + }, issuedOnAll: false }, - message: "Certificate has not been issued", + reason: { + code: 1, + codeString: "DOCUMENT_NOT_ISSUED", + message: + "Certificate 0x76cb959f49db0cffc05107af4a3ecef14092fd445d9acb0c2e7e27908d262142 has not been issued under contract 0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3" + }, status: "INVALID", name: "OpenAttestationEthereumDocumentStoreIssued", type: "DOCUMENT_STATUS" }, { - message: 'Document issuers doesn\'t have "tokenRegistry" property or TOKEN_REGISTRY method', + reason: { + code: 4, + codeString: "SKIPPED", + message: 'Document issuers doesn\'t have "tokenRegistry" property or TOKEN_REGISTRY method' + }, name: "OpenAttestationEthereumTokenRegistryMinted", status: "SKIPPED", type: "DOCUMENT_STATUS" }, { data: { - details: [ - { - address: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3", - revoked: false - } - ], + details: { + address: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3", + revoked: false + }, revokedOnAny: false }, status: "VALID", @@ -119,7 +138,11 @@ describe("verify v3(integration)", () => { status: "INVALID", value: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3" }, - message: "Certificate issuer identity is invalid", + reason: { + code: 1, + codeString: "INVALID_IDENTITY", + message: "Certificate issuer identity is invalid" + }, name: "OpenAttestationDnsTxt", status: "INVALID", type: "ISSUER_IDENTITY" @@ -141,12 +164,10 @@ describe("verify v3(integration)", () => { }, { data: { - details: [ - { - address: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3", - issued: true - } - ], + details: { + address: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3", + issued: true + }, issuedOnAll: true }, status: "VALID", @@ -154,22 +175,35 @@ describe("verify v3(integration)", () => { type: "DOCUMENT_STATUS" }, { - message: 'Document issuers doesn\'t have "tokenRegistry" property or TOKEN_REGISTRY method', + reason: { + code: 4, + codeString: "SKIPPED", + message: 'Document issuers doesn\'t have "tokenRegistry" property or TOKEN_REGISTRY method' + }, name: "OpenAttestationEthereumTokenRegistryMinted", status: "SKIPPED", type: "DOCUMENT_STATUS" }, { data: { - details: [ - { - address: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3", - revoked: true + details: { + address: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3", + revoked: true, + reason: { + code: 1, + codeString: "DOCUMENT_REVOKED", + message: + "Certificate 0xba106f273697b46862f5842fc805902fa65d1f41d50953e0aeb815e43e989fc1 has been revoked under contract 0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3" } - ], + }, revokedOnAny: true }, - message: "Certificate has been revoked", + reason: { + code: 1, + codeString: "DOCUMENT_REVOKED", + message: + "Certificate 0xba106f273697b46862f5842fc805902fa65d1f41d50953e0aeb815e43e989fc1 has been revoked under contract 0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3" + }, status: "INVALID", name: "OpenAttestationEthereumDocumentStoreRevoked", type: "DOCUMENT_STATUS" @@ -180,7 +214,11 @@ describe("verify v3(integration)", () => { status: "INVALID", value: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3" }, - message: "Certificate issuer identity is invalid", + reason: { + code: 1, + codeString: "INVALID_IDENTITY", + message: "Certificate issuer identity is invalid" + }, name: "OpenAttestationDnsTxt", status: "INVALID", type: "ISSUER_IDENTITY" @@ -202,12 +240,10 @@ describe("verify v3(integration)", () => { }, { data: { - details: [ - { - address: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3", - issued: true - } - ], + details: { + address: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3", + issued: true + }, issuedOnAll: true }, status: "VALID", @@ -215,19 +251,21 @@ describe("verify v3(integration)", () => { type: "DOCUMENT_STATUS" }, { - message: 'Document issuers doesn\'t have "tokenRegistry" property or TOKEN_REGISTRY method', + reason: { + code: 4, + codeString: "SKIPPED", + message: 'Document issuers doesn\'t have "tokenRegistry" property or TOKEN_REGISTRY method' + }, name: "OpenAttestationEthereumTokenRegistryMinted", status: "SKIPPED", type: "DOCUMENT_STATUS" }, { data: { - details: [ - { - address: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3", - revoked: false - } - ], + details: { + address: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3", + revoked: false + }, revokedOnAny: false }, status: "VALID", @@ -240,7 +278,11 @@ describe("verify v3(integration)", () => { status: "INVALID", value: "0x8Fc57204c35fb9317D91285eF52D6b892EC08cD3" }, - message: "Certificate issuer identity is invalid", + reason: { + code: 1, + codeString: "INVALID_IDENTITY", + message: "Certificate issuer identity is invalid" + }, name: "OpenAttestationDnsTxt", status: "INVALID", type: "ISSUER_IDENTITY" @@ -262,20 +304,22 @@ describe("verify v3(integration)", () => { type: "DOCUMENT_INTEGRITY" }, { - message: - 'Document issuers doesn\'t have "documentStore" or "certificateStore" property or DOCUMENT_STORE method', + reason: { + code: 4, + codeString: "SKIPPED", + message: + 'Document issuers doesn\'t have "documentStore" or "certificateStore" property or DOCUMENT_STORE method' + }, name: "OpenAttestationEthereumDocumentStoreIssued", status: "SKIPPED", type: "DOCUMENT_STATUS" }, { data: { - details: [ - { - address: "0xb53499ee758352fAdDfCed863d9ac35C809E2F20", - minted: true - } - ], + details: { + address: "0xb53499ee758352fAdDfCed863d9ac35C809E2F20", + minted: true + }, mintedOnAll: true }, status: "VALID", @@ -283,8 +327,12 @@ describe("verify v3(integration)", () => { type: "DOCUMENT_STATUS" }, { - message: - 'Document issuers doesn\'t have "documentStore" or "certificateStore" property or DOCUMENT_STORE method', + reason: { + code: 4, + codeString: "SKIPPED", + message: + 'Document issuers doesn\'t have "documentStore" or "certificateStore" property or DOCUMENT_STORE method' + }, name: "OpenAttestationEthereumDocumentStoreRevoked", status: "SKIPPED", type: "DOCUMENT_STATUS" @@ -295,7 +343,11 @@ describe("verify v3(integration)", () => { status: "INVALID", value: "0xb53499ee758352fAdDfCed863d9ac35C809E2F20" }, - message: "Certificate issuer identity is invalid", + reason: { + code: 1, + codeString: "INVALID_IDENTITY", + message: "Certificate issuer identity is invalid" + }, name: "OpenAttestationDnsTxt", status: "INVALID", type: "ISSUER_IDENTITY" diff --git a/test/fixtures/v2/documentRopstenRevokedWithDocumentStore.ts b/test/fixtures/v2/documentRopstenRevokedWithDocumentStore.ts index c9512689..ecbd23f8 100644 --- a/test/fixtures/v2/documentRopstenRevokedWithDocumentStore.ts +++ b/test/fixtures/v2/documentRopstenRevokedWithDocumentStore.ts @@ -1,6 +1,18 @@ -import { WrappedDocument } from "@govtechsg/open-attestation"; +import { v2, WrappedDocument } from "@govtechsg/open-attestation"; -export const documentRopstenRevokedWithDocumentStore: WrappedDocument = { +interface CustomDocument extends v2.OpenAttestationDocument { + recipient: { + name: string; + address: { + street: string; + country: string; + }; + }; + consignment: any; + declaration: any; +} + +export const documentRopstenRevokedWithDocumentStore: WrappedDocument = { version: "open-attestation/2.0", schema: "tradetrust/1.0", data: { diff --git a/test/fixtures/v2/tamperedDocument.ts b/test/fixtures/v2/tamperedDocument.ts index a834bd4f..2925c6f7 100644 --- a/test/fixtures/v2/tamperedDocument.ts +++ b/test/fixtures/v2/tamperedDocument.ts @@ -57,3 +57,50 @@ export const tamperedDocumentWithCertificateStore: WrappedDocument = { + version: "open-attestation/2.0", + schema: "tradetrust/1.0", + data: { + id: "046bebd9-1c59-4d82-b70b-b6c8aa5c502d:string:2018091259", + name: "e91c1c2e-534b-43d1-b73b-e2de2c799242:string:SEAB Certificate for SEAB", + issuedOn: "8332eaaa-cb87-46ae-a5d0-1f9cae661fba:string:2018-08-31T23:59:32+08:00", + issuers: [ + { + name: "1f525e1b-50c3-49b7-bfbf-0f110453ff3b:string:Singapore Examination and Assessment Board", + url: "de6fcb26-c53a-49fc-a872-432213d135f3:string:https://www.seab.gov.sg/", + certificateStore: "4b467479-77ed-47c7-bfdf-7be8e6618dcd:string:0x20bc9C354A18C8178A713B9BcCFFaC2152b53991" + } + ], + recipient: { + name: "9dcfe063-b692-4ff6-901c-134c29e4e1df:string:John Snow", + email: "03a578d3-ab41-4572-bf7b-fc8763832918:string:johnsnow@gmail.com", + phone: "a2be0e32-4a9e-45fa-9690-a300fadaa5f7:string:+6588888888" + }, + transcript: [ + { + name: "97fb29de-56db-4cde-9032-5ef91423bcf4:string:Introduction to SEAB", + grade: "df28e06b-d06f-4283-ae72-b5b3e30aee28:string:A+", + courseCredit: "50dc8869-ef3c-4276-a72e-c247d6e64b72:number:3", + courseCode: "519f79a3-4f8c-4345-9474-d173e6c9a7fe:string:SEAB-HIST", + url: "c3954ba1-023c-45ad-bbd4-eb4a8df82dc2:string:https://www.seab.gov.sg/pages/about/introduction", + description: "8d6e4172-51e2-49ba-b98b-d1c5906a0339:string:Understanding the vision, mission, and values" + }, + { + name: "4da6b8d8-21d3-4228-937b-acc65a648bf7:string:SEAB - About Us", + grade: "1cf384cd-8951-4a56-9e42-e3d9954d0c85:string:A", + courseCredit: "f000cbd2-cf8d-4bf6-94b7-6e73f8037c3f:number:3", + courseCode: "d52233d5-a137-4e94-ad75-daac38536c87:string:SEAB-ABT", + url: "148a3906-8cfe-4971-9f17-6f2751a54b8e:string:https://www.seab.gov.sg/pages/about/aboutus", + description: "69179ec5-78fc-427e-8680-ba91f86d761f:string:About the history of SEAB" + } + ] + }, + privacy: {}, + signature: { + type: "SHA3MerkleProof", + targetHash: "85df2b4e905a82cf10c317df8f4b659b5cf38cc12bd5fbaffba5fc901ef0011b", + proof: [], + merkleRoot: "85df2b4e905a82cf10c317df8f4b659b5cf38cc12bd5fbaffba5fc901ef0011b" + } +};