diff --git a/.eslintrc.json b/.eslintrc.json index 77c37989..e2ba6eff 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -27,6 +27,7 @@ "@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/no-explicit-any": "off", "no-unused-expressions": "off", - "no-else-return": "off" + "no-else-return": "off", + "import/no-extraneous-dependencies": ["error", {"devDependencies": ["**/*.test.ts", "**/*.test.tsx"]}] } } diff --git a/package-lock.json b/package-lock.json index b1e92773..0d31317f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2598,6 +2598,12 @@ "@types/node": ">= 8" } }, + "@open-draft/until": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-1.0.3.tgz", + "integrity": "sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q==", + "dev": true + }, "@semantic-release/commit-analyzer": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/@semantic-release/commit-analyzer/-/commit-analyzer-8.0.1.tgz", @@ -3396,6 +3402,12 @@ "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" }, + "@types/cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg==", + "dev": true + }, "@types/debug": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", @@ -5602,6 +5614,12 @@ "safe-buffer": "~5.1.1" } }, + "cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "dev": true + }, "copy-descriptor": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", @@ -7847,6 +7865,12 @@ "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==" }, + "graphql": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-15.3.0.tgz", + "integrity": "sha512-GTCJtzJmkFLWRfFJuoo9RWWa/FfamUHgiFosxi/X1Ani4AVWbeyBenZTNX6dM+7WSbbFfTo/25eh0LLkwHMw2w==", + "dev": true + }, "growly": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", @@ -7994,6 +8018,12 @@ "minimalistic-assert": "^1.0.1" } }, + "headers-utils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/headers-utils/-/headers-utils-1.2.0.tgz", + "integrity": "sha512-4/BMXcWrJErw7JpM87gF8MNEXcIMLzepYZjNRv/P9ctgupl2Ywa3u1PgHtNhSRq84bHH9Ndlkdy7bSi+bZ9I9A==", + "dev": true + }, "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", @@ -12020,6 +12050,140 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" }, + "msw": { + "version": "0.20.5", + "resolved": "https://registry.npmjs.org/msw/-/msw-0.20.5.tgz", + "integrity": "sha512-hmEsey5BbVicMGt7aOh/GZ9ltga5N3tK6NiJXnbfCkAGKgnAVnjASr3i7Z+sWlZyY5uuMUFyLCEcqrlXxC6qIA==", + "dev": true, + "requires": { + "@open-draft/until": "^1.0.3", + "@types/cookie": "^0.4.0", + "chalk": "^4.1.0", + "cookie": "^0.4.1", + "graphql": "^15.3.0", + "headers-utils": "^1.2.0", + "node-fetch": "^2.6.0", + "node-match-path": "^0.4.4", + "node-request-interceptor": "^0.3.5", + "statuses": "^2.0.0", + "yargs": "^15.4.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "statuses": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.0.tgz", + "integrity": "sha512-w9jNUUQdpuVoYqXxnyOakhckBbOxRaoYqJscyIBYCS5ixyCnO7nQn7zBZvP9zf5QOPZcz2DLUpE3KsNPbJBOFA==", + "dev": true + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + } + } + } + }, "mute-stream": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", @@ -12150,9 +12314,9 @@ } }, "node-fetch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.3.0.tgz", - "integrity": "sha512-MOd8pV3fxENbryESLgVIeaGKrdl+uaYhCSSVkjeOb/31/njTpcis5aWfdqgNlHIrKOLRbMnfPINPOML2CIFeXA==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", + "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==", "dev": true }, "node-int64": { @@ -12167,6 +12331,12 @@ "integrity": "sha1-RaBgHGky395mRKIzYfG+Fzx1068=", "dev": true }, + "node-match-path": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/node-match-path/-/node-match-path-0.4.4.tgz", + "integrity": "sha512-pBq9gp7TG0r0VXuy/oeZmQsjBSnYQo7G886Ly/B3azRwZuEtHCY155dzmfoKWcDPGgyfIGD8WKVC7h3+6y7yTg==", + "dev": true + }, "node-modules-regexp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", @@ -12214,6 +12384,17 @@ } } }, + "node-request-interceptor": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/node-request-interceptor/-/node-request-interceptor-0.3.6.tgz", + "integrity": "sha512-pN8Tt43XuLamN+bspAZ9tEMGDp1bOfaxluYop1/qmNRQmM5BOelFqr4jRvARD6y/7iuhLjOEjMQgy5HweXN7Kg==", + "dev": true, + "requires": { + "@open-draft/until": "^1.0.3", + "debug": "^4.1.1", + "headers-utils": "^1.2.0" + } + }, "node.extend": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/node.extend/-/node.extend-2.0.2.tgz", diff --git a/package.json b/package.json index 6d21e5c6..3d4e75bc 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "eslint-plugin-prettier": "^3.1.4", "git-cz": "^4.7.0", "jest": "^26.1.0", + "msw": "^0.20.5", "prettier": "^2.0.5", "semantic-release": "^17.1.1", "ts-jest": "^26.1.1", diff --git a/src/types/error.ts b/src/types/error.ts index 1ce22570..157883be 100644 --- a/src/types/error.ts +++ b/src/types/error.ts @@ -7,7 +7,9 @@ export enum OpenAttestationEthereumDocumentStoreStatusCode { ETHERS_UNHANDLED_ERROR = 3, SKIPPED = 4, DOCUMENT_REVOKED = 5, + INVALID_ARGUMENT = 6, CONTRACT_NOT_FOUND = 404, + SERVER_ERROR = 500, } export enum OpenAttestationDocumentSignedCode { UNEXPECTED_ERROR = 0, @@ -21,7 +23,9 @@ export enum OpenAttestationEthereumTokenRegistryStatusCode { CONTRACT_ADDRESS_INVALID = 2, ETHERS_UNHANDLED_ERROR = 3, SKIPPED = 4, + INVALID_ARGUMENT = 6, CONTRACT_NOT_FOUND = 404, + SERVER_ERROR = 500, } export enum OpenAttestationDnsTxtCode { UNEXPECTED_ERROR = 0, diff --git a/src/common/smartContract/documentStoreErrors.ts b/src/verifiers/documentStoreStatus/errors.ts similarity index 71% rename from src/common/smartContract/documentStoreErrors.ts rename to src/verifiers/documentStoreStatus/errors.ts index d441a1ff..480b0fe8 100644 --- a/src/common/smartContract/documentStoreErrors.ts +++ b/src/verifiers/documentStoreStatus/errors.ts @@ -9,6 +9,7 @@ const contractNotFound = (address: Hash): Reason => { message: `Contract ${address} was not found`, }; }; + const contractAddressInvalid = (address: Hash): Reason => { return { code: OpenAttestationEthereumDocumentStoreStatusCode.CONTRACT_ADDRESS_INVALID, @@ -19,6 +20,7 @@ const contractAddressInvalid = (address: Hash): Reason => { message: `Contract address ${address} is invalid`, }; }; + export const contractNotIssued = (merkleRoot: Hash, address: string): Reason => { return { code: OpenAttestationEthereumDocumentStoreStatusCode.DOCUMENT_NOT_ISSUED, @@ -39,6 +41,27 @@ export const contractRevoked = (merkleRoot: string, address: string): Reason => }; }; +// This function handles ALL of Ethers SERVER_ERRORs, most likely caused by HTTP 4xx or 5xx errors. +export const serverError = (): Reason => { + return { + code: OpenAttestationEthereumDocumentStoreStatusCode.SERVER_ERROR, + codeString: + OpenAttestationEthereumDocumentStoreStatusCode[OpenAttestationEthereumDocumentStoreStatusCode.SERVER_ERROR], + message: `Unable to connect to the Ethereum network, please try again later`, + }; +}; + +// This function handles all INVALID_ARGUMENT errors likely due to invalid hex string, +// hex data is odd-length or incorrect data length +export const invalidArgument = (error: EthersError, address: string): Reason => { + return { + code: OpenAttestationEthereumDocumentStoreStatusCode.INVALID_ARGUMENT, + codeString: + OpenAttestationEthereumDocumentStoreStatusCode[OpenAttestationEthereumDocumentStoreStatusCode.INVALID_ARGUMENT], + message: `Error with smart contract ${address}: ${error.reason}`, + }; +}; + export const getErrorReason = (error: EthersError, address: string): Reason | null => { const reason = error.reason && Array.isArray(error.reason) ? error.reason[0] : error.reason ?? ""; if ( @@ -55,7 +78,12 @@ export const getErrorReason = (error: EthersError, address: string): Reason | nu (reason.toLowerCase() === "invalid address".toLowerCase() && error.code === errors.INVALID_ARGUMENT) ) { return contractAddressInvalid(address); + } else if (error.code === errors.SERVER_ERROR) { + return serverError(); + } else if (error.code === errors.INVALID_ARGUMENT) { + return invalidArgument(error, address); } + return { message: `Error with smart contract ${address}: ${error.reason}`, code: OpenAttestationEthereumDocumentStoreStatusCode.ETHERS_UNHANDLED_ERROR, diff --git a/src/verifiers/documentStoreStatus/openAttestationEthereumDocumentStoreStatus.ts b/src/verifiers/documentStoreStatus/openAttestationEthereumDocumentStoreStatus.ts index fb280cd6..a43c00d5 100644 --- a/src/verifiers/documentStoreStatus/openAttestationEthereumDocumentStoreStatus.ts +++ b/src/verifiers/documentStoreStatus/openAttestationEthereumDocumentStoreStatus.ts @@ -3,7 +3,7 @@ import { DocumentStoreFactory } from "@govtechsg/document-store"; import { DocumentStore } from "@govtechsg/document-store/src/contracts/DocumentStore"; import { Hash, VerificationFragmentType, VerificationFragment, Verifier } from "../../types/core"; import { OpenAttestationEthereumDocumentStoreStatusCode } from "../../types/error"; -import { contractNotIssued, getErrorReason, contractRevoked } from "../../common/smartContract/documentStoreErrors"; +import { contractNotIssued, getErrorReason, contractRevoked } from "./errors"; import { getIssuersDocumentStore, getProvider } from "../../common/utils"; interface IssuanceStatus { diff --git a/src/verifiers/tokenRegistryStatus/errors.ts b/src/verifiers/tokenRegistryStatus/errors.ts index c1e18aa3..32ea8ffc 100644 --- a/src/verifiers/tokenRegistryStatus/errors.ts +++ b/src/verifiers/tokenRegistryStatus/errors.ts @@ -10,6 +10,7 @@ const contractNotFound = (address: Hash): Reason => { message: `Contract ${address} was not found`, }; }; + const contractAddressInvalid = (address: Hash): Reason => { return { code: OpenAttestationEthereumTokenRegistryStatusCode.CONTRACT_ADDRESS_INVALID, @@ -20,6 +21,7 @@ const contractAddressInvalid = (address: Hash): Reason => { message: `Contract address ${address} is invalid`, }; }; + export const contractNotMinted = (merkleRoot: Hash, address: string): Reason => { return { code: OpenAttestationEthereumTokenRegistryStatusCode.DOCUMENT_NOT_MINTED, @@ -31,6 +33,27 @@ export const contractNotMinted = (merkleRoot: Hash, address: string): Reason => }; }; +// This function handles ALL of Ethers SERVER_ERRORs, most likely caused by HTTP 4xx or 5xx errors. +export const serverError = (): Reason => { + return { + code: OpenAttestationEthereumTokenRegistryStatusCode.SERVER_ERROR, + codeString: + OpenAttestationEthereumTokenRegistryStatusCode[OpenAttestationEthereumTokenRegistryStatusCode.SERVER_ERROR], + message: `Unable to connect to the Ethereum network, please try again later`, + }; +}; + +// This function handles all INVALID_ARGUMENT errors likely due to invalid hex string, +// hex data is odd-length or incorrect data length +export const invalidArgument = (error: EthersError, address: string): Reason => { + return { + code: OpenAttestationEthereumTokenRegistryStatusCode.INVALID_ARGUMENT, + codeString: + OpenAttestationEthereumTokenRegistryStatusCode[OpenAttestationEthereumTokenRegistryStatusCode.INVALID_ARGUMENT], + message: `Error with smart contract ${address}: ${error.reason}`, + }; +}; + export const getErrorReason = (error: EthersError, address: string, hash: Hash): Reason => { const reason = error.reason && Array.isArray(error.reason) ? error.reason[0] : error.reason ?? ""; if ( @@ -50,7 +73,12 @@ export const getErrorReason = (error: EthersError, address: string, hash: Hash): (reason.toLowerCase() === "invalid address".toLowerCase() && error.code === errors.INVALID_ARGUMENT) ) { return contractAddressInvalid(address); + } else if (error.code === errors.SERVER_ERROR) { + return serverError(); + } else if (error.code === errors.INVALID_ARGUMENT) { + return invalidArgument(error, address); } + return { message: `Error with smart contract ${address}: ${error.reason}`, code: OpenAttestationEthereumTokenRegistryStatusCode.ETHERS_UNHANDLED_ERROR, diff --git a/src/verify.v2.integration.test.ts b/src/verify.v2.integration.test.ts index 708e9872..98699038 100644 --- a/src/verify.v2.integration.test.ts +++ b/src/verify.v2.integration.test.ts @@ -2,6 +2,8 @@ * @jest-environment node */ +import { rest } from "msw"; +import { setupServer } from "msw/node"; import { isValid, verify } from "./index"; import { documentMainnetValidWithCertificateStore } from "../test/fixtures/v2/documentMainnetValidWithCertificateStore"; import { @@ -18,6 +20,8 @@ import { documentRinkebyRevokedWithDocumentStore, documentRinkebyValidWithDocumentStore, } from "../test/fixtures/v2/documentRinkebyWithDocumentStore"; +import { documentMainnetInvalidWithOddLengthMerkleRoot } from "../test/fixtures/v2/documentMainnetInvalidWithOddLengthMerkleRoot"; +import { documentMainnetInvalidWithIncorrectMerkleRoot } from "../test/fixtures/v2/documentMainnetInvalidWithIncorrectMerkleRoot"; describe("verify(integration)", () => { afterEach(() => { @@ -181,7 +185,7 @@ describe("verify(integration)", () => { expect(isValid(results, ["DOCUMENT_INTEGRITY"])).toStrictEqual(false); }); - it("should be valid for all checks when document with certificate store is valid on mainnet using cloudflare", async () => { + it("should be valid for all checks when document with certificate store is valid on mainnet using Cloudflare", async () => { process.env.ETHEREUM_PROVIDER = "cloudflare"; const results = await verify(documentMainnetValidWithCertificateStore, { network: "homestead", @@ -664,7 +668,7 @@ describe("verify(integration)", () => { expect(isValid(results, ["DOCUMENT_STATUS"])).toStrictEqual(false); }); - it("should should work when document with document store has been issued to rinkeby network", async () => { + it("should work when document with document store has been issued to rinkeby network", async () => { const results = await verify(documentRinkebyValidWithDocumentStore, { network: "rinkeby", }); @@ -736,7 +740,7 @@ describe("verify(integration)", () => { expect(isValid(results)).toStrictEqual(true); }); - it("should should work when document with document store has been issued and revoked to rinkeby network", async () => { + it("should work when document with document store has been issued and revoked to rinkeby network", async () => { const results = await verify(documentRinkebyRevokedWithDocumentStore, { network: "rinkeby", }); @@ -820,4 +824,515 @@ describe("verify(integration)", () => { expect(isValid(results, ["DOCUMENT_INTEGRITY"])).toStrictEqual(true); expect(isValid(results, ["ISSUER_IDENTITY"])).toStrictEqual(true); }); + + it("should be invalid with a merkle root that is odd-length", async () => { + const results = await verify(documentMainnetInvalidWithOddLengthMerkleRoot, { + network: "mainnet", + }); + expect(results).toMatchInlineSnapshot(` + Array [ + Object { + "data": false, + "name": "OpenAttestationHash", + "reason": Object { + "code": 0, + "codeString": "DOCUMENT_TAMPERED", + "message": "Document has been tampered with", + }, + "status": "INVALID", + "type": "DOCUMENT_INTEGRITY", + }, + Object { + "name": "OpenAttestationSignedProof", + "reason": Object { + "code": 4, + "codeString": "SKIPPED", + "message": "Document does not have a proof block", + }, + "status": "SKIPPED", + "type": "DOCUMENT_STATUS", + }, + Object { + "name": "OpenAttestationEthereumTokenRegistryStatus", + "reason": Object { + "code": 4, + "codeString": "SKIPPED", + "message": "Document issuers doesn't have \\"tokenRegistry\\" property or TOKEN_REGISTRY method", + }, + "status": "SKIPPED", + "type": "DOCUMENT_STATUS", + }, + Object { + "data": Object { + "details": Object { + "issuance": Array [ + Object { + "address": "0x6d71da10Ae0e5B73d0565E2De46741231Eb247C7", + "issued": false, + "reason": Object { + "code": 6, + "codeString": "INVALID_ARGUMENT", + "message": "Error with smart contract 0x6d71da10Ae0e5B73d0565E2De46741231Eb247C7: hex data is odd-length", + }, + }, + ], + }, + "issuedOnAll": false, + }, + "name": "OpenAttestationEthereumDocumentStoreStatus", + "reason": Object { + "code": 6, + "codeString": "INVALID_ARGUMENT", + "message": "Error with smart contract 0x6d71da10Ae0e5B73d0565E2De46741231Eb247C7: hex data is odd-length", + }, + "status": "INVALID", + "type": "DOCUMENT_STATUS", + }, + Object { + "data": Array [ + Object { + "location": "demo.tradetrust.io", + "status": "VALID", + "value": "0x6d71da10Ae0e5B73d0565E2De46741231Eb247C7", + }, + ], + "name": "OpenAttestationDnsTxt", + "status": "VALID", + "type": "ISSUER_IDENTITY", + }, + ] + `); + expect(isValid(results)).toStrictEqual(false); + // Ethers would return INVALID_ARGUMENT, as merkle root is odd-length which we tampered it by removing the last char + expect(isValid(results, ["DOCUMENT_INTEGRITY"])).toStrictEqual(false); + expect(isValid(results, ["DOCUMENT_STATUS"])).toStrictEqual(false); + expect(isValid(results, ["ISSUER_IDENTITY"])).toStrictEqual(true); + }); + + it("should be invalid with a merkle root that is of incorrect length", async () => { + // incorrect length means even-length, but not 64 characters as required of merkleRoots + const results = await verify(documentMainnetInvalidWithIncorrectMerkleRoot, { + network: "mainnet", + }); + expect(results).toMatchInlineSnapshot(` + Array [ + Object { + "data": false, + "name": "OpenAttestationHash", + "reason": Object { + "code": 0, + "codeString": "DOCUMENT_TAMPERED", + "message": "Document has been tampered with", + }, + "status": "INVALID", + "type": "DOCUMENT_INTEGRITY", + }, + Object { + "name": "OpenAttestationSignedProof", + "reason": Object { + "code": 4, + "codeString": "SKIPPED", + "message": "Document does not have a proof block", + }, + "status": "SKIPPED", + "type": "DOCUMENT_STATUS", + }, + Object { + "name": "OpenAttestationEthereumTokenRegistryStatus", + "reason": Object { + "code": 4, + "codeString": "SKIPPED", + "message": "Document issuers doesn't have \\"tokenRegistry\\" property or TOKEN_REGISTRY method", + }, + "status": "SKIPPED", + "type": "DOCUMENT_STATUS", + }, + Object { + "data": Object { + "details": Object { + "issuance": Array [ + Object { + "address": "0x6d71da10Ae0e5B73d0565E2De46741231Eb247C7", + "issued": false, + "reason": Object { + "code": 6, + "codeString": "INVALID_ARGUMENT", + "message": "Error with smart contract 0x6d71da10Ae0e5B73d0565E2De46741231Eb247C7: incorrect data length", + }, + }, + ], + }, + "issuedOnAll": false, + }, + "name": "OpenAttestationEthereumDocumentStoreStatus", + "reason": Object { + "code": 6, + "codeString": "INVALID_ARGUMENT", + "message": "Error with smart contract 0x6d71da10Ae0e5B73d0565E2De46741231Eb247C7: incorrect data length", + }, + "status": "INVALID", + "type": "DOCUMENT_STATUS", + }, + Object { + "data": Array [ + Object { + "location": "demo.tradetrust.io", + "status": "VALID", + "value": "0x6d71da10Ae0e5B73d0565E2De46741231Eb247C7", + }, + ], + "name": "OpenAttestationDnsTxt", + "status": "VALID", + "type": "ISSUER_IDENTITY", + }, + ] + `); + expect(isValid(results)).toStrictEqual(false); + // Ethers would return INVALID_ARGUMENT, as merkle root is odd-length which we tampered it by removing the last char + expect(isValid(results, ["DOCUMENT_INTEGRITY"])).toStrictEqual(false); + expect(isValid(results, ["DOCUMENT_STATUS"])).toStrictEqual(false); + expect(isValid(results, ["ISSUER_IDENTITY"])).toStrictEqual(true); + }); + + describe("Handling HTTP response errors", () => { + const server = setupServer(); // Placing the following tests in a separate block due to how msw intercepts ALL connections + beforeAll(() => server.listen()); // Enable API mocking before tests + afterEach(() => server.resetHandlers()); // Reset any runtime request handlers we may add during the tests + afterAll(() => server.close()); // Disable API mocking after the tests are done + + it("should return SERVER_ERROR when Ethers cannot connect to Infura with a valid certificate (HTTP 429)", async () => { + server.use( + rest.post("https://mainnet.infura.io/v3/bb46da3f80e040e8ab73c0a9ff365d18", (req, res, ctx) => { + return res( + ctx.status(429, "Mocked rate limit error"), + ctx.json({ jsonrpc: "2.0", result: "0xs0meR4nd0mErr0r", id: 1 }) + ); + }) + ); + const results = await verify(documentMainnetValidWithCertificateStore, { + network: "homestead", + }); + expect(results).toMatchInlineSnapshot(` + Array [ + Object { + "data": true, + "name": "OpenAttestationHash", + "status": "VALID", + "type": "DOCUMENT_INTEGRITY", + }, + Object { + "name": "OpenAttestationSignedProof", + "reason": Object { + "code": 4, + "codeString": "SKIPPED", + "message": "Document does not have a proof block", + }, + "status": "SKIPPED", + "type": "DOCUMENT_STATUS", + }, + Object { + "name": "OpenAttestationEthereumTokenRegistryStatus", + "reason": Object { + "code": 4, + "codeString": "SKIPPED", + "message": "Document issuers doesn't have \\"tokenRegistry\\" property or TOKEN_REGISTRY method", + }, + "status": "SKIPPED", + "type": "DOCUMENT_STATUS", + }, + Object { + "data": Object { + "details": Object { + "issuance": Array [ + Object { + "address": "0x007d40224f6562461633ccfbaffd359ebb2fc9ba", + "issued": false, + "reason": Object { + "code": 500, + "codeString": "SERVER_ERROR", + "message": "Unable to connect to the Ethereum network, please try again later", + }, + }, + ], + }, + "issuedOnAll": false, + }, + "name": "OpenAttestationEthereumDocumentStoreStatus", + "reason": Object { + "code": 500, + "codeString": "SERVER_ERROR", + "message": "Unable to connect to the Ethereum network, please try again later", + }, + "status": "INVALID", + "type": "DOCUMENT_STATUS", + }, + Object { + "name": "OpenAttestationDnsTxt", + "reason": Object { + "code": 2, + "codeString": "SKIPPED", + "message": "Document issuers doesn't have \\"documentStore\\" / \\"tokenRegistry\\" property or doesn't use DNS-TXT type", + }, + "status": "SKIPPED", + "type": "ISSUER_IDENTITY", + }, + ] + `); + // it's not valid on ISSUER_IDENTITY (skipped) so making sure the rest is valid + expect(isValid(results)).toStrictEqual(false); + expect(isValid(results, ["DOCUMENT_INTEGRITY"])).toStrictEqual(true); + expect(isValid(results, ["DOCUMENT_STATUS"])).toStrictEqual(false); // Because of SERVER_ERROR + }); + it("should return SERVER_ERROR when Ethers cannot connect to Infura with a valid certificate (HTTP 502)", async () => { + server.use( + rest.post("https://mainnet.infura.io/v3/bb46da3f80e040e8ab73c0a9ff365d18", (req, res, ctx) => { + return res( + ctx.status(502, "Mocked rate limit error"), + ctx.json({ jsonrpc: "2.0", result: "0xs0meR4nd0mErr0r", id: 2 }) + ); + }) + ); + const results = await verify(documentMainnetValidWithCertificateStore, { + network: "homestead", + }); + expect(results).toMatchInlineSnapshot(` + Array [ + Object { + "data": true, + "name": "OpenAttestationHash", + "status": "VALID", + "type": "DOCUMENT_INTEGRITY", + }, + Object { + "name": "OpenAttestationSignedProof", + "reason": Object { + "code": 4, + "codeString": "SKIPPED", + "message": "Document does not have a proof block", + }, + "status": "SKIPPED", + "type": "DOCUMENT_STATUS", + }, + Object { + "name": "OpenAttestationEthereumTokenRegistryStatus", + "reason": Object { + "code": 4, + "codeString": "SKIPPED", + "message": "Document issuers doesn't have \\"tokenRegistry\\" property or TOKEN_REGISTRY method", + }, + "status": "SKIPPED", + "type": "DOCUMENT_STATUS", + }, + Object { + "data": Object { + "details": Object { + "issuance": Array [ + Object { + "address": "0x007d40224f6562461633ccfbaffd359ebb2fc9ba", + "issued": false, + "reason": Object { + "code": 500, + "codeString": "SERVER_ERROR", + "message": "Unable to connect to the Ethereum network, please try again later", + }, + }, + ], + }, + "issuedOnAll": false, + }, + "name": "OpenAttestationEthereumDocumentStoreStatus", + "reason": Object { + "code": 500, + "codeString": "SERVER_ERROR", + "message": "Unable to connect to the Ethereum network, please try again later", + }, + "status": "INVALID", + "type": "DOCUMENT_STATUS", + }, + Object { + "name": "OpenAttestationDnsTxt", + "reason": Object { + "code": 2, + "codeString": "SKIPPED", + "message": "Document issuers doesn't have \\"documentStore\\" / \\"tokenRegistry\\" property or doesn't use DNS-TXT type", + }, + "status": "SKIPPED", + "type": "ISSUER_IDENTITY", + }, + ] + `); + // it's not valid on ISSUER_IDENTITY (skipped) so making sure the rest is valid + expect(isValid(results)).toStrictEqual(false); + expect(isValid(results, ["DOCUMENT_INTEGRITY"])).toStrictEqual(true); + expect(isValid(results, ["DOCUMENT_STATUS"])).toStrictEqual(false); // Because of SERVER_ERROR + }); + it("should return SERVER_ERROR when Ethers cannot connect to Infura with an invalid certificate (HTTP 429)", async () => { + // NOTE: Purpose of this test is to use a mainnet cert on ropsten. The mainnet cert store is perfectly valid, but does not exist on ropsten. + server.use( + rest.post("https://ropsten.infura.io/v3/bb46da3f80e040e8ab73c0a9ff365d18", (req, res, ctx) => { + return res( + ctx.status(429, "Mocked rate limit error"), + ctx.json({ jsonrpc: "2.0", result: "0xs0meR4nd0mErr0r", id: 3 }) + ); + }) + ); + const results = await verify(documentMainnetValidWithCertificateStore, { + network: "ropsten", + }); + expect(results).toMatchInlineSnapshot(` + Array [ + Object { + "data": true, + "name": "OpenAttestationHash", + "status": "VALID", + "type": "DOCUMENT_INTEGRITY", + }, + Object { + "name": "OpenAttestationSignedProof", + "reason": Object { + "code": 4, + "codeString": "SKIPPED", + "message": "Document does not have a proof block", + }, + "status": "SKIPPED", + "type": "DOCUMENT_STATUS", + }, + Object { + "name": "OpenAttestationEthereumTokenRegistryStatus", + "reason": Object { + "code": 4, + "codeString": "SKIPPED", + "message": "Document issuers doesn't have \\"tokenRegistry\\" property or TOKEN_REGISTRY method", + }, + "status": "SKIPPED", + "type": "DOCUMENT_STATUS", + }, + Object { + "data": Object { + "details": Object { + "issuance": Array [ + Object { + "address": "0x007d40224f6562461633ccfbaffd359ebb2fc9ba", + "issued": false, + "reason": Object { + "code": 500, + "codeString": "SERVER_ERROR", + "message": "Unable to connect to the Ethereum network, please try again later", + }, + }, + ], + }, + "issuedOnAll": false, + }, + "name": "OpenAttestationEthereumDocumentStoreStatus", + "reason": Object { + "code": 500, + "codeString": "SERVER_ERROR", + "message": "Unable to connect to the Ethereum network, please try again later", + }, + "status": "INVALID", + "type": "DOCUMENT_STATUS", + }, + Object { + "name": "OpenAttestationDnsTxt", + "reason": Object { + "code": 2, + "codeString": "SKIPPED", + "message": "Document issuers doesn't have \\"documentStore\\" / \\"tokenRegistry\\" property or doesn't use DNS-TXT type", + }, + "status": "SKIPPED", + "type": "ISSUER_IDENTITY", + }, + ] + `); + // it's not valid on ISSUER_IDENTITY (skipped) so making sure the rest is valid + expect(isValid(results)).toStrictEqual(false); + expect(isValid(results, ["DOCUMENT_INTEGRITY"])).toStrictEqual(true); + expect(isValid(results, ["DOCUMENT_STATUS"])).toStrictEqual(false); // Because of SERVER_ERROR + }); + it("should return SERVER_ERROR when Ethers cannot connect to Infura with an invalid certificate (HTTP 502)", async () => { + // NOTE: Purpose of this test is to use a mainnet cert on ropsten. The mainnet cert store is perfectly valid, but does not exist on ropsten. + server.use( + rest.post("https://ropsten.infura.io/v3/bb46da3f80e040e8ab73c0a9ff365d18", (req, res, ctx) => { + return res( + ctx.status(502, "Mocked rate limit error"), + ctx.json({ jsonrpc: "2.0", result: "0xs0meR4nd0mErr0r", id: 4 }) + ); + }) + ); + const results = await verify(documentMainnetValidWithCertificateStore, { + network: "ropsten", + }); + expect(results).toMatchInlineSnapshot(` + Array [ + Object { + "data": true, + "name": "OpenAttestationHash", + "status": "VALID", + "type": "DOCUMENT_INTEGRITY", + }, + Object { + "name": "OpenAttestationSignedProof", + "reason": Object { + "code": 4, + "codeString": "SKIPPED", + "message": "Document does not have a proof block", + }, + "status": "SKIPPED", + "type": "DOCUMENT_STATUS", + }, + Object { + "name": "OpenAttestationEthereumTokenRegistryStatus", + "reason": Object { + "code": 4, + "codeString": "SKIPPED", + "message": "Document issuers doesn't have \\"tokenRegistry\\" property or TOKEN_REGISTRY method", + }, + "status": "SKIPPED", + "type": "DOCUMENT_STATUS", + }, + Object { + "data": Object { + "details": Object { + "issuance": Array [ + Object { + "address": "0x007d40224f6562461633ccfbaffd359ebb2fc9ba", + "issued": false, + "reason": Object { + "code": 500, + "codeString": "SERVER_ERROR", + "message": "Unable to connect to the Ethereum network, please try again later", + }, + }, + ], + }, + "issuedOnAll": false, + }, + "name": "OpenAttestationEthereumDocumentStoreStatus", + "reason": Object { + "code": 500, + "codeString": "SERVER_ERROR", + "message": "Unable to connect to the Ethereum network, please try again later", + }, + "status": "INVALID", + "type": "DOCUMENT_STATUS", + }, + Object { + "name": "OpenAttestationDnsTxt", + "reason": Object { + "code": 2, + "codeString": "SKIPPED", + "message": "Document issuers doesn't have \\"documentStore\\" / \\"tokenRegistry\\" property or doesn't use DNS-TXT type", + }, + "status": "SKIPPED", + "type": "ISSUER_IDENTITY", + }, + ] + `); + // it's not valid on ISSUER_IDENTITY (skipped) so making sure the rest is valid + expect(isValid(results)).toStrictEqual(false); + expect(isValid(results, ["DOCUMENT_INTEGRITY"])).toStrictEqual(true); + expect(isValid(results, ["DOCUMENT_STATUS"])).toStrictEqual(false); // Because of SERVER_ERROR + }); + }); }); diff --git a/test/fixtures/v2/documentMainnetInvalidWithIncorrectMerkleRoot.ts b/test/fixtures/v2/documentMainnetInvalidWithIncorrectMerkleRoot.ts new file mode 100644 index 00000000..b8f6f307 --- /dev/null +++ b/test/fixtures/v2/documentMainnetInvalidWithIncorrectMerkleRoot.ts @@ -0,0 +1,79 @@ +import { v2, WrappedDocument, SchemaId } from "@govtechsg/open-attestation"; + +interface CustomDocument extends v2.OpenAttestationDocument { + recipient: { + name: string; + address: { + street: string; + country: string; + }; + }; + certification: any; + consignment: any; + declaration: any; +} +export const documentMainnetInvalidWithIncorrectMerkleRoot: WrappedDocument = { + version: SchemaId.v2, + schema: "tradetrust/v1.0", + data: { + id: "26e3bba1-7649-420e-a27e-f38fdbec3469:string:SGCNM21566325", + $template: { + name: "8fb7df2e-3ba1-479e-a355-5467d38bd52c:string:CERTIFICATE_OF_NON_MANIPULATION", + type: "3ce0413d-9047-4a18-8789-7483632a512e:string:EMBEDDED_RENDERER", + url: "983fff6e-5385-4bd0-a635-c20a50c27691:string:https://demo-cnm.openattestation.com", + }, + issuers: [ + { + name: "c1414e34-a87b-467c-946b-361edb0d426d:string:TradeTrust Demo", + documentStore: "7762001c-bf13-4ec3-b5a5-adcc149d039f:string:0x6d71da10Ae0e5B73d0565E2De46741231Eb247C7", + identityProof: { + type: "1416fc74-d941-4370-8cfb-88997097e309:string:DNS-TXT", + location: "f542bcfc-00fb-4926-9145-bc22adff4edb:string:demo.tradetrust.io", + }, + }, + ], + recipient: { + name: "9727b3c8-2d64-43ca-9fc4-9ce4ee7fa203:string:SG FREIGHT", + address: { + street: "ecf485df-8ff4-416e-bde2-63e1b6072e0a:string:101 ORCHARD ROAD", + country: "47929d19-ec95-4869-b6a4-c706ec50530a:string:SINGAPORE", + }, + }, + consignment: { + description: "eb8ef708-0335-4ece-8bbb-139b06f264e8:string:16667 CARTONS OF RED WINE", + quantity: { + value: "556317b3-1fb1-4334-a7d7-f88a7477d268:number:5000", + unit: "10c54e5a-4f7d-45d5-801b-c17787b2d770:string:LITRES", + }, + countryOfOrigin: "4b3d95fd-6aaf-4694-a994-8b4cc9f2eaa0:string:AUSTRALIA", + outwardBillNo: "cee74dbd-6117-490c-9bff-9665ea0cbabe:string:AQSIQ170923130", + dateOfDischarge: "b2b91700-a71f-44e0-ac7c-16ddbe1b5577:string:2018-01-26", + dateOfDeparture: "f3bb7427-86b4-4a75-adf6-a41c675a9f0f:string:2018-01-30", + countryOfFinalDestination: "4fdd92ba-2ec9-4eb3-9bde-2c167e957e12:string:CHINA", + outgoingVehicleNo: "4d9c9e3e-e844-4660-96c3-4a8bf07e22f2:string:COSCO JAPAN 074E/30-JAN", + }, + declaration: { + name: "882d9945-b2c1-4b22-ba82-3d9e049af588:string:PETER LEE", + designation: "370d8b2f-78ea-4235-ab91-e1a426deef91:string:SHIPPING MANAGER", + date: "92c1285c-71e8-4494-b082-ed29ff3668b9:string:2018-01-28", + }, + certification: { + name: "1a60ca05-c279-466e-8905-7e6ef4e6054e:string:DEMO JOHN TAN", + designation: "d75ff67f-b931-4161-84d8-6fd514d646a3:string:DEMO", + date: "ea2b71b1-e347-4074-8bdd-e95fee8297f0:string:2018-01-28", + }, + }, + privacy: { + obfuscatedData: [ + "323cf61a32c24a193aea9609caeeb5b5cf5d47a8fa1dbc8f67921f330dd406e5", + "f03cbac0ac3876ccb82e489140fc8a6cd93126f1d60161ae319558a25df985b7", + ], + }, + signature: { + type: "SHA3MerkleProof", + targetHash: "61dc9186345e05cc2ae53dc72af880a3b66e2fa7983feaa6254d1518540de50a", + proof: [], + // merkleRoot's last 2 characters have been removed to make it even-length (62 char), but not 64 char + merkleRoot: "61dc9186345e05cc2ae53dc72af880a3b66e2fa7983feaa6254d1518540de5", + }, +}; diff --git a/test/fixtures/v2/documentMainnetInvalidWithOddLengthMerkleRoot.ts b/test/fixtures/v2/documentMainnetInvalidWithOddLengthMerkleRoot.ts new file mode 100644 index 00000000..a4616150 --- /dev/null +++ b/test/fixtures/v2/documentMainnetInvalidWithOddLengthMerkleRoot.ts @@ -0,0 +1,79 @@ +import { v2, WrappedDocument, SchemaId } from "@govtechsg/open-attestation"; + +interface CustomDocument extends v2.OpenAttestationDocument { + recipient: { + name: string; + address: { + street: string; + country: string; + }; + }; + certification: any; + consignment: any; + declaration: any; +} +export const documentMainnetInvalidWithOddLengthMerkleRoot: WrappedDocument = { + version: SchemaId.v2, + schema: "tradetrust/v1.0", + data: { + id: "26e3bba1-7649-420e-a27e-f38fdbec3469:string:SGCNM21566325", + $template: { + name: "8fb7df2e-3ba1-479e-a355-5467d38bd52c:string:CERTIFICATE_OF_NON_MANIPULATION", + type: "3ce0413d-9047-4a18-8789-7483632a512e:string:EMBEDDED_RENDERER", + url: "983fff6e-5385-4bd0-a635-c20a50c27691:string:https://demo-cnm.openattestation.com", + }, + issuers: [ + { + name: "c1414e34-a87b-467c-946b-361edb0d426d:string:TradeTrust Demo", + documentStore: "7762001c-bf13-4ec3-b5a5-adcc149d039f:string:0x6d71da10Ae0e5B73d0565E2De46741231Eb247C7", + identityProof: { + type: "1416fc74-d941-4370-8cfb-88997097e309:string:DNS-TXT", + location: "f542bcfc-00fb-4926-9145-bc22adff4edb:string:demo.tradetrust.io", + }, + }, + ], + recipient: { + name: "9727b3c8-2d64-43ca-9fc4-9ce4ee7fa203:string:SG FREIGHT", + address: { + street: "ecf485df-8ff4-416e-bde2-63e1b6072e0a:string:101 ORCHARD ROAD", + country: "47929d19-ec95-4869-b6a4-c706ec50530a:string:SINGAPORE", + }, + }, + consignment: { + description: "eb8ef708-0335-4ece-8bbb-139b06f264e8:string:16667 CARTONS OF RED WINE", + quantity: { + value: "556317b3-1fb1-4334-a7d7-f88a7477d268:number:5000", + unit: "10c54e5a-4f7d-45d5-801b-c17787b2d770:string:LITRES", + }, + countryOfOrigin: "4b3d95fd-6aaf-4694-a994-8b4cc9f2eaa0:string:AUSTRALIA", + outwardBillNo: "cee74dbd-6117-490c-9bff-9665ea0cbabe:string:AQSIQ170923130", + dateOfDischarge: "b2b91700-a71f-44e0-ac7c-16ddbe1b5577:string:2018-01-26", + dateOfDeparture: "f3bb7427-86b4-4a75-adf6-a41c675a9f0f:string:2018-01-30", + countryOfFinalDestination: "4fdd92ba-2ec9-4eb3-9bde-2c167e957e12:string:CHINA", + outgoingVehicleNo: "4d9c9e3e-e844-4660-96c3-4a8bf07e22f2:string:COSCO JAPAN 074E/30-JAN", + }, + declaration: { + name: "882d9945-b2c1-4b22-ba82-3d9e049af588:string:PETER LEE", + designation: "370d8b2f-78ea-4235-ab91-e1a426deef91:string:SHIPPING MANAGER", + date: "92c1285c-71e8-4494-b082-ed29ff3668b9:string:2018-01-28", + }, + certification: { + name: "1a60ca05-c279-466e-8905-7e6ef4e6054e:string:DEMO JOHN TAN", + designation: "d75ff67f-b931-4161-84d8-6fd514d646a3:string:DEMO", + date: "ea2b71b1-e347-4074-8bdd-e95fee8297f0:string:2018-01-28", + }, + }, + privacy: { + obfuscatedData: [ + "323cf61a32c24a193aea9609caeeb5b5cf5d47a8fa1dbc8f67921f330dd406e5", + "f03cbac0ac3876ccb82e489140fc8a6cd93126f1d60161ae319558a25df985b7", + ], + }, + signature: { + type: "SHA3MerkleProof", + targetHash: "61dc9186345e05cc2ae53dc72af880a3b66e2fa7983feaa6254d1518540de50a", + proof: [], + // merkleRoot's last character has been removed to make it odd-length (63 char) + merkleRoot: "61dc9186345e05cc2ae53dc72af880a3b66e2fa7983feaa6254d1518540de50", + }, +};