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) {