From 661d150b92e0a651ae80851e3693783b1f872fdd Mon Sep 17 00:00:00 2001 From: Nebulis Date: Thu, 12 Dec 2019 18:29:58 +0800 Subject: [PATCH] feat: implement verifier manager v2 --- .eslintrc.json | 5 +- .../smartContract/contractInstance.test.ts | 5 +- src/common/smartContract/contractInstance.ts | 5 +- .../documentToSmartContracts.test.ts | 4 +- .../smartContract/documentToSmartContracts.ts | 10 +- .../issuerToSmartContract.test.ts | 8 +- .../smartContract/issuerToSmartContract.ts | 14 ++- src/hash/hash.ts | 5 +- src/index.ts | 1 + .../contractInterface.integration.test.ts | 41 +++---- src/issued/verify.test.ts | 20 +--- .../contractInterface.integration.test.ts | 86 ++++----------- src/revoked/verify.test.ts | 55 +++------- src/types/core.ts | 17 ++- .../openAttestationDnsTxtIdentity.ts | 100 ++++++++++-------- ...nAttestationEthereumDocumentStoreIssued.ts | 16 ++- ...AttestationEthereumDocumentStoreRevoked.ts | 15 ++- src/verifiers/openAttestationTamperCheck.ts | 10 +- src/verifiers/verificationManager.ts | 20 ++-- src/verify.integration.test.ts | 39 ++++--- src/verify.test.ts | 93 ---------------- 21 files changed, 208 insertions(+), 361 deletions(-) delete mode 100644 src/verify.test.ts diff --git a/.eslintrc.json b/.eslintrc.json index 21325d0c..840eadf8 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -23,7 +23,8 @@ "import/no-unresolved": "off", "import/prefer-default-export": "off", "import/extensions": "off", - "@typescript-eslint/explicit-function-return-type":"off", - "@typescript-eslint/no-explicit-any":"off" + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-explicit-any": "off", + "no-unused-expressions": "off" } } diff --git a/src/common/smartContract/contractInstance.test.ts b/src/common/smartContract/contractInstance.test.ts index ebc7c13a..afc56cf3 100644 --- a/src/common/smartContract/contractInstance.test.ts +++ b/src/common/smartContract/contractInstance.test.ts @@ -12,10 +12,7 @@ it("creates a ethers.Contract instance with the right provider", () => { }); // @ts-ignore - expect(ethers.providers.InfuraProvider.mock.calls[0]).toEqual([ - "NETWORK", - "INFURA_API_KEY" - ]); + expect(ethers.providers.InfuraProvider.mock.calls[0]).toEqual(["NETWORK", "INFURA_API_KEY"]); // @ts-ignore expect(ethers.Contract.mock.calls[0][0]).toEqual("0x0A"); diff --git a/src/common/smartContract/contractInstance.ts b/src/common/smartContract/contractInstance.ts index 49561708..580a8741 100644 --- a/src/common/smartContract/contractInstance.ts +++ b/src/common/smartContract/contractInstance.ts @@ -6,8 +6,9 @@ interface ContractInstance { abi: any; // type is any of json file in abi folder network: string; contractAddress: Hash; + infuraApiKey?: string; } -export const contractInstance = ({ abi, network, contractAddress }: ContractInstance) => { - const provider = new ethers.providers.InfuraProvider(network, INFURA_API_KEY); +export const contractInstance = ({ abi, network, contractAddress, infuraApiKey }: ContractInstance) => { + const provider = new ethers.providers.InfuraProvider(network, infuraApiKey || INFURA_API_KEY); return new ethers.Contract(contractAddress, abi, provider); }; diff --git a/src/common/smartContract/documentToSmartContracts.test.ts b/src/common/smartContract/documentToSmartContracts.test.ts index 2197cd13..839009a0 100644 --- a/src/common/smartContract/documentToSmartContracts.test.ts +++ b/src/common/smartContract/documentToSmartContracts.test.ts @@ -15,7 +15,7 @@ it("returns an array of smart contract resolved from the issuers of the document ]; // @ts-ignore issuerToSmartContract.mockReturnValue(expectedValue[0]); - const results = documentToSmartContracts(document, network); + const results = documentToSmartContracts(document, { network }); // @ts-ignore expect(issuerToSmartContract.mock.calls[0]).toEqual([ { @@ -23,7 +23,7 @@ it("returns an array of smart contract resolved from the issuers of the document url: "https://www.seab.gov.sg/", certificateStore: "0x20bc9C354A18C8178A713B9BcCFFaC2152b53990" }, - network + { network } ]); expect(results).toEqual(expectedValue); }); diff --git a/src/common/smartContract/documentToSmartContracts.ts b/src/common/smartContract/documentToSmartContracts.ts index 335c1033..133ea682 100644 --- a/src/common/smartContract/documentToSmartContracts.ts +++ b/src/common/smartContract/documentToSmartContracts.ts @@ -2,14 +2,10 @@ import { Document, getData } from "@govtechsg/open-attestation"; import { issuerToSmartContract } from "./issuerToSmartContract"; import { Issuer } from "../../types/core"; -// Given a list of issuers, convert to smart contract -const mapIssuersToSmartContracts = (issuers: Issuer[], network: string) => - issuers.map(issuer => issuerToSmartContract(issuer, network)); - // Given a raw document, return list of all smart contracts -export const documentToSmartContracts = (document: Document, network: string) => { +export const documentToSmartContracts = (document: Document, options: { network: string; infuraApiKey?: string }) => { const data = getData(document); - const issuers = data.issuers || []; + const issuers: Issuer[] = data.issuers || []; - return mapIssuersToSmartContracts(issuers, network); + return issuers.map(issuer => issuerToSmartContract(issuer, options)); }; diff --git a/src/common/smartContract/issuerToSmartContract.test.ts b/src/common/smartContract/issuerToSmartContract.test.ts index 7d46abcc..ca47f133 100644 --- a/src/common/smartContract/issuerToSmartContract.test.ts +++ b/src/common/smartContract/issuerToSmartContract.test.ts @@ -6,7 +6,7 @@ it("maps issuer with certificateStore to documentStore contract", () => { certificateStore: "0xc36484efa1544c32ffed2e80a1ea9f0dfc517495" }; - const result = issuerToSmartContract(issuer, "homestead"); + const result = issuerToSmartContract(issuer, { network: "homestead" }); expect(result).toEqual( expect.objectContaining({ type: "DOCUMENT_STORE", @@ -20,7 +20,7 @@ it("maps issuer with documentStore to documentStore contract", () => { name: "Org A", documentStore: "0xc36484efa1544c32ffed2e80a1ea9f0dfc517495" }; - const result = issuerToSmartContract(issuer, "homestead"); + const result = issuerToSmartContract(issuer, { network: "homestead" }); expect(result).toEqual( expect.objectContaining({ type: "DOCUMENT_STORE", @@ -34,7 +34,7 @@ it("maps issuer with tokenRegistry to tokenRegistry contract", () => { name: "Org A", tokenRegistry: "0xc36484efa1544c32ffed2e80a1ea9f0dfc517495" }; - const result = issuerToSmartContract(issuer, "homestead"); + const result = issuerToSmartContract(issuer, { network: "homestead" }); expect(result).toEqual( expect.objectContaining({ type: "TOKEN_REGISTRY", @@ -49,7 +49,7 @@ it("throws if the issuer does not have a valid smart contract address defined", foo: "bar" }; // @ts-ignore - expect(() => issuerToSmartContract(issuer, "homestead")).toThrow( + expect(() => issuerToSmartContract(issuer, { network: "homestead" })).toThrow( "Issuer does not have a smart contract" ); }); diff --git a/src/common/smartContract/issuerToSmartContract.ts b/src/common/smartContract/issuerToSmartContract.ts index 7bf158ad..e0c36471 100644 --- a/src/common/smartContract/issuerToSmartContract.ts +++ b/src/common/smartContract/issuerToSmartContract.ts @@ -5,7 +5,10 @@ import tokenRegistryAbi from "./abi/tokenRegistry.json"; import documentStoreAbi from "./abi/documentStore.json"; import { Issuer, OpenAttestationContract } from "../../types/core"; -export const issuerToSmartContract = (issuer: Issuer, network: string): OpenAttestationContract => { +export const issuerToSmartContract = ( + issuer: Issuer, + options: { network: string; infuraApiKey?: string } +): OpenAttestationContract => { switch (true) { case "tokenRegistry" in issuer: return { @@ -14,7 +17,8 @@ export const issuerToSmartContract = (issuer: Issuer, network: string): OpenAtte instance: contractInstance({ contractAddress: issuer.tokenRegistry!, abi: tokenRegistryAbi, - network + network: options.network, + infuraApiKey: options.infuraApiKey }) }; case "certificateStore" in issuer: @@ -24,7 +28,8 @@ export const issuerToSmartContract = (issuer: Issuer, network: string): OpenAtte instance: contractInstance({ contractAddress: issuer.certificateStore!, abi: documentStoreAbi, - network + network: options.network, + infuraApiKey: options.infuraApiKey }) }; case "documentStore" in issuer: @@ -34,7 +39,8 @@ export const issuerToSmartContract = (issuer: Issuer, network: string): OpenAtte instance: contractInstance({ contractAddress: issuer.documentStore!, abi: documentStoreAbi, - network + network: options.network, + infuraApiKey: options.infuraApiKey }) }; default: diff --git a/src/hash/hash.ts b/src/hash/hash.ts index 599b41b2..f87c984b 100644 --- a/src/hash/hash.ts +++ b/src/hash/hash.ts @@ -1,7 +1,4 @@ -import { - verifySignature, - SchematisedDocument -} from "@govtechsg/open-attestation"; +import { verifySignature, SchematisedDocument } from "@govtechsg/open-attestation"; export const verifyHash = async (document: SchematisedDocument) => ({ checksumMatch: await verifySignature(document) diff --git a/src/index.ts b/src/index.ts index c7b5ed6f..3ab222db 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,6 +11,7 @@ const defaultVerifiers: Verifier[] = [ openAttestationEthereumDocumentStoreRevoked, openAttestationDnsTxtIdentity ]; + const defaultVerificationManager = verificationManager(defaultVerifiers); export { diff --git a/src/issued/contractInterface.integration.test.ts b/src/issued/contractInterface.integration.test.ts index cef59af7..db666343 100644 --- a/src/issued/contractInterface.integration.test.ts +++ b/src/issued/contractInterface.integration.test.ts @@ -1,16 +1,12 @@ import { constants } from "ethers"; -import { - isIssued, - isIssuedOnDocumentStore, - isIssuedOnTokenRegistry -} from "./contractInterface"; +import { isIssued, isIssuedOnDocumentStore, isIssuedOnTokenRegistry } from "./contractInterface"; import { issuerToSmartContract } from "../common/smartContract/issuerToSmartContract"; describe("isIssuedOnTokenRegistry", () => { it("returns true if token is created on tokenRegistry", async () => { const smartContract = issuerToSmartContract( { tokenRegistry: "0x48399Fb88bcD031C556F53e93F690EEC07963Af3" }, - "ropsten" + { network: "ropsten" } ); const issued = await isIssuedOnTokenRegistry( smartContract, @@ -22,11 +18,9 @@ describe("isIssuedOnTokenRegistry", () => { it("allows error to bubble if token is nonexistent on tokenRegistry", async () => { const smartContract = issuerToSmartContract( { tokenRegistry: "0x48399Fb88bcD031C556F53e93F690EEC07963Af3" }, - "ropsten" + { network: "ropsten" } ); - await expect( - isIssuedOnTokenRegistry(smartContract, constants.HashZero) - ).rejects.toThrow(); + await expect(isIssuedOnTokenRegistry(smartContract, constants.HashZero)).rejects.toThrow(); }); }); @@ -34,7 +28,7 @@ describe("isIssuedOnDocumentStore", () => { it("returns true if document is issued on documentStore", async () => { const smartContract = issuerToSmartContract( { documentStore: "0x008486e2b14Cb1B5231DbA10B2170271af3196d6" }, - "ropsten" + { network: "ropsten" } ); const issued = await isIssuedOnDocumentStore( smartContract, @@ -47,20 +41,15 @@ describe("isIssuedOnDocumentStore", () => { const hash = constants.HashZero; const smartContract = issuerToSmartContract( { documentStore: "0x008486e2b14Cb1B5231DbA10B2170271af3196d6" }, - "ropsten" + { network: "ropsten" } ); const issued = await isIssuedOnDocumentStore(smartContract, hash); expect(issued).toBe(false); }); it("allows error to bubble if documentStore is not deployed", async () => { - const smartContract = issuerToSmartContract( - { documentStore: constants.AddressZero }, - "ropsten" - ); - await expect( - isIssuedOnDocumentStore(smartContract, constants.HashZero) - ).rejects.toThrow(); + const smartContract = issuerToSmartContract({ documentStore: constants.AddressZero }, { network: "ropsten" }); + await expect(isIssuedOnDocumentStore(smartContract, constants.HashZero)).rejects.toThrow(); }); }); @@ -68,20 +57,18 @@ describe("isIssued", () => { it("works for tokenRegistry", async () => { const smartContract = issuerToSmartContract( { tokenRegistry: "0x48399Fb88bcD031C556F53e93F690EEC07963Af3" }, - "ropsten" + { network: "ropsten" } ); - const hash = - "0x30cc3db1f2b26e25d63a67b6f232c4cf2acd1402f632847a4857e73516a0762f"; + const hash = "0x30cc3db1f2b26e25d63a67b6f232c4cf2acd1402f632847a4857e73516a0762f"; const issued = await isIssued(smartContract, hash); expect(issued).toBe(true); }); it("works for documentStore", async () => { - const hash = - "0x85df2b4e905a82cf10c317df8f4b659b5cf38cc12bd5fbaffba5fc901ef0011b"; + const hash = "0x85df2b4e905a82cf10c317df8f4b659b5cf38cc12bd5fbaffba5fc901ef0011b"; const smartContract = issuerToSmartContract( { documentStore: "0x008486e2b14Cb1B5231DbA10B2170271af3196d6" }, - "ropsten" + { network: "ropsten" } ); const issued = await isIssued(smartContract, hash); expect(issued).toBe(true); @@ -90,8 +77,6 @@ describe("isIssued", () => { 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" - ); + expect(() => isIssued(smartContract, constants.HashZero)).toThrow("Smart contract type not supported"); }); }); diff --git a/src/issued/verify.test.ts b/src/issued/verify.test.ts index 83d82846..bc270db3 100644 --- a/src/issued/verify.test.ts +++ b/src/issued/verify.test.ts @@ -101,14 +101,8 @@ describe("verifyIssued", () => { }); // @ts-ignore expect(isIssued.mock.calls).toEqual([ - [ - { address: "0x0A", type: "type", instance: contract, foo: "bar" }, - "0xMERKLE_ROOT" - ], - [ - { address: "0x0B", type: "type", instance: contract, foo: "bar" }, - "0xMERKLE_ROOT" - ] + [{ address: "0x0A", type: "type", instance: contract, foo: "bar" }, "0xMERKLE_ROOT"], + [{ address: "0x0B", type: "type", instance: contract, foo: "bar" }, "0xMERKLE_ROOT"] ]); }); @@ -143,14 +137,8 @@ describe("verifyIssued", () => { }); // @ts-ignore expect(isIssued.mock.calls).toEqual([ - [ - { address: "0x0A", type: "type", instance: contract, foo: "bar" }, - "0xMERKLE_ROOT" - ], - [ - { address: "0x0B", type: "type", instance: contract, foo: "bar" }, - "0xMERKLE_ROOT" - ] + [{ address: "0x0A", type: "type", instance: contract, foo: "bar" }, "0xMERKLE_ROOT"], + [{ address: "0x0B", type: "type", instance: contract, foo: "bar" }, "0xMERKLE_ROOT"] ]); }); }); diff --git a/src/revoked/contractInterface.integration.test.ts b/src/revoked/contractInterface.integration.test.ts index 278e58e5..c3c08f01 100644 --- a/src/revoked/contractInterface.integration.test.ts +++ b/src/revoked/contractInterface.integration.test.ts @@ -1,113 +1,65 @@ import { constants } from "ethers"; -import { - isRevoked, - isRevokedOnDocumentStore, - isRevokedOnTokenRegistry -} from "./contractInterface"; +import { isRevoked, isRevokedOnDocumentStore, isRevokedOnTokenRegistry } from "./contractInterface"; import { issuerToSmartContract } from "../common/smartContract/issuerToSmartContract"; const TOKEN_REGISTRY = "0x48399Fb88bcD031C556F53e93F690EEC07963Af3"; -const TOKEN_WITH_OWNER = - "0x30cc3db1f2b26e25d63a67b6f232c4cf2acd1402f632847a4857e73516a0762f"; -const TOKEN_WITH_SMART_CONTRACT = - "0x30cc3db1f2b26e25d63a67b6f232c4cf2acd1402f632847a4857e73516a0762e"; +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 = issuerToSmartContract( - { tokenRegistry: TOKEN_REGISTRY }, - "ropsten" - ); - const issued = await isRevokedOnTokenRegistry( - smartContract, - TOKEN_WITH_OWNER - ); + const smartContract = issuerToSmartContract({ tokenRegistry: TOKEN_REGISTRY }, { network: "ropsten" }); + const issued = await isRevokedOnTokenRegistry(smartContract, TOKEN_WITH_OWNER); expect(issued).toBe(false); }); it("returns true if owner of token is the smart contract itself", async () => { - const smartContract = issuerToSmartContract( - { tokenRegistry: TOKEN_REGISTRY }, - "ropsten" - ); - const issued = await isRevokedOnTokenRegistry( - smartContract, - TOKEN_WITH_SMART_CONTRACT - ); + const smartContract = issuerToSmartContract({ tokenRegistry: TOKEN_REGISTRY }, { network: "ropsten" }); + const issued = await isRevokedOnTokenRegistry(smartContract, TOKEN_WITH_SMART_CONTRACT); expect(issued).toBe(true); }); it("allow errors to bubble if token is not minted", async () => { - const smartContract = issuerToSmartContract( - { tokenRegistry: TOKEN_REGISTRY }, - "ropsten" - ); - await expect( - isRevokedOnTokenRegistry(smartContract, TOKEN_UNMINTED) - ).rejects.toThrow(); + const smartContract = issuerToSmartContract({ tokenRegistry: TOKEN_REGISTRY }, { network: "ropsten" }); + await expect(isRevokedOnTokenRegistry(smartContract, TOKEN_UNMINTED)).rejects.toThrow(); }); }); const DOCUMENT_STORE = "0x008486e2b14Cb1B5231DbA10B2170271af3196d6"; -const DOCUMENT_REVOKED = - "0x85df2b4e905a82cf10c317df8f4b659b5cf38cc12bd5fbaffba5fc901ef0011a"; -const DOCUMENT_UNREVOKED = - "0x85df2b4e905a82cf10c317df8f4b659b5cf38cc12bd5fbaffba5fc901ef0011b"; +const DOCUMENT_REVOKED = "0x85df2b4e905a82cf10c317df8f4b659b5cf38cc12bd5fbaffba5fc901ef0011a"; +const DOCUMENT_UNREVOKED = "0x85df2b4e905a82cf10c317df8f4b659b5cf38cc12bd5fbaffba5fc901ef0011b"; describe("isRevokedOnDocumentStore", () => { it("returns true if document is revoked on documentStore", async () => { - const smartContract = issuerToSmartContract( - { documentStore: DOCUMENT_STORE }, - "ropsten" - ); - const revoked = await isRevokedOnDocumentStore( - smartContract, - DOCUMENT_REVOKED - ); + const smartContract = issuerToSmartContract({ documentStore: DOCUMENT_STORE }, { network: "ropsten" }); + const revoked = await isRevokedOnDocumentStore(smartContract, DOCUMENT_REVOKED); expect(revoked).toBe(true); }); it("returns false if document is not issued on documentStore", async () => { - const smartContract = issuerToSmartContract( - { documentStore: DOCUMENT_STORE }, - "ropsten" - ); - const issued = await isRevokedOnDocumentStore( - smartContract, - DOCUMENT_UNREVOKED - ); + const smartContract = issuerToSmartContract({ documentStore: DOCUMENT_STORE }, { network: "ropsten" }); + const issued = await isRevokedOnDocumentStore(smartContract, DOCUMENT_UNREVOKED); expect(issued).toBe(false); }); it("allows error to bubble up if documentStore is not deployed", async () => { - const smartContract = issuerToSmartContract( - { documentStore: constants.AddressZero }, - "ropsten" - ); - await expect( - isRevokedOnDocumentStore(smartContract, DOCUMENT_UNREVOKED) - ).rejects.toThrow(); + const smartContract = issuerToSmartContract({ documentStore: constants.AddressZero }, { network: "ropsten" }); + await expect(isRevokedOnDocumentStore(smartContract, DOCUMENT_UNREVOKED)).rejects.toThrow(); }); }); describe("isRevoked", () => { it("works for tokenRegistry", async () => { - const smartContract = issuerToSmartContract( - { tokenRegistry: TOKEN_REGISTRY }, - "ropsten" - ); + const smartContract = issuerToSmartContract({ tokenRegistry: TOKEN_REGISTRY }, { network: "ropsten" }); const issued = await isRevoked(smartContract, TOKEN_WITH_OWNER); expect(issued).toBe(false); }); it("works for documentStore", async () => { - const smartContract = issuerToSmartContract( - { documentStore: DOCUMENT_STORE }, - "ropsten" - ); + const smartContract = issuerToSmartContract({ documentStore: DOCUMENT_STORE }, { network: "ropsten" }); const revoked = await isRevoked(smartContract, DOCUMENT_REVOKED); expect(revoked).toBe(true); }); diff --git a/src/revoked/verify.test.ts b/src/revoked/verify.test.ts index c1e38632..9ecee10e 100644 --- a/src/revoked/verify.test.ts +++ b/src/revoked/verify.test.ts @@ -36,10 +36,7 @@ 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 - ); + const revoked = await isAnyHashRevokedOnStore(TOKEN_REGISTRY_CONTRACT, INTERMEDIATE_HASHES); expect(revoked).toBe(false); }); @@ -50,10 +47,7 @@ describe("isAnyHashRevokedOnStore", () => { isRevoked.mockResolvedValueOnce(true); // @ts-ignore isRevoked.mockResolvedValueOnce(false); - const revoked = await isAnyHashRevokedOnStore( - TOKEN_REGISTRY_CONTRACT, - INTERMEDIATE_HASHES - ); + const revoked = await isAnyHashRevokedOnStore(TOKEN_REGISTRY_CONTRACT, INTERMEDIATE_HASHES); expect(revoked).toBe(true); }); }); @@ -63,10 +57,7 @@ describe("revokedStatusOnContracts", () => { // @ts-ignore isRevoked.mockResolvedValue(false); const smartContracts = [TOKEN_REGISTRY_CONTRACT, DOCUMENT_STORE_CONTRACT]; - const revokedStatus = await revokedStatusOnContracts( - smartContracts, - INTERMEDIATE_HASHES - ); + const revokedStatus = await revokedStatusOnContracts(smartContracts, INTERMEDIATE_HASHES); expect(revokedStatus).toEqual([ { address: "0x0A", revoked: false }, { address: "0x0B", revoked: false } @@ -125,10 +116,7 @@ describe("revokedStatusOnContracts", () => { }); it("should return empty array if no smart contract is provided", async () => { - const revokedStatus = await revokedStatusOnContracts( - [], - INTERMEDIATE_HASHES - ); + const revokedStatus = await revokedStatusOnContracts([], INTERMEDIATE_HASHES); expect(revokedStatus).toEqual([]); }); @@ -138,10 +126,7 @@ describe("revokedStatusOnContracts", () => { // @ts-ignore isRevoked.mockRejectedValueOnce(new Error("Some error")); const smartContracts = [TOKEN_REGISTRY_CONTRACT, DOCUMENT_STORE_CONTRACT]; - const revokedStatus = await revokedStatusOnContracts( - smartContracts, - INTERMEDIATE_HASHES - ); + const revokedStatus = await revokedStatusOnContracts(smartContracts, INTERMEDIATE_HASHES); expect(revokedStatus).toEqual([ { address: "0x0A", revoked: true, error: "Some error" }, { address: "0x0B", revoked: false } @@ -169,19 +154,15 @@ describe("isRevokedOnAny", () => { describe("getIntermediateHashes", () => { it("returns array with single hash if no proof is present", () => { - const targetHash = - "f7432b3219b2aa4122e289f44901830fa32f224ee9dfce28565677f1d279b2c7"; - const expected = [ - "0xf7432b3219b2aa4122e289f44901830fa32f224ee9dfce28565677f1d279b2c7" - ]; + 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 targetHash = "f7432b3219b2aa4122e289f44901830fa32f224ee9dfce28565677f1d279b2c7"; const proofs = [ "2bb9dd186994f38084ee68e06be848b9d43077c307684c300d81df343c7858cf", "ed8bdba60a24af04bcdcd88b939251f3843e03839164fdd2dd502aaeef3bfb99" @@ -226,18 +207,12 @@ describe("verifyRevoked", () => { }); // @ts-ignore expect(isRevoked.mock.calls).toEqual([ - [ - { address: "0x0A", type: "type", instance: contract, foo: "bar" }, - "0x0d" - ], + [{ address: "0x0A", type: "type", instance: contract, foo: "bar" }, "0x0d"], [ { address: "0x0A", type: "type", instance: contract, foo: "bar" }, "0x96851128a70d034965e58c4ef4681d4ffcf60ba27322aa9015cf340f2b242e3d" ], - [ - { address: "0x0B", type: "type", instance: contract, foo: "bar" }, - "0x0d" - ], + [{ address: "0x0B", type: "type", instance: contract, foo: "bar" }, "0x0d"], [ { address: "0x0B", type: "type", instance: contract, foo: "bar" }, "0x96851128a70d034965e58c4ef4681d4ffcf60ba27322aa9015cf340f2b242e3d" @@ -276,14 +251,8 @@ describe("verifyRevoked", () => { }); // @ts-ignore expect(isRevoked.mock.calls).toEqual([ - [ - { address: "0x0A", type: "type", instance: contract, foo: "bar" }, - "0x0d" - ], - [ - { address: "0x0B", type: "type", instance: contract, foo: "bar" }, - "0x0d" - ] + [{ address: "0x0A", type: "type", instance: contract, foo: "bar" }, "0x0d"], + [{ address: "0x0B", type: "type", instance: contract, foo: "bar" }, "0x0d"] ]); }); }); diff --git a/src/types/core.ts b/src/types/core.ts index 8ad33412..672c7502 100644 --- a/src/types/core.ts +++ b/src/types/core.ts @@ -3,14 +3,19 @@ import { SignedDocument } from "@govtechsg/open-attestation"; import { TYPES } from "../common/smartContract/constants"; export interface VerificationManagerOptions { - networkName: string; - networkId: string; + network: string; + infuraApiKey?: string; + promisesCallback?: (promises: Promise[]) => void; } /** * A verification fragment is the result of a verification * It will *always* - * - return the status: VALID or ERROR + * - return the status + * - VALID: when the verification is successful + * - INVALID: when the verification is unsuccessful + * - ERROR: when an unexpected error is met + * - SKIPPED: when the verification was skipped by the manager * - return the type who can help to determine the verifier that created the result * * Additional fields might be populated @@ -21,7 +26,7 @@ export interface VerificationFragment { type: string; message?: string; data?: T; - status: "ERROR" | "VALID"; + status: "ERROR" | "VALID" | "INVALID" | "SKIPPED"; } /** @@ -30,6 +35,10 @@ export interface VerificationFragment { * - a *verify* function, who must return the result of the verification as a {@link VerificationFragment} */ export interface Verifier { + skip: ( + document: SignedDocument, + options: VerificationManagerOptions + ) => Promise<{ type: string; status: "SKIPPED"; message: string }>; test: (document: SignedDocument, options: VerificationManagerOptions) => boolean; verify: (document: SignedDocument, options: VerificationManagerOptions) => Promise; } diff --git a/src/verifiers/openAttestationDnsTxtIdentity.ts b/src/verifiers/openAttestationDnsTxtIdentity.ts index 94b08616..e6b79bf9 100644 --- a/src/verifiers/openAttestationDnsTxtIdentity.ts +++ b/src/verifiers/openAttestationDnsTxtIdentity.ts @@ -1,5 +1,6 @@ import { getData } from "@govtechsg/open-attestation"; import { getDocumentStoreRecords } from "@govtechsg/dnsprove"; +import { getNetwork } from "ethers/utils"; import { Issuer, VerificationManagerOptions, Verifier } from "../types/core"; const getSmartContractAddress = (issuer: Issuer) => issuer.documentStore || issuer.tokenRegistry; @@ -17,43 +18,42 @@ type Identity = }; // Resolve identity of an issuer, currently supporting only DNS-TXT const resolveIssuerIdentity = async (issuer: Issuer, options: VerificationManagerOptions): Promise => { - try { - // we expect the test function to prevent this issue => smart contract address MUST be populated - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const smartContractAddress = getSmartContractAddress(issuer)!; - const type = issuer?.identityProof?.type ?? ""; - const location = issuer?.identityProof?.location ?? ""; - if (type !== "DNS-TXT") throw new Error("Identity type not supported"); - if (!location) throw new Error("Location is missing"); - const records = await getDocumentStoreRecords(location); - const matchingRecord = records.find( - record => - record.addr.toLowerCase() === smartContractAddress.toLowerCase() && - record.netId === options.networkId && - record.type === "openatts" && - record.net === "ethereum" - ); - return matchingRecord - ? { - identified: true, - dns: location, - smartContract: smartContractAddress - } - : { - identified: false, - smartContract: smartContractAddress - }; - } catch (e) { - return { - identified: false, - smartContract: getSmartContractAddress(issuer) ?? "", - error: e.message || e - }; - } + // we expect the test function to prevent this issue => smart contract address MUST be populated + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const smartContractAddress = getSmartContractAddress(issuer)!; + const type = issuer?.identityProof?.type ?? ""; + const location = issuer?.identityProof?.location ?? ""; + if (type !== "DNS-TXT") throw new Error("Identity type not supported"); + if (!location) throw new Error("Location is missing"); + const records = await getDocumentStoreRecords(location); + const matchingRecord = records.find( + record => + record.addr.toLowerCase() === smartContractAddress.toLowerCase() && + record.netId === getNetwork(options.network).chainId.toString(10) && + record.type === "openatts" && + record.net === "ethereum" + ); + return matchingRecord + ? { + identified: true, + dns: location, + smartContract: smartContractAddress + } + : { + identified: false, + smartContract: smartContractAddress + }; }; -// OpenAttestationDnsTxtIdentity +const type = "OpenAttestationDnsTxtIdentity"; export const openAttestationDnsTxtIdentity: Verifier = { + skip: () => { + return Promise.resolve({ + status: "SKIPPED", + type, + message: 'Document issuers doesn\'t have "documentStore" or "token" property' + }); + }, test: document => { const documentData: { issuers: Issuer[] } = getData(document); return documentData.issuers.some(getSmartContractAddress); @@ -61,20 +61,30 @@ export const openAttestationDnsTxtIdentity: Verifier = { verify: async (document, options) => { const documentData: { issuers: Issuer[] } = getData(document); - const identities = await Promise.all(documentData.issuers.map(issuer => resolveIssuerIdentity(issuer, options))); - const invalidIdentity = identities.findIndex(identity => !identity.identified); - if (invalidIdentity !== -1) { + try { + const identities = await Promise.all(documentData.issuers.map(issuer => resolveIssuerIdentity(issuer, options))); + const invalidIdentity = identities.findIndex(identity => !identity.identified); + if (invalidIdentity !== -1) { + return { + type, + data: documentData.issuers[invalidIdentity]?.identityProof?.location ?? "No Identity found", + message: "Certificate issuer identity is invalid", + status: "INVALID" + }; + } + return { - type: "OpenAttestationDnsTxtIdentity", - data: documentData.issuers[invalidIdentity]?.identityProof?.location ?? "No Identity found", - message: "Certificate issuer identity is invalid", + type, + data: identities, + status: "VALID" + }; + } catch (e) { + return { + type, + data: e, + message: e.message, status: "ERROR" }; } - return { - type: "OpenAttestationDnsTxtIdentity", - data: identities, - status: "VALID" - }; } }; diff --git a/src/verifiers/openAttestationEthereumDocumentStoreIssued.ts b/src/verifiers/openAttestationEthereumDocumentStoreIssued.ts index fa0af6be..d7dde62d 100644 --- a/src/verifiers/openAttestationEthereumDocumentStoreIssued.ts +++ b/src/verifiers/openAttestationEthereumDocumentStoreIssued.ts @@ -2,21 +2,29 @@ import { Verifier } from "../types/core"; import { verifyIssued } from "../issued/verify"; import { documentToSmartContracts } from "../common/smartContract/documentToSmartContracts"; +const type = "OpenAttestationEthereumDocumentStoreIssued"; + export const openAttestationEthereumDocumentStoreIssued: Verifier = { + skip: () => { + throw new Error("This verifier is never skipped"); + }, test: () => true, verify: async (document, options) => { - const smartContracts = documentToSmartContracts(document, options.networkName); + const smartContracts = documentToSmartContracts(document, { + network: options.network, + infuraApiKey: options.infuraApiKey + }); const status = await verifyIssued(document, smartContracts); if (!status.issuedOnAll) { return { - type: "OpenAttestationEthereumDocumentStoreIssued", + type, data: status, message: "Certificate has not been issued", - status: "ERROR" + status: "INVALID" }; } return { - type: "OpenAttestationEthereumDocumentStoreIssued", + type, data: status, status: "VALID" }; diff --git a/src/verifiers/openAttestationEthereumDocumentStoreRevoked.ts b/src/verifiers/openAttestationEthereumDocumentStoreRevoked.ts index 712e7e40..790c6630 100644 --- a/src/verifiers/openAttestationEthereumDocumentStoreRevoked.ts +++ b/src/verifiers/openAttestationEthereumDocumentStoreRevoked.ts @@ -2,21 +2,28 @@ import { Verifier } from "../types/core"; import { documentToSmartContracts } from "../common/smartContract/documentToSmartContracts"; import { verifyRevoked } from "../revoked/verify"; +const type = "OpenAttestationEthereumDocumentStoreRevoked"; export const openAttestationEthereumDocumentStoreRevoked: Verifier = { + skip: () => { + throw new Error("This verifier is never skipped"); + }, test: () => true, verify: async (document, options) => { - const smartContracts = documentToSmartContracts(document, options.networkName); + const smartContracts = documentToSmartContracts(document, { + network: options.network, + infuraApiKey: options.infuraApiKey + }); const status = await verifyRevoked(document, smartContracts); if (status.revokedOnAny) { return { - type: "OpenAttestationEthereumDocumentStoreRevoked", + type, data: status, message: "Certificate has been revoked", - status: "ERROR" + status: "INVALID" }; } return { - type: "OpenAttestationEthereumDocumentStoreRevoked", + type, data: status, status: "VALID" }; diff --git a/src/verifiers/openAttestationTamperCheck.ts b/src/verifiers/openAttestationTamperCheck.ts index ca48f9fa..2b1b010b 100644 --- a/src/verifiers/openAttestationTamperCheck.ts +++ b/src/verifiers/openAttestationTamperCheck.ts @@ -2,21 +2,25 @@ import { SignedDocument } from "@govtechsg/open-attestation"; import { verifyHash } from "../hash/hash"; import { Verifier } from "../types/core"; +const type = "OpenAttestationTamperCheck"; export const openAttestationTamperCheck: Verifier = { + skip: () => { + throw new Error("This verifier is never skipped"); + }, test: () => true, verify: async (document: SignedDocument) => { const hash = await verifyHash(document); if (!hash.checksumMatch) { return { - type: "OpenAttestationTamperCheck", + type, data: hash, message: "Certificate has been tampered with", - status: "ERROR" + status: "INVALID" }; } return { + type, data: hash, - type: "OpenAttestationTamperCheck", status: "VALID" }; } diff --git a/src/verifiers/verificationManager.ts b/src/verifiers/verificationManager.ts index 078c215e..17b23e9d 100644 --- a/src/verifiers/verificationManager.ts +++ b/src/verifiers/verificationManager.ts @@ -6,17 +6,17 @@ import { VerificationManagerOptions, VerificationFragment, Verifier } from "../t * Before running each verifier, the manager will make sure the verifier can handle the specific document by calling its exposed test function. * The manager will return the consolidated list of {@link VerificationFragment} */ -export const verificationManager = (verifiers: Verifier[]) => async ( +export const verificationManager = (verifiers: Verifier[]) => ( document: SignedDocument, options: VerificationManagerOptions ): Promise => { - const results = await Promise.all( - verifiers.map(verifier => { - if (verifier.test(document, options)) { - return verifier.verify(document, options); - } - return null; - }) - ); - return results.filter((element: any): element is VerificationFragment => !!element); + const promises: Promise[] = verifiers.map(verifier => { + if (verifier.test(document, options)) { + return verifier.verify(document, options); + } + return verifier.skip(document, options); + }); + + options.promisesCallback?.(promises); + return Promise.all(promises); }; diff --git a/src/verify.integration.test.ts b/src/verify.integration.test.ts index f9d7313b..e8d3e7cf 100644 --- a/src/verify.integration.test.ts +++ b/src/verify.integration.test.ts @@ -8,13 +8,11 @@ import { documentTamperedWithCertificateStore } from "../test/fixtures/tampered- import { documentRopstenValidWithCertificateStore } from "../test/fixtures/documentRopstenValidWithCertificateStore"; import { documentRopstenValidWithToken } from "../test/fixtures/documentRopstenValidWithToken"; import { documentRopstenRevokedWithToken } from "../test/fixtures/documentRopstenRevokedWithToken"; -import { getData } from "@govtechsg/open-attestation"; describe("verify(integration)", () => { it("should for OpenAttestationTamperCheck and OpenAttestationEthereumDocumentStoreIssued when document's hash is invalid and was not issued", async () => { const results = await defaultVerificationManager(documentTamperedWithCertificateStore, { - networkName: "ropsten", - networkId: "3" + network: "ropsten" }); expect(results).toStrictEqual([ @@ -23,7 +21,7 @@ describe("verify(integration)", () => { checksumMatch: false }, message: "Certificate has been tampered with", - status: "ERROR", + status: "INVALID", type: "OpenAttestationTamperCheck" }, { @@ -39,7 +37,7 @@ describe("verify(integration)", () => { issuedOnAll: false }, message: "Certificate has not been issued", - status: "ERROR", + status: "INVALID", type: "OpenAttestationEthereumDocumentStoreIssued" }, { @@ -54,14 +52,18 @@ describe("verify(integration)", () => { }, status: "VALID", type: "OpenAttestationEthereumDocumentStoreRevoked" + }, + { + message: 'Document issuers doesn\'t have "documentStore" or "token" property', + status: "SKIPPED", + type: "OpenAttestationDnsTxtIdentity" } ]); }); it("should be valid for all checks when document is valid on mainnet", async () => { const results = await defaultVerificationManager(documentMainnetValidWithCertificateStore, { - networkId: "1", - networkName: "homestead" + network: "homestead" }); expect(results).toStrictEqual([ @@ -97,14 +99,18 @@ describe("verify(integration)", () => { }, status: "VALID", type: "OpenAttestationEthereumDocumentStoreRevoked" + }, + { + message: 'Document issuers doesn\'t have "documentStore" or "token" property', + status: "SKIPPED", + type: "OpenAttestationDnsTxtIdentity" } ]); }); it("should be valid for all checks when document is valid on ropsten", async () => { const results = await defaultVerificationManager(documentRopstenValidWithCertificateStore, { - networkName: "ropsten", - networkId: "3" + network: "ropsten" }); expect(results).toStrictEqual([ @@ -140,14 +146,18 @@ describe("verify(integration)", () => { }, status: "VALID", type: "OpenAttestationEthereumDocumentStoreRevoked" + }, + { + message: 'Document issuers doesn\'t have "documentStore" or "token" property', + status: "SKIPPED", + type: "OpenAttestationDnsTxtIdentity" } ]); }); it("returns true if Ropsten token is valid", async () => { const results = await defaultVerificationManager(documentRopstenValidWithToken, { - networkName: "ropsten", - networkId: "3" + network: "ropsten" }); expect(results).toStrictEqual([ @@ -200,8 +210,7 @@ describe("verify(integration)", () => { it("should fail for OpenAttestationEthereumDocumentStoreIssued and OpenAttestationEthereumDocumentStoreRevoked when document was not issued and was revoked", async () => { const results = await defaultVerificationManager(documentRopstenRevokedWithToken, { - networkName: "ropsten", - networkId: "3" + network: "ropsten" }); expect(results).toStrictEqual([ @@ -225,7 +234,7 @@ describe("verify(integration)", () => { issuedOnAll: false }, message: "Certificate has not been issued", - status: "ERROR", + status: "INVALID", type: "OpenAttestationEthereumDocumentStoreIssued" }, { @@ -241,7 +250,7 @@ describe("verify(integration)", () => { revokedOnAny: true }, message: "Certificate has been revoked", - status: "ERROR", + status: "INVALID", type: "OpenAttestationEthereumDocumentStoreRevoked" }, { diff --git a/src/verify.test.ts b/src/verify.test.ts deleted file mode 100644 index cd12bf34..00000000 --- a/src/verify.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -// /* eslint-disable import/first */ -// const mockVerifyHash = jest.fn(); -// const mockVerifyIssued = jest.fn(); -// const mockVerifyRevoked = jest.fn(); -// -// jest.useFakeTimers(); -// -// jest.doMock("./hash/hash", () => ({ -// verifyHash: mockVerifyHash -// })); -// -// jest.doMock("./issued/verify", () => ({ -// verifyIssued: mockVerifyIssued -// })); -// -// jest.doMock("./revoked/verify", () => ({ -// verifyRevoked: mockVerifyRevoked -// })); -// -// import { verifyWithIndividualChecks } from "./index"; -// import { document } from "../test/fixtures/document"; -// -// describe("verifyWithIndividualChecks", () => { -// beforeEach(() => { -// mockVerifyHash.mockReset(); -// mockVerifyIssued.mockReset(); -// mockVerifyRevoked.mockReset(); -// }); -// -// it("returns valid as true only after all tests have passed", async () => { -// mockVerifyHash.mockResolvedValue({ checksumMatch: true }); -// mockVerifyIssued.mockImplementation( -// () => -// new Promise(res => setTimeout(() => res({ issuedOnAll: true }), 1000)) -// ); -// mockVerifyRevoked.mockImplementation( -// () => -// new Promise(res => setTimeout(() => res({ revokedOnAny: false }), 1500)) -// ); -// -// let hasResolvedValid = false; -// const [hash, issued, revoked, valid] = verifyWithIndividualChecks(document); -// valid.then(() => { -// hasResolvedValid = true; -// }); -// -// expect(await hash).toEqual({ checksumMatch: true }); -// expect(hasResolvedValid).toBe(false); -// -// jest.advanceTimersByTime(1000); -// -// expect(await issued).toEqual({ issuedOnAll: true }); -// expect(hasResolvedValid).toBe(false); -// -// jest.runAllTimers(); -// -// expect(await revoked).toEqual({ revokedOnAny: false }); -// expect(await valid).toBe(true); -// expect(hasResolvedValid).toBe(true); -// }); -// -// it("returns valid as false immediately when any test fails", async () => { -// mockVerifyHash.mockResolvedValue({ checksumMatch: true }); -// mockVerifyIssued.mockImplementation( -// () => -// new Promise(res => setTimeout(() => res({ issuedOnAll: false }), 1000)) -// ); -// mockVerifyRevoked.mockImplementation( -// () => -// new Promise(res => setTimeout(() => res({ revokedOnAny: false }), 1500)) -// ); -// -// let hasResolvedRevoked = false; -// const [hash, issued, revoked, valid] = verifyWithIndividualChecks(document); -// revoked.then(() => { -// hasResolvedRevoked = true; -// }); -// -// expect(await hash).toEqual({ checksumMatch: true }); -// expect(hasResolvedRevoked).toBe(false); -// -// jest.advanceTimersByTime(1000); -// -// expect(await issued).toEqual({ issuedOnAll: false }); // Since issued check is falsy, document is overall invalid -// expect(await valid).toBe(false); // Return the overall validity early -// expect(hasResolvedRevoked).toBe(false); // The result of this is inconsequential to the overal validity -// -// jest.runAllTimers(); -// -// expect(await revoked).toEqual({ revokedOnAny: false }); -// expect(hasResolvedRevoked).toBe(true); -// }); -// });