Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: upgrade doc store #282

Merged
merged 8 commits into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ npx -p @govtechsg/open-attestation-cli open-attestation <arguments>
| [Dns txt get](#dns-txt-record) | ❎ | ❎ | ❎ |
| [Document store issue](#issue-document-to-document-store) | ✔ | ✔ | ✔ |
| [Document store revoke](#revoke-document-in-document-store) | ✔ | ✔ | ✔ |
| [Document store grant ownership](#grant-role-on-document-store) | ✔ | ✔ | ✔ |
| [Document store revoke ownership](#revoke-role-on-document-store) | ✔ | ✔ | ✔ |
| [Document store transfer ownership](#transfer-ownership-of-document-store) | ✔ | ✔ | ✔ |
| [Token registry issue](#issue-document-to-token-registry) | ✔ | ✔ | ✔ |
| [Token registry mint](#issue-document-to-token-registry) | ✔ | ✔ | ✔ |
Expand Down Expand Up @@ -344,6 +346,42 @@ open-attestation document-store revoke --network sepolia --address 0x19f89607b52
✔ success Document/Document Batch with hash 0x0c1a666aa55d17d26412bb57fbed96f40ec5a08e2f995a108faf45429ae3511f has been revoked on 0x19f89607b52268D0A19543e48F790c65750869c6
```

#### Grant role on document store

Grant role on document store deployed on the blockchain to a wallet

```bash
open-attestation document-store grant-role --address <DOCUMENT_STORE_ADDRESS> --account <ACCOUNT_ADDRESS> --role <ROLE> [options]
```

Roles options: "admin", "issuer", "revoker"

Example - with private key set in `OA_PRIVATE_KEY` environment variable (recommended). [More options](#providing-the-wallet).

```bash
open-attestation document-store grant-role --address 0x80732bF5CA47A85e599f3ac9572F602c249C8A28 --new-owner 0xf81ea9d2c0133de728d28b8d7f186bed61079997 --role admin --network sepolia

✔ success Document store 0x80732bF5CA47A85e599f3ac9572F602c249C8A28's role of: admin has been granted to wallet 0xf81ea9d2c0133de728d28b8d7f186bed61079997
```

#### Revoke role on document store

Revoke role on document store deployed on the blockchain to a wallet

Roles options: "admin", "issuer", "revoker"

```bash
open-attestation document-store revoke-role --address <DOCUMENT_STORE_ADDRESS> --account <ACCOUNT_ADDRESS> --role <ROLE> [options]
```

Example - with private key set in `OA_PRIVATE_KEY` environment variable (recommended). [More options](#providing-the-wallet).

```bash
open-attestation document-store revoke-role --address 0x80732bF5CA47A85e599f3ac9572F602c249C8A28 --new-owner 0xf81ea9d2c0133de728d28b8d7f186bed61079997 --role admin --network sepolia

✔ success Document store 0x80732bF5CA47A85e599f3ac9572F602c249C8A28's role of: admin has been revoked from wallet 0xf81ea9d2c0133de728d28b8d7f186bed61079997
```

#### Transfer ownership of document store

Transfer ownership of a document store deployed on the blockchain to another wallet
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
},
"dependencies": {
"@govtechsg/dnsprove": "^2.3.0",
"@govtechsg/document-store": "^2.4.0",
"@govtechsg/document-store": "^2.6.1",
"@govtechsg/oa-encryption": "^1.3.3",
"@govtechsg/oa-verify": "^7.11.0",
"@govtechsg/open-attestation": "^6.4.1",
Expand Down
7 changes: 7 additions & 0 deletions src/commands/document-store/document-store-command.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,10 @@ export type DocumentStoreTransferOwnershipCommand = NetworkAndWalletSignerOption
address: string;
newOwner: string;
};

export type DocumentStoreRoleCommand = NetworkAndWalletSignerOption &
GasOption & {
address: string;
account: string;
role: string;
};
57 changes: 57 additions & 0 deletions src/commands/document-store/grant-role.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Argv } from "yargs";
import { error, info, success } from "signale";
import { getLogger } from "../../logger";
import { DocumentStoreRoleCommand } from "./document-store-command.type";
import { grantDocumentStoreRole } from "../../implementations/document-store/grant-role";
import { withGasPriceOption, withNetworkAndWalletSignerOption } from "../shared";
import { getErrorMessage, getEtherscanAddress, addAddressPrefix } from "../../utils";
import { rolesList } from "../../implementations/document-store/document-store-roles";

const { trace } = getLogger("document-store:grant-role");

export const command = "grant-role [options]";

export const describe = "grant role of the document store to a wallet";

export const builder = (yargs: Argv): Argv =>
withGasPriceOption(
withNetworkAndWalletSignerOption(
yargs
.option("address", {
alias: "a",
description: "Address of document store to be granted role",
type: "string",
demandOption: true,
})
.option("account", {
alias: ["h", "newOwner"],
description: "Address of wallet to transfer role to",
type: "string",
demandOption: true,
})
.option("role", {
alias: "r",
description: "Role to be transferred",
type: "string",
options: rolesList,
demandOption: true,
})
)
);

export const handler = async (args: DocumentStoreRoleCommand): Promise<string | undefined> => {
trace(`Args: ${JSON.stringify(args, null, 2)}`);
try {
info(`Granting role to wallet ${args.account}`);
const { transactionHash } = await grantDocumentStoreRole({
...args,
// add 0x automatically in front of the hash if it's not provided
account: addAddressPrefix(args.account),
});
success(`Document store ${args.address}'s role of: ${args.role} has been granted to wallet ${args.account}`);
info(`Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transactionHash}`);
return args.address;
} catch (e) {
error(getErrorMessage(e));
}
};
57 changes: 57 additions & 0 deletions src/commands/document-store/revoke-role.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Argv } from "yargs";
import { error, info, success } from "signale";
import { getLogger } from "../../logger";
import { DocumentStoreRoleCommand } from "./document-store-command.type";
import { revokeDocumentStoreRole } from "../../implementations/document-store/revoke-role";
import { withGasPriceOption, withNetworkAndWalletSignerOption } from "../shared";
import { getErrorMessage, getEtherscanAddress, addAddressPrefix } from "../../utils";
import { rolesList } from "../../implementations/document-store/document-store-roles";

const { trace } = getLogger("document-store:revoke-role");

export const command = "revoke-role [options]";

export const describe = "revoke role of the document store to a wallet";

export const builder = (yargs: Argv): Argv =>
withGasPriceOption(
withNetworkAndWalletSignerOption(
yargs
.option("address", {
alias: "a",
description: "Address of document store to be revoked role",
type: "string",
demandOption: true,
})
.option("account", {
alias: ["h", "newOwner"],
description: "Address of wallet to revoke role from",
type: "string",
demandOption: true,
})
.option("role", {
alias: "r",
description: "Role to be revoked",
type: "string",
options: rolesList,
demandOption: true,
})
)
);

export const handler = async (args: DocumentStoreRoleCommand): Promise<string | undefined> => {
trace(`Args: ${JSON.stringify(args, null, 2)}`);
try {
info(`Revoking role from wallet ${args.account}`);
const { transactionHash } = await revokeDocumentStoreRole({
...args,
// add 0x automatically in front of the hash if it's not provided
account: addAddressPrefix(args.account),
});
success(`Document store ${args.address}'s role of: ${args.role} has been revoked from wallet ${args.account}`);
info(`Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transactionHash}`);
return args.address;
} catch (e) {
error(getErrorMessage(e));
}
};
26 changes: 20 additions & 6 deletions src/commands/document-store/transfer-ownership.ts
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the transfer-ownership command have its own tests too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i've created transfer-ownership as a new implementation as opposed to using grant and revoke seperately. tests written

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, got it.

Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { Argv } from "yargs";
import { error, info, success } from "signale";
import { getLogger } from "../../logger";
import { DocumentStoreTransferOwnershipCommand } from "./document-store-command.type";
import { transferDocumentStoreOwnershipToWallet } from "../../implementations/document-store/transfer-ownership";
import { withGasPriceOption, withNetworkAndWalletSignerOption } from "../shared";
import { getErrorMessage, getEtherscanAddress, addAddressPrefix } from "../../utils";
import { transferDocumentStoreOwnership } from "../../implementations/document-store/transfer-ownership";

const { trace } = getLogger("document-store:transfer-ownership");

Expand All @@ -23,7 +23,7 @@ export const builder = (yargs: Argv): Argv =>
demandOption: true,
})
.option("newOwner", {
alias: "h",
alias: ["h", "account"],
description: "Address of new wallet to transfer ownership to",
type: "string",
demandOption: true,
Expand All @@ -35,13 +35,27 @@ export const handler = async (args: DocumentStoreTransferOwnershipCommand): Prom
trace(`Args: ${JSON.stringify(args, null, 2)}`);
try {
info(`Transferring ownership to wallet ${args.newOwner}`);
const { transactionHash } = await transferDocumentStoreOwnershipToWallet({
const { address, newOwner } = args;
const { grantTransaction, revokeTransaction } = await transferDocumentStoreOwnership({
...args,
// add 0x automatically in front of the hash if it's not provided
newOwner: addAddressPrefix(args.newOwner),
newOwner: addAddressPrefix(newOwner),
address: addAddressPrefix(address),
});
success(`Ownership of document store ${args.address} has been transferred to new wallet ${args.newOwner}`);
info(`Find more details at ${getEtherscanAddress({ network: args.network })}/tx/${transactionHash}`);

const grantTransactionHash = (await grantTransaction).transactionHash;
const revokeTransactionHash = (await revokeTransaction).transactionHash;

success(`Ownership of document store ${args.address} has been transferred to wallet ${args.newOwner}`);

info(
`Find more details at ${getEtherscanAddress({
network: args.network,
})}/tx/${grantTransactionHash} (grant) and ${getEtherscanAddress({
network: args.network,
})}/tx/${revokeTransactionHash} (revoke)`
);
Comment on lines +51 to +57
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we send the output as soon as the grant transaction is completed instead of waiting for both to finish then show them together? I'm thinking what if grant was successful but revoke failed? Then the display for the first successful transaction won't be known?


return args.address;
} catch (e) {
error(getErrorMessage(e));
Expand Down
16 changes: 16 additions & 0 deletions src/implementations/document-store/document-store-roles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { DocumentStore } from "@govtechsg/document-store";

export const getRoleString = async (documentStore: DocumentStore, role: string): Promise<string> => {
switch (role) {
case "admin":
return await documentStore.DEFAULT_ADMIN_ROLE();
case "issuer":
return await documentStore.ISSUER_ROLE();
case "revoker":
return await documentStore.REVOKER_ROLE();
default:
throw new Error("Invalid role");
}
};

export const rolesList = ["admin", "issuer", "revoker"];
109 changes: 109 additions & 0 deletions src/implementations/document-store/grant-role.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { grantDocumentStoreRole } from "./grant-role";

import { Wallet } from "ethers";
import { DocumentStoreFactory } from "@govtechsg/document-store";
import { DocumentStoreRoleCommand } from "../../commands/document-store/document-store-command.type";
import { addAddressPrefix } from "../../utils";
import { join } from "path";

jest.mock("@govtechsg/document-store");

const deployParams: DocumentStoreRoleCommand = {
account: "0xabcd",
role: "issuer",
address: "0x1234",
network: "sepolia",
key: "0000000000000000000000000000000000000000000000000000000000000001",
dryRun: false,
};

// TODO the following test is very fragile and might break on every interface change of DocumentStoreFactory
// ideally must setup ganache, and run the function over it
describe("document-store", () => {
// increase timeout because ethers is throttling
jest.setTimeout(30000);
describe("grant document store issuer role to wallet", () => {
const mockedDocumentStoreFactory: jest.Mock<DocumentStoreFactory> = DocumentStoreFactory as any;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore mock static method
const mockedConnect: jest.Mock = mockedDocumentStoreFactory.connect;
const mockedGrantRole = jest.fn();
const mockedCallStaticGrantRole = jest.fn().mockResolvedValue(undefined);

beforeEach(() => {
delete process.env.OA_PRIVATE_KEY;
mockedDocumentStoreFactory.mockReset();
mockedConnect.mockReset();
mockedCallStaticGrantRole.mockClear();
mockedConnect.mockReturnValue({
grantRole: mockedGrantRole,
DEFAULT_ADMIN_ROLE: jest.fn().mockResolvedValue("ADMIN"),
ISSUER_ROLE: jest.fn().mockResolvedValue("ISSUER"),
REVOKER_ROLE: jest.fn().mockResolvedValue("REVOKER"),
callStatic: {
grantRole: mockedCallStaticGrantRole,
},
});
mockedGrantRole.mockReturnValue({
hash: "hash",
wait: () => Promise.resolve({ transactionHash: "transactionHash" }),
});
});
it("should pass in the correct params and return the deployed instance", async () => {
const instance = await grantDocumentStoreRole(deployParams);

const passedSigner: Wallet = mockedConnect.mock.calls[0][1];

expect(passedSigner.privateKey).toBe(`0x${deployParams.key}`);
expect(mockedConnect.mock.calls[0][0]).toEqual(deployParams.address);
expect(mockedCallStaticGrantRole).toHaveBeenCalledTimes(1);
expect(mockedGrantRole.mock.calls[0][0]).toEqual("ISSUER");
expect(mockedGrantRole.mock.calls[0][1]).toEqual(deployParams.account);
expect(instance).toStrictEqual({ transactionHash: "transactionHash" });
});

it("should accept account without 0x prefix and return deployed instance", async () => {
const instance = await grantDocumentStoreRole({
...deployParams,
account: addAddressPrefix("abcd"),
});

const passedSigner: Wallet = mockedConnect.mock.calls[0][1];

expect(passedSigner.privateKey).toBe(`0x${deployParams.key}`);
expect(mockedConnect.mock.calls[0][0]).toEqual(deployParams.address);
expect(mockedCallStaticGrantRole).toHaveBeenCalledTimes(1);
expect(mockedGrantRole.mock.calls[0][0]).toEqual("ISSUER");
expect(mockedGrantRole.mock.calls[0][1]).toEqual(deployParams.account);

expect(instance).toStrictEqual({ transactionHash: "transactionHash" });
});

it("should take in the key from environment variable", async () => {
process.env.OA_PRIVATE_KEY = "0000000000000000000000000000000000000000000000000000000000000002";
await grantDocumentStoreRole({
account: "0xabcd",
address: "0x1234",
network: "sepolia",
dryRun: false,
role: "admin",
});

const passedSigner: Wallet = mockedConnect.mock.calls[0][1];
expect(passedSigner.privateKey).toBe(`0x${process.env.OA_PRIVATE_KEY}`);
});
it("should take in the key from key file", async () => {
await grantDocumentStoreRole({
account: "0xabcd",
address: "0x1234",
network: "sepolia",
keyFile: join(__dirname, "..", "..", "..", "examples", "sample-key"),
dryRun: false,
role: "admin",
});

const passedSigner: Wallet = mockedConnect.mock.calls[0][1];
expect(passedSigner.privateKey).toBe(`0x0000000000000000000000000000000000000000000000000000000000000003`);
});
});
});
Loading
Loading