diff --git a/e2e/node/basic/mainnet.test.ts b/e2e/node/basic/mainnet.test.ts index b3a530ad..bc1ee723 100644 --- a/e2e/node/basic/mainnet.test.ts +++ b/e2e/node/basic/mainnet.test.ts @@ -7,6 +7,7 @@ import { fromHex, polling, requestIdOf, + ReplicaTimeError, } from '@dfinity/agent'; import { IDL } from '@dfinity/candid'; import { Ed25519KeyIdentity } from '@dfinity/identity'; @@ -21,7 +22,7 @@ const createWhoamiActor = async (identity: Identity) => { const idlFactory = () => { return IDL.Service({ whoami: IDL.Func([], [IDL.Principal], ['query']), - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any }) as unknown as any; }; vi.useFakeTimers(); @@ -142,7 +143,6 @@ describe('call forwarding', () => { }, 15_000); }); - test('it should allow you to set an incorrect root key', async () => { const agent = HttpAgent.createSync({ rootKey: new Uint8Array(31), @@ -159,3 +159,79 @@ test('it should allow you to set an incorrect root key', async () => { expect(actor.whoami).rejects.toThrowError(`Invalid certificate:`); }); + +test('it should throw an error when the clock is out of sync during a query', async () => { + const canisterId = 'ivcos-eqaaa-aaaab-qablq-cai'; + const idlFactory = () => { + return IDL.Service({ + whoami: IDL.Func([], [IDL.Principal], ['query']), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }) as unknown as any; + }; + vi.useRealTimers(); + + // set date to long ago + vi.spyOn(Date, 'now').mockImplementation(() => { + return new Date('2021-01-01T00:00:00Z').getTime(); + }); + // vi.setSystemTime(new Date('2021-01-01T00:00:00Z')); + + const agent = await HttpAgent.create({ host: 'https://icp-api.io', fetch: globalThis.fetch }); + + const actor = Actor.createActor(idlFactory, { + agent, + canisterId, + }); + try { + // should throw an error + await actor.whoami(); + } catch (err) { + // handle the replica time error + if (err.name === 'ReplicaTimeError') { + const error = err as ReplicaTimeError; + // use the replica time to sync the agent + error.agent.replicaTime = error.replicaTime; + } + } + // retry the call + const result = await actor.whoami(); + expect(Principal.from(result)).toBeInstanceOf(Principal); +}); + +test('it should throw an error when the clock is out of sync during an update', async () => { + const canisterId = 'ivcos-eqaaa-aaaab-qablq-cai'; + const idlFactory = () => { + return IDL.Service({ + whoami: IDL.Func([], [IDL.Principal], []), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }) as unknown as any; + }; + vi.useRealTimers(); + + // set date to long ago + vi.spyOn(Date, 'now').mockImplementation(() => { + return new Date('2021-01-01T00:00:00Z').getTime(); + }); + // vi.setSystemTime(new Date('2021-01-01T00:00:00Z')); + + const agent = await HttpAgent.create({ host: 'https://icp-api.io', fetch: globalThis.fetch }); + + const actor = Actor.createActor(idlFactory, { + agent, + canisterId, + }); + try { + // should throw an error + await actor.whoami(); + } catch (err) { + // handle the replica time error + if (err.name === 'ReplicaTimeError') { + const error = err as ReplicaTimeError; + // use the replica time to sync the agent + error.agent.replicaTime = error.replicaTime; + // retry the call + const result = await actor.whoami(); + expect(Principal.from(result)).toBeInstanceOf(Principal); + } + } +}); diff --git a/packages/agent/src/actor.ts b/packages/agent/src/actor.ts index f347f6fa..f447da3e 100644 --- a/packages/agent/src/actor.ts +++ b/packages/agent/src/actor.ts @@ -2,6 +2,7 @@ import { Buffer } from 'buffer/'; import { Agent, getDefaultAgent, + HttpAgent, HttpDetailsResponse, QueryResponseRejected, QueryResponseStatus, @@ -535,13 +536,19 @@ function _createActorMethod( }); let reply: ArrayBuffer | undefined; let certificate: Certificate | undefined; + const certTime = (agent as HttpAgent).replicaTime + ? (agent as HttpAgent).replicaTime + : undefined; + + certTime; + if (response.body && response.body.certificate) { const cert = response.body.certificate; certificate = await Certificate.create({ certificate: bufFromBufLike(cert), rootKey: agent.rootKey, canisterId: Principal.from(canisterId), - blsVerify, + certTime, }); const path = [new TextEncoder().encode('request_status'), requestId]; const status = new TextDecoder().decode( diff --git a/packages/agent/src/agent/api.ts b/packages/agent/src/agent/api.ts index b11bd1e4..d4b6283a 100644 --- a/packages/agent/src/agent/api.ts +++ b/packages/agent/src/agent/api.ts @@ -119,6 +119,7 @@ export interface CallOptions { export interface ReadStateResponse { certificate: ArrayBuffer; + replicaTime?: Date; } export interface SubmitResponse { diff --git a/packages/agent/src/agent/http/__snapshots__/calculateReplicaTime.test.ts.snap b/packages/agent/src/agent/http/__snapshots__/calculateReplicaTime.test.ts.snap new file mode 100644 index 00000000..6f7dd4b3 --- /dev/null +++ b/packages/agent/src/agent/http/__snapshots__/calculateReplicaTime.test.ts.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`calculateReplicaTime 1`] = `2024-08-13T22:49:30.148Z`; diff --git a/packages/agent/src/agent/http/calculateReplicaTime.test.ts b/packages/agent/src/agent/http/calculateReplicaTime.test.ts new file mode 100644 index 00000000..ba770587 --- /dev/null +++ b/packages/agent/src/agent/http/calculateReplicaTime.test.ts @@ -0,0 +1,7 @@ +import { calculateReplicaTime } from './calculateReplicaTime'; +const exampleMessage = `Specified ingress_expiry not within expected range: Minimum allowed expiry: 2024-08-13 22:49:30.148075776 UTC, Maximum allowed expiry: 2024-08-13 22:55:00.148075776 UTC, Provided expiry: 2021-01-01 00:04:00 UTC`; + +test('calculateReplicaTime', () => { + const parsedTime = calculateReplicaTime(exampleMessage); + expect(parsedTime).toMatchSnapshot(); +}); diff --git a/packages/agent/src/agent/http/calculateReplicaTime.ts b/packages/agent/src/agent/http/calculateReplicaTime.ts new file mode 100644 index 00000000..6983506d --- /dev/null +++ b/packages/agent/src/agent/http/calculateReplicaTime.ts @@ -0,0 +1,22 @@ +/** + * Parse the expiry from the message + * @param message an error message + * @returns diff in milliseconds + */ +export const calculateReplicaTime = (message: string): Date => { + const [min, max] = message.split('UTC'); + + const minsplit = min.trim().split(' ').reverse(); + + const minDateString = `${minsplit[1]} ${minsplit[0]} UTC`; + + const maxsplit = max.trim().split(' ').reverse(); + + const maxDateString = `${maxsplit[1]} ${maxsplit[0]} UTC`; + + return new Date(minDateString); +}; + +function midwayBetweenDates(date1: Date, date2: Date) { + return new Date((date1.getTime() + date2.getTime()) / 2); +} diff --git a/packages/agent/src/agent/http/errors.ts b/packages/agent/src/agent/http/errors.ts index 8874e14d..4f701379 100644 --- a/packages/agent/src/agent/http/errors.ts +++ b/packages/agent/src/agent/http/errors.ts @@ -1,10 +1,27 @@ +import { HttpAgent } from '.'; import { AgentError } from '../../errors'; import { HttpDetailsResponse } from '../api'; export class AgentHTTPResponseError extends AgentError { - constructor(message: string, public readonly response: HttpDetailsResponse) { + constructor( + message: string, + public readonly response: HttpDetailsResponse, + ) { super(message); this.name = this.constructor.name; Object.setPrototypeOf(this, new.target.prototype); } } + +export class ReplicaTimeError extends AgentError { + public readonly replicaTime: Date; + public readonly agent: HttpAgent; + + constructor(message: string, replicaTime: Date, agent: HttpAgent) { + super(message); + this.name = 'ReplicaTimeError'; + this.replicaTime = replicaTime; + this.agent = agent; + Object.setPrototypeOf(this, new.target.prototype); + } +} diff --git a/packages/agent/src/agent/http/http.test.ts b/packages/agent/src/agent/http/http.test.ts index 386b6bda..0cf25bc9 100644 --- a/packages/agent/src/agent/http/http.test.ts +++ b/packages/agent/src/agent/http/http.test.ts @@ -16,7 +16,8 @@ import { JSDOM } from 'jsdom'; import { Actor, AnonymousIdentity, SignIdentity, toHex } from '../..'; import { Ed25519KeyIdentity } from '@dfinity/identity'; import { AgentError } from '../../errors'; -import { AgentHTTPResponseError } from './errors'; +import { AgentHTTPResponseError, ReplicaTimeError } from './errors'; +import { IDL } from '@dfinity/candid'; const { window } = new JSDOM(`

Hello world

`); window.fetch = global.fetch; (global as any).window = window; @@ -813,3 +814,4 @@ test('it should log errors to console if the option is set', async () => { await agent.syncTime(); }); +jest.setTimeout(5000); diff --git a/packages/agent/src/agent/http/index.ts b/packages/agent/src/agent/http/index.ts index 067cbe9e..07c0b956 100644 --- a/packages/agent/src/agent/http/index.ts +++ b/packages/agent/src/agent/http/index.ts @@ -27,7 +27,7 @@ import { ReadRequestType, SubmitRequestType, } from './types'; -import { AgentHTTPResponseError } from './errors'; +import { AgentHTTPResponseError, ReplicaTimeError } from './errors'; import { SubnetStatus, request } from '../../canisterStatus'; import { CertificateVerificationError, @@ -41,6 +41,7 @@ import { Ed25519PublicKey } from '../../public_key'; import { decodeTime } from '../../utils/leb'; import { ObservableLog } from '../../observable'; import { BackoffStrategy, BackoffStrategyFactory, ExponentialBackoff } from '../../polling/backoff'; +import { calculateReplicaTime } from './calculateReplicaTime'; export * from './transforms'; export { Nonce, makeNonce } from './types'; @@ -138,6 +139,10 @@ export interface HttpAgentOptions { * Whether to log to the console. Defaults to false. */ logToConsole?: boolean; + /** + * Provide an expected replica time. This can be used to set the baseline for the time to use when making requests against the replica. + */ + replicaTime?: Date; /** * Alternate root key to use for verifying certificates. If not provided, the default IC root key will be used. @@ -243,7 +248,6 @@ export class HttpAgent implements Agent { readonly #fetch: typeof fetch; readonly #fetchOptions?: Record; readonly #callOptions?: Record; - #timeDiffMsecs = 0; readonly host: URL; readonly #credentials: string | undefined; #rootKeyFetched = false; @@ -257,6 +261,19 @@ export class HttpAgent implements Agent { // The UTC time in milliseconds when the latest request was made #waterMark = 0; + // Manage the time offset between the client and the replica + #initialClientTime: Date = new Date(Date.now()); + #initialReplicaTime: Date = new Date(Date.now()); + get replicaTime(): Date { + const offset = Date.now() - this.#initialClientTime.getTime(); + return new Date(this.#initialReplicaTime.getTime() + offset); + } + + set replicaTime(replicaTime: Date) { + this.#initialClientTime = new Date(Date.now()); + this.#initialReplicaTime = replicaTime; + } + get waterMark(): number { return this.#waterMark; } @@ -431,8 +448,9 @@ export class HttpAgent implements Agent { let ingress_expiry = new Expiry(DEFAULT_INGRESS_EXPIRY_DELTA_IN_MSECS); // If the value is off by more than 30 seconds, reconcile system time with the network - if (Math.abs(this.#timeDiffMsecs) > 1_000 * 30) { - ingress_expiry = new Expiry(DEFAULT_INGRESS_EXPIRY_DELTA_IN_MSECS + this.#timeDiffMsecs); + const timeDiffMsecs = this.replicaTime && this.replicaTime.getTime() - Date.now(); + if (Math.abs(timeDiffMsecs) > 1_000 * 30) { + ingress_expiry = new Expiry(DEFAULT_INGRESS_EXPIRY_DELTA_IN_MSECS + timeDiffMsecs); } const submit: CallRequest = { @@ -499,7 +517,6 @@ export class HttpAgent implements Agent { }); }; - const request = this.#requestAndRetry({ request: callSync ? requestSync : requestAsync, backoff, @@ -622,6 +639,8 @@ export class HttpAgent implements Agent { ); } } catch (error) { + this.log.error('Caught exception while attempting to read state', error as AgentError); + this.#handleReplicaTimeError(error as AgentError); if (tries < this.#retryTimes) { this.log.warn( `Caught exception while attempting to make query:\n` + @@ -717,6 +736,11 @@ export class HttpAgent implements Agent { } const responseText = await response.clone().text(); + + if (response.status === 400 && responseText.includes('ingress_expiry')) { + this.#handleReplicaTimeError(new AgentError(responseText)); + } + const errorMessage = `Server returned an error:\n` + ` Code: ${response.status} (${response.statusText})\n` + @@ -765,13 +789,21 @@ export class HttpAgent implements Agent { const canister = Principal.from(canisterId); const sender = id?.getPrincipal() || Principal.anonymous(); + let ingress_expiry = new Expiry(DEFAULT_INGRESS_EXPIRY_DELTA_IN_MSECS); + + // If the value is off by more than 30 seconds, reconcile system time with the network + const timeDiffMsecs = this.replicaTime && this.replicaTime.getTime() - Date.now(); + if (Math.abs(timeDiffMsecs) > 1_000 * 30) { + ingress_expiry = new Expiry(DEFAULT_INGRESS_EXPIRY_DELTA_IN_MSECS + timeDiffMsecs); + } + const request: QueryRequest = { request_type: ReadRequestType.Query, canister_id: canister, method_name: fields.methodName, arg: fields.arg, sender, - ingress_expiry: new Expiry(DEFAULT_INGRESS_EXPIRY_DELTA_IN_MSECS), + ingress_expiry, }; const requestId = await requestIdOf(request); @@ -943,9 +975,15 @@ export class HttpAgent implements Agent { } const sender = id?.getPrincipal() || Principal.anonymous(); - // TODO: remove this any. This can be a Signed or UnSigned request. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const transformedRequest: any = await this._transform({ + let ingress_expiry = new Expiry(DEFAULT_INGRESS_EXPIRY_DELTA_IN_MSECS); + + // If the value is off by more than 30 seconds, reconcile system time with the network + const timeDiffMsecs = this.replicaTime && this.replicaTime.getTime() - Date.now(); + if (Math.abs(timeDiffMsecs) > 1_000 * 30) { + ingress_expiry = new Expiry(DEFAULT_INGRESS_EXPIRY_DELTA_IN_MSECS + timeDiffMsecs); + } + + const transformedRequest = await this._transform({ request: { method: 'POST', headers: { @@ -958,7 +996,7 @@ export class HttpAgent implements Agent { request_type: ReadRequestType.ReadState, paths: fields.paths, sender, - ingress_expiry: new Expiry(DEFAULT_INGRESS_EXPIRY_DELTA_IN_MSECS), + ingress_expiry, }, }); @@ -971,7 +1009,7 @@ export class HttpAgent implements Agent { fields: ReadStateOptions, identity?: Identity | Promise, // eslint-disable-next-line - request?: any, + request?: Request, ): Promise { const canister = typeof canisterId === 'string' ? Principal.fromText(canisterId) : canisterId; @@ -984,6 +1022,7 @@ export class HttpAgent implements Agent { ); // TODO - https://dfinity.atlassian.net/browse/SDK-1092 const backoff = this.#backoffStrategy(); + try { const response = await this.#requestAndRetry({ request: () => @@ -1014,15 +1053,24 @@ export class HttpAgent implements Agent { this.log.print('Read state response time:', parsedTime); this.#waterMark = parsedTime; } - return decodedResponse; } catch (error) { - this.log.error('Caught exception while attempting to read state', error as AgentError); - throw error; + this.#handleReplicaTimeError(error as AgentError); } + throw new AgentError('Failed to read state'); } - public async parseTimeFromResponse(response: { certificate: ArrayBuffer }): Promise { + #handleReplicaTimeError = (error: AgentError): void => { + const message = error.message; + if (message?.includes('ingress_expiry')) { + { + const replicaTime = calculateReplicaTime(message); + throw new ReplicaTimeError(message, replicaTime, this); + } + } + }; + + public async parseTimeFromResponse(response: ReadStateResponse): Promise { let tree: HashTree; if (response.certificate) { const decoded: { tree: HashTree } | undefined = cbor.decode(response.certificate); @@ -1052,10 +1100,10 @@ export class HttpAgent implements Agent { /** * Allows agent to sync its time with the network. Can be called during intialization or mid-lifecycle if the device's clock has drifted away from the network time. This is necessary to set the Expiry for a request * @param {Principal} canisterId - Pass a canister ID if you need to sync the time with a particular replica. Uses the management canister by default + * @throws {ReplicaTimeError} - this method is not guaranteed to work if the device's clock is off by more than 30 seconds. In such cases, the agent will throw an error. */ public async syncTime(canisterId?: Principal): Promise { const CanisterStatus = await import('../../canisterStatus'); - const callTime = Date.now(); try { if (!canisterId) { this.log.print( @@ -1071,7 +1119,7 @@ export class HttpAgent implements Agent { const replicaTime = status.get('time'); if (replicaTime) { - this.#timeDiffMsecs = Number(replicaTime as bigint) - Number(callTime); + this.replicaTime = new Date(Number(replicaTime as bigint)); } } catch (error) { this.log.error('Caught exception while attempting to sync time', error as AgentError); diff --git a/packages/agent/src/canisterStatus/index.test.ts b/packages/agent/src/canisterStatus/index.test.ts index 34096676..cac1fe0a 100644 --- a/packages/agent/src/canisterStatus/index.test.ts +++ b/packages/agent/src/canisterStatus/index.test.ts @@ -63,7 +63,6 @@ const getRealStatus = async () => { const agent = new HttpAgent({ host: 'http://127.0.0.1:4943', fetch, identity }); await agent.fetchRootKey(); const canisterBuffer = new DataView(testPrincipal.toUint8Array().buffer).buffer; - canisterBuffer; const response = await agent.readState( testPrincipal, // Note: subnet is not currently working due to a bug diff --git a/packages/agent/src/canisterStatus/index.ts b/packages/agent/src/canisterStatus/index.ts index c46ba2ca..a0f9b5fe 100644 --- a/packages/agent/src/canisterStatus/index.ts +++ b/packages/agent/src/canisterStatus/index.ts @@ -145,10 +145,12 @@ export const request = async (options: { const response = await agent.readState(canisterId, { paths: [encodedPaths[index]], }); + const certTime = agent.replicaTime ? agent.replicaTime : undefined; const cert = await Certificate.create({ certificate: response.certificate, rootKey: agent.rootKey, canisterId: canisterId, + certTime, }); const lookup = (cert: Certificate, path: Path) => { diff --git a/packages/agent/src/certificate.ts b/packages/agent/src/certificate.ts index 8ddac8ca..d941c0e4 100644 --- a/packages/agent/src/certificate.ts +++ b/packages/agent/src/certificate.ts @@ -40,7 +40,7 @@ export type HashTree = /** * Make a human readable string out of a hash tree. - * @param tree + * @param tree - the tree to stringify */ export function hashTreeToString(tree: HashTree): string { const indent = (s: string) => @@ -52,7 +52,7 @@ export function hashTreeToString(tree: HashTree): string { const decoder = new TextDecoder(undefined, { fatal: true }); try { return JSON.stringify(decoder.decode(label)); - } catch (e) { + } catch { return `data(...${label.byteLength} bytes)`; } } @@ -146,10 +146,16 @@ export interface CreateCertificateOptions { * older than the specified age, it will fail verification. */ maxAgeInMinutes?: number; + + /** + * For comparing the time of the certificate to an expected date instead of the result of Date.now. + */ + certTime?: Date; } export class Certificate { public cert: Cert; + #certTime?: Date; /** * Create a new instance of a certificate, automatically verifying it. Throws a @@ -164,7 +170,6 @@ export class Certificate { */ public static async create(options: CreateCertificateOptions): Promise { const cert = Certificate.createUnverified(options); - await cert.verify(); return cert; } @@ -180,6 +185,7 @@ export class Certificate { options.canisterId, blsVerify, options.maxAgeInMinutes, + options.certTime, ); } @@ -190,8 +196,10 @@ export class Certificate { private _blsVerify: VerifyFunc, // Default to 5 minutes private _maxAgeInMinutes: number = 5, + certTime?: Date, ) { this.cert = cbor.decode(new Uint8Array(certificate)); + this.#certTime = certTime; } public lookup(path: Array): LookupResult { @@ -220,8 +228,10 @@ export class Certificate { const FIVE_MINUTES_IN_MSEC = 5 * 60 * 1000; const MAX_AGE_IN_MSEC = this._maxAgeInMinutes * 60 * 1000; const now = Date.now(); - const earliestCertificateTime = now - MAX_AGE_IN_MSEC; - const fiveMinutesFromNow = now + FIVE_MINUTES_IN_MSEC; + // Use a provided time in case `Date.now()` is inaccurate + const compareTime = this.#certTime || new Date(now); + const earliestCertificateTime = compareTime.getTime() - MAX_AGE_IN_MSEC; + const fiveMinutesFromNow = compareTime.getTime() + FIVE_MINUTES_IN_MSEC; const certTime = decodeTime(lookupTime); @@ -230,20 +240,20 @@ export class Certificate { `Certificate is signed more than ${this._maxAgeInMinutes} minutes in the past. Certificate time: ` + certTime.toISOString() + ' Current time: ' + - new Date(now).toISOString(), + compareTime.toISOString(), ); } else if (certTime.getTime() > fiveMinutesFromNow) { throw new CertificateVerificationError( 'Certificate is signed more than 5 minutes in the future. Certificate time: ' + certTime.toISOString() + ' Current time: ' + - new Date(now).toISOString(), + compareTime.toISOString(), ); } try { sigVer = await this._blsVerify(new Uint8Array(key), new Uint8Array(sig), new Uint8Array(msg)); - } catch (err) { + } catch { sigVer = false; } if (!sigVer) { @@ -261,6 +271,7 @@ export class Certificate { rootKey: this._rootKey, canisterId: this._canisterId, blsVerify: this._blsVerify, + certTime: this.#certTime, // Do not check max age for delegation certificates maxAgeInMinutes: Infinity, }); @@ -335,7 +346,7 @@ export function lookupResultToBuffer(result: LookupResult): ArrayBuffer | undefi } /** - * @param t + * @param t - the tree to reconstruct */ export async function reconstruct(t: HashTree): Promise { switch (t[0]) { @@ -408,6 +419,12 @@ interface LookupResultLess { type LabelLookupResult = LookupResult | LookupResultGreater | LookupResultLess; +/** + * Lookup a path in a tree + * @param path - the path to look up + * @param tree - the tree to search + * @returns LookupResult + */ export function lookup_path(path: Array, tree: HashTree): LookupResult { if (path.length === 0) { switch (tree[0]) { @@ -482,6 +499,12 @@ export function flatten_forks(t: HashTree): HashTree[] { } } +/** + * Find a label in a tree + * @param label - the label to find + * @param tree - the tree to search + * @returns LabelLookupResult + */ export function find_label(label: ArrayBuffer, tree: HashTree): LabelLookupResult { switch (tree[0]) { // if we have a labelled node, compare the node's label to the one we are @@ -514,6 +537,7 @@ export function find_label(label: ArrayBuffer, tree: HashTree): LabelLookupResul // if we have a fork node, we need to search both sides, starting with the left case NodeType.Fork: // search in the left node + // eslint-disable-next-line no-case-declarations const leftLookupResult = find_label(label, tree[1]); switch (leftLookupResult.status) { @@ -538,7 +562,7 @@ export function find_label(label: ArrayBuffer, tree: HashTree): LabelLookupResul // if the left node returns an uncertain result, we need to search the // right node case LookupStatus.Unknown: { - let rightLookupResult = find_label(label, tree[2]); + const rightLookupResult = find_label(label, tree[2]); // if the label we're searching for is less than the right node lookup, // then we also need to return an uncertain result @@ -580,9 +604,11 @@ export function find_label(label: ArrayBuffer, tree: HashTree): LabelLookupResul /** * Check if a canister falls within a range of canisters - * @param canisterId Principal - * @param ranges [Principal, Principal][] - * @returns + * @param params - the parameters to check + * @param params.canisterId Principal + * @param params.subnetId Principal + * @param params.tree HashTree + * @returns boolean */ export function check_canister_ranges(params: { canisterId: Principal; diff --git a/packages/agent/src/polling/index.ts b/packages/agent/src/polling/index.ts index ccd399eb..baebbc7a 100644 --- a/packages/agent/src/polling/index.ts +++ b/packages/agent/src/polling/index.ts @@ -1,5 +1,5 @@ import { Principal } from '@dfinity/principal'; -import { Agent, RequestStatusResponseStatus } from '../agent'; +import { Agent, HttpAgent, RequestStatusResponseStatus } from '../agent'; import { Certificate, CreateCertificateOptions, lookupResultToBuffer } from '../certificate'; import { RequestId } from '../request_id'; import { toHex } from '../utils/buffer'; @@ -40,10 +40,17 @@ export async function pollForResponse( const currentRequest = request ?? (await agent.createReadStateRequest?.({ paths: [path] })); const state = await agent.readState(canisterId, { paths: [path] }, undefined, currentRequest); if (agent.rootKey == null) throw new Error('Agent root key not initialized before polling'); + + // if agent has replicaTime, otherwise omit + const certTime = (agent as HttpAgent)?.replicaTime + ? (agent as HttpAgent)?.replicaTime + : undefined; + const cert = await Certificate.create({ certificate: state.certificate, rootKey: agent.rootKey, canisterId: canisterId, + certTime: certTime, blsVerify, }); diff --git a/packages/assets/src/index.ts b/packages/assets/src/index.ts index db2187bf..73e6fd27 100644 --- a/packages/assets/src/index.ts +++ b/packages/assets/src/index.ts @@ -7,6 +7,7 @@ import { compare, getDefaultAgent, HashTree, + HttpAgent, lookup_path, lookupResultToBuffer, LookupStatus, @@ -530,10 +531,15 @@ class Asset { return false; } + const replicaTime = (agent as HttpAgent).replicaTime + ? (agent as HttpAgent).replicaTime + : undefined; + const cert = await Certificate.create({ certificate: new Uint8Array(certificate), rootKey: agent.rootKey, canisterId, + certTime: replicaTime, }).catch(() => Promise.resolve()); if (!cert) {