From 08b1257afce2da63d4bc13be21ab2f34d0fbcb19 Mon Sep 17 00:00:00 2001 From: ilbertt Date: Wed, 26 Nov 2025 15:53:18 +0100 Subject: [PATCH 1/4] feat(agent): use `/api/v4` for call requests --- e2e/node/basic/syncTime.test.ts | 94 +++++++++---------- e2e/node/utils/mock-replica.ts | 36 +++---- packages/core/src/agent/actor.test.ts | 6 +- packages/core/src/agent/actor.ts | 4 +- packages/core/src/agent/agent/api.ts | 16 ++-- .../core/src/agent/agent/http/http.test.ts | 4 +- packages/core/src/agent/agent/http/index.ts | 18 ++-- packages/core/src/agent/errors.ts | 8 +- 8 files changed, 93 insertions(+), 93 deletions(-) diff --git a/e2e/node/basic/syncTime.test.ts b/e2e/node/basic/syncTime.test.ts index 13b79304d..6eaaf3153 100644 --- a/e2e/node/basic/syncTime.test.ts +++ b/e2e/node/basic/syncTime.test.ts @@ -20,7 +20,7 @@ import { MockReplica, mockSyncTimeResponse, prepareV2ReadStateTimeResponse, - prepareV3Response, + prepareV4Response, } from '../utils/mock-replica.ts'; import { randomIdentity, randomKeyPair } from '../utils/identity.ts'; import { concatBytes } from '@noble/hashes/utils'; @@ -68,7 +68,7 @@ describe('syncTime', () => { const actor = await createActor(canisterId, { agent }); const sender = identity.getPrincipal(); - const { responseBody, requestId } = await prepareV3Response({ + const { responseBody, requestId } = await prepareV4Response({ canisterId, methodName: greetMethodName, arg: greetArgs, @@ -79,23 +79,23 @@ describe('syncTime', () => { nonce, }); const signature = await identity.sign(concatBytes(IC_REQUEST_DOMAIN_SEPARATOR, requestId)); - mockReplica.setV3CallSpyImplOnce(canisterId.toString(), (_req, res) => { + mockReplica.setV4CallSpyImplOnce(canisterId.toString(), (_req, res) => { res.status(200).send(responseBody); }); const actorResponse = await actor.greet.withOptions({ nonce })(greetReq); expect(actorResponse).toEqual(greetRes); - expect(mockReplica.getV3CallSpy(canisterId.toString())).toHaveBeenCalledTimes(1); - expectV3CallRequest( - mockReplica.getV3CallReq(canisterId.toString(), 0), + expect(mockReplica.getV4CallSpy(canisterId.toString())).toHaveBeenCalledTimes(1); + expectV4CallRequest( + mockReplica.getV4CallReq(canisterId.toString(), 0), { nonce, sender, pubKey: identity.getPublicKey().toDer(), signature, }, - 'V3 call body', + 'V4 call body', ); expect(agent.hasSyncedTime()).toBe(false); }); @@ -109,7 +109,7 @@ describe('syncTime', () => { const actor = await createActor(canisterId, { agent }); const sender = identity.getPrincipal(); - mockReplica.setV3CallSpyImplOnce(canisterId.toString(), (_req, res) => { + mockReplica.setV4CallSpyImplOnce(canisterId.toString(), (_req, res) => { res.status(400).send(new TextEncoder().encode(INVALID_EXPIRY_ERROR)); }); @@ -119,7 +119,7 @@ describe('syncTime', () => { canisterId, }); - const { responseBody: callResponse, requestId } = await prepareV3Response({ + const { responseBody: callResponse, requestId } = await prepareV4Response({ canisterId, methodName: greetMethodName, arg: greetArgs, @@ -130,17 +130,17 @@ describe('syncTime', () => { nonce, }); const signature = await identity.sign(concatBytes(IC_REQUEST_DOMAIN_SEPARATOR, requestId)); - mockReplica.setV3CallSpyImplOnce(canisterId.toString(), (_req, res) => { + mockReplica.setV4CallSpyImplOnce(canisterId.toString(), (_req, res) => { res.status(200).send(callResponse); }); const actorResponse = await actor.greet.withOptions({ nonce })(greetReq); expect(actorResponse).toEqual(greetRes); - expect(mockReplica.getV3CallSpy(canisterId.toString())).toHaveBeenCalledTimes(2); + expect(mockReplica.getV4CallSpy(canisterId.toString())).toHaveBeenCalledTimes(2); - const req = mockReplica.getV3CallReq(canisterId.toString(), 0); - expectV3CallRequest( + const req = mockReplica.getV4CallReq(canisterId.toString(), 0); + expectV4CallRequest( req, { nonce, @@ -148,10 +148,10 @@ describe('syncTime', () => { pubKey: identity.getPublicKey().toDer(), signature, }, - 'V3 call body', + 'V4 call body', ); - const reqTwo = mockReplica.getV3CallReq(canisterId.toString(), 1); + const reqTwo = mockReplica.getV4CallReq(canisterId.toString(), 1); expect(reqTwo).toEqual(req); expect(mockReplica.getV2ReadStateSpy(canisterId.toString())).toHaveBeenCalledTimes(3); @@ -167,14 +167,14 @@ describe('syncTime', () => { { sender: anonIdentity.getPrincipal(), }, - 'V3 read state body two', + 'V4 read state body two', ); expectV2ReadStateRequest( mockReplica.getV2ReadStateReq(canisterId.toString(), 2), { sender: anonIdentity.getPrincipal(), }, - 'V3 read state body three', + 'V4 read state body three', ); expect(agent.hasSyncedTime()).toBe(true); }); @@ -187,7 +187,7 @@ describe('syncTime', () => { }); const actor = await createActor(canisterId, { agent }); - mockReplica.setV3CallSpyImplOnce(canisterId.toString(), (_req, res) => { + mockReplica.setV4CallSpyImplOnce(canisterId.toString(), (_req, res) => { res.status(400).send(new TextEncoder().encode(INVALID_EXPIRY_ERROR)); }); @@ -197,7 +197,7 @@ describe('syncTime', () => { canisterId, }); - mockReplica.setV3CallSpyImplOnce(canisterId.toString(), (_req, res) => { + mockReplica.setV4CallSpyImplOnce(canisterId.toString(), (_req, res) => { res.status(400).send(new TextEncoder().encode(INVALID_EXPIRY_ERROR)); }); @@ -214,7 +214,7 @@ describe('syncTime', () => { ); } - expect(mockReplica.getV3CallSpy(canisterId.toString())).toHaveBeenCalledTimes(2); + expect(mockReplica.getV4CallSpy(canisterId.toString())).toHaveBeenCalledTimes(2); expect(mockReplica.getV2ReadStateSpy(canisterId.toString())).toHaveBeenCalledTimes(3); expect(agent.hasSyncedTime()).toBe(true); }); @@ -247,21 +247,21 @@ describe('syncTime', () => { { sender: anonIdentity.getPrincipal(), }, - 'V3 read state body one', + 'V4 read state body one', ); expectV2ReadStateRequest( mockReplica.getV2ReadStateReq(ICP_LEDGER, 1), { sender: anonIdentity.getPrincipal(), }, - 'V3 read state body two', + 'V4 read state body two', ); expectV2ReadStateRequest( mockReplica.getV2ReadStateReq(ICP_LEDGER, 2), { sender: anonIdentity.getPrincipal(), }, - 'V3 read state body three', + 'V4 read state body three', ); expect(agent.hasSyncedTime()).toBe(true); }); @@ -328,7 +328,7 @@ describe('syncTime', () => { res.status(200).send(readStateResponse); }); - const { responseBody, requestId } = await prepareV3Response({ + const { responseBody, requestId } = await prepareV4Response({ canisterId, methodName: greetMethodName, arg: greetArgs, @@ -339,16 +339,16 @@ describe('syncTime', () => { nonce, }); const signature = await identity.sign(concatBytes(IC_REQUEST_DOMAIN_SEPARATOR, requestId)); - mockReplica.setV3CallSpyImplOnce(canisterId.toString(), (_req, res) => { + mockReplica.setV4CallSpyImplOnce(canisterId.toString(), (_req, res) => { res.status(200).send(responseBody); }); const actorResponse = await actor.greet.withOptions({ nonce })(greetReq); expect(actorResponse).toEqual(greetRes); - expect(mockReplica.getV3CallSpy(canisterId.toString())).toHaveBeenCalledTimes(1); - const req = mockReplica.getV3CallReq(canisterId.toString(), 0); - expectV3CallRequest( + expect(mockReplica.getV4CallSpy(canisterId.toString())).toHaveBeenCalledTimes(1); + const req = mockReplica.getV4CallReq(canisterId.toString(), 0); + expectV4CallRequest( req, { nonce, @@ -356,7 +356,7 @@ describe('syncTime', () => { pubKey: identity.getPublicKey().toDer(), signature, }, - 'V3 call body', + 'V4 call body', ); expect(mockReplica.getV2ReadStateSpy(canisterId.toString())).toHaveBeenCalledTimes(3); @@ -365,21 +365,21 @@ describe('syncTime', () => { { sender: anonIdentity.getPrincipal(), }, - 'V3 read state body one', + 'V4 read state body one', ); expectV2ReadStateRequest( mockReplica.getV2ReadStateReq(canisterId.toString(), 1), { sender: anonIdentity.getPrincipal(), }, - 'V3 read state body two', + 'V4 read state body two', ); expectV2ReadStateRequest( mockReplica.getV2ReadStateReq(canisterId.toString(), 2), { sender: anonIdentity.getPrincipal(), }, - 'V3 read state body three', + 'V4 read state body three', ); expect(agent.hasSyncedTime()).toBe(true); }); @@ -400,7 +400,7 @@ describe('syncTime', () => { res.status(200).send(readStateResponse); }); - const { responseBody, requestId } = await prepareV3Response({ + const { responseBody, requestId } = await prepareV4Response({ canisterId, methodName: greetMethodName, arg: greetArgs, @@ -411,23 +411,23 @@ describe('syncTime', () => { nonce, }); const signature = await identity.sign(concatBytes(IC_REQUEST_DOMAIN_SEPARATOR, requestId)); - mockReplica.setV3CallSpyImplOnce(canisterId.toString(), (_req, res) => { + mockReplica.setV4CallSpyImplOnce(canisterId.toString(), (_req, res) => { res.status(200).send(responseBody); }); const actorResponse = await actor.greet.withOptions({ nonce })(greetReq); expect(actorResponse).toEqual(greetRes); - expect(mockReplica.getV3CallSpy(canisterId.toString())).toHaveBeenCalledTimes(1); - expectV3CallRequest( - mockReplica.getV3CallReq(canisterId.toString(), 0), + expect(mockReplica.getV4CallSpy(canisterId.toString())).toHaveBeenCalledTimes(1); + expectV4CallRequest( + mockReplica.getV4CallReq(canisterId.toString(), 0), { nonce, sender, pubKey: identity.getPublicKey().toDer(), signature, }, - 'V3 call body', + 'V4 call body', ); expect(mockReplica.getV2ReadStateSpy(canisterId.toString())).toHaveBeenCalledTimes(0); @@ -451,7 +451,7 @@ describe('syncTime', () => { res.status(200).send(readStateResponse); }); - const { responseBody, requestId } = await prepareV3Response({ + const { responseBody, requestId } = await prepareV4Response({ canisterId, methodName: greetMethodName, arg: greetArgs, @@ -462,16 +462,16 @@ describe('syncTime', () => { nonce, }); const signature = await identity.sign(concatBytes(IC_REQUEST_DOMAIN_SEPARATOR, requestId)); - mockReplica.setV3CallSpyImplOnce(canisterId.toString(), (_req, res) => { + mockReplica.setV4CallSpyImplOnce(canisterId.toString(), (_req, res) => { res.status(200).send(responseBody); }); const actorResponse = await actor.greet.withOptions({ nonce })(greetReq); expect(actorResponse).toEqual(greetRes); - expect(mockReplica.getV3CallSpy(canisterId.toString())).toHaveBeenCalledTimes(1); - const req = mockReplica.getV3CallReq(canisterId.toString(), 0); - expectV3CallRequest( + expect(mockReplica.getV4CallSpy(canisterId.toString())).toHaveBeenCalledTimes(1); + const req = mockReplica.getV4CallReq(canisterId.toString(), 0); + expectV4CallRequest( req, { nonce, @@ -479,7 +479,7 @@ describe('syncTime', () => { pubKey: identity.getPublicKey().toDer(), signature, }, - 'V3 call body', + 'V4 call body', ); expect(mockReplica.getV2ReadStateSpy(canisterId.toString())).toHaveBeenCalledTimes(0); @@ -488,16 +488,16 @@ describe('syncTime', () => { }); }); -interface ExpectedV3CallRequest { +interface ExpectedV4CallRequest { nonce: Nonce; sender: Principal; pubKey: Uint8Array; signature: Signature; } -function expectV3CallRequest( +function expectV4CallRequest( actual: Signed, - expected: ExpectedV3CallRequest, + expected: ExpectedV4CallRequest, snapshotName?: string, ) { expect(actual.content.nonce).toEqual(expected.nonce); diff --git a/e2e/node/utils/mock-replica.ts b/e2e/node/utils/mock-replica.ts index 584546a2c..26e4e80e7 100644 --- a/e2e/node/utils/mock-replica.ts +++ b/e2e/node/utils/mock-replica.ts @@ -4,7 +4,7 @@ import { Cbor, requestIdOf, SubmitRequestType, - v3ResponseBody, + v4ResponseBody, calculateIngressExpiry, Cert, reconstruct, @@ -33,7 +33,7 @@ import { concatBytes, toBytes } from '@noble/hashes/utils'; const NANOSECONDS_TO_MSECS = 1_000_000; export enum MockReplicaSpyType { - CallV3 = 'CallV3', + CallV4 = 'CallV4', ReadStateV2 = 'ReadStateV2', QueryV2 = 'QueryV2', } @@ -45,7 +45,7 @@ export type MockReplicaSpyImpl = (req: MockReplicaRequest, res: MockReplicaRespo export type MockReplicaSpy = Mock; export interface MockReplicaSpies { - [MockReplicaSpyType.CallV3]?: MockReplicaSpy; + [MockReplicaSpyType.CallV4]?: MockReplicaSpy; [MockReplicaSpyType.ReadStateV2]?: MockReplicaSpy; [MockReplicaSpyType.QueryV2]?: MockReplicaSpy; } @@ -69,8 +69,8 @@ export class MockReplica { ) { app.use(express.raw({ type: 'application/cbor' })); app.post( - '/api/v3/canister/:canisterId/call', - this.#createEndpointSpy(MockReplicaSpyType.CallV3), + '/api/v4/canister/:canisterId/call', + this.#createEndpointSpy(MockReplicaSpyType.CallV4), ); app.post( '/api/v2/canister/:canisterId/read_state', @@ -101,8 +101,8 @@ export class MockReplica { }); } - public setV3CallSpyImplOnce(canisterId: string, impl: MockReplicaSpyImpl): void { - this.#setSpyImplOnce(canisterId, MockReplicaSpyType.CallV3, impl); + public setV4CallSpyImplOnce(canisterId: string, impl: MockReplicaSpyImpl): void { + this.#setSpyImplOnce(canisterId, MockReplicaSpyType.CallV4, impl); } public setV2ReadStateSpyImplOnce(canisterId: string, impl: MockReplicaSpyImpl): void { @@ -113,8 +113,8 @@ export class MockReplica { this.#setSpyImplOnce(canisterId, MockReplicaSpyType.QueryV2, impl); } - public getV3CallSpy(canisterId: string): MockReplicaSpy { - return this.#getSpy(canisterId, MockReplicaSpyType.CallV3); + public getV4CallSpy(canisterId: string): MockReplicaSpy { + return this.#getSpy(canisterId, MockReplicaSpyType.CallV4); } public getV2ReadStateSpy(canisterId: string): MockReplicaSpy { @@ -125,8 +125,8 @@ export class MockReplica { return this.#getSpy(canisterId, MockReplicaSpyType.QueryV2); } - public getV3CallReq(canisterId: string, callNumber: number): Signed { - const [req] = this.#getCallParams(canisterId, callNumber, MockReplicaSpyType.CallV3); + public getV4CallReq(canisterId: string, callNumber: number): Signed { + const [req] = this.#getCallParams(canisterId, callNumber, MockReplicaSpyType.CallV4); return Cbor.decode>(req.body); } @@ -219,7 +219,7 @@ export class MockReplica { } } -interface V3ResponseOptions { +interface V4ResponseOptions { canisterId: Principal | string; methodName: string; arg: Uint8Array; @@ -232,14 +232,14 @@ interface V3ResponseOptions { nonce?: Nonce; } -interface V3Response { +interface V4Response { responseBody: Uint8Array; requestId: RequestId; } /** * Prepares a version 3 response for a canister call. - * @param {V3ResponseOptions} options - The options for preparing the response. + * @param {V4ResponseOptions} options - The options for preparing the response. * @param {string} options.canisterId - The ID of the canister. * @param {string} options.methodName - The name of the method being called. * @param {Uint8Array} options.arg - The arguments for the method call. @@ -250,9 +250,9 @@ interface V3Response { * @param {KeyPair} options.keyPair - The key pair for signing. * @param {Date} options.date - The date of the request. * @param {Uint8Array} options.nonce - The nonce for the request. - * @returns {Promise} A promise that resolves to the prepared response. + * @returns {Promise} A promise that resolves to the prepared response. */ -export async function prepareV3Response({ +export async function prepareV4Response({ canisterId, methodName, arg, @@ -263,7 +263,7 @@ export async function prepareV3Response({ keyPair, date, nonce, -}: V3ResponseOptions): Promise { +}: V4ResponseOptions): Promise { canisterId = Principal.from(canisterId); sender = Principal.from(sender); ingressExpiryInMinutes = ingressExpiryInMinutes ?? 5; @@ -295,7 +295,7 @@ export async function prepareV3Response({ tree, signature, }; - const responseBody: v3ResponseBody = { + const responseBody: v4ResponseBody = { certificate: Cbor.encode(cert), }; diff --git a/packages/core/src/agent/actor.test.ts b/packages/core/src/agent/actor.test.ts index d5e856670..ceb8917f5 100644 --- a/packages/core/src/agent/actor.test.ts +++ b/packages/core/src/agent/actor.test.ts @@ -570,7 +570,7 @@ describe('makeActor', () => { }); }; - // Mock fetch to return a v3-style response with a dummy certificate + // Mock fetch to return a v4-style response with a dummy certificate const mockFetch = jest.fn(() => { const body = cbor.encode({ certificate: new Uint8Array([1, 2, 3]) }); return Promise.resolve( @@ -645,7 +645,7 @@ describe('makeActor', () => { }); }; - // Mock fetch to return a v3-style response with a dummy certificate + // Mock fetch to return a v4-style response with a dummy certificate const mockFetch = jest.fn(() => { const body = cbor.encode({ certificate: new Uint8Array([1, 2, 3]) }); return Promise.resolve( @@ -718,7 +718,7 @@ describe('makeActor', () => { }); }; - // Mock fetch to return a v3-style response with a dummy certificate + // Mock fetch to return a v4-style response with a dummy certificate const mockFetch = jest.fn(() => { const body = cbor.encode({ certificate: new Uint8Array([1, 2, 3]) }); return Promise.resolve( diff --git a/packages/core/src/agent/actor.ts b/packages/core/src/agent/actor.ts index 38402eeeb..59d511b0d 100644 --- a/packages/core/src/agent/actor.ts +++ b/packages/core/src/agent/actor.ts @@ -2,7 +2,7 @@ import { type Agent, type HttpDetailsResponse, isV2ResponseBody, - isV3ResponseBody, + isV4ResponseBody, QueryResponseStatus, } from './agent/index.ts'; import { @@ -452,7 +452,7 @@ function _createActorMethod( }); let reply: Uint8Array | undefined; let certificate: Certificate | undefined; - if (isV3ResponseBody(response.body)) { + if (isV4ResponseBody(response.body)) { if (agent.rootKey == null) { throw ExternalError.fromCode(new MissingRootKeyErrorCode()); } diff --git a/packages/core/src/agent/agent/api.ts b/packages/core/src/agent/agent/api.ts index ca6a0646b..b4fec9100 100644 --- a/packages/core/src/agent/agent/api.ts +++ b/packages/core/src/agent/agent/api.ts @@ -133,23 +133,23 @@ export interface v2ResponseBody { * @returns boolean indicating if the body is a v2ResponseBody */ export function isV2ResponseBody( - body: v2ResponseBody | v3ResponseBody | null, + body: v2ResponseBody | v4ResponseBody | null, ): body is v2ResponseBody { return body !== null && body !== undefined && 'reject_code' in body; } -export interface v3ResponseBody { +export interface v4ResponseBody { certificate: Uint8Array; } /** - * Utility function to check if a body is a v3ResponseBody for type safety. + * Utility function to check if a body is a v4ResponseBody for type safety. * @param body The body to check - * @returns boolean indicating if the body is a v3ResponseBody + * @returns boolean indicating if the body is a v4ResponseBody */ -export function isV3ResponseBody( - body: v2ResponseBody | v3ResponseBody | null, -): body is v3ResponseBody { +export function isV4ResponseBody( + body: v2ResponseBody | v4ResponseBody | null, +): body is v4ResponseBody { return body !== null && body !== undefined && 'certificate' in body; } @@ -159,7 +159,7 @@ export interface SubmitResponse { ok: boolean; status: number; statusText: string; - body: v2ResponseBody | v3ResponseBody | null; + body: v2ResponseBody | v4ResponseBody | null; headers: HttpHeaderField[]; }; requestDetails?: CallRequest; diff --git a/packages/core/src/agent/agent/http/http.test.ts b/packages/core/src/agent/agent/http/http.test.ts index f6f59dfb8..afedd5320 100644 --- a/packages/core/src/agent/agent/http/http.test.ts +++ b/packages/core/src/agent/agent/http/http.test.ts @@ -113,7 +113,7 @@ test('call', async () => { const callUrl = calls[0][0]; const callParams = calls[0][1]; - expect(callUrl.toString()).toBe(`http://127.0.0.1/api/v3/canister/${canisterId.toText()}/call`); + expect(callUrl.toString()).toBe(`http://127.0.0.1/api/v4/canister/${canisterId.toText()}/call`); expect(callParams.method).toEqual('POST'); // Get the body from the request and ensure nonce matches @@ -355,7 +355,7 @@ test('use anonymous principal if unspecified', async () => { expect(requestId).toBeTruthy(); expect(calls[0][0].toString()).toBe( - `http://127.0.0.1/api/v3/canister/${canisterId.toText()}/call`, + `http://127.0.0.1/api/v4/canister/${canisterId.toText()}/call`, ); const call2 = calls[0][1]; expect(call2.method).toEqual('POST'); diff --git a/packages/core/src/agent/agent/http/index.ts b/packages/core/src/agent/agent/http/index.ts index a79532d2d..d05aed361 100644 --- a/packages/core/src/agent/agent/http/index.ts +++ b/packages/core/src/agent/agent/http/index.ts @@ -20,7 +20,7 @@ import { UnexpectedErrorCode, UnknownError, HttpErrorCode, - HttpV3ApiNotSupportedErrorCode, + HttpV4ApiNotSupportedErrorCode, TransportError, HttpFetchErrorCode, AgentError, @@ -570,9 +570,9 @@ export class HttpAgent implements Agent { const backoff = this.#backoffStrategy(); const requestId = requestIdOf(submit); try { - // Attempt v3 sync call + // Attempt v4 sync call const requestSync = () => { - const url = new URL(`/api/v3/canister/${ecid.toText()}/call`, this.host); + const url = new URL(`/api/v4/canister/${ecid.toText()}/call`, this.host); this.log.print(`fetching "${url.pathname}" with request:`, transformedRequest); return this.#fetch(url, { ...this.#callOptions, @@ -613,14 +613,14 @@ export class HttpAgent implements Agent { } catch (error) { let callError: AgentError; if (error instanceof AgentError) { - // If the error is due to the v3 api not being supported, fall back to v2 - if (error.hasCode(HttpV3ApiNotSupportedErrorCode)) { - this.log.warn('v3 api not supported. Fall back to v2'); + // If the error is due to the v4 api not being supported, fall back to v2 + if (error.hasCode(HttpV4ApiNotSupportedErrorCode)) { + this.log.warn('v4 api not supported. Fall back to v2'); return this.call( canisterId, { ...options, - // disable v3 api + // disable v4 api callSync: false, }, identity, @@ -839,8 +839,8 @@ export class HttpAgent implements Agent { const responseText = await response.text(); - if (response.status === HTTP_STATUS_NOT_FOUND && response.url.includes('api/v3')) { - throw ProtocolError.fromCode(new HttpV3ApiNotSupportedErrorCode()); + if (response.status === HTTP_STATUS_NOT_FOUND && response.url.includes('api/v4')) { + throw ProtocolError.fromCode(new HttpV4ApiNotSupportedErrorCode()); } // The error message comes from https://github.com/dfinity/ic/blob/23d5990bfc5277c32e54f0087b5a38fa412171e1/rs/validator/src/ingress_validation.rs#L233 diff --git a/packages/core/src/agent/errors.ts b/packages/core/src/agent/errors.ts index a66b7a235..2a54b459b 100644 --- a/packages/core/src/agent/errors.ts +++ b/packages/core/src/agent/errors.ts @@ -742,16 +742,16 @@ export class HttpErrorCode extends ErrorCode { } } -export class HttpV3ApiNotSupportedErrorCode extends ErrorCode { - public name = 'HttpV3ApiNotSupportedErrorCode'; +export class HttpV4ApiNotSupportedErrorCode extends ErrorCode { + public name = 'HttpV4ApiNotSupportedErrorCode'; constructor() { super(); - Object.setPrototypeOf(this, HttpV3ApiNotSupportedErrorCode.prototype); + Object.setPrototypeOf(this, HttpV4ApiNotSupportedErrorCode.prototype); } public toErrorMessage(): string { - return 'HTTP request failed: v3 API is not supported'; + return 'HTTP request failed: v4 API is not supported'; } } From b1b4c1f53fa0a48aa638c2fd5c8e07c36d4c3faa Mon Sep 17 00:00:00 2001 From: ilbertt Date: Wed, 26 Nov 2025 15:55:48 +0100 Subject: [PATCH 2/4] chore: update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 640f66798..d3c556caa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## [Unreleased] - feat(core)!: removes `@dfinity/{agent,candid,identity,identity-secp256k1,principal}` peer dependencies and moves their source code to the `@icp-sdk/core` package +- feat(agent)!: use `/api/v4` for call requests - feat(assets)!: replaces `@dfinity/{agent,candid,principal}` deps with `@icp-sdk/core` - feat(assets)!: drops support for cjs for the `@dfinity/assets` package - feat(auth-client)!: `@dfinity/auth-client` has been deprecated. Migrate to [`@icp-sdk/auth`](https://js.icp.build/auth/latest/upgrading/v4) From 0395a9c2a6bdaf1bc80506f2d6b71e67fbfe3276 Mon Sep 17 00:00:00 2001 From: ilbertt Date: Wed, 26 Nov 2025 15:59:09 +0100 Subject: [PATCH 3/4] chore: update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3c556caa..f9401f96c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ - feat(core)!: removes `@dfinity/{agent,candid,identity,identity-secp256k1,principal}` peer dependencies and moves their source code to the `@icp-sdk/core` package - feat(agent)!: use `/api/v4` for call requests + - Renames `v3ResponseBody` to `v4ResponseBody` + - Renames `isV3ResponseBody` to `isV4ResponseBody` + - Renames `HttpV3ApiNotSupportedErrorCode` to `HttpV4ApiNotSupportedErrorCode` - feat(assets)!: replaces `@dfinity/{agent,candid,principal}` deps with `@icp-sdk/core` - feat(assets)!: drops support for cjs for the `@dfinity/assets` package - feat(auth-client)!: `@dfinity/auth-client` has been deprecated. Migrate to [`@icp-sdk/auth`](https://js.icp.build/auth/latest/upgrading/v4) From 50e21bc06c425e3a530ba1d802c7ff5363a48d65 Mon Sep 17 00:00:00 2001 From: ilbertt Date: Wed, 26 Nov 2025 16:05:08 +0100 Subject: [PATCH 4/4] test: update snapshots --- .../basic/__snapshots__/syncTime.test.ts.snap | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/e2e/node/basic/__snapshots__/syncTime.test.ts.snap b/e2e/node/basic/__snapshots__/syncTime.test.ts.snap index 3f71dcc20..f23347570 100644 --- a/e2e/node/basic/__snapshots__/syncTime.test.ts.snap +++ b/e2e/node/basic/__snapshots__/syncTime.test.ts.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`syncTime > on async creation > should sync time on when enabled > V3 read state body one 1`] = ` +exports[`syncTime > on async creation > should sync time on when enabled > V4 read state body one 1`] = ` { "content": { "ingress_expiry": 1746103140000000000n, @@ -23,7 +23,7 @@ exports[`syncTime > on async creation > should sync time on when enabled > V3 re } `; -exports[`syncTime > on async creation > should sync time on when enabled > V3 read state body three 1`] = ` +exports[`syncTime > on async creation > should sync time on when enabled > V4 read state body three 1`] = ` { "content": { "ingress_expiry": 1746103140000000000n, @@ -46,7 +46,7 @@ exports[`syncTime > on async creation > should sync time on when enabled > V3 re } `; -exports[`syncTime > on async creation > should sync time on when enabled > V3 read state body two 1`] = ` +exports[`syncTime > on async creation > should sync time on when enabled > V4 read state body two 1`] = ` { "content": { "ingress_expiry": 1746103140000000000n, @@ -69,7 +69,7 @@ exports[`syncTime > on async creation > should sync time on when enabled > V3 re } `; -exports[`syncTime > on error > should not sync time by default > V3 call body 1`] = ` +exports[`syncTime > on error > should not sync time by default > V4 call body 1`] = ` { "content": { "arg": { @@ -139,7 +139,7 @@ exports[`syncTime > on error > should sync time when the local time does not mat } `; -exports[`syncTime > on error > should sync time when the local time does not match the subnet time > V3 call body 1`] = ` +exports[`syncTime > on error > should sync time when the local time does not match the subnet time > V4 call body 1`] = ` { "content": { "arg": { @@ -186,7 +186,7 @@ exports[`syncTime > on error > should sync time when the local time does not mat } `; -exports[`syncTime > on error > should sync time when the local time does not match the subnet time > V3 read state body three 1`] = ` +exports[`syncTime > on error > should sync time when the local time does not match the subnet time > V4 read state body three 1`] = ` { "content": { "ingress_expiry": 1746103140000000000n, @@ -209,7 +209,7 @@ exports[`syncTime > on error > should sync time when the local time does not mat } `; -exports[`syncTime > on error > should sync time when the local time does not match the subnet time > V3 read state body two 1`] = ` +exports[`syncTime > on error > should sync time when the local time does not match the subnet time > V4 read state body two 1`] = ` { "content": { "ingress_expiry": 1746103140000000000n, @@ -232,7 +232,7 @@ exports[`syncTime > on error > should sync time when the local time does not mat } `; -exports[`syncTime > on first call > should not sync time by default > V3 call body 1`] = ` +exports[`syncTime > on first call > should not sync time by default > V4 call body 1`] = ` { "content": { "arg": { @@ -279,7 +279,7 @@ exports[`syncTime > on first call > should not sync time by default > V3 call bo } `; -exports[`syncTime > on first call > should not sync time when explicitly disabled > V3 call body 1`] = ` +exports[`syncTime > on first call > should not sync time when explicitly disabled > V4 call body 1`] = ` { "content": { "arg": { @@ -326,7 +326,7 @@ exports[`syncTime > on first call > should not sync time when explicitly disable } `; -exports[`syncTime > on first call > should sync time when enabled > V3 call body 1`] = ` +exports[`syncTime > on first call > should sync time when enabled > V4 call body 1`] = ` { "content": { "arg": { @@ -373,7 +373,7 @@ exports[`syncTime > on first call > should sync time when enabled > V3 call body } `; -exports[`syncTime > on first call > should sync time when enabled > V3 read state body one 1`] = ` +exports[`syncTime > on first call > should sync time when enabled > V4 read state body one 1`] = ` { "content": { "ingress_expiry": 1746103140000000000n, @@ -396,7 +396,7 @@ exports[`syncTime > on first call > should sync time when enabled > V3 read stat } `; -exports[`syncTime > on first call > should sync time when enabled > V3 read state body three 1`] = ` +exports[`syncTime > on first call > should sync time when enabled > V4 read state body three 1`] = ` { "content": { "ingress_expiry": 1746103140000000000n, @@ -419,7 +419,7 @@ exports[`syncTime > on first call > should sync time when enabled > V3 read stat } `; -exports[`syncTime > on first call > should sync time when enabled > V3 read state body two 1`] = ` +exports[`syncTime > on first call > should sync time when enabled > V4 read state body two 1`] = ` { "content": { "ingress_expiry": 1746103140000000000n,