Skip to content

Commit

Permalink
feat: implement IC mgmt - copy getCanisterDetails and updateSettings …
Browse files Browse the repository at this point in the history
…from NNS dapp (#353)
  • Loading branch information
peterpeterparker authored Jun 20, 2023
1 parent 1264292 commit a25fbfa
Show file tree
Hide file tree
Showing 17 changed files with 457 additions and 25 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@

## Features

- introduces `getMinterInfo` for ckBTC which returns internal minter parameters such as the minimal amount to retrieve BTC, minimal number of confirmations or KYT fee
- introducing `@dfinity/ic-management` — a new library for interfacing with IC management canister
- add `getMinterInfo` for ckBTC which returns internal minter parameters such as the minimal amount to retrieve BTC, minimal number of confirmations or KYT fee

# 0.16.0 (2023-05-24)

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ The libraries are still in active development, and new features will incremental
- [cmc](/packages/cmc): interfacing with the **cmc** canister of the IC
- [ledger](/packages/ledger): interacting with ICRC compatible **ledgers**
- [ckBTC](/packages/ckbtc): interfacing with **ckBTC**
- [ic-management](/packages/ic-management): interfacing with the **IC management canister**
- [utils](/packages/utils): a collection of utilities and constants
- [nns-proto](/packages/nns-proto): the protobuf source used by `nns-js` to support hardware wallets

Expand Down
42 changes: 29 additions & 13 deletions package-lock.json

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

12 changes: 11 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"packages/cmc",
"packages/ckbtc",
"packages/rosetta-client",
"package/ic-management"
"packages/ic-management"
],
"scripts": {
"did": "scripts/compile-idl-js",
Expand Down Expand Up @@ -136,6 +136,16 @@
"path": "./packages/rosetta-client/dist/index.js",
"limit": "1 kB",
"ignore": []
},
{
"name": "@dfinity/ic-management",
"path": "./packages/ic-management/dist/index.js",
"limit": "3 kB",
"ignore": [
"@dfinity/agent",
"@dfinity/candid",
"@dfinity/principal"
]
}
]
}
2 changes: 1 addition & 1 deletion packages/ic-management/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const { getCanisterDetails } = ICMgmtCanister.create({
agent,
});

const details = await getCanisterDetails({ canisterId: YOUR_CANISTER_ID });
const details = await getCanisterDetails(YOUR_CANISTER_ID);
```

## Features
Expand Down
26 changes: 26 additions & 0 deletions packages/ic-management/src/errors/ic-management.errors.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { mapError, UserNotTheControllerError } from "./ic-management.errors";

describe("IC Management Error utils", () => {
describe("mapError", () => {
it("returns error based on code", () => {
expect(mapError(new Error("code: 403"))).toBeInstanceOf(
UserNotTheControllerError
);
expect(
mapError(new Error("This is an error message with\ncode: 514"))
).toBeInstanceOf(Error);
expect(
mapError(new Error("And this is yet another one with\ncode: 509"))
).toBeInstanceOf(Error);
});

it("returns Error if no code is found", () => {
expect(mapError(new Error("no code is found here"))).toBeInstanceOf(
Error
);
expect(
mapError(new Error("erro message with code: 509 in the same line"))
).toBeInstanceOf(Error);
});
});
});
23 changes: 23 additions & 0 deletions packages/ic-management/src/errors/ic-management.errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Parses and throws convenient error class.
*
* @throws UserNotTheControllerError and Error.
*/
export function mapError(error: Error | unknown): Error | unknown {
const statusLine =
error instanceof Error
? error.message
: ""
.split("\n")
.map((l) => l.trim().toLowerCase())
.find(
(l) => l.startsWith("code:") || l.startsWith("http status code:")
);

if (statusLine?.includes("403")) {
return new UserNotTheControllerError();
}
return error;
}

export class UserNotTheControllerError extends Error {}
145 changes: 145 additions & 0 deletions packages/ic-management/src/ic-management.canister.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import type { ManagementCanisterRecord } from "@dfinity/agent";
import { ActorSubclass, HttpAgent } from "@dfinity/agent";
import { Principal } from "@dfinity/principal";
import { mock } from "jest-mock-extended";
import { UserNotTheControllerError } from "./errors/ic-management.errors";
import { ICManagementCanister } from "./ic-management.canister";
import {
mockCanisterDetails,
mockCanisterId,
mockCanisterSettings,
} from "./ic-management.mock";
import { CanisterStatusDidResponse } from "./types/ic-management.did";
import { toCanisterDetails } from "./utils/ic-management.converters";

describe("ICManagementCanister", () => {
const mockAgent: HttpAgent = mock<HttpAgent>();

const createICManagement = async (service: ManagementCanisterRecord) => {
return ICManagementCanister.create({
agent: mockAgent,
serviceOverride: service as ActorSubclass<ManagementCanisterRecord>,
});
};

describe("ICManagementCanister.getCanisterDetails", () => {
it("returns account identifier when success", async () => {
const settings = {
freezing_threshold: BigInt(2),
controllers: [
Principal.fromText(
"xlmdg-vkosz-ceopx-7wtgu-g3xmd-koiyc-awqaq-7modz-zf6r6-364rh-oqe"
),
],
memory_allocation: BigInt(4),
compute_allocation: BigInt(10),
};
const response: CanisterStatusDidResponse = {
status: { running: null },
memory_size: BigInt(1000),
cycles: BigInt(10_000),
settings,
module_hash: [],
};
const service = mock<ManagementCanisterRecord>();
service.canister_status.mockResolvedValue(response);

const icManagement = await createICManagement(service);

const res = await icManagement.getCanisterDetails(mockCanisterDetails.id);

expect(res).toEqual(
toCanisterDetails({ response, canisterId: mockCanisterDetails.id })
);
});

it("throws UserNotTheControllerError", async () => {
const error = new Error("code: 403");
const service = mock<ManagementCanisterRecord>();
service.canister_status.mockRejectedValue(error);

const icManagement = await createICManagement(service);

const call = () =>
icManagement.getCanisterDetails(Principal.fromText("aaaaa-aa"));

expect(call).rejects.toThrowError(UserNotTheControllerError);
});

it("throws Error", async () => {
const error = new Error("Test");
const service = mock<ManagementCanisterRecord>();
service.canister_status.mockRejectedValue(error);

const icManagement = await createICManagement(service);

const call = () =>
icManagement.getCanisterDetails(Principal.fromText("aaaaa-aa"));

expect(call).rejects.toThrowError(Error);
});
});

describe("updateSettings", () => {
it("calls update_settings with new settings", async () => {
const service = mock<ManagementCanisterRecord>();
service.update_settings.mockResolvedValue(undefined);

const icManagement = await createICManagement(service);

await icManagement.updateSettings({
canisterId: mockCanisterId,
settings: mockCanisterSettings,
});
expect(service.update_settings).toBeCalled();
});

it("works when passed partial settings", async () => {
const partialSettings = {
controllers: [
"xlmdg-vkosz-ceopx-7wtgu-g3xmd-koiyc-awqaq-7modz-zf6r6-364rh-oqe",
],
};
const service = mock<ManagementCanisterRecord>();
service.update_settings.mockResolvedValue(undefined);

const icManagement = await createICManagement(service);

await icManagement.updateSettings({
canisterId: mockCanisterId,
settings: partialSettings,
});
expect(service.update_settings).toBeCalled();
});

it("throws UserNotTheControllerError", async () => {
const error = new Error("code: 403");
const service = mock<ManagementCanisterRecord>();
service.update_settings.mockRejectedValue(error);

const icManagement = await createICManagement(service);

const call = () =>
icManagement.updateSettings({
canisterId: mockCanisterId,
settings: mockCanisterSettings,
});
expect(call).rejects.toThrowError(UserNotTheControllerError);
});

it("throws Error", async () => {
const error = new Error("Test");
const service = mock<ManagementCanisterRecord>();
service.update_settings.mockRejectedValue(error);

const icManagement = await createICManagement(service);

const call = () =>
icManagement.updateSettings({
canisterId: mockCanisterId,
settings: mockCanisterSettings,
});
expect(call).rejects.toThrowError(Error);
});
});
});
Loading

0 comments on commit a25fbfa

Please sign in to comment.