Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 55 additions & 5 deletions src/userlib/js/src/actor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -14,6 +13,16 @@ import { BinaryBlob } from './types';
export type Actor = Record<string, (...args: unknown[]) => Promise<unknown>> & {
__canisterId(): string;
__getAsset(path: string): Promise<Uint8Array>;
__install(
fields: {
module: BinaryBlob;
arg?: BinaryBlob;
},
options?: {
maxAttempts?: number;
throttleDurationInMSecs?: number;
},
): Promise<void>;
};

export interface ActorConfig {
Expand Down Expand Up @@ -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,
Expand All @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -181,7 +231,7 @@ export function makeActorFactory(
return requestStatusAndLoop(
agent,
requestId,
func,
func.retTypes,
maxAttempts,
maxAttempts,
throttleDurationInMSecs,
Expand Down
7 changes: 5 additions & 2 deletions src/userlib/js/src/canisterId.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
// Canister IDs are represented as an array of bytes in the HTTP handler of the client.
export class CanisterId {
public static fromHex(hex: string): CanisterId {
Comment thread
hansl marked this conversation as resolved.
Outdated
return new this(hex);
}
public static fromText(hex: string): 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);
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/userlib/js/src/cbor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class BufferEncoder implements CborEncoder<Buffer> {
}

public encode(v: Buffer): cbor.CborValue {
return cbor.value.bytes(new Uint8Array(v.buffer));
return cbor.value.bytes(new Uint8Array(v));
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/userlib/js/src/http_agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
17 changes: 16 additions & 1 deletion src/userlib/js/src/http_agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -153,6 +153,21 @@ export class HttpAgent {
});
}

public install(
canisterId: CanisterId | string,
fields: {
module: BinaryBlob;
arg?: BinaryBlob;
},
): Promise<SubmitResponse> {
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<QueryResponse> {
return this.read({
request_type: ReadRequestType.Query,
Expand Down
9 changes: 8 additions & 1 deletion src/userlib/js/src/http_agent_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,21 @@ export interface CallRequest extends Record<string, any> {
method_name: string;
arg: BinaryBlob;
}
export interface InstallCodeRequest extends Record<string, any> {
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;
Expand Down
1 change: 1 addition & 0 deletions src/userlib/js/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
3 changes: 3 additions & 0 deletions src/userlib/js/src/request_id.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -23,6 +24,8 @@ async function hashValue(value: unknown): Promise<Buffer> {
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
Expand Down
4 changes: 4 additions & 0 deletions src/userlib/js/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ export type JsonValue = boolean | string | number | JsonArray | JsonObject;
// https://developer.mozilla.org/en-US/docs/Web/API/Blob
export type BinaryBlob = Buffer & { __BLOB: never };

export function blobFromUint8Array(arr: Uint8Array): BinaryBlob {
Comment thread
hansl marked this conversation as resolved.
Outdated
return Buffer.from(arr) as BinaryBlob;
}

export function blobFromHex(hex: string): BinaryBlob {
return Buffer.from(hex, 'hex') as BinaryBlob;
}
Expand Down
13 changes: 10 additions & 3 deletions src/userlib/js/src/utils/leb128.ts
Original file line number Diff line number Diff line change
@@ -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/';
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down