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
10 changes: 10 additions & 0 deletions e2e/node/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"trailingComma": "all",
"tabWidth": 2,
"printWidth": 100,
"semi": true,
"bracketSpacing": true,
"useTabs": false,
"singleQuote": true,
"arrowParens": "avoid"
}
37 changes: 21 additions & 16 deletions e2e/node/utils/canisters/counter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,35 @@ import { readFileSync } from 'fs';
const wasm = readFileSync(path.join(__dirname, 'counter.wasm'));

type CounterActor = Actor & {
read(): Promise<number>,
inc_read(): Promise<number>,
write(n: number): Promise<void>,
read(): Promise<number>;
inc_read(): Promise<number>;
write(n: number): Promise<void>;
};

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<CounterActor> {
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;
}
21 changes: 12 additions & 9 deletions e2e/node/utils/canisters/identity.ts
Original file line number Diff line number Diff line change
@@ -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<Identity> {
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;
}
3 changes: 2 additions & 1 deletion src/agent/javascript/.prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
"semi": true,
"bracketSpacing": true,
"useTabs": false,
"singleQuote": true
"singleQuote": true,
"arrowParens": "avoid"
}
6 changes: 3 additions & 3 deletions src/agent/javascript/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/agent/javascript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
13 changes: 3 additions & 10 deletions src/agent/javascript/src/actor.test.ts
Original file line number Diff line number Diff line change
@@ -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 = () => {
Expand Down Expand Up @@ -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]);
Expand Down
87 changes: 48 additions & 39 deletions src/agent/javascript/src/actor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Buffer } from 'buffer/';
import { Agent } from './agent';
import { CanisterId } from './canisterId';
import { HttpAgent } from './http_agent';
import {
QueryResponseStatus,
RequestStatusResponse,
Expand All @@ -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';

Expand Down Expand Up @@ -39,23 +40,30 @@ export type Actor = Record<string, (...args: unknown[]) => Promise<unknown>> & {

export interface ActorConfig {
canisterId?: string | CanisterId;
httpAgent?: HttpAgent;
agent?: Agent;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we're moving to "agent" instead of httpAgent? I don't totally follow

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

httpAgent is the Agent implementation that talks over HTTP. In this PR we introduced ProxyAgent that talks to a proxy function, and we could have more agents (MockAgent, SmithAgent, etc)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh okay, nice

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
Expand Down Expand Up @@ -86,31 +94,31 @@ 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();
// ```
//
// or using a different HTTP agent for the same actor if necessary:
//
// ```
// 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,
): ActorConstructor {
const actorInterface = actorInterfaceFactory({ IDL });

async function requestStatusAndLoop<T>(
httpAgent: HttpAgent,
agent: Agent,
requestId: RequestId,
decoder: (response: RequestStatusResponseReplied) => T,
attempts: number,
maxAttempts: number,
throttle: number,
): Promise<T> {
const status = await httpAgent.requestStatus({ requestId });
const status = await agent.requestStatus({ requestId });

switch (status.status) {
case RequestStatusResponseStatus.Replied: {
Expand All @@ -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:
Expand All @@ -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<ActorConfig>;
Expand All @@ -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) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to have default case do something similar to this, or do we control the agent.query response type? Perhaps adding a little logging so that if you get an unexpected response type, we can log it. I figure that'd avoid issues like you fixed the other day :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The object is built from the public spec, so there is only two status possible (see https://github.com/dfinity-lab/ic-ref/blob/master/spec/index.adoc#read-query-call)

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;
Expand All @@ -185,11 +204,7 @@ export function makeActorFactory(
throttleDurationInMSecs?: number;
} = {},
): Promise<CanisterId> {
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;
Expand Down Expand Up @@ -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.');
}
Expand Down Expand Up @@ -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.');
}
Expand Down
47 changes: 47 additions & 0 deletions src/agent/javascript/src/agent/api.ts
Original file line number Diff line number Diff line change
@@ -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<RequestStatusResponse>;

call(
canisterId: CanisterId | string,
fields: {
methodName: string;
arg: BinaryBlob;
},
principal?: Principal | Promise<Principal>,
): Promise<SubmitResponse>;

createCanister(principal?: Principal): Promise<SubmitResponse>;

install(
canisterId: CanisterId | string,
fields: {
module: BinaryBlob;
arg?: BinaryBlob;
},
principal?: Principal,
): Promise<SubmitResponse>;

query(
canisterId: CanisterId | string,
fields: QueryFields,
principal?: Principal,
): Promise<QueryResponse>;

makeActorFactory(
actorInterfaceFactory: (_: { IDL: typeof IDL }) => IDL.ServiceClass,
): ActorConstructor;
}
Loading