Skip to content

Commit

Permalink
feat: add verifyWithIndividualChecks (#72)
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastianquek authored Dec 2, 2019
1 parent 7e4f1e2 commit e2f888d
Show file tree
Hide file tree
Showing 5 changed files with 315 additions and 47 deletions.
10 changes: 6 additions & 4 deletions src/hash/hash.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import { documentTampered } from "../../test/fixtures/tampered-document";

describe("verify/hash", () => {
describe("verifyHash", () => {
it("should return true for untampered document", () => {
expect(verifyHash(document)).toEqual({ checksumMatch: true });
it("should return true for untampered document", async () => {
expect(await verifyHash(document)).toEqual({ checksumMatch: true });
});
it("should return false for tampered document", () => {
expect(verifyHash(documentTampered)).toEqual({ checksumMatch: false });
it("should return false for tampered document", async () => {
expect(await verifyHash(documentTampered)).toEqual({
checksumMatch: false
});
});
});
});
4 changes: 2 additions & 2 deletions src/hash/hash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ import {
SchematisedDocument
} from "@govtechsg/open-attestation";

export const verifyHash = (document: SchematisedDocument) => ({
checksumMatch: verifySignature(document)
export const verifyHash = async (document: SchematisedDocument) => ({
checksumMatch: await verifySignature(document)
});
77 changes: 71 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,43 @@ import { verifyHash } from "./hash/hash";
import { verifyIssued } from "./issued/verify";
import { verifyRevoked } from "./revoked/verify";
import { documentToSmartContracts } from "./common/smartContract/documentToSmartContracts";
import { OpenAttestationContract } from "./types";

/**
* Unwraps the resolve type of promises
* e.g.
* type foo = () => Promise<boolean>;
* type bar = ResolveType<foo>; // bar is a boolean
*/
type ResolveType<T> = T extends (...args: any[]) => Promise<infer U> ? U : T;

type VerificationChecks = [
ReturnType<typeof verifyHash>,
ReturnType<typeof verifyIssued>,
ReturnType<typeof verifyRevoked>
];

type VerificationChecksWithValidity = [
ReturnType<typeof verifyHash>,
ReturnType<typeof verifyIssued>,
ReturnType<typeof verifyRevoked>,
Promise<boolean>
];

const getAllChecks = (
document: SignedDocument,
smartContracts: OpenAttestationContract[]
): VerificationChecks => [
verifyHash(document),
verifyIssued(document, smartContracts),
verifyRevoked(document, smartContracts)
];

const isDocumentValid = (
hash: ResolveType<typeof verifyHash>,
issued: ResolveType<typeof verifyIssued>,
revoked: ResolveType<typeof verifyRevoked>
) => hash.checksumMatch && issued.issuedOnAll && !revoked.revokedOnAny;

/**
* @param {object} document Entire document object to be validated
Expand All @@ -14,18 +51,46 @@ export const verify = async (
network = "homestead"
) => {
const smartContracts = documentToSmartContracts(document, network);
const [hash, issued, revoked] = await Promise.all([
verifyHash(document),
verifyIssued(document, smartContracts),
verifyRevoked(document, smartContracts)
]);
const checks = getAllChecks(document, smartContracts);
const [hash, issued, revoked] = await Promise.all(checks);

return {
hash,
issued,
revoked,
valid: hash.checksumMatch && issued.issuedOnAll && !revoked.revokedOnAny
valid: isDocumentValid(hash, issued, revoked)
};
};

/**
* @param {object} document Entire document object to be validated
* @param {string} network Network to check against, defaults to "homestead". Other valid choices: "ropsten", "kovan", etc
* @returns {array} Array of promises, each promise corresponds to a verification check.
* The last promise resolves to the overall validity based on all the checks.
*/
export const verifyWithIndividualChecks = (
document: SignedDocument,
network = "homestead"
): VerificationChecksWithValidity => {
const smartContracts = documentToSmartContracts(document, network);
const [hash, issued, revoked] = getAllChecks(document, smartContracts);

// If any of the checks are invalid, resolve the overall validity early
const valid = Promise.all([
new Promise(async (resolve, reject) =>
(await hash).checksumMatch ? resolve() : reject()
),
new Promise(async (resolve, reject) =>
(await issued).issuedOnAll ? resolve() : reject()
),
new Promise(async (resolve, reject) =>
(await revoked).revokedOnAny ? reject() : resolve()
)
])
.then(() => true)
.catch(() => false);

return [hash, issued, revoked, valid];
};

export default verify; // backward compatible
177 changes: 174 additions & 3 deletions src/verify.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
* @jest-environment node
*/

import verify from "./index";
import verify, { verifyWithIndividualChecks } from "./index";
import { documentMainnetValid } from "../test/fixtures/documentMainnetValid";
import { documentTampered } from "../test/fixtures/tampered-document";
import { documentRopstenValid } from "../test/fixtures/documentRopstenValid";
import { tokenRopstenValid } from "../test/fixtures/tokenRopstenValid";
import { tokenRopstenInvalid } from "../test/fixtures/tokenRopstenInvalid";

describe("verify(integration)", () => {
it("returns false if document is invalid", async () => {
it("returns false if document's hash is invalid and was not issued", async () => {
const results = await verify(documentTampered, "ropsten");

expect(results).toEqual(
Expand Down Expand Up @@ -121,7 +121,7 @@ describe("verify(integration)", () => {
});
});

it("returns false if Ropsten token is invalid", async () => {
it("returns false if Ropsten token was not issued and was revoked", async () => {
const results = await verify(tokenRopstenInvalid, "ropsten");

expect(results).toEqual(
Expand Down Expand Up @@ -152,3 +152,174 @@ describe("verify(integration)", () => {
);
});
});

describe("verifyWithIndividualChecks(integration)", () => {
it("returns false if document's hash is invalid and was not issued", async () => {
const checkPromises = verifyWithIndividualChecks(
documentTampered,
"ropsten"
);
const [hash, issued, revoked, valid] = await Promise.all(checkPromises);

const results = { hash, issued, revoked, valid };

expect(results).toEqual(
expect.objectContaining({
hash: { checksumMatch: false },
issued: {
issuedOnAll: false,
details: [
{
address: "0x20bc9C354A18C8178A713B9BcCFFaC2152b53990",
error: expect.stringMatching("exception"),
issued: false
}
]
},
revoked: {
revokedOnAny: false,
details: [
{
address: "0x20bc9C354A18C8178A713B9BcCFFaC2152b53990",
revoked: false
}
]
},
valid: false
})
);
});

it("returns true if Mainnet document is valid", async () => {
const checkPromises = verifyWithIndividualChecks(documentMainnetValid);
const [hash, issued, revoked, valid] = await Promise.all(checkPromises);

const results = { hash, issued, revoked, valid };

expect(results).toEqual({
hash: { checksumMatch: true },
issued: {
issuedOnAll: true,
details: [
{
address: "0x007d40224f6562461633ccfbaffd359ebb2fc9ba",
issued: true
}
]
},
revoked: {
revokedOnAny: false,
details: [
{
address: "0x007d40224f6562461633ccfbaffd359ebb2fc9ba",
revoked: false
}
]
},
valid: true
});
});

it("returns true if Ropsten document is valid", async () => {
const checkPromises = verifyWithIndividualChecks(
documentRopstenValid,
"ropsten"
);
const [hash, issued, revoked, valid] = await Promise.all(checkPromises);

const results = { hash, issued, revoked, valid };

expect(results).toEqual({
hash: { checksumMatch: true },
issued: {
issuedOnAll: true,
details: [
{
address: "0xc36484efa1544c32ffed2e80a1ea9f0dfc517495",
issued: true
}
]
},
revoked: {
revokedOnAny: false,
details: [
{
address: "0xc36484efa1544c32ffed2e80a1ea9f0dfc517495",
revoked: false
}
]
},
valid: true
});
});

it("returns true if Ropsten token is valid", async () => {
const checkPromises = verifyWithIndividualChecks(
tokenRopstenValid,
"ropsten"
);
const [hash, issued, revoked, valid] = await Promise.all(checkPromises);

const results = { hash, issued, revoked, valid };

expect(results).toEqual({
hash: { checksumMatch: true },
issued: {
issuedOnAll: true,
details: [
{
address: "0x48399Fb88bcD031C556F53e93F690EEC07963Af3",
issued: true
}
]
},
revoked: {
revokedOnAny: false,
details: [
{
address: "0x48399Fb88bcD031C556F53e93F690EEC07963Af3",
revoked: false
}
]
},
valid: true
});
});

it("returns false if Ropsten token was not issued and was revoked", async () => {
const checkPromises = verifyWithIndividualChecks(
tokenRopstenInvalid,
"ropsten"
);
const [hash, issued, revoked, valid] = await Promise.all(checkPromises);

const results = { hash, issued, revoked, valid };

expect(results).toEqual(
expect.objectContaining({
hash: { checksumMatch: true },
issued: {
issuedOnAll: false,
details: [
{
address: "0x48399Fb88bcD031C556F53e93F690EEC07963Af3",
error: expect.stringMatching("revert"),
issued: false
}
]
},
revoked: {
revokedOnAny: true,
details: [
{
address: "0x48399Fb88bcD031C556F53e93F690EEC07963Af3",
error: expect.stringMatching("revert"),
revoked: true
}
]
},
valid: false
})
);
});
});
Loading

0 comments on commit e2f888d

Please sign in to comment.