UNPKG

pic-js-mops

Version:

An Internet Computer Protocol canister testing library for TypeScript and JavaScript.

1,033 lines (850 loc) 24 kB
import { Principal } from '@icp-sdk/core/principal'; import { base64Decode, base64DecodePrincipal, base64Encode, base64EncodePrincipal, hexDecode, isNil, isNotNil, } from './util/index.js'; import { TopologyValidationError } from './error.js'; //#region CreateInstance export interface CreateInstanceRequest { nns?: NnsSubnetConfig; sns?: SnsSubnetConfig; ii?: IiSubnetConfig; fiduciary?: FiduciarySubnetConfig; bitcoin?: BitcoinSubnetConfig; system?: SystemSubnetConfig[]; application?: ApplicationSubnetConfig[]; verifiedApplication?: VerifiedApplicationSubnetConfig[]; processingTimeoutMs?: number; nonmainnetFeatures?: boolean; } export interface SubnetConfig< T extends NewSubnetStateConfig | FromPathSubnetStateConfig = | NewSubnetStateConfig | FromPathSubnetStateConfig, > { enableDeterministicTimeSlicing?: boolean; enableBenchmarkingInstructionLimits?: boolean; state: T; } export type NnsSubnetConfig = SubnetConfig<NnsSubnetStateConfig>; export type NnsSubnetStateConfig = | NewSubnetStateConfig | FromPathSubnetStateConfig; export type SnsSubnetConfig = SubnetConfig<SnsSubnetStateConfig>; export type SnsSubnetStateConfig = NewSubnetStateConfig; export type IiSubnetConfig = SubnetConfig<IiSubnetStateConfig>; export type IiSubnetStateConfig = NewSubnetStateConfig; export type FiduciarySubnetConfig = SubnetConfig<FiduciarySubnetStateConfig>; export type FiduciarySubnetStateConfig = NewSubnetStateConfig; export type BitcoinSubnetConfig = SubnetConfig<BitcoinSubnetStateConfig>; export type BitcoinSubnetStateConfig = NewSubnetStateConfig; export type SystemSubnetConfig = SubnetConfig<SystemSubnetStateConfig>; export type SystemSubnetStateConfig = NewSubnetStateConfig; export type ApplicationSubnetConfig = SubnetConfig<ApplicationSubnetStateConfig>; export type ApplicationSubnetStateConfig = NewSubnetStateConfig; export type VerifiedApplicationSubnetConfig = SubnetConfig<VerifiedApplicationSubnetStateConfig>; export type VerifiedApplicationSubnetStateConfig = NewSubnetStateConfig; export interface NewSubnetStateConfig { type: SubnetStateType.New; } export interface FromPathSubnetStateConfig { type: SubnetStateType.FromPath; path: string; } export enum SubnetStateType { New = 'new', FromPath = 'fromPath', } export interface EncodedCreateInstanceRequest { subnet_config_set: EncodedCreateInstanceSubnetConfig; nonmainnet_features: boolean; } export interface EncodedCreateInstanceSubnetConfig { nns?: EncodedSubnetConfig; sns?: EncodedSubnetConfig; ii?: EncodedSubnetConfig; fiduciary?: EncodedSubnetConfig; bitcoin?: EncodedSubnetConfig; system: EncodedSubnetConfig[]; application: EncodedSubnetConfig[]; verified_application: EncodedSubnetConfig[]; } export interface EncodedSubnetConfig { dts_flag: 'Enabled' | 'Disabled'; instruction_config: 'Production' | 'Benchmarking'; state_config: 'New' | { FromPath: string }; } function encodeManySubnetConfigs<T extends SubnetConfig>( configs: T[] = [], ): EncodedSubnetConfig[] { return configs.map(encodeSubnetConfig).filter(isNotNil); } function encodeSubnetConfig<T extends SubnetConfig>( config?: T, ): EncodedSubnetConfig | undefined { if (isNil(config)) { return undefined; } switch (config.state.type) { default: { throw new Error(`Unknown subnet state type: ${config.state}`); } case SubnetStateType.New: { return { dts_flag: encodeDtsFlag(config.enableDeterministicTimeSlicing), instruction_config: encodeInstructionConfig( config.enableBenchmarkingInstructionLimits, ), state_config: 'New', }; } case SubnetStateType.FromPath: { return { dts_flag: encodeDtsFlag(config.enableDeterministicTimeSlicing), instruction_config: encodeInstructionConfig( config.enableBenchmarkingInstructionLimits, ), state_config: { FromPath: config.state.path, }, }; } } } function encodeDtsFlag( enableDeterministicTimeSlicing?: boolean, ): EncodedSubnetConfig['dts_flag'] { return enableDeterministicTimeSlicing === false ? 'Disabled' : 'Enabled'; } function encodeInstructionConfig( enableBenchmarkingInstructionLimits?: boolean, ): EncodedSubnetConfig['instruction_config'] { return enableBenchmarkingInstructionLimits === true ? 'Benchmarking' : 'Production'; } export function encodeCreateInstanceRequest( req?: CreateInstanceRequest, ): EncodedCreateInstanceRequest { const defaultApplicationSubnet: ApplicationSubnetConfig = { state: { type: SubnetStateType.New }, }; const defaultOptions: CreateInstanceRequest = req ?? { application: [defaultApplicationSubnet], }; const options: EncodedCreateInstanceRequest = { subnet_config_set: { nns: encodeSubnetConfig(defaultOptions.nns), sns: encodeSubnetConfig(defaultOptions.sns), ii: encodeSubnetConfig(defaultOptions.ii), fiduciary: encodeSubnetConfig(defaultOptions.fiduciary), bitcoin: encodeSubnetConfig(defaultOptions.bitcoin), system: encodeManySubnetConfigs(defaultOptions.system), application: encodeManySubnetConfigs( defaultOptions.application ?? [defaultApplicationSubnet], ), verified_application: encodeManySubnetConfigs( defaultOptions.verifiedApplication, ), }, nonmainnet_features: defaultOptions.nonmainnetFeatures ?? false, }; if ( (isNil(options.subnet_config_set.nns) && isNil(options.subnet_config_set.sns) && isNil(options.subnet_config_set.ii) && isNil(options.subnet_config_set.fiduciary) && isNil(options.subnet_config_set.bitcoin) && options.subnet_config_set.system.length === 0 && options.subnet_config_set.application.length === 0) || options.subnet_config_set.system.length < 0 || options.subnet_config_set.application.length < 0 ) { throw new TopologyValidationError(); } return options; } //#endregion CreateInstance //#region GetPubKey export interface GetPubKeyRequest { subnetId: Principal; } export interface EncodedGetPubKeyRequest { subnet_id: string; } export function encodeGetPubKeyRequest( req: GetPubKeyRequest, ): EncodedGetPubKeyRequest { return { subnet_id: base64EncodePrincipal(req.subnetId), }; } //#endregion GetPubKey //#region GetTopology export type InstanceTopology = Record<string, SubnetTopology>; export interface SubnetTopology { id: Principal; type: SubnetType; size: number; canisterRanges: Array<{ start: Principal; end: Principal; }>; } export enum SubnetType { Application = 'Application', Bitcoin = 'Bitcoin', Fiduciary = 'Fiduciary', InternetIdentity = 'II', NNS = 'NNS', SNS = 'SNS', System = 'System', } export interface EncodedGetTopologyResponse { subnet_configs: Record<string, EncodedSubnetTopology>; default_effective_canister_id: { canister_id: string; }; } export interface EncodedSubnetTopology { subnet_kind: EncodedSubnetKind; size: number; canister_ranges: Array<{ start: { canister_id: string; }; end: { canister_id: string; }; }>; } export type EncodedSubnetKind = | 'Application' | 'Bitcoin' | 'Fiduciary' | 'II' | 'NNS' | 'SNS' | 'System'; export function decodeGetTopologyResponse( encoded: EncodedGetTopologyResponse, ): InstanceTopology { return Object.fromEntries( Object.entries(encoded.subnet_configs).map(([subnetId, subnetTopology]) => [ subnetId, decodeSubnetTopology(subnetId, subnetTopology), ]), ); } export function decodeSubnetTopology( subnetId: string, encoded: EncodedSubnetTopology, ): SubnetTopology { return { id: Principal.fromText(subnetId), type: decodeSubnetKind(encoded.subnet_kind), size: encoded.size, canisterRanges: encoded.canister_ranges.map(range => ({ start: base64DecodePrincipal(range.start.canister_id), end: base64DecodePrincipal(range.end.canister_id), })), }; } export function decodeSubnetKind(kind: EncodedSubnetKind): SubnetType { switch (kind) { case 'Application': return SubnetType.Application; case 'Bitcoin': return SubnetType.Bitcoin; case 'Fiduciary': return SubnetType.Fiduciary; case 'II': return SubnetType.InternetIdentity; case 'NNS': return SubnetType.NNS; case 'SNS': return SubnetType.SNS; case 'System': return SubnetType.System; default: throw new Error(`Unknown subnet kind: ${kind}`); } } export interface CreateInstanceSuccessResponse { Created: { instance_id: number; topology: EncodedGetTopologyResponse; }; } export interface CreateInstanceErrorResponse { Error: { message: string; }; } export type CreateInstanceResponse = | CreateInstanceSuccessResponse | CreateInstanceErrorResponse; //#endregion GetTopology //#region GetControllers export interface GetControllersRequest { canisterId: Principal; } export interface EncodedGetControllersRequest { canister_id: string; } export function encodeGetControllersRequest( req: GetControllersRequest, ): EncodedGetControllersRequest { return { canister_id: base64EncodePrincipal(req.canisterId), }; } export type GetControllersResponse = Principal[]; export type EncodedGetControllersResponse = { principal_id: string; }[]; export function decodeGetControllersResponse( res: EncodedGetControllersResponse, ): GetControllersResponse { return res.map(({ principal_id }) => base64DecodePrincipal(principal_id)); } //#endregion GetControllers //#region GetTime export interface GetTimeResponse { millisSinceEpoch: number; } export interface EncodedGetTimeResponse { nanos_since_epoch: number; } export function decodeGetTimeResponse( res: EncodedGetTimeResponse, ): GetTimeResponse { return { millisSinceEpoch: res.nanos_since_epoch / 1_000_000, }; } //#endregion GetTime //#region SetTime export interface SetTimeRequest { millisSinceEpoch: number; } export interface EncodedSetTimeRequest { nanos_since_epoch: number; } export function encodeSetTimeRequest( req: SetTimeRequest, ): EncodedSetTimeRequest { return { nanos_since_epoch: req.millisSinceEpoch * 1_000_000, }; } //#endregion SetTime //#region GetCanisterSubnetId export interface GetSubnetIdRequest { canisterId: Principal; } export interface EncodedGetSubnetIdRequest { canister_id: string; } export function encodeGetSubnetIdRequest( req: GetSubnetIdRequest, ): EncodedGetSubnetIdRequest { return { canister_id: base64EncodePrincipal(req.canisterId), }; } export type GetSubnetIdResponse = { subnetId: Principal | null; }; export type EncodedGetSubnetIdResponse = | { subnet_id: string; } | {}; export function decodeGetSubnetIdResponse( res: EncodedGetSubnetIdResponse, ): GetSubnetIdResponse { if (isNil(res)) { return { subnetId: null }; } if ('subnet_id' in res) { return { subnetId: base64DecodePrincipal(res.subnet_id) }; } return { subnetId: null }; } //#endregion GetCanisterSubnetId //#region GetCyclesBalance export interface GetCyclesBalanceRequest { canisterId: Principal; } export interface EncodedGetCyclesBalanceRequest { canister_id: string; } export function encodeGetCyclesBalanceRequest( req: GetCyclesBalanceRequest, ): EncodedGetCyclesBalanceRequest { return { canister_id: base64EncodePrincipal(req.canisterId), }; } export interface EncodedGetCyclesBalanceResponse { cycles: number; } export interface GetCyclesBalanceResponse { cycles: number; } export function decodeGetCyclesBalanceResponse( res: EncodedGetCyclesBalanceResponse, ): GetCyclesBalanceResponse { return { cycles: res.cycles, }; } //#endregion GetCyclesBalance //#region AddCycles export interface AddCyclesRequest { canisterId: Principal; amount: number; } export interface EncodedAddCyclesRequest { canister_id: string; amount: number; } export function encodeAddCyclesRequest( req: AddCyclesRequest, ): EncodedAddCyclesRequest { return { canister_id: base64EncodePrincipal(req.canisterId), amount: req.amount, }; } export interface AddCyclesResponse { cycles: number; } export interface EncodedAddCyclesResponse { cycles: number; } export function decodeAddCyclesResponse( res: EncodedAddCyclesResponse, ): AddCyclesResponse { return { cycles: res.cycles, }; } //#endregion AddCycles //#region UploadBlob export interface UploadBlobRequest { blob: Uint8Array; } export type EncodedUploadBlobRequest = Uint8Array; export function encodeUploadBlobRequest( req: UploadBlobRequest, ): EncodedUploadBlobRequest { return req.blob; } export interface UploadBlobResponse { blobId: Uint8Array; } export type EncodedUploadBlobResponse = string; export function decodeUploadBlobResponse( res: EncodedUploadBlobResponse, ): UploadBlobResponse { return { blobId: new Uint8Array(hexDecode(res)), }; } //#endregion UploadBlob //#region SetStableMemory export interface SetStableMemoryRequest { canisterId: Principal; blobId: Uint8Array; } export interface EncodedSetStableMemoryRequest { canister_id: string; blob_id: string; } export function encodeSetStableMemoryRequest( req: SetStableMemoryRequest, ): EncodedSetStableMemoryRequest { return { canister_id: base64EncodePrincipal(req.canisterId), blob_id: base64Encode(req.blobId), }; } //#endregion SetStableMemory //#region GetStableMemory export interface GetStableMemoryRequest { canisterId: Principal; } export interface EncodedGetStableMemoryRequest { canister_id: string; } export function encodeGetStableMemoryRequest( req: GetStableMemoryRequest, ): EncodedGetStableMemoryRequest { return { canister_id: base64EncodePrincipal(req.canisterId), }; } export interface GetStableMemoryResponse { blob: Uint8Array; } export interface EncodedGetStableMemoryResponse { blob: string; } export function decodeGetStableMemoryResponse( res: EncodedGetStableMemoryResponse, ): GetStableMemoryResponse { return { blob: base64Decode(res.blob), }; } //#endregion GetStableMemory //#region GetPendingHttpsOutcalls export interface GetPendingHttpsOutcallsResponse { subnetId: Principal; requestId: number; httpMethod: CanisterHttpMethod; url: string; headers: CanisterHttpHeader[]; body: Uint8Array; maxResponseBytes?: number; } export enum CanisterHttpMethod { GET = 'GET', POST = 'POST', HEAD = 'HEAD', } export type CanisterHttpHeader = [string, string]; export interface EncodedGetPendingHttpsOutcallsResponse { subnet_id: { subnet_id: string; }; request_id: number; http_method: EncodedCanisterHttpMethod; url: string; headers: EncodedCanisterHttpHeader[]; body: string; max_response_bytes?: number; } export enum EncodedCanisterHttpMethod { GET = 'GET', POST = 'POST', HEAD = 'HEAD', } export interface EncodedCanisterHttpHeader { name: string; value: string; } export function decodeGetPendingHttpsOutcallsResponse( res: EncodedGetPendingHttpsOutcallsResponse[], ): GetPendingHttpsOutcallsResponse[] { return res.map(decodeHttpOutcall); } function decodeHttpOutcall( res: EncodedGetPendingHttpsOutcallsResponse, ): GetPendingHttpsOutcallsResponse { return { subnetId: base64DecodePrincipal(res.subnet_id.subnet_id), requestId: res.request_id, httpMethod: decodeCanisterHttpMethod(res.http_method), url: res.url, headers: res.headers.map(decodeHttpHeader), body: base64Decode(res.body), maxResponseBytes: res.max_response_bytes, }; } function decodeCanisterHttpMethod( method: EncodedCanisterHttpMethod, ): CanisterHttpMethod { switch (method) { default: throw new Error(`Unknown canister HTTP method: ${method}`); case EncodedCanisterHttpMethod.GET: return CanisterHttpMethod.GET; case EncodedCanisterHttpMethod.POST: return CanisterHttpMethod.POST; case EncodedCanisterHttpMethod.HEAD: return CanisterHttpMethod.HEAD; } } function decodeHttpHeader( header: EncodedCanisterHttpHeader, ): CanisterHttpHeader { return [header.name, header.value]; } //#endregion GetPendingHttpsOutcalls //#region MockPendingHttpsOutcall export interface MockPendingHttpsOutcallRequest { subnetId: Principal; requestId: number; response: HttpsOutcallResponseMock; additionalResponses: HttpsOutcallResponseMock[]; } export type HttpsOutcallResponseMock = | HttpsOutcallSuccessResponseMock | HttpsOutcallRejectResponseMock; export interface HttpsOutcallSuccessResponseMock { type: 'success'; statusCode: number; headers: CanisterHttpHeader[]; body: Uint8Array; } export interface HttpsOutcallRejectResponseMock { type: 'reject'; statusCode: number; message: string; } export interface EncodedMockPendingHttpsOutcallRequest { subnet_id: { subnet_id: string; }; request_id: number; response: EncodedHttpsOutcallResponseMock; additional_responses: EncodedHttpsOutcallResponseMock[]; } export type EncodedHttpsOutcallResponseMock = | EncodedHttpsOutcallSuccessResponseMock | EncodedHttpsOutcallRejectResponseMock; export interface EncodedHttpsOutcallSuccessResponseMock { CanisterHttpReply: { status: number; headers: EncodedCanisterHttpHeader[]; body: string; }; } export interface EncodedHttpsOutcallRejectResponseMock { CanisterHttpReject: { reject_code: number; message: string; }; } export function encodeMockPendingHttpsOutcallRequest( req: MockPendingHttpsOutcallRequest, ): EncodedMockPendingHttpsOutcallRequest { return { subnet_id: { subnet_id: base64EncodePrincipal(req.subnetId), }, request_id: req.requestId, response: encodeHttpsOutcallResponse(req.response), additional_responses: req.additionalResponses.map( encodeHttpsOutcallResponse, ), }; } function encodeHttpsOutcallResponse( res: HttpsOutcallResponseMock, ): EncodedHttpsOutcallResponseMock { switch (res.type) { default: throw new Error(`Unknown response type: ${res}`); case 'success': { return { CanisterHttpReply: { status: res.statusCode, headers: res.headers.map(encodeHttpHeader), body: base64Encode(res.body), }, }; } case 'reject': { return { CanisterHttpReject: { reject_code: res.statusCode, message: res.message, }, }; } } } function encodeHttpHeader( header: CanisterHttpHeader, ): EncodedCanisterHttpHeader { return { name: header[0], value: header[1], }; } //#endregion MockPendingHttpsOutcall //#region CanisterCall export interface CanisterCallRequest { sender: Principal; canisterId: Principal; method: string; payload: Uint8Array; effectivePrincipal?: EffectivePrincipal; } export type EffectivePrincipal = | { subnetId: Principal; } | { canisterId: Principal; }; export interface EncodedCanisterCallRequest { sender: string; canister_id: string; method: string; payload: string; effective_principal?: EncodedEffectivePrincipal; } export type EncodedEffectivePrincipal = | { SubnetId: string; } | { CanisterId: string; } | 'None'; export function encodeEffectivePrincipal( effectivePrincipal?: EffectivePrincipal | null, ): EncodedEffectivePrincipal { if (isNil(effectivePrincipal)) { return 'None'; } if ('subnetId' in effectivePrincipal) { return { SubnetId: base64EncodePrincipal(effectivePrincipal.subnetId), }; } else { return { CanisterId: base64EncodePrincipal(effectivePrincipal.canisterId), }; } } export function decodeEffectivePrincipal( effectivePrincipal: EncodedEffectivePrincipal, ): EffectivePrincipal | null { if (effectivePrincipal === 'None') { return null; } else if ('SubnetId' in effectivePrincipal) { return { subnetId: base64DecodePrincipal(effectivePrincipal.SubnetId), }; } else { return { canisterId: base64DecodePrincipal(effectivePrincipal.CanisterId), }; } } export function encodeCanisterCallRequest( req: CanisterCallRequest, ): EncodedCanisterCallRequest { return { sender: base64EncodePrincipal(req.sender), canister_id: base64EncodePrincipal(req.canisterId), method: req.method, payload: base64Encode(req.payload), effective_principal: encodeEffectivePrincipal(req.effectivePrincipal), }; } export type EncodedCanisterCallResult<T> = | { Ok: T } | { Err: EncodedCanisterCallRejectResponse }; export interface EncodedCanisterCallRejectResponse { reject_code: number; reject_message: string; error_code: number; certified: boolean; } export interface CanisterCallResponse { body: Uint8Array; } export type EncodedCanisterCallResponse = EncodedCanisterCallResult<string>; export function decodeCanisterCallResponse( res: EncodedCanisterCallResponse, ): CanisterCallResponse { const okRes = decodeResultResponse<string>(res); return { body: base64Decode(okRes), }; } function decodeResultResponse<T>(res: EncodedCanisterCallResult<T>): T { if ('Err' in res) { throw new Error( `Canister call failed: ${res.Err.reject_message}. Reject code: ${res.Err.reject_code}. Error code: ${res.Err.error_code}. Certified: ${res.Err.certified}`, ); } return res.Ok; } //#endregion CanisterCall //#region SubmitCanisterCall export type SubmitCanisterCallRequest = CanisterCallRequest; export type EncodedSubmitCanisterCallRequest = EncodedCanisterCallRequest; export function encodeSubmitCanisterCallRequest( req: SubmitCanisterCallRequest, ): EncodedSubmitCanisterCallRequest { return encodeCanisterCallRequest(req); } export interface SubmitCanisterCallResponse { effectivePrincipal: EffectivePrincipal | null; messageId: Uint8Array; } export interface EncodedCanisterCallId { effective_principal: EncodedEffectivePrincipal; message_id: Uint8Array; } export type EncodedSubmitCanisterCallResponse = EncodedCanisterCallResult<EncodedCanisterCallId>; export function decodeSubmitCanisterCallResponse( res: EncodedSubmitCanisterCallResponse, ): SubmitCanisterCallResponse { const okRes = decodeResultResponse<EncodedCanisterCallId>(res); return { effectivePrincipal: decodeEffectivePrincipal(okRes.effective_principal), messageId: okRes.message_id, }; } //#endregion SubmitCanisterCall //#region IngressStatus export interface IngressStatusRequest { messageId: EncodedCanisterCallId; caller?: Principal; } export interface EncodedIngressStatusRequest { raw_message_id: EncodedCanisterCallId; raw_caller?: string; } export function encodeIngressStatusRequest( req: IngressStatusRequest, ): EncodedIngressStatusRequest { return { raw_message_id: req.messageId, raw_caller: req.caller ? base64EncodePrincipal(req.caller) : undefined, }; } export type IngressStatusResponse = CanisterCallResponse; export type EncodedIngressStatusResponse = EncodedCanisterCallResponse | {}; export function decodeIngressStatusResponse( res: EncodedIngressStatusResponse, ): IngressStatusResponse | null { if (isNil(res)) { return null; } if ('Ok' in res || 'Err' in res) { return decodeCanisterCallResponse(res); } throw new Error(`Unexpected ingress status response ${res}`); } //#endregion IngressStatus //#region AwaitCanisterCall export type AwaitCanisterCallRequest = SubmitCanisterCallResponse; export type EncodedAwaitCanisterCallRequest = EncodedCanisterCallId; export function encodeAwaitCanisterCallRequest( req: AwaitCanisterCallRequest, ): EncodedAwaitCanisterCallRequest { return { effective_principal: encodeEffectivePrincipal(req.effectivePrincipal), message_id: req.messageId, }; } export type AwaitCanisterCallResponse = CanisterCallResponse; //#endregion AwaitCanisterCall