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: CanisterStatus utility #572

Merged
merged 21 commits into from
May 19, 2022
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
22d8812
time, controllers, and module hash working
krpeacock May 17, 2022
b67b59c
individual paths can fail
krpeacock May 18, 2022
bb534c1
feat: support for custom paths with decoding strategies
krpeacock May 18, 2022
7757db4
mocked tests
krpeacock May 18, 2022
45bc8f1
cleaning up
krpeacock May 18, 2022
4935739
Merge branch 'main' into SDK-493-canister-state-api
krpeacock May 18, 2022
db50f19
clean up and documentation
krpeacock May 19, 2022
41ab9cc
Update packages/agent/src/canisterStatus/index.ts
krpeacock May 19, 2022
5dcf70f
Update packages/agent/src/certificate.ts
krpeacock May 19, 2022
0819deb
lowercase paths to fit spec
krpeacock May 19, 2022
d89febe
using snapshots with static certificate instead of truthy checks
krpeacock May 19, 2022
0d6e452
reverting certificate
krpeacock May 19, 2022
7df3436
Update docs/generated/changelog.html
krpeacock May 19, 2022
b7dac1e
removing unused cbor dev dependency
krpeacock May 19, 2022
7025999
Merge branch 'SDK-493-canister-state-api' of https://github.com/dfini…
krpeacock May 19, 2022
55057c1
Update packages/agent/src/canisterStatus/index.ts
krpeacock May 19, 2022
6f812c0
remove log, consistent module_hash
krpeacock May 19, 2022
60c080b
Merge branch 'main' into SDK-493-canister-state-api
krpeacock May 19, 2022
c0d608f
Merge branch 'main' into SDK-493-canister-state-api
krpeacock May 19, 2022
99f742c
Merge branch 'SDK-493-canister-state-api' of https://github.com/dfini…
krpeacock May 19, 2022
bcfa56d
removes agent constructor and requres an agent to be passed
krpeacock May 19, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
14.15
16.0
23 changes: 23 additions & 0 deletions docs/generated/changelog.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,29 @@ <h2>Version 0.11.2</h2>
Adds a default callback to the IdleManager that will refresh the page after clearing the
storage
</li>
<li>
Adds a new utility method,
<pre>canisterStatus</pre>
, to @agent/js. Canister status now allows you to query paths from the canister
certificate with a simple interface, using the API from the<a
href="https://smartcontracts.org/docs/current/references/ic-interface-spec#state-tree-canister-information"
>interface specification</a
><br />
Comes with nicely configured options for
<pre>Time</pre>
,
<pre>Controllers</pre>
,
<pre>Subnet</pre>
,
<pre>ModuleHash</pre>
, and
<pre>Candid</pre>
krpeacock marked this conversation as resolved.
Show resolved Hide resolved
. Additionally, has a utility for reading custom MetaData set using
<a href="https://github.com/dfinity/ic-wasm">ic-wasm</a>, as well as generic custom paths
in the format of ArrayBuffers.
</li>
<li>updates to package.json files for metadata in npm</li>
</ul>
<h2>Version 0.11.1</h2>
<ul>
Expand Down
38 changes: 38 additions & 0 deletions package-lock.json

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

1 change: 1 addition & 0 deletions packages/agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"@types/jest": "^26.0.22",
"@typescript-eslint/eslint-plugin": "^4.14.2",
"@typescript-eslint/parser": "^4.14.2",
"cbor": "^8.1.0",
"eslint": "^7.19.0",
"eslint-plugin-jsdoc": "^31.6.0",
"jest": "^27.3.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Canister Status utility should query canister controllers 1`] = `
Array [
Principal {
"_arr": Uint8Array [
0,
0,
0,
0,
0,
0,
0,
0,
1,
1,
],
"_isPrincipal": true,
},
]
`;

exports[`Canister Status utility should query canister module hash 1`] = `"896f6c079f96bc3cbef782af1ab1b52847f04700ff916eb49425566995a9a064"`;

exports[`Canister Status utility should query the candid interface 1`] = `
"type Example = text;
service : {
greet: (Example) -> (text) query;
}
"
`;

exports[`Canister Status utility should query the time 1`] = `2022-05-19T20:58:22.596Z`;

exports[`Canister Status utility should support multiple requests 1`] = `2022-05-19T20:58:22.596Z`;

exports[`Canister Status utility should support multiple requests 2`] = `
Array [
Principal {
"_arr": Uint8Array [
0,
0,
0,
0,
0,
0,
0,
0,
1,
1,
],
"_isPrincipal": true,
},
]
`;

exports[`Canister Status utility should support multiple requests with a failure 1`] = `2022-05-19T20:58:22.596Z`;

exports[`Canister Status utility should support valid custom paths 1`] = `1652993902596160000n`;

exports[`Canister Status utility should support valid custom paths 2`] = `ArrayBuffer []`;

exports[`Canister Status utility should support valid custom paths 3`] = `"80f485e1a4a6a7f816"`;

exports[`Canister Status utility should support valid custom paths 4`] = `
Array [
Uint8Array [
4,
],
Uint8Array [
0,
0,
0,
0,
0,
0,
0,
0,
1,
1,
],
]
`;

exports[`Canister Status utility should support valid metadata queries 1`] = `"74797065204578616d706c65203d20746578743b0a73657276696365203a207b0a202067726565743a20284578616d706c6529202d3e202874657874292071756572793b0a7d0a"`;

exports[`Canister Status utility should support valid metadata queries 2`] = `"74797065204578616d706c65203d20746578743b0a73657276696365203a207b0a202067726565743a20284578616d706c6529202d3e202874657874292071756572793b0a7d0a"`;
180 changes: 180 additions & 0 deletions packages/agent/src/canisterStatus/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
import { request, Path, encodePath } from './index';
import { Ed25519KeyIdentity } from '@dfinity/identity';
import { Principal } from '@dfinity/principal';
import { fromHexString } from '@dfinity/candid';
import { Identity } from '../auth';
import fetch from 'node-fetch';
import { HttpAgent } from '../agent';
import { fromHex, toHex } from '../utils/buffer';

const testPrincipal = Principal.fromText('rrkah-fqaaa-aaaaa-aaaaq-cai');

// bypass bls verification so that an old certificate is accepted
jest.mock('../utils/bls', () => {
return {
blsVerify: jest.fn(() => Promise.resolve(true)),
};
});

// Utils
const encoder = new TextEncoder();
const encode = (arg: string): ArrayBuffer => {
return new DataView(encoder.encode(arg).buffer).buffer;
};
const canisterBuffer = new DataView(testPrincipal.toUint8Array().buffer).buffer;

/* Produced by deploying a dfx new canister and requesting
| 'time'
| 'controllers'
| 'subnet'
| 'moduleHash'
| 'candid'
in dfx 0.10.0
*/
const testCases = [
{
certificate:
'd9d9f7a2647472656583018301830183024863616e697374657283018301820458204c805d47bd74dbcd6c8ce23ebd2e8287c453895165db6b81d93f1daf1b12004683024a0000000000000001010183018301820458205a1ee5770842c74b6749f4d72e3c1b8c0dafdaff48e113d19da4fda687df0636830183024b636f6e74726f6c6c657273820351d9d9f78241044a000000000000000001018302486d657461646174618301830182045820e8071e9c904063629f9ab66d4a447b7a881a964d16757762f424d2ef6c6a776b83024e63616e6469643a736572766963658203584774797065204578616d706c65203d20746578743b0a73657276696365203a207b0a202067726565743a20284578616d706c6529202d3e202874657874292071756572793b0a7d0a820458203676da3cc701ead8143596204d845c31a11d483dccffd5f80e5530660322212883024b6d6f64756c655f6861736882035820896f6c079f96bc3cbef782af1ab1b52847f04700ff916eb49425566995a9a064820458202d41b194a0931a274d874a4de945f104fbcf45de1bb201ec2bbdcb036c21fb0f82045820aa2f527164a8e4d898febf2bc0a8a4f95da58c3b62c6e4185e610e7b40dc615082045820fa572fdf7872444dba23377a8a426906c4314a61ef470df0af1b173b13abe949830182045820ec68f8bfb2a3f70cf8d3d427ff595e6ddb5d4230a8c3ca1d3ccb06e7694fd83283024474696d6582034980f485e1a4a6a7f816697369676e61747572655830adbb57f847e2656f248d3eec467af3c89eb5c63fa8d56bd3a3f48e3f3c570e50d0f824502fc69772d0d637190c52e4e4',
},
];

// Used for repopulating the certificate
const getRealStatus = async () => {
const identity = (await Ed25519KeyIdentity.generate(
new Uint8Array(
fromHexString('foo23342sd-234-234a-asdf-asdf-asdf-4frsefrsdf-weafasdfe-easdfee'),
),
)) as unknown as Identity;

const agent = new HttpAgent({ host: 'http://127.0.0.1:8000', fetch, identity });
await agent.fetchRootKey();
const canisterBuffer = new DataView(testPrincipal.toUint8Array().buffer).buffer;
canisterBuffer;
const response = await agent.readState(
testPrincipal,
// Note: subnet is not currently working due to a bug
{
paths: [
encodePath('time', testPrincipal),
[encode('canister'), canisterBuffer, encode('controllers')],
[encode('canister'), canisterBuffer, encode('module_hash')],
encodePath('candid', testPrincipal),
],
},
identity,
);
console.log(toHex(response.certificate));
};

// Mocked status using precomputed certificate
const getStatus = async (paths: Path[]) => {
const agent = new HttpAgent({ host: 'https://ic0.app' });
agent.readState = jest.fn(() =>
Promise.resolve({ certificate: fromHex(testCases[0].certificate) }),
);

return await request({
canisterId: testPrincipal,
// Note: subnet is not currently working due to a bug
paths,
agent,
});
};

describe('Canister Status utility', () => {
it('should query the time', async () => {
const status = await getStatus(['time']);
expect(status.get('time')).toMatchSnapshot();
});
it('should query canister controllers', async () => {
const status = await getStatus(['controllers']);
expect(status.get('controllers')).toMatchSnapshot();
});
it('should query canister module hash', async () => {
const status = await getStatus(['moduleHash']);
expect(status.get('moduleHash')).toMatchSnapshot();
});
it('should query the candid interface', async () => {
const status = await getStatus(['candid']);
expect(status.get('candid')).toMatchSnapshot();
});
it('should support valid custom paths', async () => {
const status = await getStatus([
{
key: 'time',
path: [new DataView(new TextEncoder().encode('time').buffer).buffer],
decodeStrategy: 'leb128',
},
]);
const statusRaw = await getStatus([
{
key: 'time',
path: [new DataView(new TextEncoder().encode('time').buffer).buffer],
decodeStrategy: 'raw',
},
]);
const statusHex = await getStatus([
{
key: 'time',
path: [new DataView(new TextEncoder().encode('time').buffer).buffer],
decodeStrategy: 'hex',
},
]);
const statusCBOR = await getStatus([
{
key: 'Controller',
path: [encode('canister'), canisterBuffer, encode('controllers')],
decodeStrategy: 'cbor',
},
]);
expect(status.get('time')).toMatchSnapshot();
expect(statusRaw.get('time')).toMatchSnapshot();
expect(statusHex.get('time')).toMatchSnapshot();
expect(statusCBOR.get('Controller')).toMatchSnapshot();
});
it('should support valid metadata queries', async () => {
const status = await getStatus([
{
kind: 'medadata',
path: 'candid:service',
key: 'candid',
decodeStrategy: 'hex',
},
]);
const statusEncoded = await getStatus([
{
kind: 'medadata',
path: encode('candid:service'),
key: 'candid',
decodeStrategy: 'hex',
},
]);
expect(status.get('candid')).toMatchSnapshot();
expect(statusEncoded.get('candid')).toMatchSnapshot();
});
it('should support multiple requests', async () => {
const status = await getStatus(['time', 'controllers']);
expect(status.get('time')).toMatchSnapshot();
expect(status.get('controllers')).toMatchSnapshot();
});
it('should support multiple requests with a failure', async () => {
// Deliberately requesting a bad value
const consoleSpy = jest.spyOn(console, 'warn');
const status = await getStatus([
'time',
// subnet and this arbitrary path should fail
'subnet',
{
key: 'asdf',
path: [new DataView(new TextEncoder().encode('asdf').buffer).buffer],
decodeStrategy: 'hex',
},
]);
expect(status.get('time')).toMatchSnapshot();
// Expect null for a failed result
expect(status.get('asdf' as unknown as Path)).toBe(null);
// Expect undefined for unset value
expect(status.get('test123')).toBe(undefined);
expect(consoleSpy).toBeCalledTimes(2);
});
});