Skip to content

Commit ab68d2c

Browse files
authored
feat(pic): add chunked WASM upload and canisterStatus method (#239)
## Summary Support large WASM installation in pic-js. Users need to be able to deploy any WASM module, including large ones that exceed the 2 MB IC ingress message size limit. Without this, large canisters simply fail to install. [SDK-2149](https://dfinity.atlassian.net/browse/SDK-2149) ### Chunked WASM upload When the combined size of WASM + install args exceeds 2 MB, `installCode`, `reinstallCode`, `upgradeCanister`, and `setupCanister` now automatically use the chunked upload path: `clear_chunk_store` → `upload_chunk` (parallel batches of 12, 1 MB each) → `install_chunked_code` This is **transparent to the caller**, no API changes required. Existing calls automatically switch to the chunked path when needed. Implementation follows the same pattern used in production by other projects (e.g., [Juno's IC management test utils](https://github.com/junobuild/juno/blob/7f777dcda24504801fcb4ebdd00cde3d9303ffe2/src/tests/utils/ic-management-tests.utils.ts#L189)). ### `canisterStatus` method New first-class method on `PocketIc` to query canister status (status, settings, module hash, cycles, memory size, query stats, etc.). ### IC management canister interface All chunk-related calls and `canister_status` follow the [IC interface spec candid definitions](https://docs.internetcomputer.org/references/ic-interface-spec#ic-candid). ## Test plan - `pnpm run build` - `pnpm run test:pic` - `pnpm audit` — no vulnerabilities - `pnpm run format:check` — clean [SDK-2149]: https://dfinity.atlassian.net/browse/SDK-2149?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
1 parent d077889 commit ab68d2c

12 files changed

Lines changed: 1687 additions & 1264 deletions

File tree

.github/CONTRIBUTING.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,13 @@ If you want to submit a pull request to fix an issue or add a feature, here's a
6363
git add path-to-changed-file
6464
```
6565
8. Commit your changes to store the contents you added to the index along with a descriptive message by running a command similar to the following:
66+
6667
```shell
6768
cz commit
6869
```
70+
6971
- See [Conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) for more information on the commit message formats.
72+
7073
9. Push the changes to the remote repository by running a command similar to the following:
7174
```shell
7275
git push origin my-branch-name-here

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
1+
# Changelog
2+
13
## Unreleased
24

35
### Feat
46

57
- add http gateway support (#233)
68
- bump PocketIC server to v12.0.0 (#231)
9+
- transparent large WASM installation via chunked upload for `installCode`, `reinstallCode`, `upgradeCanister`, and `setupCanister`
10+
- add `canisterStatus` method to `PocketIc` for querying canister status
11+
12+
### Chore
13+
14+
- remove unnecessary pnpm overrides
715

816
## 0.18.0 (2026-01-21)
917

10-
### BREAKING CHANCE
18+
### BREAKING CHANGE
1119

1220
- Reverts "introduce a new `rounds` option for `awaitCall` method on `PocketIcClient` (#223)"
1321

bun.lock

Lines changed: 76 additions & 343 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,22 +36,17 @@
3636
"devDependencies": {
3737
"@icp-sdk/bindgen": "^0.2.1",
3838
"@icp-sdk/core": "^5.0.0",
39-
"@swc/core": "^1.15.3",
39+
"@swc/core": "^1.15.18",
4040
"@swc/jest": "^0.2.39",
41-
"@tsconfig/node24": "^24.0.3",
42-
"@types/jest": "^29.5.14",
43-
"@types/node": "^22.13.10",
41+
"@tsconfig/node24": "^24.0.4",
42+
"@types/jest": "^30.0.0",
43+
"@types/node": "^25.3.3",
4444
"bip39": "^3.1.0",
4545
"jest": "^30.2.0",
4646
"npm-run-all": "^4.1.5",
47-
"prettier": "3.4.1",
47+
"prettier": "3.8.1",
4848
"ts-node": "^10.9.2",
49-
"typescript": "^5.8.2",
50-
"vitest": "^4.0.15"
51-
},
52-
"overrides": {
53-
"path-to-regexp@<0.1.12": ">=0.1.12",
54-
"path-to-regexp@>=2.0.0 <3.3.0": ">=3.3.0",
55-
"esbuild@<=0.24.2": ">=0.25.0"
49+
"typescript": "^5.9.3",
50+
"vitest": "^4.0.18"
5651
}
5752
}

packages/pic/src/management-canister.ts

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,3 +156,166 @@ export function encodeUpdateCanisterSettingsRequest(
156156
): Uint8Array {
157157
return new Uint8Array(IDL.encode([UpdateCanisterSettingsRequest], [arg]));
158158
}
159+
160+
// Canister status types
161+
162+
const CanisterStatusRequest = IDL.Record({
163+
canister_id: IDL.Principal,
164+
});
165+
166+
export interface CanisterStatusRequest {
167+
canister_id: Principal;
168+
}
169+
170+
export function encodeCanisterStatusRequest(
171+
arg: CanisterStatusRequest,
172+
): Uint8Array {
173+
return new Uint8Array(IDL.encode([CanisterStatusRequest], [arg]));
174+
}
175+
176+
const DefiniteCanisterSettings = IDL.Record({
177+
controllers: IDL.Vec(IDL.Principal),
178+
compute_allocation: IDL.Nat,
179+
memory_allocation: IDL.Nat,
180+
freezing_threshold: IDL.Nat,
181+
reserved_cycles_limit: IDL.Nat,
182+
});
183+
184+
export interface DefiniteCanisterSettings {
185+
controllers: Principal[];
186+
compute_allocation: bigint;
187+
memory_allocation: bigint;
188+
freezing_threshold: bigint;
189+
reserved_cycles_limit: bigint;
190+
}
191+
192+
const QueryStats = IDL.Record({
193+
num_calls_total: IDL.Nat,
194+
num_instructions_total: IDL.Nat,
195+
request_payload_bytes_total: IDL.Nat,
196+
response_payload_bytes_total: IDL.Nat,
197+
});
198+
199+
export interface QueryStats {
200+
num_calls_total: bigint;
201+
num_instructions_total: bigint;
202+
request_payload_bytes_total: bigint;
203+
response_payload_bytes_total: bigint;
204+
}
205+
206+
const CanisterStatusResponse = IDL.Record({
207+
status: IDL.Variant({
208+
running: IDL.Null,
209+
stopping: IDL.Null,
210+
stopped: IDL.Null,
211+
}),
212+
settings: DefiniteCanisterSettings,
213+
module_hash: IDL.Opt(IDL.Vec(IDL.Nat8)),
214+
memory_size: IDL.Nat,
215+
cycles: IDL.Nat,
216+
reserved_cycles: IDL.Nat,
217+
idle_cycles_burned_per_day: IDL.Nat,
218+
query_stats: QueryStats,
219+
});
220+
221+
export interface CanisterStatusResponse {
222+
status: { running: null } | { stopping: null } | { stopped: null };
223+
settings: DefiniteCanisterSettings;
224+
module_hash: [] | [Uint8Array];
225+
memory_size: bigint;
226+
cycles: bigint;
227+
reserved_cycles: bigint;
228+
idle_cycles_burned_per_day: bigint;
229+
query_stats: QueryStats;
230+
}
231+
232+
export function decodeCanisterStatusResponse(
233+
arg: Uint8Array,
234+
): CanisterStatusResponse {
235+
const payload = decodeCandid<CanisterStatusResponse>(
236+
[CanisterStatusResponse],
237+
arg,
238+
);
239+
240+
if (isNil(payload)) {
241+
throw new Error('Failed to decode CanisterStatusResponse');
242+
}
243+
244+
return payload;
245+
}
246+
247+
// Chunked code installation types
248+
249+
const ClearChunkStoreRequest = IDL.Record({
250+
canister_id: IDL.Principal,
251+
});
252+
253+
export interface ClearChunkStoreRequest {
254+
canister_id: Principal;
255+
}
256+
257+
export function encodeClearChunkStoreRequest(
258+
arg: ClearChunkStoreRequest,
259+
): Uint8Array {
260+
return new Uint8Array(IDL.encode([ClearChunkStoreRequest], [arg]));
261+
}
262+
263+
const UploadChunkRequest = IDL.Record({
264+
canister_id: IDL.Principal,
265+
chunk: IDL.Vec(IDL.Nat8),
266+
});
267+
268+
export interface UploadChunkRequest {
269+
canister_id: Principal;
270+
chunk: Uint8Array;
271+
}
272+
273+
export function encodeUploadChunkRequest(arg: UploadChunkRequest): Uint8Array {
274+
return new Uint8Array(IDL.encode([UploadChunkRequest], [arg]));
275+
}
276+
277+
const ChunkHash = IDL.Record({
278+
hash: IDL.Vec(IDL.Nat8),
279+
});
280+
281+
export interface ChunkHash {
282+
hash: Uint8Array;
283+
}
284+
285+
const UploadChunkResponse = ChunkHash;
286+
287+
export function decodeUploadChunkResponse(arg: Uint8Array): ChunkHash {
288+
const payload = decodeCandid<ChunkHash>([UploadChunkResponse], arg);
289+
290+
if (isNil(payload)) {
291+
throw new Error('Failed to decode UploadChunkResponse');
292+
}
293+
294+
return payload;
295+
}
296+
297+
const InstallChunkedCodeRequest = IDL.Record({
298+
mode: CanisterInstallMode,
299+
target_canister: IDL.Principal,
300+
store_canister: IDL.Opt(IDL.Principal),
301+
chunk_hashes_list: IDL.Vec(ChunkHash),
302+
wasm_module_hash: IDL.Vec(IDL.Nat8),
303+
arg: IDL.Vec(IDL.Nat8),
304+
sender_canister_version: IDL.Opt(IDL.Nat64),
305+
});
306+
307+
export interface InstallChunkedCodeRequest {
308+
mode: CanisterInstallMode;
309+
target_canister: Principal;
310+
store_canister: [] | [Principal];
311+
chunk_hashes_list: ChunkHash[];
312+
wasm_module_hash: Uint8Array;
313+
arg: Uint8Array;
314+
sender_canister_version: [] | [bigint];
315+
}
316+
317+
export function encodeInstallChunkedCodeRequest(
318+
arg: InstallChunkedCodeRequest,
319+
): Uint8Array {
320+
return new Uint8Array(IDL.encode([InstallChunkedCodeRequest], [arg]));
321+
}

packages/pic/src/pocket-ic-types.ts

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -685,8 +685,7 @@ export interface UpgradeCanisterOptions {
685685
* @category Types
686686
* @see [Principal](https://js.icp.build/core/latest/libs/principal/api/classes/principal/)
687687
*/
688-
export interface UpdateCanisterSettingsOptions
689-
extends Partial<CanisterSettings> {
688+
export interface UpdateCanisterSettingsOptions extends Partial<CanisterSettings> {
690689
/**
691690
* The Principal of the canister to update the settings for.
692691
*/
@@ -699,6 +698,102 @@ export interface UpdateCanisterSettingsOptions
699698
sender?: Principal;
700699
}
701700

701+
/**
702+
* Options for querying the status of a given canister.
703+
*
704+
* @category Types
705+
* @see [Principal](https://js.icp.build/core/latest/libs/principal/api/classes/principal/)
706+
*/
707+
export interface CanisterStatusOptions {
708+
/**
709+
* The Principal of the canister to query the status of.
710+
*/
711+
canisterId: Principal;
712+
713+
/**
714+
* The Principal to send the request as.
715+
* Defaults to the anonymous principal.
716+
*/
717+
sender?: Principal;
718+
}
719+
720+
/**
721+
* The status of a canister.
722+
*
723+
* @category Types
724+
*/
725+
export type CanisterStatus =
726+
| { running: null }
727+
| { stopping: null }
728+
| { stopped: null };
729+
730+
/**
731+
* Query statistics for a canister.
732+
*
733+
* @category Types
734+
*/
735+
export interface CanisterQueryStats {
736+
numCallsTotal: bigint;
737+
numInstructionsTotal: bigint;
738+
requestPayloadBytesTotal: bigint;
739+
responsePayloadBytesTotal: bigint;
740+
}
741+
742+
/**
743+
* The result of querying the status of a canister.
744+
* Matches the IC management canister `canister_status` response.
745+
*
746+
* @category Types
747+
* @see [Principal](https://js.icp.build/core/latest/libs/principal/api/classes/principal/)
748+
*/
749+
export interface CanisterStatusResult {
750+
/**
751+
* The current status of the canister.
752+
*/
753+
status: CanisterStatus;
754+
755+
/**
756+
* The definite settings of the canister.
757+
*/
758+
settings: {
759+
controllers: Principal[];
760+
computeAllocation: bigint;
761+
memoryAllocation: bigint;
762+
freezingThreshold: bigint;
763+
reservedCyclesLimit: bigint;
764+
};
765+
766+
/**
767+
* The SHA-256 hash of the installed WASM module, if any.
768+
*/
769+
moduleHash: Uint8Array | null;
770+
771+
/**
772+
* The total memory size of the canister in bytes.
773+
*/
774+
memorySize: bigint;
775+
776+
/**
777+
* The current cycle balance of the canister.
778+
*/
779+
cycles: bigint;
780+
781+
/**
782+
* The reserved cycles of the canister.
783+
*/
784+
reservedCycles: bigint;
785+
786+
/**
787+
* The amount of cycles burned per day when idle.
788+
*/
789+
idleCyclesBurnedPerDay: bigint;
790+
791+
/**
792+
* Query call statistics for the canister.
793+
*/
794+
queryStats: CanisterQueryStats;
795+
}
796+
702797
//#endregion CanisterLifecycle
703798

704799
//#region CanisterCall

0 commit comments

Comments
 (0)