diff --git a/packages/pic/src/management-canister.ts b/packages/pic/src/management-canister.ts index a01c240..d75d476 100644 --- a/packages/pic/src/management-canister.ts +++ b/packages/pic/src/management-canister.ts @@ -371,3 +371,52 @@ export function encodeInstallChunkedCodeRequest( ): Uint8Array { return new Uint8Array(IDL.encode([InstallChunkedCodeRequest], [arg])); } + +const CanisterLogRecord = IDL.Record({ + idx: IDL.Nat64, + timestamp_nanos: IDL.Nat64, + content: IDL.Vec(IDL.Nat8), +}); + +export interface CanisterLogRecord { + idx: bigint; + timestamp_nanos: bigint; + content: Uint8Array; +} + +const FetchCanisterLogsRequest = IDL.Record({ + canister_id: IDL.Principal, +}); + +export interface FetchCanisterLogsRequest { + canister_id: Principal; +} + +export function encodeFetchCanisterLogsRequest( + arg: FetchCanisterLogsRequest, +): Uint8Array { + return new Uint8Array(IDL.encode([FetchCanisterLogsRequest], [arg])); +} + +const FetchCanisterLogsResponse = IDL.Record({ + canister_log_records: IDL.Vec(CanisterLogRecord), +}); + +export interface FetchCanisterLogsResponse { + canister_log_records: CanisterLogRecord[]; +} + +export function decodeFetchCanisterLogsResponse( + arg: Uint8Array, +): FetchCanisterLogsResponse { + const payload = decodeCandid( + [FetchCanisterLogsResponse], + arg, + ); + + if (isNil(payload)) { + throw new Error('Failed to decode FetchCanisterLogsResponse'); + } + + return payload; +} diff --git a/packages/pic/src/pocket-ic-types.ts b/packages/pic/src/pocket-ic-types.ts index 43fd887..05295e4 100644 --- a/packages/pic/src/pocket-ic-types.ts +++ b/packages/pic/src/pocket-ic-types.ts @@ -1157,3 +1157,46 @@ export interface HttpsOutcallRejectResponse { } //#endregion HTTPS Outcalls + +//#region FetchCanisterLogs + +/** + * Options for fetching canister logs. + * + * @category Types + * @see [Principal](https://js.icp.build/core/latest/libs/principal/api/classes/principal/) + */ +export interface FetchCanisterLogsOptions { + /** + * The Principal of the canister to fetch logs for. + */ + canisterId: Principal; + + /** + * The Principal to send the request as. + * Defaults to the anonymous principal. + */ + sender?: Principal; +} + +/** + * A canister log record. + */ +export interface CanisterLogRecord { + /** + * The index of the log record. + */ + idx: bigint; + + /** + * The timestamp of the log record in nanoseconds since epoch. + */ + timestampNanos: bigint; + + /** + * The content of the log record. + */ + content: Uint8Array; +} + +//#endregion FetchCanisterLogs diff --git a/packages/pic/src/pocket-ic.ts b/packages/pic/src/pocket-ic.ts index 13d537d..3b5b456 100644 --- a/packages/pic/src/pocket-ic.ts +++ b/packages/pic/src/pocket-ic.ts @@ -31,6 +31,8 @@ import { MockPendingHttpsOutcallOptions, CanisterStatusOptions, CanisterStatusResult, + FetchCanisterLogsOptions, + CanisterLogRecord as CanisterLogRecordPublic, } from './pocket-ic-types'; import { MANAGEMENT_CANISTER_ID, @@ -47,6 +49,8 @@ import { encodeUploadChunkRequest, decodeCanisterStatusResponse, encodeCanisterStatusRequest, + encodeFetchCanisterLogsRequest, + decodeFetchCanisterLogsResponse, } from './management-canister'; import { createDeferredActorClass, @@ -861,6 +865,37 @@ export class PocketIc { return res.body; } + /** + * Fetches the logs for the given canister. + * + * @param options Options for fetching canister logs, see {@link FetchCanisterLogsOptions}. + * @returns An array of {@link CanisterLogRecord} entries. + */ + public async fetchCanisterLogs({ + canisterId, + sender = Principal.anonymous(), + }: FetchCanisterLogsOptions): Promise { + const payload = encodeFetchCanisterLogsRequest({ + canister_id: canisterId, + }); + + const res = await this.client.queryCall({ + canisterId: MANAGEMENT_CANISTER_ID, + sender, + method: 'fetch_canister_logs', + payload, + effectivePrincipal: { canisterId }, + }); + + const decoded = decodeFetchCanisterLogsResponse(res.body); + + return decoded.canister_log_records.map(record => ({ + idx: record.idx, + timestampNanos: record.timestamp_nanos, + content: new Uint8Array(record.content), + })); + } + /** * Makes an update call to the given canister. * diff --git a/packages/pic/tests/src/fetchCanisterLogs.spec.ts b/packages/pic/tests/src/fetchCanisterLogs.spec.ts new file mode 100644 index 0000000..bfd3ce6 --- /dev/null +++ b/packages/pic/tests/src/fetchCanisterLogs.spec.ts @@ -0,0 +1,42 @@ +import { CONTROLLER, TestFixture } from './util'; + +describe('fetchCanisterLogs', () => { + let fixture: TestFixture; + + beforeEach(async () => { + fixture = await TestFixture.create(); + }); + + afterEach(async () => { + await fixture.tearDown(); + }); + + it('should fetch canister logs', async () => { + const message = 'Hello from canister'; + + await fixture.actor.print_log(message); + + const logs = await fixture.pic.fetchCanisterLogs({ + canisterId: fixture.canisterId, + sender: CONTROLLER.getPrincipal(), + }); + + expect(logs.length).toBeGreaterThanOrEqual(1); + + const lastLog = logs[logs.length - 1]; + const logContent = new TextDecoder().decode(lastLog.content); + + expect(logContent).toBe(message); + expect(lastLog.idx).toBeDefined(); + expect(lastLog.timestampNanos).toBeDefined(); + }); + + it('should return empty logs for a canister with no logs', async () => { + const logs = await fixture.pic.fetchCanisterLogs({ + canisterId: fixture.canisterId, + sender: CONTROLLER.getPrincipal(), + }); + + expect(logs).toEqual([]); + }); +}); diff --git a/packages/pic/tests/test-canister/declarations/test_canister.did.d.ts b/packages/pic/tests/test-canister/declarations/test_canister.did.d.ts index ed3d861..27f4dfb 100644 --- a/packages/pic/tests/test-canister/declarations/test_canister.did.d.ts +++ b/packages/pic/tests/test-canister/declarations/test_canister.did.d.ts @@ -2,7 +2,7 @@ // @ts-nocheck -// This file was automatically generated by @icp-sdk/bindgen@0.2.1. +// This file was automatically generated by @icp-sdk/bindgen@0.2.3. // You should NOT make any changes in this file as it will be overwritten. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. @@ -11,6 +11,9 @@ import type { IDL } from '@icp-sdk/core/candid'; import type { Principal } from '@icp-sdk/core/principal'; export type Time = bigint; -export interface _SERVICE { 'get_time' : ActorMethod<[], Time> } +export interface _SERVICE { + 'get_time' : ActorMethod<[], Time>, + 'print_log' : ActorMethod<[string], undefined>, +} export declare const idlFactory: IDL.InterfaceFactory; export declare const init: (args: { IDL: typeof IDL }) => IDL.Type[]; \ No newline at end of file diff --git a/packages/pic/tests/test-canister/declarations/test_canister.did.js b/packages/pic/tests/test-canister/declarations/test_canister.did.js index 08229ee..546aa38 100644 --- a/packages/pic/tests/test-canister/declarations/test_canister.did.js +++ b/packages/pic/tests/test-canister/declarations/test_canister.did.js @@ -2,7 +2,7 @@ // @ts-nocheck -// This file was automatically generated by @icp-sdk/bindgen@0.2.1. +// This file was automatically generated by @icp-sdk/bindgen@0.2.3. // You should NOT make any changes in this file as it will be overwritten. // Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified. @@ -11,7 +11,10 @@ import { IDL } from '@icp-sdk/core/candid'; export const idlFactory = ({ IDL }) => { const Time = IDL.Int; - return IDL.Service({ 'get_time' : IDL.Func([], [Time], ['query']) }); + return IDL.Service({ + 'get_time' : IDL.Func([], [Time], ['query']), + 'print_log' : IDL.Func([IDL.Text], [], []), + }); }; export const init = ({ IDL }) => { return []; }; \ No newline at end of file diff --git a/packages/pic/tests/test-canister/main.mo b/packages/pic/tests/test-canister/main.mo index 5fa458e..dd66b3e 100644 --- a/packages/pic/tests/test-canister/main.mo +++ b/packages/pic/tests/test-canister/main.mo @@ -1,7 +1,12 @@ import Time "mo:core/Time"; +import Debug "mo:core/Debug"; persistent actor TestCanister { public query func get_time() : async Time.Time { return Time.now(); }; + + public func print_log(message : Text) : async () { + Debug.print(message); + }; }; diff --git a/packages/pic/tests/test-canister/test_canister.did b/packages/pic/tests/test-canister/test_canister.did index 6cab63b..66f565e 100644 --- a/packages/pic/tests/test-canister/test_canister.did +++ b/packages/pic/tests/test-canister/test_canister.did @@ -2,4 +2,5 @@ type Time = int; service : { get_time : () -> (Time) query; + print_log : (text) -> (); } diff --git a/packages/pic/tests/test-canister/test_canister.wasm.gz b/packages/pic/tests/test-canister/test_canister.wasm.gz index a8fc2e6..5e61f8c 100644 Binary files a/packages/pic/tests/test-canister/test_canister.wasm.gz and b/packages/pic/tests/test-canister/test_canister.wasm.gz differ