From 8843eeb0fc35c7a59c19876059aa7a53a4d5538a Mon Sep 17 00:00:00 2001 From: Kai Peacock Date: Tue, 15 Oct 2024 17:12:49 -0700 Subject: [PATCH] wip --- e2e/node/basic/callAndPoll.test.ts | 8 ++- packages/agent/src/agent/api.ts | 6 ++ packages/agent/src/agent/http/index.ts | 1 + packages/agent/src/agent/http/types.ts | 3 + packages/agent/src/certificate.ts | 4 +- packages/agent/src/utils/callAndPoll.ts | 81 ++++++++++++++++++------- 6 files changed, 75 insertions(+), 28 deletions(-) diff --git a/e2e/node/basic/callAndPoll.test.ts b/e2e/node/basic/callAndPoll.test.ts index 9befe1da..7cfe2f03 100644 --- a/e2e/node/basic/callAndPoll.test.ts +++ b/e2e/node/basic/callAndPoll.test.ts @@ -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(); }); }); diff --git a/packages/agent/src/agent/api.ts b/packages/agent/src/agent/api.ts index b521116b..3a097071 100644 --- a/packages/agent/src/agent/api.ts +++ b/packages/agent/src/agent/api.ts @@ -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. @@ -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 { diff --git a/packages/agent/src/agent/http/index.ts b/packages/agent/src/agent/http/index.ts index 51f181f9..ab0afc99 100644 --- a/packages/agent/src/agent/http/index.ts +++ b/packages/agent/src/agent/http/index.ts @@ -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, }); diff --git a/packages/agent/src/agent/http/types.ts b/packages/agent/src/agent/http/types.ts index baafb260..4dbb2a50 100644 --- a/packages/agent/src/agent/http/types.ts +++ b/packages/agent/src/agent/http/types.ts @@ -131,3 +131,6 @@ export function makeNonce(): Nonce { return buffer as Nonce; } + +export type Omit = Pick>; +export type PartialBy = Omit & Partial>; diff --git a/packages/agent/src/certificate.ts b/packages/agent/src/certificate.ts index 9303a4fd..f5a546e9 100644 --- a/packages/agent/src/certificate.ts +++ b/packages/agent/src/certificate.ts @@ -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'; @@ -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)); } diff --git a/packages/agent/src/utils/callAndPoll.ts b/packages/agent/src/utils/callAndPoll.ts index 6a8617e0..8ef5e98c 100644 --- a/packages/agent/src/utils/callAndPoll.ts +++ b/packages/agent/src/utils/callAndPoll.ts @@ -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, + '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) { @@ -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', @@ -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, @@ -90,6 +102,21 @@ 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'); @@ -97,7 +124,15 @@ function assertV3ResponseBody(body: unknown): asserts body is v3ResponseBody { 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); + } }