Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
8 changes: 6 additions & 2 deletions src/userlib/js/src/canisterId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
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
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