diff --git a/src/userlib/js/src/actor.ts b/src/userlib/js/src/actor.ts index bf0e6b4d14..68961c74ea 100644 --- a/src/userlib/js/src/actor.ts +++ b/src/userlib/js/src/actor.ts @@ -3,7 +3,6 @@ import { CanisterId } from './canisterId'; import { HttpAgent } from './http_agent'; import { QueryResponseStatus, RequestStatusResponseStatus } from './http_agent_types'; import * as IDL from './idl'; -import { FuncClass } from './idl'; import { RequestId, toHex as requestIdToHex } from './request_id'; import { BinaryBlob } from './types'; @@ -14,6 +13,16 @@ import { BinaryBlob } from './types'; export type Actor = Record Promise> & { __canisterId(): string; __getAsset(path: string): Promise; + __install( + fields: { + module: BinaryBlob; + arg?: BinaryBlob; + }, + options?: { + maxAttempts?: number; + throttleDurationInMSecs?: number; + }, + ): Promise; }; export interface ActorConfig { @@ -83,7 +92,7 @@ export function makeActorFactory( async function requestStatusAndLoop( httpAgent: HttpAgent, requestId: RequestId, - func: FuncClass, + returnType: IDL.Type[], attempts: number, maxAttempts: number, throttle: number, @@ -92,7 +101,7 @@ export function makeActorFactory( switch (status.status) { case RequestStatusResponseStatus.Replied: { - return decodeReturnValue(func.retTypes, status.reply.arg); + return decodeReturnValue(returnType, status.reply.arg); } case RequestStatusResponseStatus.Unknown: @@ -107,7 +116,7 @@ export function makeActorFactory( // Wait a little, then retry. return new Promise(resolve => setTimeout(resolve, throttle)).then(() => - requestStatusAndLoop(httpAgent, requestId, func, attempts, maxAttempts, throttle), + requestStatusAndLoop(httpAgent, requestId, returnType, attempts, maxAttempts, throttle), ); case RequestStatusResponseStatus.Rejected: @@ -138,6 +147,47 @@ export function makeActorFactory( return agent.retrieveAsset(canisterId, path); }, + async __install( + fields: { + module: BinaryBlob; + arg?: BinaryBlob; + }, + options: { + maxAttempts?: number; + throttleDurationInMSecs?: number; + } = {}, + ) { + const agent = httpAgent || getDefaultHttpAgent(); + if (!agent) { + throw new Error('Cannot make call. httpAgent is undefined.'); + } + + // Resolve the options that can be used globally or locally. + const effectiveMaxAttempts = options.maxAttempts?.valueOf() || 0; + const effectiveThrottle = options.throttleDurationInMSecs?.valueOf() || 0; + + const { requestId, response } = await agent.install(canisterId, fields); + if (!response.ok) { + throw new Error( + [ + 'Install failed:', + ` Canister ID: ${cid.toHex()}`, + ` Request ID: ${requestIdToHex(requestId)}`, + ` HTTP status code: ${response.status}`, + ` HTTP status text: ${response.statusText}`, + ].join('\n'), + ); + } + + return requestStatusAndLoop( + agent, + requestId, + [], + effectiveMaxAttempts, + effectiveMaxAttempts, + effectiveThrottle, + ); + }, } as Actor; for (const [methodName, func] of Object.entries(actorInterface._fields)) { @@ -181,7 +231,7 @@ export function makeActorFactory( return requestStatusAndLoop( agent, requestId, - func, + func.retTypes, maxAttempts, maxAttempts, throttleDurationInMSecs, diff --git a/src/userlib/js/src/canisterId.ts b/src/userlib/js/src/canisterId.ts index 1d40088465..8b7942d91b 100644 --- a/src/userlib/js/src/canisterId.ts +++ b/src/userlib/js/src/canisterId.ts @@ -4,12 +4,16 @@ export class CanisterId { if (hex.startsWith('ic:')) { // Remove the checksum from the hexadecimal. // TODO: validate the checksum. - return new this(hex.slice(3, -2)); + return this.fromHex(hex.slice(3, -2)); } else { - throw new Error('CanisterId not a ic: url: ' + hex); + throw new Error('CanisterId not a "ic:" url: ' + hex); } } + private static fromHex(hex: string): CanisterId { + return new this(hex); + } + protected constructor(private _idHex: string) {} public toHex(): string { diff --git a/src/userlib/js/src/cbor.ts b/src/userlib/js/src/cbor.ts index 6d0f55b13c..66703f1a8d 100644 --- a/src/userlib/js/src/cbor.ts +++ b/src/userlib/js/src/cbor.ts @@ -52,7 +52,7 @@ class BufferEncoder implements CborEncoder { } public encode(v: Buffer): cbor.CborValue { - return cbor.value.bytes(new Uint8Array(v.buffer)); + return cbor.value.bytes(new Uint8Array(v)); } } diff --git a/src/userlib/js/src/http_agent.test.ts b/src/userlib/js/src/http_agent.test.ts index 425c8440dc..255c2ffb23 100644 --- a/src/userlib/js/src/http_agent.test.ts +++ b/src/userlib/js/src/http_agent.test.ts @@ -66,7 +66,7 @@ test('call', async () => { ...mockPartialRequest, sender_pubkey: keyPair.publicKey, sender_sig: senderSig, - }; + } as CallRequest; const expectedRequestId = await requestIdOf(expectedRequest); expect(expectedRequestId).toEqual(mockPartialsRequestId); diff --git a/src/userlib/js/src/http_agent.ts b/src/userlib/js/src/http_agent.ts index a6582afce2..a733eafa61 100644 --- a/src/userlib/js/src/http_agent.ts +++ b/src/userlib/js/src/http_agent.ts @@ -23,7 +23,7 @@ import { } from './http_agent_types'; import * as IDL from './idl'; import { requestIdOf } from './request_id'; -import { BinaryBlob } from './types'; +import { BinaryBlob, blobFromHex } from './types'; const API_VERSION = 'v1'; // HttpAgent options that can be used at construction. @@ -153,6 +153,21 @@ export class HttpAgent { }); } + public install( + canisterId: CanisterId | string, + fields: { + module: BinaryBlob; + arg?: BinaryBlob; + }, + ): Promise { + return this.submit({ + request_type: SubmitRequestType.InstallCode, + canister_id: typeof canisterId === 'string' ? CanisterId.fromText(canisterId) : canisterId, + module: fields.module, + arg: fields.arg || blobFromHex(''), + }); + } + public query(canisterId: CanisterId | string, fields: QueryFields): Promise { return this.read({ request_type: ReadRequestType.Query, diff --git a/src/userlib/js/src/http_agent_types.ts b/src/userlib/js/src/http_agent_types.ts index e11f0217b1..8ef0a63c6e 100644 --- a/src/userlib/js/src/http_agent_types.ts +++ b/src/userlib/js/src/http_agent_types.ts @@ -48,14 +48,21 @@ export interface CallRequest extends Record { method_name: string; arg: BinaryBlob; } +export interface InstallCodeRequest extends Record { + request_type: SubmitRequestType.InstallCode; + canister_id: CanisterId; + module: BinaryBlob; + arg?: BinaryBlob; +} // tslint:enable:camel-case // The types of values allowed in the `request_type` field for submit requests. export enum SubmitRequestType { Call = 'call', + InstallCode = 'install_code', } -export type SubmitRequest = CallRequest; +export type SubmitRequest = CallRequest | InstallCodeRequest; export interface SubmitResponse { requestId: RequestId; response: Response; diff --git a/src/userlib/js/src/index.ts b/src/userlib/js/src/index.ts index 2f13d44199..eb21142d31 100644 --- a/src/userlib/js/src/index.ts +++ b/src/userlib/js/src/index.ts @@ -4,6 +4,7 @@ export * from './canisterId'; export * from './http_agent'; export * from './http_agent_transforms'; export * from './http_agent_types'; +export * from './types'; import * as IDL from './idl'; export { IDL }; diff --git a/src/userlib/js/src/request_id.ts b/src/userlib/js/src/request_id.ts index d547658af2..2afce2b3b0 100644 --- a/src/userlib/js/src/request_id.ts +++ b/src/userlib/js/src/request_id.ts @@ -2,6 +2,7 @@ import borc from 'borc'; import { Buffer } from 'buffer/'; import { CanisterId } from './canisterId'; import { BinaryBlob, blobFromHex, blobToHex } from './types'; +import { lebEncode } from './utils/leb128'; export type RequestId = BinaryBlob & { __requestId__: void }; export function toHex(requestId: RequestId): string { @@ -23,6 +24,8 @@ async function hashValue(value: unknown): Promise { return hashValue(value.value); } else if (typeof value === 'string') { return hashString(value); + } else if (typeof value === 'number') { + return hash(lebEncode(value) as BinaryBlob); } else if (value instanceof CanisterId) { // HTTP handler expects canister_id to be an u64 & hashed in this way. // work-around for endianness problem until we switch to blobs diff --git a/src/userlib/js/src/utils/leb128.ts b/src/userlib/js/src/utils/leb128.ts index e0767f87ab..399a60b57d 100644 --- a/src/userlib/js/src/utils/leb128.ts +++ b/src/userlib/js/src/utils/leb128.ts @@ -1,4 +1,11 @@ // tslint:disable:no-bitwise +// Note: this file uses buffer-pipe, which on Node only, uses the Node Buffer +// implementation, which isn't compatible with the NPM buffer package +// which we use everywhere else. This means that we have to transform +// one into the other, hence why every function that returns a Buffer +// actually return `new Buffer(pipe.buffer)`. +// TODO: The best solution would be to have our own buffer type around +// Uint8Array which is standard. import BigNumber from 'bignumber.js'; import Pipe = require('buffer-pipe'); import { Buffer } from 'buffer/'; @@ -24,7 +31,7 @@ export function lebEncode(value: number | BigNumber): Buffer { } } - return pipe.buffer; + return new Buffer(pipe.buffer); } export function lebDecode(pipe: Pipe): BigNumber { @@ -73,7 +80,7 @@ export function slebEncode(value: BigNumber | number): Buffer { return bytes; } } - return pipe.buffer; + return new Buffer(pipe.buffer); } export function slebDecode(pipe: Pipe): BigNumber { @@ -128,7 +135,7 @@ export function writeIntLE(value: BigNumber | number, byteLength: number): Buffe pipe.write([byte]); mul = mul.times(256); } - return pipe.buffer; + return new Buffer(pipe.buffer); } export function readUIntLE(pipe: Pipe, byteLength: number): BigNumber {