Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
krpeacock committed Oct 16, 2024
1 parent 7b52f29 commit 8843eeb
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 28 deletions.
8 changes: 5 additions & 3 deletions e2e/node/basic/callAndPoll.test.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { HttpAgent, fromHex, callAndPoll } from '@dfinity/agent';
import { Principal } from '@dfinity/principal';
import { expect, describe, it, vi } from 'vitest';
describe('call and poll', () => {
it('should handle call and poll', async () => {
vi.useRealTimers();

const options = {
canisterId: 'tnnnb-2yaaa-aaaab-qaiiq-cai',
methodName: 'inc_read',
canister_id: Principal.from('tnnnb-2yaaa-aaaab-qaiiq-cai'),
method_name: 'inc_read',
agent: await HttpAgent.create({ host: 'https://icp-api.io' }),
arg: fromHex('4449444c0000'),
};

const certificate = await callAndPoll(options);
const { certificate, contentMap } = await callAndPoll(options);
expect(certificate instanceof ArrayBuffer).toBe(true);
expect(contentMap).toMatchInlineSnapshot();
});
});
6 changes: 6 additions & 0 deletions packages/agent/src/agent/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { RequestId } from '../request_id';
import { JsonObject } from '@dfinity/candid';
import { Identity } from '../auth';
import { CallRequest, HttpHeaderField, QueryRequest } from './http/types';
import { Expiry } from './http';

/**
* Codes used by the replica for rejecting a message.
Expand Down Expand Up @@ -115,6 +116,11 @@ export interface CallOptions {
* @see https://internetcomputer.org/docs/current/references/ic-interface-spec/#http-effective-canister-id
*/
effectiveCanisterId: Principal | string;

/**
* The expiry for the ingress message. Defaults to 4 minutes, rounded down to the nearest minute.
*/
ingressExpiry?: Expiry;
}

export interface ReadStateResponse {
Expand Down
1 change: 1 addition & 0 deletions packages/agent/src/agent/http/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,7 @@ export class HttpAgent implements Agent {

// Update the watermark with the latest time from consensus
if (responseBody && 'certificate' in (responseBody as v3ResponseBody)) {
responseBody['certificate'] = bufFromBufLike(responseBody['certificate']);
const time = await this.parseTimeFromResponse({
certificate: (responseBody as v3ResponseBody).certificate,
});
Expand Down
3 changes: 3 additions & 0 deletions packages/agent/src/agent/http/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,6 @@ export function makeNonce(): Nonce {

return buffer as Nonce;
}

export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
export type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
4 changes: 2 additions & 2 deletions packages/agent/src/certificate.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as cbor from './cbor';
import { AgentError } from './errors';
import { hash } from './request_id';
import { bufEquals, concat, fromHex, toHex } from './utils/buffer';
import { bufEquals, bufFromBufLike, concat, fromHex, toHex } from './utils/buffer';
import { Principal } from '@dfinity/principal';
import * as bls from './utils/bls';
import { decodeTime } from './utils/leb';
Expand Down Expand Up @@ -192,7 +192,7 @@ export class Certificate {
// Default to 5 minutes
private _maxAgeInMinutes: number = 5,
) {
this.rawCert = certificate;
this.rawCert = bufFromBufLike(certificate);
this.cert = cbor.decode(new Uint8Array(certificate));
}

Expand Down
81 changes: 58 additions & 23 deletions packages/agent/src/utils/callAndPoll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,54 +4,60 @@ import {
Certificate,
ContentMap,
Expiry,
PartialBy,
bufFromBufLike,
polling,
v3ResponseBody,
} from '..';
import { AgentCallError, AgentError } from '../errors';
import { isArrayBuffer } from 'util/types';
import { DEFAULT_INGRESS_EXPIRY_DELTA_IN_MSECS } from '../constants';

export type CallAndPollOptions = PartialBy<
Omit<ContentMap, 'sender' | 'request_type'>,
'ingress_expiry'
> & {
agent: Agent;
};

/**
* Call a canister using the v3 api and either return the response or fall back to polling
* @param options - The options to use when calling the canister
* @param options.canisterId - The canister id to call
* @param options.methodName - The method name to call
* @param options.canister_id - The canister id to call
* @param options.method_name - The method name to call
* @param options.agent - The agent to use to make the call
* @param options.arg - The argument to pass to the canister
* @returns The certificate response from the canister (which includes the reply)
*/
export async function callAndPoll(options: {
canisterId: Principal | string;
methodName: string;
agent: Agent;
arg: ArrayBuffer;
ingressExpiry?: Expiry;
}): Promise<{
export async function callAndPoll(options: CallAndPollOptions): Promise<{
certificate: ArrayBuffer;
contentMap: ContentMap;
}> {
const { canisterId, methodName, agent, arg } = options;
const cid = Principal.from(options.canisterId);
assertContentMap(options);
const { canister_id, method_name, agent, arg } = options;
const cid = Principal.from(options.canister_id);

const { defaultStrategy } = polling.strategy;

if (agent.rootKey == null) throw new Error('Agent root key not initialized before making call');

const ingress_expiry = options.ingressExpiry ?? new Expiry(DEFAULT);
const ingress_expiry =
options.ingress_expiry ?? new Expiry(DEFAULT_INGRESS_EXPIRY_DELTA_IN_MSECS);

const { requestId, response } = await agent.call(cid, {
methodName,
arg,
effectiveCanisterId: cid,
});
const contentMap: ContentMap = {
canister_id: Principal.from(canisterId),
canister_id: Principal.from(canister_id),
request_type: 'call',
method_name: methodName,
method_name: method_name,
arg,
sender: await agent.getPrincipal(),
ingress_expiry,
};
const { requestId, response } = await agent.call(cid, {
methodName: method_name,
arg,
effectiveCanisterId: cid,
ingressExpiry: ingress_expiry,
});

if (response.status === 200) {
if ('body' in response) {
Expand All @@ -63,10 +69,13 @@ export async function callAndPoll(options: {
const certificate = await Certificate.create({
certificate: bufFromBufLike(cert),
rootKey: agent.rootKey,
canisterId: Principal.from(canisterId),
canisterId: Principal.from(canister_id),
});

return certificate.rawCert;
return {
certificate: certificate.rawCert,
contentMap,
};
} else {
throw new AgentCallError(
'unexpected call error: no certificate in response',
Expand All @@ -80,7 +89,10 @@ export async function callAndPoll(options: {
const pollStrategy = defaultStrategy();
// Contains the certificate and the reply from the boundary node
const response = await polling.pollForResponse(agent, cid, requestId, pollStrategy);
return response.certificate.rawCert;
return {
certificate: response.certificate.rawCert,
contentMap,
};
} else {
console.error('The network returned a response but the result could not be determined.', {
response,
Expand All @@ -90,14 +102,37 @@ export async function callAndPoll(options: {
}
}

function assertContentMap(contentMap: unknown): asserts contentMap is ContentMap {
if (!contentMap || typeof contentMap !== 'object') {
throw new AgentError('unexpected call error: no contentMap provided for call');
}
if (!('canister_id' in contentMap)) {
throw new AgentError('unexpected call error: no canister_id provided for call');
}
if (!('method_name' in contentMap)) {
throw new AgentError('unexpected call error: no method_name provided for call');
}
if (!('arg' in contentMap)) {
throw new AgentError('unexpected call error: no arg provided for call');
}
}

function assertV3ResponseBody(body: unknown): asserts body is v3ResponseBody {
if (!body || typeof body !== 'object') {
throw new AgentError('unexpected call error: no body in response');
}
if (!('certificate' in body)) {
throw new AgentError('unexpected call error: no certificate in response');
}
if (!isArrayBuffer(body['certificate'])) {
if (body['certificate'] === undefined || body['certificate'] === null) {
throw new AgentError('unexpected call error: certificate is not an ArrayBuffer');
}
try {
const cert = bufFromBufLike(body['certificate'] as ArrayBufferLike);
if (!isArrayBuffer(cert)) {
throw new AgentError('unexpected call error: certificate is not an ArrayBuffer');
}
} catch (error) {
throw new AgentError('unexpected call error: while presenting certificate: ' + error);
}
}

0 comments on commit 8843eeb

Please sign in to comment.