diff --git a/e2e/node/.prettierrc b/e2e/node/.prettierrc new file mode 100644 index 0000000000..e3291ef711 --- /dev/null +++ b/e2e/node/.prettierrc @@ -0,0 +1,10 @@ +{ + "trailingComma": "all", + "tabWidth": 2, + "printWidth": 100, + "semi": true, + "bracketSpacing": true, + "useTabs": false, + "singleQuote": true, + "arrowParens": "avoid" +} diff --git a/e2e/node/utils/canisters/counter.ts b/e2e/node/utils/canisters/counter.ts index da6d400645..12ae5f49d4 100644 --- a/e2e/node/utils/canisters/counter.ts +++ b/e2e/node/utils/canisters/counter.ts @@ -6,30 +6,35 @@ import { readFileSync } from 'fs'; const wasm = readFileSync(path.join(__dirname, 'counter.wasm')); type CounterActor = Actor & { - read(): Promise, - inc_read(): Promise, - write(n: number): Promise, + read(): Promise; + inc_read(): Promise; + write(n: number): Promise; }; -const factory = httpAgent.makeActorFactory(({ IDL }) => IDL.Service({ - 'read': IDL.Func([], [IDL.Nat], ['query']), - 'inc_read': IDL.Func([], [IDL.Nat], []), - 'inc': IDL.Func([], [], []), - 'write': IDL.Func([IDL.Nat], [], []), -})); +const factory = httpAgent.makeActorFactory(({ IDL }) => + IDL.Service({ + read: IDL.Func([], [IDL.Nat], ['query']), + inc_read: IDL.Func([], [IDL.Nat], []), + inc: IDL.Func([], [], []), + write: IDL.Func([IDL.Nat], [], []), + }), +); // TODO(hansl): Add a type to create an Actor interface from a IDL.Service definition. export async function counterFactory(): Promise { - let actor = await factory({ httpAgent }) as CounterActor; + let actor = (await factory({ agent: httpAgent })) as CounterActor; let cid = await actor.__createCanister(); actor.__setCanisterId(cid); - await actor.__install({ - module: blobFromUint8Array(wasm), - }, { - maxAttempts: 600, - throttleDurationInMSecs: 100, - }); + await actor.__install( + { + module: blobFromUint8Array(wasm), + }, + { + maxAttempts: 600, + throttleDurationInMSecs: 100, + }, + ); return actor; } diff --git a/e2e/node/utils/canisters/identity.ts b/e2e/node/utils/canisters/identity.ts index 0f88cdad3c..27a916c4df 100644 --- a/e2e/node/utils/canisters/identity.ts +++ b/e2e/node/utils/canisters/identity.ts @@ -1,24 +1,27 @@ import { blobFromUint8Array } from '@dfinity/agent'; -import {httpAgent, canisterIdFactory} from '../agent'; +import { httpAgent, canisterIdFactory } from '../agent'; import * as path from 'path'; import { readFileSync } from 'fs'; -import { default as idl, Identity } from "./identity/main.did"; +import { default as idl, Identity } from './identity/main.did'; const wasm = readFileSync(path.join(__dirname, 'identity/main.wasm')); const factory = httpAgent.makeActorFactory(idl); // TODO(hansl): Add a type to create an Actor interface from a IDL.Service definition. export async function identityFactory(): Promise { - let actor = await factory({ httpAgent }) as Identity; + let actor = (await factory({ agent: httpAgent })) as Identity; let cid = await actor.__createCanister(); actor.__setCanisterId(cid); - await actor.__install({ - module: blobFromUint8Array(wasm), - }, { - maxAttempts: 600, - throttleDurationInMSecs: 100, - }); + await actor.__install( + { + module: blobFromUint8Array(wasm), + }, + { + maxAttempts: 600, + throttleDurationInMSecs: 100, + }, + ); return actor; } diff --git a/src/agent/javascript/.prettierrc b/src/agent/javascript/.prettierrc index d05393bed7..e3291ef711 100644 --- a/src/agent/javascript/.prettierrc +++ b/src/agent/javascript/.prettierrc @@ -5,5 +5,6 @@ "semi": true, "bracketSpacing": true, "useTabs": false, - "singleQuote": true + "singleQuote": true, + "arrowParens": "avoid" } diff --git a/src/agent/javascript/package-lock.json b/src/agent/javascript/package-lock.json index 2bd3d6e76e..b8edf16998 100644 --- a/src/agent/javascript/package-lock.json +++ b/src/agent/javascript/package-lock.json @@ -4050,9 +4050,9 @@ "dev": true }, "prettier": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", - "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz", + "integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==", "dev": true }, "pretty-format": { diff --git a/src/agent/javascript/package.json b/src/agent/javascript/package.json index d22a4a6b0a..eea23aa8e5 100644 --- a/src/agent/javascript/package.json +++ b/src/agent/javascript/package.json @@ -30,7 +30,7 @@ "jest": "^24.9.0", "jest-expect-message": "^1.0.2", "node-fetch": "2.6.0", - "prettier": "^1.19.1", + "prettier": "^2.0.5", "text-encoding": "^0.7.0", "ts-jest": "^24.2.0", "tslint": "^5.20.0", diff --git a/src/agent/javascript/src/actor.test.ts b/src/agent/javascript/src/actor.test.ts index b7513cb1d1..e29fae94a7 100644 --- a/src/agent/javascript/src/actor.test.ts +++ b/src/agent/javascript/src/actor.test.ts @@ -1,22 +1,15 @@ import { Buffer } from 'buffer/'; import { makeActorFactory } from './actor'; +import { HttpAgent } from './agent'; import { makeAuthTransform, SenderPubKey, SenderSecretKey, SenderSig } from './auth'; import { CanisterId } from './canisterId'; import * as cbor from './cbor'; -import { HttpAgent } from './http_agent'; import { makeNonceTransform } from './http_agent_transforms'; -import { - CallRequest, - Signed, - SignedHttpAgentSubmitRequest, - SubmitRequest, - SubmitRequestType, -} from './http_agent_types'; +import { CallRequest, Signed, SubmitRequestType } from './http_agent_types'; import * as IDL from './idl'; import { Principal } from './principal'; import { requestIdOf } from './request_id'; import { blobFromHex, Nonce } from './types'; -import { sha256 } from './utils/sha256'; test('makeActor', async () => { const actorInterface = () => { @@ -126,7 +119,7 @@ test('makeActor', async () => { ), ); - const actor = makeActorFactory(actorInterface)({ canisterId, httpAgent }); + const actor = makeActorFactory(actorInterface)({ canisterId, agent: httpAgent }); const reply = await actor.greet(argValue); expect(reply).toEqual(IDL.decode([IDL.Text], expectedReplyArg)[0]); diff --git a/src/agent/javascript/src/actor.ts b/src/agent/javascript/src/actor.ts index c513072fb0..a26f4df441 100644 --- a/src/agent/javascript/src/actor.ts +++ b/src/agent/javascript/src/actor.ts @@ -1,6 +1,6 @@ import { Buffer } from 'buffer/'; +import { Agent } from './agent'; import { CanisterId } from './canisterId'; -import { HttpAgent } from './http_agent'; import { QueryResponseStatus, RequestStatusResponse, @@ -9,6 +9,7 @@ import { SubmitResponse, } from './http_agent_types'; import * as IDL from './idl'; +import { GlobalInternetComputer } from './index'; import { RequestId, toHex as requestIdToHex } from './request_id'; import { BinaryBlob } from './types'; @@ -39,23 +40,30 @@ export type Actor = Record Promise> & { export interface ActorConfig { canisterId?: string | CanisterId; - httpAgent?: HttpAgent; + agent?: Agent; maxAttempts?: number; throttleDurationInMSecs?: number; } -declare const window: { icHttpAgent?: HttpAgent }; -declare const global: { icHttpAgent?: HttpAgent }; -declare const self: { icHttpAgent?: HttpAgent }; - -function getDefaultHttpAgent() { - return typeof window === 'undefined' - ? typeof global === 'undefined' - ? typeof self === 'undefined' - ? undefined - : self.icHttpAgent - : global.icHttpAgent - : window.icHttpAgent; +declare const window: GlobalInternetComputer; +declare const global: GlobalInternetComputer; +declare const self: GlobalInternetComputer; + +function getDefaultAgent(): Agent { + const agent = + typeof window === 'undefined' + ? typeof global === 'undefined' + ? typeof self === 'undefined' + ? undefined + : self.ic.agent + : global.ic.agent + : window.ic.agent; + + if (!agent) { + throw new Error('No Agent could be found.'); + } + + return agent; } // IDL functions can have multiple return values, so decoding always @@ -86,7 +94,7 @@ export type ActorConstructor = (config: ActorConfig) => Actor; // Allows for one HTTP agent for the lifetime of the actor: // // ``` -// const actor = makeActor(actorInterface)(httpAgent); +// const actor = makeActor(actorInterface)({ agent }); // const reply = await actor.greet(); // ``` // @@ -94,8 +102,8 @@ export type ActorConstructor = (config: ActorConfig) => Actor; // // ``` // const actor = makeActor(actorInterface); -// const reply1 = await actor(httpAgent1).greet(); -// const reply2 = await actor(httpAgent2).greet(); +// const reply1 = await actor(agent1).greet(); +// const reply2 = await actor(agent2).greet(); // ``` export function makeActorFactory( actorInterfaceFactory: (_: { IDL: typeof IDL }) => IDL.ServiceClass, @@ -103,14 +111,14 @@ export function makeActorFactory( const actorInterface = actorInterfaceFactory({ IDL }); async function requestStatusAndLoop( - httpAgent: HttpAgent, + agent: Agent, requestId: RequestId, decoder: (response: RequestStatusResponseReplied) => T, attempts: number, maxAttempts: number, throttle: number, ): Promise { - const status = await httpAgent.requestStatus({ requestId }); + const status = await agent.requestStatus({ requestId }); switch (status.status) { case RequestStatusResponseStatus.Replied: { @@ -130,7 +138,7 @@ export function makeActorFactory( // Wait a little, then retry. return new Promise(resolve => setTimeout(resolve, throttle)).then(() => - requestStatusAndLoop(httpAgent, requestId, decoder, attempts, maxAttempts, throttle), + requestStatusAndLoop(agent, requestId, decoder, attempts, maxAttempts, throttle), ); case RequestStatusResponseStatus.Rejected: @@ -144,7 +152,7 @@ export function makeActorFactory( } return (config: ActorConfig) => { - const { canisterId, maxAttempts, throttleDurationInMSecs, httpAgent } = { + const { canisterId, maxAttempts, throttleDurationInMSecs, agent: configAgent } = { ...DEFAULT_ACTOR_CONFIG, ...config, } as Required; @@ -166,15 +174,26 @@ export function makeActorFactory( return cid?.toHex(); }, async __getAsset(path: string) { - const agent = httpAgent || getDefaultHttpAgent(); - if (!agent) { - throw new Error('Cannot make call. httpAgent is undefined.'); - } + const agent = configAgent || getDefaultAgent(); if (!cid) { throw new Error('Cannot make call. Canister ID is undefined.'); } - return agent.retrieveAsset(cid, path); + const arg = IDL.encode([IDL.Text], [path]) as BinaryBlob; + return agent.query(canisterId, { methodName: 'retrieve', arg }).then(response => { + switch (response.status) { + case QueryResponseStatus.Rejected: + throw new Error( + `An error happened while retrieving asset "${path}":\n` + + ` Status: ${response.status}\n` + + ` Message: ${response.reject_message}\n`, + ); + + case QueryResponseStatus.Replied: + const [content] = IDL.decode([IDL.Vec(IDL.Nat8)], response.reply.arg); + return new Uint8Array(content as number[]); + } + }); }, __setCanisterId(newCid: CanisterId): void { cid = newCid; @@ -185,11 +204,7 @@ export function makeActorFactory( throttleDurationInMSecs?: number; } = {}, ): Promise { - const agent = httpAgent || getDefaultHttpAgent(); - if (!agent) { - throw new Error('Cannot make call. httpAgent is undefined.'); - } - + const agent = configAgent || getDefaultAgent(); // Resolve the options that can be used globally or locally. const effectiveMaxAttempts = options.maxAttempts?.valueOf() || 0; const effectiveThrottle = options.throttleDurationInMSecs?.valueOf() || 0; @@ -233,10 +248,7 @@ export function makeActorFactory( throttleDurationInMSecs?: number; } = {}, ) { - const agent = httpAgent || getDefaultHttpAgent(); - if (!agent) { - throw new Error('Cannot make call. httpAgent is undefined.'); - } + const agent = configAgent || getDefaultAgent(); if (!cid) { throw new Error('Cannot make call. Canister ID is undefined.'); } @@ -271,10 +283,7 @@ export function makeActorFactory( for (const [methodName, func] of actorInterface._fields) { actor[methodName] = async (...args: any[]) => { - const agent = httpAgent || getDefaultHttpAgent(); - if (!agent) { - throw new Error('Cannot make call. httpAgent is undefined.'); - } + const agent = configAgent || getDefaultAgent(); if (!cid) { throw new Error('Cannot make call. Canister ID is undefined.'); } diff --git a/src/agent/javascript/src/agent/api.ts b/src/agent/javascript/src/agent/api.ts new file mode 100644 index 0000000000..a919aa73e8 --- /dev/null +++ b/src/agent/javascript/src/agent/api.ts @@ -0,0 +1,47 @@ +import { ActorConstructor } from '../actor'; +import { CanisterId } from '../canisterId'; +import { + QueryFields, + QueryResponse, + RequestStatusFields, + RequestStatusResponse, + SubmitResponse, +} from '../http_agent_types'; +import * as IDL from '../idl'; +import { Principal } from '../principal'; +import { BinaryBlob } from '../types'; + +// An Agent able to make calls and queries to a Replica. +export interface Agent { + requestStatus(fields: RequestStatusFields, principal?: Principal): Promise; + + call( + canisterId: CanisterId | string, + fields: { + methodName: string; + arg: BinaryBlob; + }, + principal?: Principal | Promise, + ): Promise; + + createCanister(principal?: Principal): Promise; + + install( + canisterId: CanisterId | string, + fields: { + module: BinaryBlob; + arg?: BinaryBlob; + }, + principal?: Principal, + ): Promise; + + query( + canisterId: CanisterId | string, + fields: QueryFields, + principal?: Principal, + ): Promise; + + makeActorFactory( + actorInterfaceFactory: (_: { IDL: typeof IDL }) => IDL.ServiceClass, + ): ActorConstructor; +} diff --git a/src/agent/javascript/src/http_agent.ts b/src/agent/javascript/src/agent/http.ts similarity index 80% rename from src/agent/javascript/src/http_agent.ts rename to src/agent/javascript/src/agent/http.ts index 4b20a66876..68d3889362 100644 --- a/src/agent/javascript/src/http_agent.ts +++ b/src/agent/javascript/src/agent/http.ts @@ -1,8 +1,8 @@ -import { toByteArray } from 'base64-js'; import { Buffer } from 'buffer/'; -import * as actor from './actor'; -import { CanisterId } from './canisterId'; -import * as cbor from './cbor'; +import * as actor from '../actor'; +import { Agent } from '../agent'; +import { CanisterId } from '../canisterId'; +import * as cbor from '../cbor'; import { AuthHttpAgentRequestTransformFn, Endpoint, @@ -16,26 +16,25 @@ import { ReadRequest, ReadRequestType, ReadResponse, + RequestStatusFields, RequestStatusResponse, - ResponseStatusFields, SignedHttpAgentRequest, SubmitRequest, SubmitRequestType, SubmitResponse, -} from './http_agent_types'; -import * as IDL from './idl'; -import { Principal } from './principal'; -import { requestIdOf } from './request_id'; -import { BinaryBlob, blobFromHex } from './types'; +} from '../http_agent_types'; +import * as IDL from '../idl'; +import { Principal } from '../principal'; +import { requestIdOf } from '../request_id'; +import { BinaryBlob, blobFromHex } from '../types'; const API_VERSION = 'v1'; // HttpAgent options that can be used at construction. export interface HttpAgentOptions { - // A parent to inherit configuration (pipeline and fetch) of. This is only - // used at construction; if the parent is changed we don't propagate those - // changes to the children. - parent?: HttpAgent; + // Another HttpAgent to inherit configuration (pipeline and fetch) of. This + // is only used at construction. + source?: HttpAgent; // A surrogate to the global fetch function. Useful for testing. fetch?: typeof fetch; @@ -72,7 +71,7 @@ function getDefaultFetch() { // it to the client. This is to decouple signature, nonce generation and // other computations so that this class can stay as simple as possible while // allowing extensions. -export class HttpAgent { +export class HttpAgent implements Agent { private readonly _pipeline: HttpAgentRequestTransformFn[] = []; private _authTransform: AuthHttpAgentRequestTransformFn | null = null; private readonly _fetch: typeof fetch; @@ -80,10 +79,10 @@ export class HttpAgent { private readonly _principal: Promise | null = null; constructor(options: HttpAgentOptions = {}) { - if (options.parent) { - this._pipeline = [...options.parent._pipeline]; - this._authTransform = options.parent._authTransform; - this._principal = options.parent._principal; + if (options.source) { + this._pipeline = [...options.source._pipeline]; + this._authTransform = options.source._authTransform; + this._principal = options.source._principal; } this._fetch = options.fetch || getDefaultFetch() || fetch.bind(global); if (options.host) { @@ -108,55 +107,6 @@ export class HttpAgent { this._authTransform = fn; } - public async submit(submit: SubmitRequest): Promise { - const transformedRequest = (await this._transform({ - request: { - body: null, - method: 'POST', - headers: { - 'Content-Type': 'application/cbor', - }, - }, - endpoint: Endpoint.Submit, - body: submit, - })) as HttpAgentSubmitRequest; - - const body = cbor.encode(transformedRequest.body); - - // Run both in parallel. The fetch is quite expensive, so we have plenty of time to - // calculate the requestId locally. - const [response, requestId] = await Promise.all([ - this._fetch(`${this._host}/api/${API_VERSION}/${Endpoint.Submit}`, { - ...transformedRequest.request, - body, - }), - requestIdOf(submit), - ]); - - return { requestId, response }; - } - - public async read(request: ReadRequest): Promise { - const transformedRequest = (await this._transform({ - request: { - method: 'POST', - headers: { - 'Content-Type': 'application/cbor', - }, - }, - endpoint: Endpoint.Read, - body: request, - })) as HttpAgentReadRequest; - - const body = cbor.encode(transformedRequest.body); - - const response = await this._fetch(`${this._host}/api/${API_VERSION}/${Endpoint.Read}`, { - ...transformedRequest.request, - body, - }); - return cbor.decode(Buffer.from(await response.arrayBuffer())); - } - public async call( canisterId: CanisterId | string, fields: { @@ -236,26 +186,8 @@ export class HttpAgent { }) as Promise; } - public retrieveAsset(canisterId: CanisterId | string, path: string): Promise { - const arg = IDL.encode([IDL.Text], [path]) as BinaryBlob; - return this.query(canisterId, { methodName: 'retrieve', arg }).then(response => { - switch (response.status) { - case QueryResponseStatus.Rejected: - throw new Error( - `An error happened while retrieving asset "${path}":\n` + - ` Status: ${response.status}\n` + - ` Message: ${response.reject_message}\n`, - ); - - case QueryResponseStatus.Replied: - const [content] = IDL.decode([IDL.Vec(IDL.Nat8)], response.reply.arg); - return new Uint8Array(content as number[]); - } - }); - } - public async requestStatus( - fields: ResponseStatusFields, + fields: RequestStatusFields, principal?: Principal, ): Promise { let p = this._principal || principal; @@ -290,4 +222,60 @@ export class HttpAgent { return p; } } + + protected async submit(submit: SubmitRequest): Promise { + const transformedRequest = (await this._transform({ + request: { + body: null, + method: 'POST', + headers: { + 'Content-Type': 'application/cbor', + }, + }, + endpoint: Endpoint.Submit, + body: submit, + })) as HttpAgentSubmitRequest; + + const body = cbor.encode(transformedRequest.body); + + // Run both in parallel. The fetch is quite expensive, so we have plenty of time to + // calculate the requestId locally. + const [response, requestId] = await Promise.all([ + this._fetch(`${this._host}/api/${API_VERSION}/${Endpoint.Submit}`, { + ...transformedRequest.request, + body, + }), + requestIdOf(submit), + ]); + + return { + requestId, + response: { + ok: response.ok, + status: response.status, + statusText: response.statusText, + }, + }; + } + + protected async read(request: ReadRequest): Promise { + const transformedRequest = (await this._transform({ + request: { + method: 'POST', + headers: { + 'Content-Type': 'application/cbor', + }, + }, + endpoint: Endpoint.Read, + body: request, + })) as HttpAgentReadRequest; + + const body = cbor.encode(transformedRequest.body); + + const response = await this._fetch(`${this._host}/api/${API_VERSION}/${Endpoint.Read}`, { + ...transformedRequest.request, + body, + }); + return cbor.decode(Buffer.from(await response.arrayBuffer())); + } } diff --git a/src/agent/javascript/src/agent/index.ts b/src/agent/javascript/src/agent/index.ts new file mode 100644 index 0000000000..3d0fbaf87f --- /dev/null +++ b/src/agent/javascript/src/agent/index.ts @@ -0,0 +1,3 @@ +export * from './api'; +export * from './http'; +export * from './proxy'; diff --git a/src/agent/javascript/src/agent/proxy.ts b/src/agent/javascript/src/agent/proxy.ts new file mode 100644 index 0000000000..f5be26f980 --- /dev/null +++ b/src/agent/javascript/src/agent/proxy.ts @@ -0,0 +1,205 @@ +import { + BinaryBlob, + CallFields, + CanisterId, + Principal, + QueryFields, + QueryResponse, + RequestStatusFields, + RequestStatusResponse, + SubmitResponse, +} from '@dfinity/agent'; +import * as actor from '../actor'; +import { Agent } from './api'; + +export enum ProxyMessageKind { + Error = 'err', + Query = 'q', + QueryResponse = 'qr', + Call = 'c', + CallResponse = 'cr', + RequestStatus = 's', + RequestStatusResponse = 'sr', +} + +export interface ProxyMessageBase { + id: number; + type: ProxyMessageKind; +} + +export interface ProxyMessageQuery extends ProxyMessageBase { + type: ProxyMessageKind.Query; + args: [string, QueryFields, Principal | undefined]; +} + +export interface ProxyMessageCall extends ProxyMessageBase { + type: ProxyMessageKind.Call; + args: [string, CallFields, Principal | undefined]; +} + +export interface ProxyMessageRequestStatus extends ProxyMessageBase { + type: ProxyMessageKind.RequestStatus; + args: [RequestStatusFields, Principal | undefined]; +} + +export interface ProxyMessageError extends ProxyMessageBase { + type: ProxyMessageKind.Error; + error: any; +} + +export interface ProxyMessageQueryResponse extends ProxyMessageBase { + type: ProxyMessageKind.QueryResponse; + response: QueryResponse; +} + +export interface ProxyMessageCallResponse extends ProxyMessageBase { + type: ProxyMessageKind.CallResponse; + response: SubmitResponse; +} + +export interface ProxyMessageRequestStatusResponse extends ProxyMessageBase { + type: ProxyMessageKind.RequestStatusResponse; + response: RequestStatusResponse; +} + +export type ProxyMessage = + | ProxyMessageError + | ProxyMessageQueryResponse + | ProxyMessageCallResponse + | ProxyMessageRequestStatusResponse + | ProxyMessageQuery + | ProxyMessageCall + | ProxyMessageRequestStatus; + +// A Stub Agent that forwards calls to another Agent implementation. +export class ProxyStubAgent { + constructor(private _frontend: (msg: ProxyMessage) => void, private _agent: Agent) {} + + public onmessage(msg: ProxyMessage): void { + switch (msg.type) { + case ProxyMessageKind.Query: + this._agent.query(...msg.args).then(response => { + this._frontend({ + id: msg.id, + type: ProxyMessageKind.QueryResponse, + response, + }); + }); + break; + case ProxyMessageKind.Call: + this._agent.call(...msg.args).then(response => { + this._frontend({ + id: msg.id, + type: ProxyMessageKind.CallResponse, + response, + }); + }); + break; + case ProxyMessageKind.RequestStatus: + this._agent.requestStatus(...msg.args).then(response => { + this._frontend({ + id: msg.id, + type: ProxyMessageKind.RequestStatusResponse, + response, + }); + }); + break; + + default: + throw new Error(`Invalid message received: ${JSON.stringify(msg)}`); + } + } +} + +// An Agent that forwards calls to a backend. The calls are serialized +export class ProxyAgent implements Agent { + private _nextId = 0; + private _pendingCalls = new Map void, (reject: any) => void]>(); + + constructor(private _backend: (msg: ProxyMessage) => void) {} + + public onmessage(msg: ProxyMessage): void { + const id = msg.id; + + const maybePromise = this._pendingCalls.get(id); + if (!maybePromise) { + throw new Error('A proxy get the same message twice...'); + } + + this._pendingCalls.delete(id); + const [resolve, reject] = maybePromise; + + switch (msg.type) { + case ProxyMessageKind.Error: + return reject(msg.error); + case ProxyMessageKind.CallResponse: + case ProxyMessageKind.QueryResponse: + case ProxyMessageKind.RequestStatusResponse: + return resolve(msg.response); + default: + throw new Error(`Invalid message being sent to ProxyAgent: ${JSON.stringify(msg)}`); + } + } + + public requestStatus( + fields: RequestStatusFields, + principal?: Principal, + ): Promise { + return this._sendAndWait({ + id: this._nextId++, + type: ProxyMessageKind.RequestStatus, + args: [fields, principal], + }) as Promise; + } + + public call( + canisterId: CanisterId | string, + fields: CallFields, + principal?: Principal, + ): Promise { + return this._sendAndWait({ + id: this._nextId++, + type: ProxyMessageKind.Call, + args: [canisterId.toString(), fields, principal], + }) as Promise; + } + + public createCanister(principal?: Principal): Promise { + throw new Error('unimplemented. This will be removed when we upgrade the spec to 0.8'); + } + + public install( + canisterId: CanisterId | string, + fields: { + module: BinaryBlob; + arg?: BinaryBlob; + }, + principal?: Principal, + ): Promise { + throw new Error('unimplemented. This will be removed when we upgrade the spec to 0.8'); + } + + public query( + canisterId: CanisterId | string, + fields: QueryFields, + principal?: Principal, + ): Promise { + return this._sendAndWait({ + id: this._nextId++, + type: ProxyMessageKind.Query, + args: [canisterId.toString(), fields, principal], + }) as Promise; + } + + public get makeActorFactory() { + return actor.makeActorFactory; + } + + private async _sendAndWait(msg: ProxyMessage): Promise { + return new Promise((resolve, reject) => { + this._pendingCalls.set(msg.id, [resolve, reject]); + + this._backend(msg); + }); + } +} diff --git a/src/agent/javascript/src/candid/candid-ui.ts b/src/agent/javascript/src/candid/candid-ui.ts index d17d8fe53b..83d6c72c7f 100644 --- a/src/agent/javascript/src/candid/candid-ui.ts +++ b/src/agent/javascript/src/candid/candid-ui.ts @@ -141,9 +141,7 @@ class Random extends IDL.Visitor { return Math.random() < 0.5; } public visitText(t: IDL.TextClass, v: string): string { - return Math.random() - .toString(36) - .substring(6); + return Math.random().toString(36).substring(6); } public visitFloat(t: IDL.FloatClass, v: string): number { return Math.random(); diff --git a/src/agent/javascript/src/canisterId.ts b/src/agent/javascript/src/canisterId.ts index 6e83bf81f1..5df710e533 100644 --- a/src/agent/javascript/src/canisterId.ts +++ b/src/agent/javascript/src/canisterId.ts @@ -2,10 +2,7 @@ import { crc8 } from 'crc'; import { BinaryBlob, blobFromHex, blobToHex } from './types'; function getCrc(hex: string): string { - return crc8(Buffer.from(hex, 'hex')) - .toString(16) - .toUpperCase() - .padStart(2, '0'); + return crc8(Buffer.from(hex, 'hex')).toString(16).toUpperCase().padStart(2, '0'); } // Canister IDs are represented as an array of bytes in the HTTP handler of the client. @@ -50,4 +47,7 @@ export class CanisterId { const crc = getCrc(this._idHex); return 'ic:' + this.toHex() + crc; } + public toString(): string { + return this.toText(); + } } diff --git a/src/agent/javascript/src/http_agent.test.ts b/src/agent/javascript/src/http_agent.test.ts index c3e3e738c1..62a06aeed6 100644 --- a/src/agent/javascript/src/http_agent.test.ts +++ b/src/agent/javascript/src/http_agent.test.ts @@ -1,20 +1,19 @@ import { Buffer } from 'buffer/'; +import { HttpAgent } from './agent'; import { createKeyPairFromSeed, makeAuthTransform, SenderSig, sign, verify } from './auth'; import { CanisterId } from './canisterId'; import * as cbor from './cbor'; -import { HttpAgent } from './http_agent'; import { makeNonceTransform } from './http_agent_transforms'; import { CallRequest, ReadRequestType, RequestStatusResponseReplied, - RequestStatusResponseStatus, Signed, SubmitRequestType, } from './http_agent_types'; import { Principal } from './principal'; import { requestIdOf } from './request_id'; -import { BinaryBlob, blobFromHex } from './types'; +import { BinaryBlob } from './types'; import { Nonce } from './types'; test('call', async () => { diff --git a/src/agent/javascript/src/http_agent_types.ts b/src/agent/javascript/src/http_agent_types.ts index 5423cc06e8..aadf42075a 100644 --- a/src/agent/javascript/src/http_agent_types.ts +++ b/src/agent/javascript/src/http_agent_types.ts @@ -59,10 +59,15 @@ export interface QueryFields { methodName: string; arg: BinaryBlob; } -export interface ResponseStatusFields { +export interface RequestStatusFields { requestId: RequestId; } +export interface CallFields { + methodName: string; + arg: BinaryBlob; +} + // The fields in a "call" submit request. // tslint:disable:camel-case export interface CallRequest extends Record { @@ -95,7 +100,11 @@ export enum SubmitRequestType { export type SubmitRequest = CallRequest | InstallCodeRequest | CreateCanisterRequest; export interface SubmitResponse { requestId: RequestId; - response: Response; + response: { + ok: boolean; + status: number; + statusText: string; + }; } // An ADT that represents responses to a "query" read request. diff --git a/src/agent/javascript/src/idl.ts b/src/agent/javascript/src/idl.ts index 0da9467fed..c2befb712d 100644 --- a/src/agent/javascript/src/idl.ts +++ b/src/agent/javascript/src/idl.ts @@ -1048,10 +1048,7 @@ function decodePrincipalId(b: Pipe): CanisterId { throw new Error('Cannot decode principal'); } const len = lebDecode(b).toNumber(); - const hex = b - .read(len) - .toString('hex') - .toUpperCase(); + const hex = b.read(len).toString('hex').toUpperCase(); return CanisterId.fromHex(hex); } diff --git a/src/agent/javascript/src/index.ts b/src/agent/javascript/src/index.ts index 8e99c36d40..b13464ae32 100644 --- a/src/agent/javascript/src/index.ts +++ b/src/agent/javascript/src/index.ts @@ -1,12 +1,21 @@ export * from './actor'; -export { generateKeyPair, makeAuthTransform, makeKeyPair } from './auth'; +export * from './agent'; +export { + KeyPair, + SenderPubKey, + SenderSecretKey, + SenderSig, + generateKeyPair, + makeAuthTransform, + makeKeyPair, +} from './auth'; export * from './canisterId'; -export * from './http_agent'; export * from './http_agent_transforms'; export * from './http_agent_types'; export * from './principal'; export * from './types'; +import { Agent, HttpAgent } from './agent'; import * as IDL from './idl'; export { IDL }; @@ -14,3 +23,11 @@ export { IDL }; import * as UICore from './candid/candid-core'; import * as UI from './candid/candid-ui'; export { UICore, UI }; + +export interface GlobalInternetComputer { + ic: { + agent: Agent; + HttpAgent: typeof HttpAgent; + IDL: typeof IDL; + }; +} diff --git a/src/agent/javascript/src/utils/leb128.ts b/src/agent/javascript/src/utils/leb128.ts index 156d95e495..0fea391460 100644 --- a/src/agent/javascript/src/utils/leb128.ts +++ b/src/agent/javascript/src/utils/leb128.ts @@ -126,11 +126,7 @@ export function writeIntLE(value: BigNumber | number, byteLength: number): Buffe if (value.lt(0) && sub === 0 && byte !== 0) { sub = 1; } - byte = value - .idiv(mul) - .minus(sub) - .mod(256) - .toNumber(); + byte = value.idiv(mul).minus(sub).mod(256).toNumber(); pipe.write([byte]); mul = mul.times(256); } diff --git a/src/agent/javascript/tslint.json b/src/agent/javascript/tslint.json index 4bb79af45e..b9a807b944 100644 --- a/src/agent/javascript/tslint.json +++ b/src/agent/javascript/tslint.json @@ -11,6 +11,7 @@ ] }, "rules": { + "max-classes-per-file": false, "interface-name": [true, "never-prefix"], "no-consecutive-blank-lines": [true, 2], "no-empty": [true, "allow-empty-functions"], diff --git a/src/bootstrap/.gitignore b/src/bootstrap/.gitignore index 8e10d560c9..44e0e8dbad 100644 --- a/src/bootstrap/.gitignore +++ b/src/bootstrap/.gitignore @@ -1,6 +1,7 @@ build_info.json node_modules/ dist/ +ts-out/ **/*.js **/*.js.map **/*.d.ts diff --git a/src/bootstrap/.prettierrc b/src/bootstrap/.prettierrc index d05393bed7..e3291ef711 100644 --- a/src/bootstrap/.prettierrc +++ b/src/bootstrap/.prettierrc @@ -5,5 +5,6 @@ "semi": true, "bracketSpacing": true, "useTabs": false, - "singleQuote": true + "singleQuote": true, + "arrowParens": "avoid" } diff --git a/src/bootstrap/README.adoc b/src/bootstrap/README.adoc new file mode 100644 index 0000000000..2f0912245d --- /dev/null +++ b/src/bootstrap/README.adoc @@ -0,0 +1,46 @@ += Bootstrap Server + +== How to Run Locally + +Locally in your sdk repo, execute: + +. `npm install`. To install all Node dependencies. +. `npm run webpack -- --watch` will start webpack in watch mode. +. In two separate terminals in a DFX project (create one if needed); +.. Start a `dfx replica`. +.. Start `dfx bootstrap --root $SDK_REPO_PATH/src/bootstrap/dist/ --providers http://localhost:8080 --port 8000`. +. Open your browser to `http://localhost:8000`. Change code, wait a few seconds for webpack to + build, reload browser. + +If you need HTTPs (for example, using lvh or ic0.app using redirects), you will need to setup +your own nginx reverse proxy. Look up instructions online. + +**Note that HTTPS is needed for the crypto API if you're accessing a non-localhost URL. This is +a limitation of the web API (see +https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts/features_restricted_to_secure_contexts[ +the MDN documentation])**. + + + +== Startup Process +. The bootstrap server determines which worker host it is using; +.. If there is a query param `workerHost`, use that value. +.. If there is a `dfinity-ic-host` value in local storage, uses that value. +.. If the host ends with `localhost` and contains more than 1 subdomain, use `dfinity.localhost`. + _This is used to test cross-domain worker._ +.. If the host ends with `lvh.me`, use `dfinity.lvh.me`. _This is used to test cross-domain worker._ +.. If the host ends with `ic0.app`, use `dfinity.ic0.app`. +.. Otherwise, don't use a worker (this is for localhost and development purposes). + +. The bootstrap server determines the canister ID; +.. If there is a query param for `canisterId`, decode that value as text. +.. If there is a `dfinity-canister-id` value in local storage, uses that value. +.. If the host ends with `lvh.me`, split the host and use the first subdomain before + `ic0.app`. For example, `some-sub.01234567.lvh.me` would result in `01234567`. + _This is used to test cross-domain worker._ +.. If the host ends with `ic0.app`, split the host and use the first subdomain before + `ic0.app`. For example, `some-sub.01234567.ic0.app` would result in `01234567`. +.. Otherwise, show a UI for the user to enter a canister ID. + +. Create a worker with `${workerHost}/worker.js` using the same protocol. +. Get the canister's `/index.js` through the worker. diff --git a/src/bootstrap/README.html b/src/bootstrap/README.html new file mode 100644 index 0000000000..b72713e717 --- /dev/null +++ b/src/bootstrap/README.html @@ -0,0 +1,560 @@ + + + + + + + +Bootstrap Server + + + + + +
+
+

How to Run Locally

+
+
+

Locally in your sdk repo, execute:

+
+
+
    +
  1. +

    npm install. To install all Node dependencies.

    +
  2. +
  3. +

    npm run webpack — --watch will start webpack in watch mode.

    +
  4. +
  5. +

    In two separate terminals in a DFX project (create one if needed);

    +
    +
      +
    1. +

      Start a dfx replica.

      +
    2. +
    3. +

      Start dfx bootstrap --root $SDK_REPO_PATH/src/bootstrap/dist/ --providers http://localhost:8080 --port 8000.

      +
    4. +
    +
    +
  6. +
  7. +

    Open your browser to http://localhost:8000. Change code, wait a few seconds for webpack to +build, reload browser.

    +
  8. +
+
+
+

If you need HTTPs (for example, using lvh or ic0.app using redirects), you will need to setup +your own nginx reverse proxy. Look up instructions online.

+
+
+

Note that HTTPS is needed for the crypto API if you’re accessing a non-localhost URL. This is +a limitation of the web API (see ).

+
+
+
+
+

Startup Process

+
+
+
    +
  1. +

    The bootstrap server determines which worker host it is using;

    +
    +
      +
    1. +

      If there is a query param workerHost, use that value.

      +
    2. +
    3. +

      If there is a dfinity-ic-host value in local storage, uses that value.

      +
    4. +
    5. +

      If the host ends with localhost and contains more than 1 subdomain, use dfinity.localhost. +This is used to test cross-domain worker.

      +
    6. +
    7. +

      If the host ends with lvh.me, use dfinity.lvh.me. This is used to test cross-domain worker.

      +
    8. +
    9. +

      If the host ends with ic0.app, use dfinity.ic0.app.

      +
    10. +
    11. +

      Otherwise, don’t use a worker (this is for localhost and development purposes).

      +
    12. +
    +
    +
  2. +
  3. +

    The bootstrap server determines the canister ID;

    +
    +
      +
    1. +

      If there is a query param for canisterId, decode that value as text.

      +
    2. +
    3. +

      If there is a dfinity-canister-id value in local storage, uses that value.

      +
    4. +
    5. +

      If the host ends with lvh.me, split the host and use the first subdomain before +ic0.app. For example, some-sub.01234567.lvh.me would result in 01234567. +This is used to test cross-domain worker.

      +
    6. +
    7. +

      If the host ends with ic0.app, split the host and use the first subdomain before +ic0.app. For example, some-sub.01234567.ic0.app would result in 01234567.

      +
    8. +
    9. +

      Otherwise, show a UI for the user to enter a canister ID.

      +
    10. +
    +
    +
  4. +
  5. +

    Create a worker with ${workerHost}/worker.js using the same protocol.

    +
  6. +
  7. +

    Get the canister’s /index.js through the worker.

    +
  8. +
+
+
+
+
+ + + \ No newline at end of file diff --git a/src/bootstrap/package-lock.json b/src/bootstrap/package-lock.json index 7f5d52706b..680199af6a 100644 --- a/src/bootstrap/package-lock.json +++ b/src/bootstrap/package-lock.json @@ -1,6 +1,6 @@ { "name": "@dfinity/bootstrap", - "version": "0.5.8", + "version": "0.5.9", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -625,6 +625,11 @@ "integrity": "sha512-7+2BITlgjgDhH0vvwZU/HZJVyk+2XUlvxXe8dFMedNX/aMkaOq++rMAFXc0tM7ij15QaWlbdQASBR9dihi+bDQ==", "dev": true }, + "@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=" + }, "@types/node": { "version": "13.13.12", "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.12.tgz", @@ -931,7 +936,6 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, "requires": { "color-convert": "^1.9.0" } @@ -1263,8 +1267,7 @@ "big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==" }, "binary-extensions": { "version": "2.0.0", @@ -1582,7 +1585,6 @@ "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, "requires": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -1784,7 +1786,6 @@ "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, "requires": { "color-name": "1.1.3" } @@ -1792,8 +1793,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" }, "combined-stream": { "version": "1.0.8", @@ -1944,8 +1944,7 @@ "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "create-ecdh": { "version": "4.0.3", @@ -2416,8 +2415,7 @@ "emojis-list": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "dev": true + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==" }, "end-of-stream": { "version": "1.4.4", @@ -2432,7 +2430,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.1.tgz", "integrity": "sha512-98p2zE+rL7/g/DzMHMTF4zZlCgeVdJ7yr6xzEpJRYwFYrGi9ANdn5DnJURg6RpBkyk60XYDnWIv51VfIhfNGuA==", - "dev": true, "requires": { "graceful-fs": "^4.1.2", "memory-fs": "^0.5.0", @@ -2443,7 +2440,6 @@ "version": "0.5.0", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", - "dev": true, "requires": { "errno": "^0.1.3", "readable-stream": "^2.0.1" @@ -2453,7 +2449,6 @@ "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -2467,14 +2462,12 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "requires": { "safe-buffer": "~5.1.0" } @@ -2491,7 +2484,6 @@ "version": "0.1.7", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", - "dev": true, "requires": { "prr": "~1.0.1" } @@ -2538,8 +2530,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "escodegen": { "version": "1.14.2", @@ -3214,8 +3205,7 @@ "graceful-fs": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", - "dev": true + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" }, "growly": { "version": "1.3.0", @@ -3251,8 +3241,7 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "has-symbols": { "version": "1.0.1", @@ -3504,6 +3493,11 @@ "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", "dev": true }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" + }, "import-local": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", @@ -3551,8 +3545,7 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { "version": "1.3.5", @@ -3790,8 +3783,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", @@ -4447,7 +4439,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, "requires": { "minimist": "^1.2.0" } @@ -4507,6 +4498,14 @@ "type-check": "~0.3.2" } }, + "lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=", + "requires": { + "immediate": "~3.0.5" + } + }, "load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", @@ -4537,13 +4536,20 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", - "dev": true, "requires": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", "json5": "^1.0.1" } }, + "localforage": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.7.4.tgz", + "integrity": "sha512-3EmVZatmNVeCo/t6Te7P06h2alGwbq8wXlSkcSXMvDE2/edPmsVqTPlzGnZaqwZZDBs6v+kxWpqjVsqsNJT8jA==", + "requires": { + "lie": "3.1.1" + } + }, "locate-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", @@ -4791,8 +4797,7 @@ "minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "minipass": { "version": "3.1.3", @@ -5512,9 +5517,7 @@ "picomatch": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", - "dev": true, - "optional": true + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==" }, "pify": { "version": "4.0.1", @@ -5639,9 +5642,9 @@ "dev": true }, "prettier": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", - "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz", + "integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==", "dev": true }, "pretty-error": { @@ -5675,8 +5678,7 @@ "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "promise-inflight": { "version": "1.0.1", @@ -5697,8 +5699,7 @@ "prr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", - "dev": true + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=" }, "psl": { "version": "1.8.0", @@ -6673,8 +6674,7 @@ "strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" }, "strip-eof": { "version": "1.0.0", @@ -6729,7 +6729,6 @@ "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, "requires": { "has-flag": "^3.0.0" } @@ -6743,8 +6742,7 @@ "tapable": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", - "dev": true + "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==" }, "terser": { "version": "4.7.0", @@ -7130,6 +7128,84 @@ } } }, + "ts-loader": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-7.0.5.tgz", + "integrity": "sha512-zXypEIT6k3oTc+OZNx/cqElrsbBtYqDknf48OZos0NQ3RTt045fBIU8RRSu+suObBzYB355aIPGOe/3kj9h7Ig==", + "requires": { + "chalk": "^2.3.0", + "enhanced-resolve": "^4.0.0", + "loader-utils": "^1.0.2", + "micromatch": "^4.0.0", + "semver": "^6.0.0" + }, + "dependencies": { + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "requires": { + "fill-range": "^7.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "tsconfig-paths": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz", + "integrity": "sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==", + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.1", + "minimist": "^1.2.0", + "strip-bom": "^3.0.0" + } + }, + "tsconfig-paths-webpack-plugin": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-3.2.0.tgz", + "integrity": "sha512-S/gOOPOkV8rIL4LurZ1vUdYCVgo15iX9ZMJ6wx6w2OgcpT/G4wMyHB6WM+xheSqGMrWKuxFul+aXpCju3wmj/g==", + "requires": { + "chalk": "^2.3.0", + "enhanced-resolve": "^4.0.0", + "tsconfig-paths": "^3.4.0" + } + }, "tslib": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", @@ -7368,8 +7444,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "util.promisify": { "version": "1.0.0", @@ -7781,6 +7856,15 @@ "errno": "~0.1.7" } }, + "worker-plugin": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/worker-plugin/-/worker-plugin-4.0.3.tgz", + "integrity": "sha512-7hFDYWiKcE3yHZvemsoM9lZis/PzurHAEX1ej8PLCu818Rt6QqUAiDdxHPCKZctzmhqzPpcFSgvMCiPbtooqAg==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0" + } + }, "wrap-ansi": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", diff --git a/src/bootstrap/package.json b/src/bootstrap/package.json index deff56ee5d..8738da54fe 100644 --- a/src/bootstrap/package.json +++ b/src/bootstrap/package.json @@ -9,7 +9,8 @@ "lint:fix": "npm run lint -- --fix", "prettier": "npx prettier --check \"src/**/*.ts\"", "prettier:write": "npm run prettier -- --write", - "test": "jest --verbose" + "test": "jest --verbose", + "webpack": "webpack" }, "devDependencies": { "@trust/webcrypto": "^0.9.2", @@ -22,18 +23,22 @@ "jest": "^24.9.0", "jest-expect-message": "^1.0.2", "node-fetch": "2.6.0", - "prettier": "^1.19.1", + "prettier": "^2.0.5", "style-loader": "^1.1.3", "terser-webpack-plugin": "^2.3.2", "text-encoding": "^0.7.0", "ts-jest": "^24.2.0", "tslint": "^5.20.0", - "typescript": "^3.6.3", + "typescript": "3.9.5", "webpack": "^4.41.2", "webpack-cli": "^3.3.10", - "whatwg-fetch": "^3.0.0" + "whatwg-fetch": "^3.0.0", + "worker-plugin": "^4.0.3" }, "dependencies": { - "buffer": "^5.6.0" + "buffer": "5.6.0", + "localforage": "^1.7.4", + "ts-loader": "7.0.5", + "tsconfig-paths-webpack-plugin": "^3.2.0" } } diff --git a/src/bootstrap/src/host.ts b/src/bootstrap/src/host.ts new file mode 100644 index 0000000000..549b56e226 --- /dev/null +++ b/src/bootstrap/src/host.ts @@ -0,0 +1,206 @@ +import { + Agent, + CanisterId, + generateKeyPair, + HttpAgent, + KeyPair, + makeAuthTransform, + makeKeyPair, + makeNonceTransform, + Principal, + ProxyAgent, + ProxyMessage, +} from '@dfinity/agent'; +import localforage from 'localforage'; + +const localStorageIdentityKey = 'dfinity-ic-user-identity'; +const localStorageCanisterIdKey = 'dfinity-ic-canister-id'; +const localStorageHostKey = 'dfinity-ic-host'; + +async function _getVariable(name: string, localStorageName: string): Promise; +async function _getVariable( + name: string, + localStorageName: string, + defaultValue: string, +): Promise; +async function _getVariable( + name: string, + localStorageName: string, + defaultValue?: string, +): Promise { + const params = new URLSearchParams(window.location.search); + + const maybeValue = params.get(name); + if (maybeValue) { + return maybeValue; + } + + const lsValue = await localforage.getItem(localStorageName); + if (lsValue) { + return lsValue; + } + + return defaultValue; +} + +export enum DomainKind { + Unknown, + Localhost, + Ic0, + Lvh, +} + +export class SiteInfo { + public static async worker(): Promise { + return new SiteInfo(DomainKind.Unknown); + } + + public static async unknown(): Promise { + const canisterId = await _getVariable('canisterId', localStorageCanisterIdKey); + return new SiteInfo( + DomainKind.Unknown, + canisterId !== undefined ? CanisterId.fromText(canisterId) : undefined, + ); + } + + public static async fromWindow(): Promise { + const { hostname } = window.location; + const components = hostname.split('.'); + const [maybeCId, maybeIc0, maybeApp] = components.slice(-3); + const subdomain = components.slice(0, -3).join('.'); + + if (maybeIc0 === 'ic0' && maybeApp === 'app') { + return new SiteInfo(DomainKind.Ic0, CanisterId.fromHex(maybeCId), subdomain); + } else if (maybeIc0 === 'lvh' && maybeApp === 'me') { + return new SiteInfo(DomainKind.Lvh, CanisterId.fromHex(maybeCId), subdomain); + } else if (maybeIc0 === 'localhost' && maybeApp === undefined) { + /// Allow subdomain of localhost. + return new SiteInfo(DomainKind.Localhost, CanisterId.fromHex(maybeCId), subdomain); + } else if (maybeApp === 'localhost') { + /// Allow subdomain of localhost, but maybeIc0 is the canister ID. + return new SiteInfo( + DomainKind.Localhost, + CanisterId.fromHex(maybeIc0), + `${maybeCId}.${subdomain}`, + ); + } else { + return this.unknown(); + } + } + + constructor( + public readonly kind: DomainKind, + public readonly canisterId?: CanisterId, + public readonly subdomain = '', + ) {} + + public async getWorkerHost(): Promise { + const { port, protocol } = window.location; + + switch (this.kind) { + case DomainKind.Unknown: + return ''; + case DomainKind.Ic0: + return `${protocol}//z.ic0.app:${port}`; + case DomainKind.Lvh: + return `${protocol}//z.lvh.me:${port}`; + case DomainKind.Localhost: + return `${protocol}//z.localhost:${port}`; + } + } + + public async getHost(): Promise { + // Figure out the host. + let host = await _getVariable('host', localStorageHostKey, ''); + if (host) { + try { + host = JSON.parse(host); + + if (Array.isArray(host)) { + host = '' + host[Math.floor(Math.random() * host.length)]; + } else { + host = '' + host; + } + } catch (_) { + host = ''; + } + } else { + host = ''; + } + + return host; + } +} + +async function getKeyPair(forceNewPair = false): Promise { + const k = forceNewPair ? null : await _getVariable('userIdentity', localStorageIdentityKey); + let keyPair: KeyPair; + + if (k) { + const kp = JSON.parse(k); + keyPair = makeKeyPair(new Uint8Array(kp.publicKey.data), new Uint8Array(kp.secretKey.data)); + } else { + const kp = generateKeyPair(); + // TODO(eftycis): use a parser+an appropriate format to avoid + // leaking the key when constructing the string for + // localStorage. + if (!forceNewPair) { + await localforage.setItem(localStorageIdentityKey, JSON.stringify(kp)); + } + + keyPair = kp; + } + + return keyPair; +} + +export async function createAgent(site: SiteInfo): Promise { + const workerHost = await site.getWorkerHost(); + + if (!workerHost) { + const keyPair = await getKeyPair(); + const host = await site.getHost(); + const principal = Principal.selfAuthenticating(keyPair.publicKey); + const agent = new HttpAgent({ host, principal }); + agent.addTransform(makeNonceTransform()); + agent.setAuthTransform(makeAuthTransform(keyPair)); + + return agent; + } else { + // Create the IFRAME. + let messageQueue: ProxyMessage[] | null = []; + let loaded = false; + const agent = new ProxyAgent((msg: ProxyMessage) => { + if (!loaded) { + if (!messageQueue) { + throw new Error('No Message Queue but need Queueing...'); + } + messageQueue.push(msg); + } else { + iframeEl.contentWindow!.postMessage(msg, '*'); + } + }); + + const iframeEl = document.createElement('iframe'); + + iframeEl.src = workerHost + '/worker.html'; + window.addEventListener('message', ev => { + if (ev.origin === workerHost) { + if (ev.data === 'ready') { + const q = messageQueue?.splice(0, messageQueue.length) || []; + for (const msg of q) { + iframeEl.contentWindow!.postMessage(msg, workerHost); + } + + loaded = true; + messageQueue = null; + } else { + agent.onmessage(ev.data); + } + } + }); + + document.head.append(iframeEl); + return agent; + } +} diff --git a/src/bootstrap/src/index.ts b/src/bootstrap/src/index.ts index 36db67a83e..3717f0b506 100644 --- a/src/bootstrap/src/index.ts +++ b/src/bootstrap/src/index.ts @@ -1,102 +1,44 @@ -import { - generateKeyPair, - HttpAgent, - IDL, - makeAuthTransform, - makeKeyPair, - makeNonceTransform, - Principal, -} from '@dfinity/agent'; +import { CanisterId, GlobalInternetComputer, HttpAgent, IDL } from '@dfinity/agent'; +import { createAgent, SiteInfo } from './host'; -interface WindowWithInternetComputer extends Window { - icHttpAgent: HttpAgent; - ic: { - httpAgent: HttpAgent; - }; -} -declare const window: WindowWithInternetComputer; - -const localStorageIdentityKey = 'dfinity-ic-user-identity'; -const localStorageCanisterIdKey = 'dfinity-ic-canister-id'; -const localStorageHostKey = 'dfinity-ic-host'; - -function _getVariable( - queryName: string, - localStorageName: string, - defaultValue?: string, -): string | undefined { - const queryValue = window.location.search.match(new RegExp(`[?&]${queryName}=([^&]*)(?:&|$)`)); - if (queryValue) { - return decodeURIComponent(queryValue[1]); - } - const lsValue = window.localStorage.getItem(localStorageName); - if (lsValue) { - return lsValue; - } - return defaultValue; -} +declare const window: GlobalInternetComputer & Window; // Retrieve and execute a JavaScript file from the server. async function _loadJs( - canisterId: string, + canisterId: CanisterId, filename: string, onload = async () => {}, ): Promise { - const content = await window.icHttpAgent.retrieveAsset(canisterId, filename); - const js = new TextDecoder().decode(content); - const dataUri = 'data:text/javascript;charset=utf-8,' + encodeURIComponent(js); + const idlFn = ({ IDL: idl }: any) => { + return idl.Service({ + retrieve: idl.Func([idl.Text], [idl.Vec(idl.Nat8)], ['query']), + }); + }; + + const actor = window.ic.agent.makeActorFactory(idlFn)({ + canisterId, + }); + + const content = (await actor.retrieve(filename)) as number[]; + const js = new TextDecoder().decode(new Uint8Array(content)); + // const dataUri = new Function(js); // Run an event function so the callee can execute some code before loading the // Javascript. await onload(); + // TODO(hansl): either get rid of eval, or rid of webpack, or make this // work without this horrible hack. - return eval('import("' + dataUri + '")'); // tslint:disable-line + return eval(js); // tslint:disable-line } -const k = _getVariable('userIdentity', localStorageIdentityKey); -let keyPair; -if (k) { - keyPair = JSON.parse(k); - keyPair = makeKeyPair( - new Uint8Array(keyPair.publicKey.data), - new Uint8Array(keyPair.secretKey.data), - ); -} else { - keyPair = generateKeyPair(); - // TODO(eftycis): use a parser+an appropriate format to avoid - // leaking the key when constructing the string for - // localStorage. - window.localStorage.setItem(localStorageIdentityKey, JSON.stringify(keyPair)); -} - -// Figure out the host. -let host = _getVariable('host', localStorageHostKey, ''); -if (host) { - try { - host = JSON.parse(host); - - if (Array.isArray(host)) { - host = '' + host[Math.floor(Math.random() * host.length)]; - } else { - host = '' + host; - } - } catch (_) { - host = ''; - } -} - -const principal = Principal.selfAuthenticating(keyPair.publicKey); -const agent = new HttpAgent({ host, principal }); -agent.addTransform(makeNonceTransform()); -agent.setAuthTransform(makeAuthTransform(keyPair)); - -window.icHttpAgent = agent; -window.ic = { httpAgent: agent }; - async function _main() { + const site = await SiteInfo.fromWindow(); + const agent = await createAgent(site); + window.ic = { agent, HttpAgent, IDL }; + // Find the canister ID. Allow override from the url with 'canister_id=1234..' - const canisterId = _getVariable('canisterId', localStorageCanisterIdKey, ''); + const canisterId = site.canisterId; if (!canisterId) { // Show an error. const div = document.createElement('div'); @@ -108,7 +50,7 @@ async function _main() { if (window.location.pathname === '/candid') { // Load candid.js from the canister. const candid = await _loadJs(canisterId, 'candid.js'); - const canister = window.icHttpAgent.makeActorFactory(candid.default)({ canisterId }); + const canister = window.ic.agent.makeActorFactory(candid.default)({ canisterId }); // @ts-ignore: Could not find a declaration file for module const render: any = await import('./candid/candid.js'); render.render(canisterId, canister); diff --git a/src/bootstrap/src/worker.html b/src/bootstrap/src/worker.html new file mode 100644 index 0000000000..d3a3d8abf0 --- /dev/null +++ b/src/bootstrap/src/worker.html @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/bootstrap/src/worker.ts b/src/bootstrap/src/worker.ts new file mode 100644 index 0000000000..6054104d78 --- /dev/null +++ b/src/bootstrap/src/worker.ts @@ -0,0 +1,26 @@ +import { ProxyMessageKind, ProxyStubAgent } from '@dfinity/agent'; +import { createAgent, SiteInfo } from './host'; + +async function bootstrap() { + const agent = await createAgent(await SiteInfo.worker()); + const stub = new ProxyStubAgent(msg => { + switch (msg.type) { + case ProxyMessageKind.CallResponse: + const response = msg.response.response; + msg.response.response = JSON.parse(JSON.stringify(response)); + } + window.parent.postMessage(msg, '*'); + }, agent); + + window.addEventListener('message', ev => { + stub.onmessage(ev.data); + }); + + // Send our ACK message to the parent. + window.parent.postMessage('ready', '*'); +} + +bootstrap().catch(error => { + (console as any).error(error); + window.parent.postMessage({ error }, '*'); +}); diff --git a/src/bootstrap/tsconfig.json b/src/bootstrap/tsconfig.json index 355a957ff7..241f61f5a4 100644 --- a/src/bootstrap/tsconfig.json +++ b/src/bootstrap/tsconfig.json @@ -1,24 +1,27 @@ { "compilerOptions": { "incremental": true, - "target": "es2017", + "outDir": "ts-out/", + "target": "ES2017", "module": "commonjs", "lib": [ "dom", "es2017" ], - "declaration": true, "sourceMap": true, - "tsBuildInfoFile": "./build_info.json", "strict": true, "paths": { "@dfinity/agent": [ - "../agent/javascript", + "../../src/agent/javascript/src", "node_modules/@dfinity/agent" ] }, "baseUrl": "./", "esModuleInterop": true, "forceConsistentCasingInFileNames": true - } + }, + "include": [ + "types/*", + "src/**/*.ts" + ] } diff --git a/src/bootstrap/tslint.json b/src/bootstrap/tslint.json index 4bb79af45e..b9a807b944 100644 --- a/src/bootstrap/tslint.json +++ b/src/bootstrap/tslint.json @@ -11,6 +11,7 @@ ] }, "rules": { + "max-classes-per-file": false, "interface-name": [true, "never-prefix"], "no-consecutive-blank-lines": [true, 2], "no-empty": [true, "allow-empty-functions"], diff --git a/src/bootstrap/webpack.config.js b/src/bootstrap/webpack.config.js index 8bf607f628..71019d4f34 100644 --- a/src/bootstrap/webpack.config.js +++ b/src/bootstrap/webpack.config.js @@ -1,27 +1,37 @@ -const fs = require("fs"); -const path = require("path"); +const fs = require('fs'); +const path = require('path'); const TerserPlugin = require('terser-webpack-plugin'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CopyWebpackPlugin = require('copy-webpack-plugin'); +const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); // If we're in Nix, we need to let the resolution works normally. const agentPath = path.join(__dirname, '../agent/javascript/src'); -const resolve = fs.existsSync(agentPath) ? { - alias: { - '@dfinity/agent': agentPath, - } -} : {}; +const resolve = fs.existsSync(agentPath) + ? { + alias: { + '@dfinity/agent': agentPath, + }, + } + : {}; module.exports = { - mode: "production", - entry: "./src/index.js", - target: "web", + mode: 'production', + entry: { + bootstrap: './src/index.ts', + candid: './src/candid/candid.js', + worker: './src/worker.ts', + }, + target: 'web', output: { - path: path.resolve(__dirname, "./dist"), - filename: "index.js", + path: path.resolve(__dirname, './dist'), + filename: '[name].js', + }, + resolve: { + plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.json' })], + extensions: ['.tsx', '.ts', '.js'], }, - resolve, - devtool: "none", + devtool: 'source-map', optimization: { minimize: true, minimizer: [ @@ -32,30 +42,47 @@ module.exports = { terserOptions: { ecma: 8, minimize: true, - comments: false + comments: false, // https://github.com/webpack-contrib/terser-webpack-plugin#terseroptions - } + }, }), ], }, module: { - rules: [{ - test: /\.css$/, - use: ['style-loader', 'css-loader'] - }] + rules: [ + { + test: /\.css$/, + use: ['style-loader', 'css-loader'], + }, + { + test: /\.tsx?$/, + use: ['ts-loader'], + }, + ], }, plugins: [ new HtmlWebpackPlugin({ template: 'src/index.html', - filename: 'index.html' + filename: 'index.html', + chunks: ['bootstrap'], + }), + new HtmlWebpackPlugin({ + template: 'src/worker.html', + filename: 'worker.html', + chunks: ['worker'], }), new HtmlWebpackPlugin({ template: 'src/candid/index.html', - filename: 'candid/index.html' + filename: 'candid/index.html', + // TODO: change candid.js to candid.ts, and make it a proper bootstrap, and + // change this chunk to candid. + chunks: ['bootstrap'], }), - new CopyWebpackPlugin([{ + new CopyWebpackPlugin([ + { from: 'src/dfinity.png', to: 'favicon.ico', - }]), - ] + }, + ]), + ], }; diff --git a/src/dfx/assets/language_bindings/canister.js b/src/dfx/assets/language_bindings/canister.js index a4ad93a250..09b35d1711 100644 --- a/src/dfx/assets/language_bindings/canister.js +++ b/src/dfx/assets/language_bindings/canister.js @@ -1,5 +1,5 @@ import actorInterface from "ic:idl/{project_name}"; -export default icHttpAgent.makeActorFactory(actorInterface)({ +export default ic.agent.makeActorFactory(actorInterface)({ canisterId: "{canister_id}", });