Skip to content

Release notes for v3.0.0

This release marks a major update to the ICP JavaScript SDK, introducing several breaking changes, new features, and improvements. This guide is intended to help developers upgrade their projects to the latest version. Among the key improvements, this release eliminates Buffer and similar dependencies, significantly reducing bundle size and improving performance across all packages.

New behavior when constructing HttpAgent with shouldFetchRootKey flag in @dfinity/agent

Section titled “New behavior when constructing HttpAgent with shouldFetchRootKey flag in @dfinity/agent”

The behavior of the shouldFetchRootKey flag has been changed. If you were using the HttpAgent constructor with the shouldFetchRootKey flag, you will need to update your code to use the rootKey option instead.

Before:

// When constructing the agent synchronously, the root key was fetched automatically in the background,
// causing potential race conditions
const agent = HttpAgent.createSync({ shouldFetchRootKey: true }); // or, even if deprecated, new HttpAgent({ shouldFetchRootKey: true })
agent.call(...); // we don't know if the root key was fetched or not at this point
// When constructing the agent asynchronously, the root key was fetched directly
const agent = await HttpAgent.create({ shouldFetchRootKey: true });
agent.call(...); // the root key has already been fetched at this point

After:

// When constructing the agent synchronously, the root key is not fetched until the first request is made
const agent = HttpAgent.createSync({ shouldFetchRootKey: true }); // or, even if deprecated, new HttpAgent({ shouldFetchRootKey: true })
agent.call(...); // the root key is fetched at this point, before the call is made
// When constructing the agent asynchronously, nothing changes
const agent = await HttpAgent.create({ shouldFetchRootKey: true });
agent.call(...); // the root key has already been fetched at this point

Removal of Management Canister from @dfinity/agent

Section titled “Removal of Management Canister from @dfinity/agent”

The agent-js’ implementation of the management canister actor was a duplicate of the one in the @dfinity/ic-management package. It has been removed and you will need to update your code to use this package instead.

Before:

import { getManagementCanister } from '@dfinity/agent';
const management = getManagementCanister({ agent });

After:

Use the @dfinity/ic-management package.

Note: The @dfinity/ic-management package may still depend on the v2 of the ICP JavaScript SDK, which could cause issues depending on your project’s dependencies.

Internally, we have replaced the usage of ArrayBuffer with Uint8Array.

Specifically, all @dfinity/candid interfaces that previously accepted or returned ArrayBuffer now use Uint8Array. You will need to update your code to handle Uint8Array instead of ArrayBuffer.

Before:

import { IDL } from '@dfinity/candid';
const myArgType = IDL.Text;
const myArgValue = 'Hello, world!';
const encoded: ArrayBuffer = IDL.encode([myArgType], [myArgValue]);
const decoded: [string] = IDL.decode([myArgType], encoded);

Or, with utility functions:

import { concat } from '@dfinity/candid';
const buffer1: ArrayBuffer = ...;
const buffer2: ArrayBuffer = ...;
const combined: ArrayBuffer = concat(buffer1, buffer2);

After:

import { IDL } from '@dfinity/candid';
const myArgType = IDL.Text;
const myArgValue = 'Hello, world!';
const encoded: Uint8Array = IDL.encode([myArgType], [myArgValue]);
const decoded: [string] = IDL.decode([myArgType], encoded);

Or, with utility functions:

import { concat } from '@dfinity/candid';
const buffer1: Uint8Array = ...;
const buffer2: Uint8Array = ...;
const combined: Uint8Array = concat(buffer1, buffer2);

The fromHex, toHex, and concat utility functions have been removed. Use the bytesToHex, hexToBytes, and concatBytes functions from @noble/hashes/utils instead.

Before:

import { toHex, fromHex, concat } from '@dfinity/agent';
const hex = toHex(myUint8Array);
const bytes = fromHex(myHexString);
const combined = concat(buffer1, buffer2);

After:

import { bytesToHex, hexToBytes, concatBytes } from '@noble/hashes/utils';
const hex = bytesToHex(myUint8Array);
const bytes = hexToBytes(myHexString);
const combined = concatBytes(buffer1, buffer2);

New AgentError Error and Better Error Handling in @dfinity/agent

Section titled “New AgentError Error and Better Error Handling in @dfinity/agent”

Several specific error classes have been removed in favor of a new, more generic AgentError. This new error class has the following properties:

  • code: an ErrorCode class value, e.g. CertifiedRejectErrorCode. See errors.ts for all error codes.
  • kind: an ErrorKindEnum enum value, e.g. ErrorKindEnum.Trust. See ErrorKindEnum for all error kinds.
  • message: a human-readable error message
  • cause: an object containing the code and kind properties

This new error class is more flexible and easier to handle in your code.

The removed error classes are:

  • AgentHTTPResponseError
  • AgentCallError
  • AgentQueryError
  • AgentReadStateError
  • CertificateVerificationError
  • ActorCallError
  • QueryCallRejectedError
  • UpdateCallRejectedError

You should update your error handling logic to catch AgentError and inspect its properties.

Example:

try {
// agent call
} catch (e) {
if (e instanceof AgentError) {
console.error(e.message);
if (
e.code instanceof CertifiedRejectErrorCode && // or e.cause.code
e.kind === ErrorKindEnum.Trust // or e.cause.kind
) {
// do something
}
}
}

Removal of defaultAgent from @dfinity/agent

Section titled “Removal of defaultAgent from @dfinity/agent”

The global defaultAgent concept and the getDefaultAgent function have been removed. The HttpAgent constructor is now the only way to create an agent.

Before:

import { getDefaultAgent } from '@dfinity/agent';
const agent = getDefaultAgent();

After:

import { HttpAgent } from '@dfinity/agent';
const agent = await HttpAgent.create(); // or use the synchronous version `HttpAgent.createSync()`

The ProxyAgent class has been removed. You must now create your own proxy agent using the HttpAgent class.

The Expiry class has been refactored to use static factory methods and no longer exposes a public constructor. It also now supports JSON serialization/deserialization.

Before:

import { Expiry } from '@dfinity/agent';
const fiveMinutesInMs = 5 * 60 * 1000;
const expiry = new Expiry(fiveMinutesInMs);

After:

import { Expiry } from '@dfinity/agent';
// Create an expiry 5 minutes from now.
const fiveMinutesInMs = 5 * 60 * 1000;
const expiry = Expiry.fromDeltaInMilliseconds(fiveMinutesInMs);
// You can also serialize and deserialize Expiry objects.
const json = expiry.toJSON();
const expiryFromJson = Expiry.fromJSON(JSON.stringify(json));

Certificate Method Renaming and Removal in @dfinity/agent

Section titled “Certificate Method Renaming and Removal in @dfinity/agent”
  • The lookup_path standalone function has been renamed to lookup_subtree.
  • The lookup method of the Certificate class has been renamed into lookup_subtree, and uses the lookup_subtree standalone function internally.
  • A new lookup_path has been introduced both as a standalone function and as a method of the Certificate class. Its behavior is now aligned with the IC Interface Specification.
  • The lookup_label method has been removed from the Certificate class. Use Certificate.lookup_subtree or Certificate.lookup_path instead, according to your use case.

Before:

// Using standalone functions
const lookupResult = lookup_path(['a', 'b'], certificate.tree);
const subtreeLookupResult = lookup_path(['subnet', delegation.subnet_id, 'node'], certificate.tree);
// Using the Certificate class
const lookupResult = certificate.lookup(['a', 'b']);
const subtreeLookupResult = certificate.lookup(['subnet', delegation.subnet_id, 'node']);
const label = certificate.lookup_label(['path', 'to', 'label']);

After:

// Using standalone functions
const lookupResult = lookup_path(['a', 'b'], certificate.tree);
const subtreeLookupResult = lookup_subtree(
['subnet', delegation.subnet_id, 'node'],
certificate.tree,
);
// Using the Certificate class
const lookupResult = certificate.lookup_path(['a', 'b']);
const subtreeLookupResult = certificate.lookup_subtree(['subnet', delegation.subnet_id, 'node']);
const labelLookupResult = certificate.lookup_path(['path', 'to', 'label']); // or certificate.lookup_subtree(['path', 'to', 'label'])

Hashing Function Replacement in @dfinity/agent

Section titled “Hashing Function Replacement in @dfinity/agent”

The hash function has been removed. You now have two options:

  • Use the native crypto.subtle.digest implementation (asynchronous)
  • Use the sha256 function from @noble/hashes/sha2 (synchronous)

Before:

import { hash } from '@dfinity/agent';
const hashed = hash(myBuffer);

After:

// Using the native implementation (asynchronous)
const hashes = await globalThis.crypto.subtle.digest('SHA-256', myBuffer);
// Using the @noble/hashes/sha2 implementation (synchronous)
import { sha256 } from '@noble/hashes/sha2';
const hashed = sha256(myBuffer);

Cbor package replacement in @dfinity/agent

Section titled “Cbor package replacement in @dfinity/agent”

The @dfinity/agent package now uses @dfinity/cbor instead of the borc and simple-cbor dependencies, reducing the bundle size of most packages by at least 30% (gzipped) 🤯.

Before:

import { Cbor } from '@dfinity/agent';
const encoded: ArrayBuffer = Cbor.encode(myValue);
const decoded: MyType = Cbor.decode(encoded);

After:

import { Cbor } from '@dfinity/agent';
const encoded: Uint8Array = Cbor.encode(myValue); // now returns Uint8Array
const decoded = Cbor.decode<MyType>(encoded); // now accepts Uint8Array

Support for Node.js v19 and lower, and Node.js v21 has been dropped. Please ensure you are using a supported version of Node.js (LTS versions like 20 or 22 are recommended).

  • Improved Polling Strategy: The polling strategy for read_state requests has been changed to support presigned requests. A new preSignReadStateRequest option allows for a single signature to be used for all polling requests, which is beneficial for hardware wallets.
  • New documentation website: The documentation website available at agent-js.icp.xyz has been renewed. It is now build using the Starlight framework.

Most packages have been reduced in size by a significant amount. Here’s the full report:

PackageBefore (gzipped)After (gzipped)Approx. Size Delta
@dfinity/identity-secp256k1106.69 kB33.03 kB- 69% 🔽
@dfinity/identity47.8 kB18.81 kB- 61% 🔽
@dfinity/auth-client50.15 kB19.38 kB- 61% 🔽
@dfinity/agent72.1 kB47.61 kB- 34% 🔽
@dfinity/assets67.97 kB52.46 kB- 23% 🔽
@dfinity/principal4.21 kB4.44 kB+ 5% 🔺
@dfinity/candid11.82 kB12.97 kB+ 10% 🔺

If an API call to the IC network fails due to an ingress expiry error, the HttpAgent will now automatically adjust the ingress expiry time based on the latest subnet’s certified time. You can also sync time before an error occurs by using the shouldSyncTime option.

Example:

const agent = await HttpAgent.create({
shouldSyncTime: true,
});

Note: The shouldSyncTime flag behavior follows the same logic as the new shouldFetchRootKey flag behavior. For this reason, depending on your use case, you may prefer to defer the time sync until the first request is made by constructing the agent synchronously:

// When constructing the agent synchronously, the time is not synced until the first request is made
const agent = HttpAgent.createSync({
shouldSyncTime: true,
// shouldFetchRootKey: true, // even more relevant if you want to sync the root key as well
});
agent.call(...); // the time is synced at this point, before the call is made
// When constructing the agent asynchronously, the time is synced when the agent is created
const agent = await HttpAgent.create({
shouldSyncTime: true,
// shouldFetchRootKey: true, // if both flags are enabled, the agent creation will take longer
});
agent.call(...); // the time is already synced at this point

No more watermark checks for query responses

Section titled “No more watermark checks for query responses”

The agent no longer checks for watermarks on query responses. This removes a lot of watermark errors that projects were frequently facing when interacting with the IC mainnet.

The HttpAgent now checks if the node signature is not older than the ingressExpiryInMinutes option, to prevent stale query responses. This takes into account clock drift, if enabled.

  • Dependency Updates: @noble/* dependencies have been updated. Old unused dependencies have been removed.
  • @noble/hashes Dependency: @noble/hashes is now correctly marked as a dependency.
  • Candid Subtyping: Subtyping relationships are now checked when decoding function or service references, improving compliance with the Candid spec and reducing the risk of calling services with incorrect argument types.
  • Request Retries: Requests that fail due to a malformed response body will now be retried.
  • isAuthenticated Fix: AuthClient.isAuthenticated now correctly returns false if the delegation chain is invalid (e.g., due to an expired session).
  • Large BigInt values encoding in Candid: A bug in the Candid’s leb128 encoding of large BigInt values has been fixed, solving the issue of the encoding breaking when encountering large BigInt values.