Skip to content

Commit

Permalink
feat: upgrade doc store (#282)
Browse files Browse the repository at this point in the history
* feat: upgrade doc store
  • Loading branch information
osslgtm committed Sep 13, 2023
1 parent 83f0392 commit c087fba
Show file tree
Hide file tree
Showing 14 changed files with 555 additions and 43 deletions.
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
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)`
);

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`);
});
});
});

0 comments on commit c087fba

Please sign in to comment.