From 9d3ddc1e3d15d5b05e2edde18c7d5596bd606dd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Pi=C4=85tkowski?= Date: Tue, 21 Apr 2026 07:26:44 +0200 Subject: [PATCH 01/12] refactor(wallet-sdk): remove unused method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mateusz Piątkowski --- .../src/wallet/namespace/transactions/signed.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/sdk/wallet-sdk/src/wallet/namespace/transactions/signed.ts b/sdk/wallet-sdk/src/wallet/namespace/transactions/signed.ts index 5b22a53aa..5c09a3407 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/transactions/signed.ts +++ b/sdk/wallet-sdk/src/wallet/namespace/transactions/signed.ts @@ -5,10 +5,6 @@ import { Ops } from '@canton-network/core-provider-ledger' import { SDKContext } from '../../sdk.js' import { ExecuteOptions } from '../ledger/types.js' import { LedgerNamespace } from '../ledger/index.js' -import { - PrivateKey, - signTransactionHash, -} from '@canton-network/core-signing-lib' export class SignedTransaction { constructor( @@ -38,15 +34,4 @@ export class SignedTransaction { } return this._execute(this, options) } - - sign(privateKey: PrivateKey): SignedTransaction { - const signedPromise = this.signedPromise.then(({ response }) => ({ - response, - signature: signTransactionHash( - response.preparedTransactionHash, - privateKey - ), - })) - return new SignedTransaction(this.ctx, signedPromise, this._execute) // pass execute function for online signing workflows - } } From b201c8d2b0bda35daaa1aee397984ad9be33cb3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Pi=C4=85tkowski?= Date: Tue, 21 Apr 2026 13:11:40 +0200 Subject: [PATCH 02/12] feat(wallet-sdk): add cache to acs read method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mateusz Piątkowski --- .../src/wallet/namespace/ledger/namespace.ts | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/namespace.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/namespace.ts index f4b739728..5b96b30ed 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/ledger/namespace.ts +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/namespace.ts @@ -13,10 +13,25 @@ import { AcsOptions } from '@canton-network/core-acs-reader' import { PreparedTransactionNamespace } from './hash/index.js' import { InternalLedgerNamespace } from './internal/index.js' +type ACSCacheKey = { + [K in + | 'templateIds' + | 'interfaceIds' + | 'parties' + | 'offset']: unknown extends Pick + ? AcsOptions[K] | undefined + : AcsOptions[K] +} + export class LedgerNamespace { public readonly dar: DarNamespace public readonly internal: InternalLedgerNamespace public readonly preparedTransaction: PreparedTransactionNamespace + private readonly acsCache: Map< + ACSCacheKey, + Awaited> + > = new Map() + constructor(private readonly sdkContext: SDKContext) { this.dar = new DarNamespace(sdkContext) this.internal = new InternalLedgerNamespace(sdkContext) @@ -164,9 +179,23 @@ export class LedgerNamespace { `Querying acs with options:` ) - return await this.sdkContext.acsReader.getActiveContracts( - resolvedOptions - ) + const cacheKey: ACSCacheKey = { + templateIds: resolvedOptions.templateIds, + parties: resolvedOptions.parties, + offset: resolvedOptions.offset, + interfaceIds: resolvedOptions.interfaceIds, + } + + if (this.acsCache.has(cacheKey)) return this.acsCache.get(cacheKey)! + + const activeContracts = + await this.sdkContext.acsReader.getActiveContracts( + resolvedOptions + ) + + this.acsCache.set(cacheKey, activeContracts) + + return activeContracts }, /** * Queries the ACS and filters for JsActiveContracts From 4c6ab4eb86be8232b1a681146e5d64f7621c507c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Pi=C4=85tkowski?= Date: Thu, 23 Apr 2026 19:23:27 +0200 Subject: [PATCH 03/12] feat(wallet-sdk): :construction: statrt working on acs cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mateusz Piątkowski --- .../src/wallet/namespace/ledger/acs/cache.ts | 127 ++++++++++++++++++ .../src/wallet/namespace/ledger/acs/index.ts | 6 + .../wallet/namespace/ledger/acs/namespace.ts | 14 ++ .../src/wallet/namespace/ledger/acs/reader.ts | 87 ++++++++++++ .../src/wallet/namespace/ledger/acs/types.ts | 60 +++++++++ .../wallet/namespace/ledger/internal/index.ts | 98 +------------- .../namespace/ledger/internal/namespace.ts | 109 +++++++++++++++ .../wallet/namespace/ledger/internal/types.ts | 44 ++++++ .../src/wallet/namespace/ledger/namespace.ts | 112 +-------------- 9 files changed, 454 insertions(+), 203 deletions(-) create mode 100644 sdk/wallet-sdk/src/wallet/namespace/ledger/acs/cache.ts create mode 100644 sdk/wallet-sdk/src/wallet/namespace/ledger/acs/index.ts create mode 100644 sdk/wallet-sdk/src/wallet/namespace/ledger/acs/namespace.ts create mode 100644 sdk/wallet-sdk/src/wallet/namespace/ledger/acs/reader.ts create mode 100644 sdk/wallet-sdk/src/wallet/namespace/ledger/acs/types.ts create mode 100644 sdk/wallet-sdk/src/wallet/namespace/ledger/internal/namespace.ts create mode 100644 sdk/wallet-sdk/src/wallet/namespace/ledger/internal/types.ts diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/cache.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/cache.ts new file mode 100644 index 000000000..2d3495b00 --- /dev/null +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/cache.ts @@ -0,0 +1,127 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { ContractId } from '@canton-network/core-token-standard' +import { SDKContext } from '../../../sdk.js' +import { LedgerNamespace } from '../namespace.js' +import { ACSReader } from './reader.js' +import { ACS_UPDATE_CONFIG, ACSKey, ACSState } from './types.js' +import { JsGetActiveContractsResponse } from '@canton-network/core-ledger-client-types' + +export class ACSCacheNamespace { + private readonly state: ACSState = { + initial: { + offset: 0, + acs: [], + }, + updates: { + offset: 0, + allACs: [], + }, + archivedACs: new Set(), + } + private readonly acsReader: ACSReader + private readonly ledger: LedgerNamespace + constructor(private readonly sdkContext: SDKContext) { + this.acsReader = new ACSReader(sdkContext) + this.ledger = new LedgerNamespace(sdkContext) + } + + private get initial() { + return this.state.initial + } + + private get updates() { + return this.state.updates + } + + private async initState(args: { offset: number; key: ACSKey }) { + const { offset, key } = args + const initialAcs = await this.acsReader.readRaw({ + offset, + parties: key.parties ?? [], + interfaceIds: key.interfaceIds ?? [], + templateIds: key.templateIds ?? [], + }) + this.state.initial = { + offset, + acs: initialAcs, + } + this.state.updates = { + offset, + allACs: [], + } + this.state.archivedACs = new Set() + } + + public async update(args: { offset: number; key: ACSKey }) { + const { offset } = args + + if (!this.initial.acs.length || this.initial.offset > offset) { + await this.initState(args) + } + + // get updates, then events, then set the new acsset + + if ( + this.updates.allACs.length >= ACS_UPDATE_CONFIG.maxEventsBeforePrune + ) { + this.rebuildCache() + } + } + + public calculateAt(offset: number) { + if (!this.initial.acs) + this.sdkContext.error.throw({ + message: 'No ACS initialized. Call `.update()` first', + type: 'Unexpected', + }) + if (this.initial.offset > offset) + this.sdkContext.error.throw({ + message: 'Provided offset cannot be smaller than ACS offset', + type: 'Unexpected', + }) + + const newContracts: JsGetActiveContractsResponse[] = [] + const newArchivedContracts: Set> = new Set() + + this.updates.allACs + .filter((ac) => ac.offset <= offset) + .map((ac) => { + if (ac.archived) { + newArchivedContracts.add( + ac.event.contractId as ContractId + ) + return + } + newContracts.push({ + workflowId: ac.workflowId ?? '', + contractEntry: { + JsActiveContract: { + createdEvent: ac.event, + synchronizerId: ac.synchronizerId ?? '', + reassignmentCounter: 0, + }, + }, + }) + }) + + const allContracts = this.initial.acs.concat(newContracts) + this.state.archivedACs = + this.state.archivedACs.union(newArchivedContracts) + + return allContracts.filter(({ contractEntry }) => { + if (!contractEntry) return false + const id = + ('JsActiveContract' in contractEntry && + contractEntry.JsActiveContract.createdEvent.contractId) ?? + '' + + return !this.state.archivedACs.has(id) + }) + } + + private rebuildCache() { + // TODO: fill this + } +} diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/index.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/index.ts new file mode 100644 index 000000000..3f20a1755 --- /dev/null +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/index.ts @@ -0,0 +1,6 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +export * from './namespace.js' +export type * from './cache.js' +export * from './types.js' diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/namespace.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/namespace.ts new file mode 100644 index 000000000..ad3df181a --- /dev/null +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/namespace.ts @@ -0,0 +1,14 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { SDKContext } from '../../../sdk.js' +import { ACSCacheNamespace } from './cache.js' +import { ACSReader } from './reader.js' + +export class ACSNamespace extends ACSReader { + private readonly cache: ACSCacheNamespace + constructor(protected readonly sdkContext: SDKContext) { + super(sdkContext) + this.cache = new ACSCacheNamespace(sdkContext) + } +} diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/reader.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/reader.ts new file mode 100644 index 000000000..47b36e292 --- /dev/null +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/reader.ts @@ -0,0 +1,87 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { AcsOptions } from '@canton-network/core-acs-reader' +import { v3_4 } from '@canton-network/core-ledger-client-types' +import { Ops } from '@canton-network/core-provider-ledger' +import { LedgerTypes, SDKContext } from '../../../sdk.js' +import { AcsRequestOptions } from '../types.js' + +export class ACSReader { + constructor(protected readonly sdkContext: SDKContext) {} + + /** + * + * @param options AcsOptions for querying the Active Contract Set (ACS). + * offset: The ledger offset at which to query the ACS. If not provided, will fetch the ledgerEnd. + * templateIds: An optional array of template IDs to filter the ACS by. If not provided, no filtering by template ID will be applied. + * parties: An optional array of party IDs to filter the ACS by. If not provided, no filtering by party will be applied. + * filterByParty: A boolean flag indicating whether to apply party-based filtering. If true, the query will filter contracts based on the specified parties. If false or not provided, party-based filtering will not be applied. + * interfaceIds: An optional array of interface IDs to filter the ACS by. If not provided, no filtering by interface ID will be applied. + * limit: An optional number specifying the maximum number of active contracts to return in a single query. If not provided, the default limit will be determined by the ledger API. + * continueUntilCompletion: A boolean flag indicating whether to continue polling the ledger until the query is complete. If true, the method will repeatedly query the ledger until all matching active contracts have been retrieved. If false or not provided, the method will return after a single query, which may return a + * @returns Active contracts matching the provided query options. + */ + async readRaw( + options: AcsRequestOptions + ): Promise> { + const resolvedOptions = await this.resolveAcsOptions(options) + + this.sdkContext.logger.debug( + resolvedOptions, + `Querying acs with options:` + ) + + const activeContracts = + await this.sdkContext.acsReader.getActiveContracts(resolvedOptions) + + return activeContracts + } + + /** + * Queries the ACS and filters for JsActiveContracts + * @param options AcsOptions for querying the Active Contract Set (ACS). + * returns the createdEvent and synchronizerId + */ + async read(options: AcsRequestOptions) { + return (await this.readRaw(options)) + + .filter( + (acs) => + acs.contractEntry != null && + 'JsActiveContract' in acs.contractEntry + ) + .map((acs) => { + const jsActiveContract = ( + acs.contractEntry as { + JsActiveContract: v3_4.components['schemas']['JsActiveContract'] + } + ).JsActiveContract + + return { + ...jsActiveContract.createdEvent, + synchronizerId: jsActiveContract.synchronizerId, + } + }) + } + + private async resolveAcsOptions( + options: AcsRequestOptions + ): Promise { + const offset = + options.offset ?? + ( + await this.sdkContext.ledgerProvider.request( + { + method: 'ledgerApi', + params: { + resource: '/v2/state/ledger-end', + requestMethod: 'get', + }, + } + ) + ).offset! + + return { ...options, offset } + } +} diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/types.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/types.ts new file mode 100644 index 000000000..0352c32b5 --- /dev/null +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/types.ts @@ -0,0 +1,60 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { AcsOptions } from '@canton-network/core-acs-reader' +import { + ArchivedEvent, + CreatedEvent, + JsGetActiveContractsResponse, +} from '@canton-network/core-ledger-client-types' +import { ContractId } from '@canton-network/core-token-standard' + +export type ACSKey = { + parties: AcsOptions['parties'] +} & ( + | { + interfaceIds: AcsOptions['interfaceIds'] + templateIds?: never + } + | { + interfaceIds?: never + templateIds: AcsOptions['templateIds'] + } +) + +export type ACEvent = { + offset: number + event: CreatedEvent | ArchivedEvent + workflowId: string | null + synchronizerId: string | null + archived?: boolean +} + +export type ACSInitialState = { + // offset at which initialAcs is valid + offset: number + // the initial ACS at acsOffset + acs: Array +} +export type ACSUpdatesState = { + // last seen update offset + offset: number + // all updates since acsOffset - will be used to calculate ACS at any offset >= acsOffset + // may be compacted (see ACSUpdateConfig.maxEventsBeforePrune and ACSUpdateConfig.safeOffsetDeltaForPrune) + allACs: Array +} + +export type ACSState = { + initial: ACSInitialState + updates: ACSUpdatesState + archivedACs: Set> +} + +export const ACS_UPDATE_CONFIG = { + // How many events do we accumulate before we prune (compact) the ACS history - set to 0 to enable to compact all events, which is more efficient as long as application always ask for increasing (or equal) offsets + maxEventsBeforePrune: 150, + // When we compact the ACS history, we keep all events within this offset delta of the last seen update offset - set 0 to allow to compact everything + safeOffsetDeltaForPrune: 200, + // How many updates do we fetch at once when fetching updates - if there are more updates, we will fetch again until we have caught up (returned data is always complete to the requested endInclusive offset - even if that means multiple fetches) + maxUpdatesToFetch: 100, +} as const diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/internal/index.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/internal/index.ts index 851febd11..8dace158d 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/ledger/internal/index.ts +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/internal/index.ts @@ -1,99 +1,5 @@ // Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { SDKContext } from '../../../sdk.js' -import { v4 } from 'uuid' -import { Ops } from '@canton-network/core-provider-ledger' - -type AllowedOperation = - | Ops.PostV2CommandsSubmitAndWait - | Ops.PostV2InteractiveSubmissionPrepare - -type OperationBodyRequest = - Operation['ledgerApi']['params']['body'] - -type RequiredParams = 'commands' | 'actAs' -type UnusedParams = 'userId' - -type InternalOperationParams = Required< - Pick, RequiredParams> -> & - Partial< - Omit, UnusedParams | RequiredParams> - > - -export class InternalLedgerNamespace { - constructor(private readonly ctx: SDKContext) {} - - async submit( - args: InternalOperationParams - ) { - const { - commands, - synchronizerId = this.ctx.defaultSynchronizerId, - disclosedContracts = [], - readAs = [], - actAs, - commandId = v4(), - packageIdSelectionPreference = [], - } = args - const request = { - commands, - commandId, - userId: this.ctx.userId, - actAs, - readAs, - disclosedContracts, - synchronizerId, - packageIdSelectionPreference, - } - - return await this.ctx.ledgerProvider.request( - { - method: 'ledgerApi', - params: { - resource: '/v2/commands/submit-and-wait', - requestMethod: 'post', - body: request, - }, - } - ) - } - - async prepare( - args: InternalOperationParams - ) { - const { - commands, - synchronizerId = this.ctx.defaultSynchronizerId, - disclosedContracts = [], - readAs = [], - actAs, - commandId = v4(), - packageIdSelectionPreference = [], - verboseHashing = false, - } = args - const request = { - commands, - commandId, - userId: this.ctx.userId, - actAs, - readAs, - disclosedContracts, - synchronizerId, - packageIdSelectionPreference, - verboseHashing, - } - - return await this.ctx.ledgerProvider.request( - { - method: 'ledgerApi', - params: { - resource: '/v2/interactive-submission/prepare', - requestMethod: 'post', - body: request, - }, - } - ) - } -} +export * from './namespace.js' +export * from './types.js' diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/internal/namespace.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/internal/namespace.ts new file mode 100644 index 000000000..84230098d --- /dev/null +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/internal/namespace.ts @@ -0,0 +1,109 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { SDKContext } from '../../../sdk.js' +import { v4 } from 'uuid' +import { Ops } from '@canton-network/core-provider-ledger' +import { InternalOperationParams } from './types.js' +import { ACS_UPDATE_CONFIG } from '../acs/index.js' + +export class InternalLedgerNamespace { + constructor(private readonly ctx: SDKContext) {} + + public async submit( + args: InternalOperationParams + ) { + const { + commands, + synchronizerId = this.ctx.defaultSynchronizerId, + disclosedContracts = [], + readAs = [], + actAs, + commandId = v4(), + packageIdSelectionPreference = [], + } = args + const request = { + commands, + commandId, + userId: this.ctx.userId, + actAs, + readAs, + disclosedContracts, + synchronizerId, + packageIdSelectionPreference, + } + + return await this.ctx.ledgerProvider.request( + { + method: 'ledgerApi', + params: { + resource: '/v2/commands/submit-and-wait', + requestMethod: 'post', + body: request, + }, + } + ) + } + + public async prepare( + args: InternalOperationParams + ) { + const { + commands, + synchronizerId = this.ctx.defaultSynchronizerId, + disclosedContracts = [], + readAs = [], + actAs, + commandId = v4(), + packageIdSelectionPreference = [], + verboseHashing = false, + } = args + const request = { + commands, + commandId, + userId: this.ctx.userId, + actAs, + readAs, + disclosedContracts, + synchronizerId, + packageIdSelectionPreference, + verboseHashing, + } + + return await this.ctx.ledgerProvider.request( + { + method: 'ledgerApi', + params: { + resource: '/v2/interactive-submission/prepare', + requestMethod: 'post', + body: request, + }, + } + ) + } + + public async flats(args: InternalOperationParams) { + const { beginExclusive, endInclusive, filter, updateFormat } = args + + const request = { + beginExclusive, + endInclusive, + filter, + updateFormat, + verbose: false, + } + + return await this.ctx.ledgerProvider.request({ + method: 'ledgerApi', + params: { + resource: '/v2/updates/flats', + requestMethod: 'post', + body: request, + query: { + limit: ACS_UPDATE_CONFIG.maxUpdatesToFetch, + stream_idle_timeout_ms: 1000, + }, + }, + }) + } +} diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/internal/types.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/internal/types.ts new file mode 100644 index 000000000..36a0895f6 --- /dev/null +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/internal/types.ts @@ -0,0 +1,44 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { Ops } from '@canton-network/core-provider-ledger' + +type AllowedOperation = + | Ops.PostV2CommandsSubmitAndWait + | Ops.PostV2InteractiveSubmissionPrepare + | Ops.PostV2UpdatesFlats + +type OperationBodyRequest = + Operation['ledgerApi']['params']['body'] + +type RequiredParamsFor = Extract< + keyof OperationBodyRequest, + Operation extends + | Ops.PostV2CommandsSubmitAndWait + | Ops.PostV2InteractiveSubmissionPrepare + ? 'commands' | 'actAs' + : Operation extends Ops.PostV2UpdatesFlats + ? 'beginExclusive' | 'endInclusive' | 'filter' | 'updateFormat' + : never +> +type UnusedParams = Extract< + keyof OperationBodyRequest, + Operation extends + | Ops.PostV2CommandsSubmitAndWait + | Ops.PostV2InteractiveSubmissionPrepare + ? 'userId' + : Operation extends Ops.PostV2UpdatesFlats + ? 'verbose' + : never +> + +export type InternalOperationParams = + Required< + Pick, RequiredParamsFor> + > & + Partial< + Omit< + OperationBodyRequest, + UnusedParams & RequiredParamsFor + > + > diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/namespace.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/namespace.ts index 5b96b30ed..0436fd164 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/ledger/namespace.ts +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/namespace.ts @@ -1,41 +1,28 @@ // Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { SDKContext, LedgerTypes } from '../../sdk.js' +import { SDKContext } from '../../sdk.js' import { v4 } from 'uuid' -import { PrepareOptions, ExecuteOptions, AcsRequestOptions } from './types.js' +import { PrepareOptions, ExecuteOptions } from './types.js' import { PreparedTransaction } from '../transactions/prepared.js' import { SignedTransaction } from '../transactions/signed.js' import { Ops } from '@canton-network/core-provider-ledger' -import { v3_4 } from '@canton-network/core-ledger-client-types' import { DarNamespace } from './dar/client.js' -import { AcsOptions } from '@canton-network/core-acs-reader' import { PreparedTransactionNamespace } from './hash/index.js' import { InternalLedgerNamespace } from './internal/index.js' - -type ACSCacheKey = { - [K in - | 'templateIds' - | 'interfaceIds' - | 'parties' - | 'offset']: unknown extends Pick - ? AcsOptions[K] | undefined - : AcsOptions[K] -} +import { ACSNamespace } from './acs/namespace.js' export class LedgerNamespace { public readonly dar: DarNamespace public readonly internal: InternalLedgerNamespace public readonly preparedTransaction: PreparedTransactionNamespace - private readonly acsCache: Map< - ACSCacheKey, - Awaited> - > = new Map() + public readonly acs: ACSNamespace constructor(private readonly sdkContext: SDKContext) { this.dar = new DarNamespace(sdkContext) this.internal = new InternalLedgerNamespace(sdkContext) this.preparedTransaction = new PreparedTransactionNamespace(sdkContext) + this.acs = new ACSNamespace(sdkContext) } public async ledgerEnd() { @@ -156,95 +143,6 @@ export class LedgerNamespace { ) } - acs = { - /** - * - * @param options AcsOptions for querying the Active Contract Set (ACS). - * offset: The ledger offset at which to query the ACS. If not provided, will fetch the ledgerEnd. - * templateIds: An optional array of template IDs to filter the ACS by. If not provided, no filtering by template ID will be applied. - * parties: An optional array of party IDs to filter the ACS by. If not provided, no filtering by party will be applied. - * filterByParty: A boolean flag indicating whether to apply party-based filtering. If true, the query will filter contracts based on the specified parties. If false or not provided, party-based filtering will not be applied. - * interfaceIds: An optional array of interface IDs to filter the ACS by. If not provided, no filtering by interface ID will be applied. - * limit: An optional number specifying the maximum number of active contracts to return in a single query. If not provided, the default limit will be determined by the ledger API. - * continueUntilCompletion: A boolean flag indicating whether to continue polling the ledger until the query is complete. If true, the method will repeatedly query the ledger until all matching active contracts have been retrieved. If false or not provided, the method will return after a single query, which may return a - * @returns Active contracts matching the provided query options. - */ - readRaw: async ( - options: AcsRequestOptions - ): Promise> => { - const resolvedOptions = await this.resolveAcsOptions(options) - - this.sdkContext.logger.debug( - resolvedOptions, - `Querying acs with options:` - ) - - const cacheKey: ACSCacheKey = { - templateIds: resolvedOptions.templateIds, - parties: resolvedOptions.parties, - offset: resolvedOptions.offset, - interfaceIds: resolvedOptions.interfaceIds, - } - - if (this.acsCache.has(cacheKey)) return this.acsCache.get(cacheKey)! - - const activeContracts = - await this.sdkContext.acsReader.getActiveContracts( - resolvedOptions - ) - - this.acsCache.set(cacheKey, activeContracts) - - return activeContracts - }, - /** - * Queries the ACS and filters for JsActiveContracts - * @param options AcsOptions for querying the Active Contract Set (ACS). - * returns the createdEvent and synchronizerId - */ - read: async (options: AcsRequestOptions) => { - return (await this.acs.readRaw(options)) - - .filter( - (acs) => - acs.contractEntry != null && - 'JsActiveContract' in acs.contractEntry - ) - .map((acs) => { - const jsActiveContract = ( - acs.contractEntry as { - JsActiveContract: v3_4.components['schemas']['JsActiveContract'] - } - ).JsActiveContract - - return { - ...jsActiveContract.createdEvent, - synchronizerId: jsActiveContract.synchronizerId, - } - }) - }, - } - - private async resolveAcsOptions( - options: AcsRequestOptions - ): Promise { - const offset = - options.offset ?? - ( - await this.sdkContext.ledgerProvider.request( - { - method: 'ledgerApi', - params: { - resource: '/v2/state/ledger-end', - requestMethod: 'get', - }, - } - ) - ).offset! - - return { ...options, offset } - } - /** * For offline signing workflows, construct a SignedTransaction from an externally produced signature. * @param response The prepare response from a previous prepare call From 490992daeabac51fee65b043b6bfd983363c1604 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Pi=C4=85tkowski?= Date: Fri, 24 Apr 2026 14:53:32 +0200 Subject: [PATCH 04/12] feat(wallet-sdk): :construction: add private methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot Signed-off-by: Mateusz Piątkowski --- sdk/wallet-sdk/src/wallet/logger/types.ts | 4 +- .../src/wallet/namespace/ledger/acs/cache.ts | 173 +++++++++++++++--- .../src/wallet/namespace/ledger/acs/types.ts | 25 +-- .../namespace/ledger/internal/namespace.ts | 2 +- .../wallet/namespace/ledger/internal/types.ts | 2 +- 5 files changed, 154 insertions(+), 52 deletions(-) diff --git a/sdk/wallet-sdk/src/wallet/logger/types.ts b/sdk/wallet-sdk/src/wallet/logger/types.ts index c9a3b6821..47dbb3989 100644 --- a/sdk/wallet-sdk/src/wallet/logger/types.ts +++ b/sdk/wallet-sdk/src/wallet/logger/types.ts @@ -13,10 +13,9 @@ import { SDKLogger } from './logger.js' // eslint-disable-line @typescript-eslin * from which the log originates. For example, in ConsoleLogAdapter and PinoLogAdapter, * the namespace is prepended to the log message, helping to distinguish logs from different * parts of the application and making log filtering and analysis easier. - * It is recommended to use {@link SDKLogger.child} to set the namespace for each logger instance. + * It is recommended to use {@link SDKLogger['child']} to set the namespace for each logger instance. * @property timestamp Optional timestamp for the log entry. This is provided by default by the logger implementation. * @property response Optional response data to include in the log. - * @property arguments Optional arguments or parameters related to the log event. * @property traceId Optional trace identifier for correlating logs across systems. * @property partyId Optional party identifier for domain-specific context. * @property [data: string] Any additional custom metadata fields. @@ -25,7 +24,6 @@ export type LogContext = Partial<{ namespace: string timestamp: string response: unknown - arguments: unknown traceId: string partyId: PartyId [data: string]: unknown diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/cache.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/cache.ts index 2d3495b00..d531e6e84 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/cache.ts +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/cache.ts @@ -2,11 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 import { ContractId } from '@canton-network/core-token-standard' -import { SDKContext } from '../../../sdk.js' +import { LedgerTypes, SDKContext } from '../../../sdk.js' import { LedgerNamespace } from '../namespace.js' import { ACSReader } from './reader.js' -import { ACS_UPDATE_CONFIG, ACSKey, ACSState } from './types.js' -import { JsGetActiveContractsResponse } from '@canton-network/core-ledger-client-types' +import { ACEvent, ACS_UPDATE_CONFIG, ACSKey, ACSState } from './types.js' +import { Ops } from '@canton-network/core-provider-ledger' +import { buildActiveContractFilter } from '@canton-network/core-acs-reader/dist/acs-reader.js' export class ACSCacheNamespace { private readonly state: ACSState = { @@ -16,12 +17,13 @@ export class ACSCacheNamespace { }, updates: { offset: 0, - allACs: [], + acs: [], }, archivedACs: new Set(), } private readonly acsReader: ACSReader private readonly ledger: LedgerNamespace + constructor(private readonly sdkContext: SDKContext) { this.acsReader = new ACSReader(sdkContext) this.ledger = new LedgerNamespace(sdkContext) @@ -49,23 +51,47 @@ export class ACSCacheNamespace { } this.state.updates = { offset, - allACs: [], + acs: [], } this.state.archivedACs = new Set() } public async update(args: { offset: number; key: ACSKey }) { - const { offset } = args + const { offset, key } = args if (!this.initial.acs.length || this.initial.offset > offset) { await this.initState(args) } - // get updates, then events, then set the new acsset + const updates = await this.updateContracts({ + beginExclusive: this.updates.offset, + endInclusive: offset, + eventFormat: buildActiveContractFilter({ + offset, + templateIds: key.templateIds ?? [], + interfaceIds: key.interfaceIds ?? [], + parties: key.parties ?? [], + }).eventFormat, + }) + + // in practise length should never be > maxUpdatesToFetch only equal (server should never return more than limit in query). This is just a safeguard. + if (updates.length >= ACS_UPDATE_CONFIG.maxUpdatesToFetch) + void this.update({ + offset, + key, + }) + + const { newEvents, newOffset } = this.extractEvents({ + offset: this.updates.offset, + updates, + }) - if ( - this.updates.allACs.length >= ACS_UPDATE_CONFIG.maxEventsBeforePrune - ) { + if (newOffset > this.updates.offset) { + this.updates.offset = newOffset + this.updates.acs = this.updates.acs.concat(newEvents) + } else this.updates.offset = offset + + if (this.updates.acs.length >= ACS_UPDATE_CONFIG.maxEventsBeforePrune) { this.rebuildCache() } } @@ -82,28 +108,28 @@ export class ACSCacheNamespace { type: 'Unexpected', }) - const newContracts: JsGetActiveContractsResponse[] = [] + const newContracts: LedgerTypes['JsGetActiveContractsResponse'][] = [] const newArchivedContracts: Set> = new Set() - this.updates.allACs + this.updates.acs .filter((ac) => ac.offset <= offset) .map((ac) => { - if (ac.archived) { + if (isCreatedEvent(ac)) { + newContracts.push({ + workflowId: ac.workflowId ?? '', + contractEntry: { + JsActiveContract: { + createdEvent: ac.event, + synchronizerId: ac.synchronizerId ?? '', + reassignmentCounter: 0, + }, + }, + }) + } else { newArchivedContracts.add( ac.event.contractId as ContractId ) - return } - newContracts.push({ - workflowId: ac.workflowId ?? '', - contractEntry: { - JsActiveContract: { - createdEvent: ac.event, - synchronizerId: ac.synchronizerId ?? '', - reassignmentCounter: 0, - }, - }, - }) }) const allContracts = this.initial.acs.concat(newContracts) @@ -112,10 +138,11 @@ export class ACSCacheNamespace { return allContracts.filter(({ contractEntry }) => { if (!contractEntry) return false - const id = - ('JsActiveContract' in contractEntry && - contractEntry.JsActiveContract.createdEvent.contractId) ?? - '' + const id = ( + 'JsActiveContract' in contractEntry + ? contractEntry.JsActiveContract.createdEvent.contractId + : '' + ) as ContractId return !this.state.archivedACs.has(id) }) @@ -124,4 +151,94 @@ export class ACSCacheNamespace { private rebuildCache() { // TODO: fill this } + + private async updateContracts(args: { + beginExclusive: number + endInclusive: number + eventFormat: LedgerTypes['EventFormat'] + }) { + const { beginExclusive, endInclusive, eventFormat } = args + const updateFormat: Ops.PostV2UpdatesFlats['ledgerApi']['params']['body']['updateFormat'] = + { + includeTransactions: { + eventFormat, + transactionShape: 'TRANSACTION_SHAPE_ACS_DELTA', + }, + } + return await this.ledger.internal.flats({ + beginExclusive, + endInclusive, + updateFormat, + }) + } + + private extractEvents(args: { + updates: Awaited> + offset: number + }) { + const { updates, offset } = args + const newEvents: Array = [] + let newOffset = offset + updates.forEach((update) => { + if (!update || !update.update) { + return + } + if ('Transaction' in update.update) { + const transaction = update.update.Transaction + const trOffset = transaction?.value?.offset + if (trOffset && trOffset > newOffset) { + const events: Array = + transaction?.value?.events ?? [] + events.forEach((event) => { + if (!event) { + return + } + if ( + 'CreatedEvent' in event || + 'ArchivedEvent' in event + ) { + const eventData = + 'CreatedEvent' in event + ? event.CreatedEvent + : event.ArchivedEvent + + const acUpdate: ACEvent = { + event: eventData, + offset: trOffset, + workflowId: + transaction?.value?.workflowId ?? null, + synchronizerId: + transaction?.value?.synchronizerId ?? null, + ...('ArchivedEvent' in event && { + archived: true, + }), + } + newEvents.push(acUpdate) + newOffset = trOffset + } + }) + } + } else if ('OffsetCheckpoint' in update.update) { + const checkpoint = update.update.OffsetCheckpoint + const offset = checkpoint?.value?.offset + if (offset) { + newOffset = offset + } + } else { + this.sdkContext.logger.warn( + { + value: JSON.stringify(update.update), + }, + 'ACS update got unknown update type' + ) + } + }) + return { newEvents, newOffset } + } +} + +function isCreatedEvent( + event: ACEvent +): event is ACEvent & { archived: true; event: LedgerTypes['CreatedEvent'] } { + return !event.archived } diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/types.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/types.ts index 0352c32b5..c43897269 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/types.ts +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/types.ts @@ -1,12 +1,8 @@ // Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +import { LedgerTypes } from '../../../sdk.js' import { AcsOptions } from '@canton-network/core-acs-reader' -import { - ArchivedEvent, - CreatedEvent, - JsGetActiveContractsResponse, -} from '@canton-network/core-ledger-client-types' import { ContractId } from '@canton-network/core-token-standard' export type ACSKey = { @@ -24,29 +20,20 @@ export type ACSKey = { export type ACEvent = { offset: number - event: CreatedEvent | ArchivedEvent + event: LedgerTypes['CreatedEvent'] | LedgerTypes['ArchivedEvent'] workflowId: string | null synchronizerId: string | null archived?: boolean } -export type ACSInitialState = { - // offset at which initialAcs is valid +export type ACSComponentState = { offset: number - // the initial ACS at acsOffset - acs: Array -} -export type ACSUpdatesState = { - // last seen update offset - offset: number - // all updates since acsOffset - will be used to calculate ACS at any offset >= acsOffset - // may be compacted (see ACSUpdateConfig.maxEventsBeforePrune and ACSUpdateConfig.safeOffsetDeltaForPrune) - allACs: Array + acs: Array } export type ACSState = { - initial: ACSInitialState - updates: ACSUpdatesState + initial: ACSComponentState + updates: ACSComponentState archivedACs: Set> } diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/internal/namespace.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/internal/namespace.ts index 84230098d..d4657e573 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/ledger/internal/namespace.ts +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/internal/namespace.ts @@ -88,9 +88,9 @@ export class InternalLedgerNamespace { const request = { beginExclusive, endInclusive, - filter, updateFormat, verbose: false, + ...(filter ? { filter } : {}), } return await this.ctx.ledgerProvider.request({ diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/internal/types.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/internal/types.ts index 36a0895f6..74f302a93 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/ledger/internal/types.ts +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/internal/types.ts @@ -18,7 +18,7 @@ type RequiredParamsFor = Extract< | Ops.PostV2InteractiveSubmissionPrepare ? 'commands' | 'actAs' : Operation extends Ops.PostV2UpdatesFlats - ? 'beginExclusive' | 'endInclusive' | 'filter' | 'updateFormat' + ? 'beginExclusive' | 'endInclusive' | 'updateFormat' : never > type UnusedParams = Extract< From 18f0320786565122c99a9ed6f253aa6826af7642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Pi=C4=85tkowski?= Date: Fri, 24 Apr 2026 14:57:28 +0200 Subject: [PATCH 05/12] feat(wallet-sdk): :construction: add prune method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mateusz Piątkowski --- .../src/wallet/namespace/ledger/acs/cache.ts | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/cache.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/cache.ts index d531e6e84..53a787ca4 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/cache.ts +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/cache.ts @@ -92,7 +92,7 @@ export class ACSCacheNamespace { } else this.updates.offset = offset if (this.updates.acs.length >= ACS_UPDATE_CONFIG.maxEventsBeforePrune) { - this.rebuildCache() + this.prune() } } @@ -148,8 +148,24 @@ export class ACSCacheNamespace { }) } - private rebuildCache() { - // TODO: fill this + private prune() { + const newOffset = Math.max( + this.initial.offset, + this.updates.offset - ACS_UPDATE_CONFIG.safeOffsetDeltaForPrune + ) + + if (newOffset > this.initial.offset) { + const responses = this.calculateAt(newOffset) + + this.state.initial = { + offset: newOffset, + acs: responses, + } + this.state.updates = { + offset: this.updates.offset, + acs: this.updates.acs.filter((ac) => ac.offset > newOffset), + } + } } private async updateContracts(args: { From fbe1ae2e33925c2552d7aabfeaa8ec497926c27e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Pi=C4=85tkowski?= Date: Fri, 24 Apr 2026 15:07:55 +0200 Subject: [PATCH 06/12] feat(wallet-sdk): :construction: add query helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mateusz Piątkowski --- .../src/wallet/namespace/ledger/acs/cache.ts | 54 +++++++++---------- .../wallet/namespace/ledger/acs/namespace.ts | 14 +++++ 2 files changed, 41 insertions(+), 27 deletions(-) diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/cache.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/cache.ts index 53a787ca4..f494a8de8 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/cache.ts +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/cache.ts @@ -29,33 +29,6 @@ export class ACSCacheNamespace { this.ledger = new LedgerNamespace(sdkContext) } - private get initial() { - return this.state.initial - } - - private get updates() { - return this.state.updates - } - - private async initState(args: { offset: number; key: ACSKey }) { - const { offset, key } = args - const initialAcs = await this.acsReader.readRaw({ - offset, - parties: key.parties ?? [], - interfaceIds: key.interfaceIds ?? [], - templateIds: key.templateIds ?? [], - }) - this.state.initial = { - offset, - acs: initialAcs, - } - this.state.updates = { - offset, - acs: [], - } - this.state.archivedACs = new Set() - } - public async update(args: { offset: number; key: ACSKey }) { const { offset, key } = args @@ -148,6 +121,33 @@ export class ACSCacheNamespace { }) } + private get initial() { + return this.state.initial + } + + private get updates() { + return this.state.updates + } + + private async initState(args: { offset: number; key: ACSKey }) { + const { offset, key } = args + const initialAcs = await this.acsReader.readRaw({ + offset, + parties: key.parties ?? [], + interfaceIds: key.interfaceIds ?? [], + templateIds: key.templateIds ?? [], + }) + this.state.initial = { + offset, + acs: initialAcs, + } + this.state.updates = { + offset, + acs: [], + } + this.state.archivedACs = new Set() + } + private prune() { const newOffset = Math.max( this.initial.offset, diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/namespace.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/namespace.ts index ad3df181a..9ceec9caf 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/namespace.ts +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/namespace.ts @@ -4,6 +4,7 @@ import { SDKContext } from '../../../sdk.js' import { ACSCacheNamespace } from './cache.js' import { ACSReader } from './reader.js' +import { ACSKey } from './types.js' export class ACSNamespace extends ACSReader { private readonly cache: ACSCacheNamespace @@ -11,4 +12,17 @@ export class ACSNamespace extends ACSReader { super(sdkContext) this.cache = new ACSCacheNamespace(sdkContext) } + + public async updateKey(args: { offset: number; key: ACSKey }) { + await this.cache.update(args) + return await this.cache.calculateAt(args.offset) + } + + public async query(offset: number, keys: ACSKey[]) { + return ( + await Promise.all( + keys.map(async (key) => await this.updateKey({ offset, key })) + ) + ).flat() + } } From 89c5fd3446c8cf049e74e96960c63f080ee6236f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Pi=C4=85tkowski?= Date: Mon, 27 Apr 2026 16:16:04 +0200 Subject: [PATCH 07/12] feat(wallet-sdk,core-acs-reader): continue working on acs cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot Signed-off-by: Mateusz Piątkowski --- core/acs-reader/src/index.ts | 6 +- .../examples/package.json | 3 +- .../examples/scripts/stress/01-acs-cache.ts | 97 +++++++++++++++++++ sdk/wallet-sdk/package.json | 1 + .../src/wallet/namespace/ledger/acs/cache.ts | 32 +++--- .../wallet/namespace/ledger/acs/namespace.ts | 78 +++++++++++++-- .../src/wallet/namespace/ledger/acs/reader.ts | 6 +- .../src/wallet/namespace/ledger/acs/types.ts | 19 ++-- .../namespace/ledger/internal/namespace.ts | 6 +- .../wallet/namespace/ledger/internal/types.ts | 6 +- .../src/wallet/namespace/ledger/types.ts | 5 +- yarn.lock | 1 + 12 files changed, 210 insertions(+), 50 deletions(-) create mode 100644 docs/wallet-integration-guide/examples/scripts/stress/01-acs-cache.ts diff --git a/core/acs-reader/src/index.ts b/core/acs-reader/src/index.ts index 13e346547..5fb53e86c 100644 --- a/core/acs-reader/src/index.ts +++ b/core/acs-reader/src/index.ts @@ -1,4 +1,8 @@ // Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -export { AcsReader, AcsOptions } from './acs-reader.js' +export { + AcsReader, + AcsOptions, + buildActiveContractFilter, +} from './acs-reader.js' diff --git a/docs/wallet-integration-guide/examples/package.json b/docs/wallet-integration-guide/examples/package.json index 09482ece5..47d7555b9 100644 --- a/docs/wallet-integration-guide/examples/package.json +++ b/docs/wallet-integration-guide/examples/package.json @@ -25,7 +25,8 @@ "run-11": "tsx ./scripts/11-hashing.ts | pino-pretty", "run-12": "tsx ./scripts/12-subscribe-to-events.ts | pino-pretty", "run-13": "tsx ./scripts/13-rewards-for-deposits/index.ts | pino-pretty", - "run-14": "tsx ./scripts/14-offline-signing.ts | pino-pretty" + "run-14": "tsx ./scripts/14-offline-signing.ts | pino-pretty", + "run-stress-01": "tsx ./scripts/stress/01-acs-cache.ts | pino-pretty" }, "devDependencies": { "@canton-network/core-signing-lib": "workspace:^", diff --git a/docs/wallet-integration-guide/examples/scripts/stress/01-acs-cache.ts b/docs/wallet-integration-guide/examples/scripts/stress/01-acs-cache.ts new file mode 100644 index 000000000..6f940a0ff --- /dev/null +++ b/docs/wallet-integration-guide/examples/scripts/stress/01-acs-cache.ts @@ -0,0 +1,97 @@ +import { localNetStaticConfig, SDK } from '@canton-network/wallet-sdk' +import { TOKEN_PROVIDER_CONFIG_DEFAULT } from '../utils/index.js' +import pino from 'pino' + +const logger = pino({ name: 'stress-01-acs-cache', level: 'info' }) + +const partiesToCreate = parseInt(process.env.PARTIES_AMOUNT ?? '') || 25 + +const acsQueries = + parseInt(process.env.ACS_QUERIES ?? '') || partiesToCreate * 5 + +const sdk = await SDK.create({ + auth: TOKEN_PROVIDER_CONFIG_DEFAULT, + ledgerClientUrl: localNetStaticConfig.LOCALNET_APP_USER_LEDGER_URL, +}) + +const setupParty = async () => { + const keys = sdk.keys.generate() + + const { partyId } = await sdk.party.external + .create(keys.publicKey) + .sign(keys.privateKey) + .execute() + + const [pingCommand] = sdk.utils.ping.create([ + { initiator: partyId, responder: partyId }, + ]) + + await sdk.ledger + .prepare({ + commands: pingCommand, + partyId, + }) + .sign(keys.privateKey) + .execute({ + partyId, + }) + + return { partyId, keys } +} + +// Create parties in batches to avoid overwhelming the server +const parties = [] +const batchSize = 5 + +logger.info(`Creating ${partiesToCreate} parties in batches of ${batchSize}...`) + +for (let i = 0; i < partiesToCreate; i += batchSize) { + const batch = Math.min(batchSize, partiesToCreate - i) + const batchNum = Math.floor(i / batchSize) + 1 + const totalBatches = Math.ceil(partiesToCreate / batchSize) + + logger.info( + `Creating batch ${batchNum}/${totalBatches} (${batch} parties)...` + ) + + const batchParties = await Promise.all( + [...Array(batch)].map(() => setupParty()) + ) + parties.push(...batchParties) + + logger.info( + `Batch ${batchNum}/${totalBatches} complete. Total parties: ${parties.length}` + ) +} + +logger.info(`All ${parties.length} parties created successfully!`) + +logger.info(`Executing ${acsQueries} ACS queries...`) +const startTime = Date.now() + +for (let i = 0; i < acsQueries; ++i) { + const randomParty = parties[Math.floor(Math.random() * parties.length)] + + // TODO: figure out why we have PERMISSION DENIED here + + await sdk.ledger.acs.read({ + parties: [randomParty.partyId], + filterByParty: true, + templateIds: [ + '#canton-builtin-admin-workflow-ping:Canton.Internal.Ping:Ping', + ], + }) + + if ((i + 1) % 10 === 0) { + logger.info(`Completed ${i + 1}/${acsQueries} queries...`) + } +} + +const endTime = Date.now() +const totalTime = (endTime - startTime) / 1000 +const avgTime = totalTime / acsQueries + +logger.info(`Completed ${acsQueries} ACS queries`) +logger.info(`Total time: ${totalTime.toFixed(2)}s`) +logger.info(`Average time per query: ${avgTime.toFixed(3)}s`) +logger.info(`Queries per second: ${(acsQueries / totalTime).toFixed(2)}`) diff --git a/sdk/wallet-sdk/package.json b/sdk/wallet-sdk/package.json index 55a9d87b9..519204efb 100644 --- a/sdk/wallet-sdk/package.json +++ b/sdk/wallet-sdk/package.json @@ -46,6 +46,7 @@ "jose": "^6.1.3", "pino": "^10.3.1", "pino-pretty": "^13.1.3", + "typescript-lru-cache": "^2.0.0", "uuid": "^11.1.0" }, "devDependencies": { diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/cache.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/cache.ts index f494a8de8..4d502cade 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/cache.ts +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/cache.ts @@ -7,7 +7,7 @@ import { LedgerNamespace } from '../namespace.js' import { ACSReader } from './reader.js' import { ACEvent, ACS_UPDATE_CONFIG, ACSKey, ACSState } from './types.js' import { Ops } from '@canton-network/core-provider-ledger' -import { buildActiveContractFilter } from '@canton-network/core-acs-reader/dist/acs-reader.js' +import { buildActiveContractFilter } from '@canton-network/core-acs-reader' export class ACSCacheNamespace { private readonly state: ACSState = { @@ -29,6 +29,14 @@ export class ACSCacheNamespace { this.ledger = new LedgerNamespace(sdkContext) } + private get initial() { + return this.state.initial + } + + private get updates() { + return this.state.updates + } + public async update(args: { offset: number; key: ACSKey }) { const { offset, key } = args @@ -41,9 +49,9 @@ export class ACSCacheNamespace { endInclusive: offset, eventFormat: buildActiveContractFilter({ offset, - templateIds: key.templateIds ?? [], - interfaceIds: key.interfaceIds ?? [], - parties: key.parties ?? [], + templateIds: key.templateId ? [key.templateId] : [], + interfaceIds: key.interfaceId ? [key.interfaceId] : [], + parties: key.party ? [key.party] : [], }).eventFormat, }) @@ -121,21 +129,13 @@ export class ACSCacheNamespace { }) } - private get initial() { - return this.state.initial - } - - private get updates() { - return this.state.updates - } - private async initState(args: { offset: number; key: ACSKey }) { const { offset, key } = args const initialAcs = await this.acsReader.readRaw({ offset, - parties: key.parties ?? [], - interfaceIds: key.interfaceIds ?? [], - templateIds: key.templateIds ?? [], + templateIds: key.templateId ? [key.templateId] : [], + interfaceIds: key.interfaceId ? [key.interfaceId] : [], + parties: key.party ? [key.party] : [], }) this.state.initial = { offset, @@ -181,7 +181,7 @@ export class ACSCacheNamespace { transactionShape: 'TRANSACTION_SHAPE_ACS_DELTA', }, } - return await this.ledger.internal.flats({ + return await this.ledger.internal.updates({ beginExclusive, endInclusive, updateFormat, diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/namespace.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/namespace.ts index 9ceec9caf..cdc0226f2 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/namespace.ts +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/namespace.ts @@ -1,28 +1,88 @@ // Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { SDKContext } from '../../../sdk.js' +import { LRUCache, LRUCacheOptions } from 'typescript-lru-cache' +import { LedgerTypes, SDKContext } from '../../../sdk.js' import { ACSCacheNamespace } from './cache.js' import { ACSReader } from './reader.js' import { ACSKey } from './types.js' +import { AcsRequestOptions } from '../index.js' +import { AcsOptions } from '@canton-network/core-acs-reader' + +export type ACSNamespaceOptions = Pick< + LRUCacheOptions, + 'maxSize' | 'entryExpirationTimeInMS' +> export class ACSNamespace extends ACSReader { - private readonly cache: ACSCacheNamespace - constructor(protected readonly sdkContext: SDKContext) { + private readonly caches: LRUCache + constructor( + protected readonly sdkContext: SDKContext, + private readonly options: ACSNamespaceOptions = { + maxSize: 100, + entryExpirationTimeInMS: 10 * 60 * 1000, + } + ) { super(sdkContext) - this.cache = new ACSCacheNamespace(sdkContext) + this.caches = new LRUCache(options) + } + + public override async readRaw( + options: AcsRequestOptions + ): Promise> { + const resolvedOptions = await this.resolveAcsOptions(options) + const { parties, interfaceIds, templateIds } = resolvedOptions + const keys: ACSKey[] = + parties?.flatMap((party) => { + const withTemplateIds = + templateIds?.map((templateId) => ({ party, templateId })) ?? + [] + const withInterfaceIds = + interfaceIds?.map((interfaceId) => ({ + party, + interfaceId, + })) ?? [] + return [...withInterfaceIds, ...withTemplateIds] + }) ?? [] + + return await this.query({ options: resolvedOptions, keys }) + } + + private getCache(key: ACSKey) { + const serializedKey = this.serializeKey(key) + const existingCache = this.caches.get(serializedKey) + if (existingCache) return existingCache + + const newCache = new ACSCacheNamespace(this.sdkContext) + this.caches.set(serializedKey, newCache) + + return newCache } - public async updateKey(args: { offset: number; key: ACSKey }) { - await this.cache.update(args) - return await this.cache.calculateAt(args.offset) + private async updateKey(args: { options: AcsOptions; key: ACSKey }) { + const cache = this.getCache(args.key) + // if (!cache) { + // } + await cache.update({ + offset: args.options.offset, + key: args.key, + }) + return await cache.calculateAt(args.options.offset) } - public async query(offset: number, keys: ACSKey[]) { + private async query(args: { + options: AcsOptions + keys: ACSKey[] + }): Promise> { + const { options, keys } = args return ( await Promise.all( - keys.map(async (key) => await this.updateKey({ offset, key })) + keys.map(async (key) => await this.updateKey({ options, key })) ) ).flat() } + + private serializeKey(key: ACSKey): string { + return `${key.party ?? 'ANY'}_T${key.templateId ?? '()'}_I${key.interfaceId ?? '()'}` + } } diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/reader.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/reader.ts index 47b36e292..95145b2ba 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/reader.ts +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/reader.ts @@ -22,7 +22,7 @@ export class ACSReader { * continueUntilCompletion: A boolean flag indicating whether to continue polling the ledger until the query is complete. If true, the method will repeatedly query the ledger until all matching active contracts have been retrieved. If false or not provided, the method will return after a single query, which may return a * @returns Active contracts matching the provided query options. */ - async readRaw( + public async readRaw( options: AcsRequestOptions ): Promise> { const resolvedOptions = await this.resolveAcsOptions(options) @@ -43,7 +43,7 @@ export class ACSReader { * @param options AcsOptions for querying the Active Contract Set (ACS). * returns the createdEvent and synchronizerId */ - async read(options: AcsRequestOptions) { + public async read(options: AcsRequestOptions) { return (await this.readRaw(options)) .filter( @@ -65,7 +65,7 @@ export class ACSReader { }) } - private async resolveAcsOptions( + protected async resolveAcsOptions( options: AcsRequestOptions ): Promise { const offset = diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/types.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/types.ts index c43897269..8a82a95fb 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/types.ts +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/types.ts @@ -2,21 +2,14 @@ // SPDX-License-Identifier: Apache-2.0 import { LedgerTypes } from '../../../sdk.js' -import { AcsOptions } from '@canton-network/core-acs-reader' import { ContractId } from '@canton-network/core-token-standard' +import { PartyId } from '@canton-network/core-types' -export type ACSKey = { - parties: AcsOptions['parties'] -} & ( - | { - interfaceIds: AcsOptions['interfaceIds'] - templateIds?: never - } - | { - interfaceIds?: never - templateIds: AcsOptions['templateIds'] - } -) +export type ACSKey = Partial<{ + party: PartyId + templateId: string + interfaceId: string +}> export type ACEvent = { offset: number diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/internal/namespace.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/internal/namespace.ts index d4657e573..85b4e4fa8 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/ledger/internal/namespace.ts +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/internal/namespace.ts @@ -82,7 +82,7 @@ export class InternalLedgerNamespace { ) } - public async flats(args: InternalOperationParams) { + public async updates(args: InternalOperationParams) { const { beginExclusive, endInclusive, filter, updateFormat } = args const request = { @@ -93,10 +93,10 @@ export class InternalLedgerNamespace { ...(filter ? { filter } : {}), } - return await this.ctx.ledgerProvider.request({ + return await this.ctx.ledgerProvider.request({ method: 'ledgerApi', params: { - resource: '/v2/updates/flats', + resource: '/v2/updates', requestMethod: 'post', body: request, query: { diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/internal/types.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/internal/types.ts index 74f302a93..3b2db5f04 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/ledger/internal/types.ts +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/internal/types.ts @@ -6,7 +6,7 @@ import { Ops } from '@canton-network/core-provider-ledger' type AllowedOperation = | Ops.PostV2CommandsSubmitAndWait | Ops.PostV2InteractiveSubmissionPrepare - | Ops.PostV2UpdatesFlats + | Ops.PostV2Updates type OperationBodyRequest = Operation['ledgerApi']['params']['body'] @@ -17,7 +17,7 @@ type RequiredParamsFor = Extract< | Ops.PostV2CommandsSubmitAndWait | Ops.PostV2InteractiveSubmissionPrepare ? 'commands' | 'actAs' - : Operation extends Ops.PostV2UpdatesFlats + : Operation extends Ops.PostV2Updates ? 'beginExclusive' | 'endInclusive' | 'updateFormat' : never > @@ -27,7 +27,7 @@ type UnusedParams = Extract< | Ops.PostV2CommandsSubmitAndWait | Ops.PostV2InteractiveSubmissionPrepare ? 'userId' - : Operation extends Ops.PostV2UpdatesFlats + : Operation extends Ops.PostV2Updates ? 'verbose' : never > diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/types.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/types.ts index 9f4c9c3ff..fafd52fca 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/ledger/types.ts +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/types.ts @@ -29,6 +29,9 @@ export type WrappedCommand< [P in K]: { [Q in P]: RawCommandMap[P] } }[K] -export type AcsRequestOptions = Omit & { +export type AcsRequestOptions = Omit< + AcsOptions, + 'offset' | 'continueUntilCompletion' +> & { offset?: number } diff --git a/yarn.lock b/yarn.lock index 501141359..9632dcece 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2245,6 +2245,7 @@ __metadata: playwright: "npm:^1.58.2" tsup: "npm:^8.5.1" typescript: "npm:^5.9.3" + typescript-lru-cache: "npm:^2.0.0" uuid: "npm:^11.1.0" vitest: "npm:^4.1.2" languageName: unknown From 23681d186dcb4249b2642150e55ede0f8a78685f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Pi=C4=85tkowski?= Date: Tue, 28 Apr 2026 12:04:25 +0200 Subject: [PATCH 08/12] fix(wallet-sdk): fix running acs cache script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot Signed-off-by: Mateusz Piątkowski --- .../examples/scripts/stress/01-acs-cache.ts | 2 - .../src/wallet/namespace/ledger/acs/cache.ts | 50 ++++++++----------- .../wallet/namespace/ledger/acs/namespace.ts | 7 +-- .../src/wallet/namespace/ledger/acs/reader.ts | 1 - .../src/wallet/namespace/ledger/namespace.ts | 6 +-- .../src/wallet/namespace/ledger/types.ts | 5 +- 6 files changed, 26 insertions(+), 45 deletions(-) diff --git a/docs/wallet-integration-guide/examples/scripts/stress/01-acs-cache.ts b/docs/wallet-integration-guide/examples/scripts/stress/01-acs-cache.ts index 6f940a0ff..d8424ce0b 100644 --- a/docs/wallet-integration-guide/examples/scripts/stress/01-acs-cache.ts +++ b/docs/wallet-integration-guide/examples/scripts/stress/01-acs-cache.ts @@ -72,8 +72,6 @@ const startTime = Date.now() for (let i = 0; i < acsQueries; ++i) { const randomParty = parties[Math.floor(Math.random() * parties.length)] - // TODO: figure out why we have PERMISSION DENIED here - await sdk.ledger.acs.read({ parties: [randomParty.partyId], filterByParty: true, diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/cache.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/cache.ts index 4d502cade..009b07793 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/cache.ts +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/cache.ts @@ -5,9 +5,12 @@ import { ContractId } from '@canton-network/core-token-standard' import { LedgerTypes, SDKContext } from '../../../sdk.js' import { LedgerNamespace } from '../namespace.js' import { ACSReader } from './reader.js' -import { ACEvent, ACS_UPDATE_CONFIG, ACSKey, ACSState } from './types.js' +import { ACEvent, ACS_UPDATE_CONFIG, ACSState } from './types.js' import { Ops } from '@canton-network/core-provider-ledger' -import { buildActiveContractFilter } from '@canton-network/core-acs-reader' +import { + AcsOptions, + buildActiveContractFilter, +} from '@canton-network/core-acs-reader' export class ACSCacheNamespace { private readonly state: ACSState = { @@ -37,30 +40,25 @@ export class ACSCacheNamespace { return this.state.updates } - public async update(args: { offset: number; key: ACSKey }) { - const { offset, key } = args - - if (!this.initial.acs.length || this.initial.offset > offset) { - await this.initState(args) + public async update(options: AcsOptions) { + if (!this.initial.acs.length || this.initial.offset > options.offset) { + await this.initState(options) } + const builtFilter = buildActiveContractFilter(options) + const updates = await this.updateContracts({ beginExclusive: this.updates.offset, - endInclusive: offset, - eventFormat: buildActiveContractFilter({ - offset, - templateIds: key.templateId ? [key.templateId] : [], - interfaceIds: key.interfaceId ? [key.interfaceId] : [], - parties: key.party ? [key.party] : [], - }).eventFormat, + endInclusive: options.offset, + eventFormat: { + verbose: Boolean(builtFilter.verbose), + ...builtFilter.filter, + }, }) // in practise length should never be > maxUpdatesToFetch only equal (server should never return more than limit in query). This is just a safeguard. if (updates.length >= ACS_UPDATE_CONFIG.maxUpdatesToFetch) - void this.update({ - offset, - key, - }) + void this.update(options) const { newEvents, newOffset } = this.extractEvents({ offset: this.updates.offset, @@ -70,7 +68,7 @@ export class ACSCacheNamespace { if (newOffset > this.updates.offset) { this.updates.offset = newOffset this.updates.acs = this.updates.acs.concat(newEvents) - } else this.updates.offset = offset + } else this.updates.offset = options.offset if (this.updates.acs.length >= ACS_UPDATE_CONFIG.maxEventsBeforePrune) { this.prune() @@ -129,20 +127,14 @@ export class ACSCacheNamespace { }) } - private async initState(args: { offset: number; key: ACSKey }) { - const { offset, key } = args - const initialAcs = await this.acsReader.readRaw({ - offset, - templateIds: key.templateId ? [key.templateId] : [], - interfaceIds: key.interfaceId ? [key.interfaceId] : [], - parties: key.party ? [key.party] : [], - }) + private async initState(options: AcsOptions) { + const initialAcs = await this.acsReader.readRaw(options) this.state.initial = { - offset, + offset: options.offset, acs: initialAcs, } this.state.updates = { - offset, + offset: options.offset, acs: [], } this.state.archivedACs = new Set() diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/namespace.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/namespace.ts index cdc0226f2..786be02e1 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/namespace.ts +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/namespace.ts @@ -61,12 +61,7 @@ export class ACSNamespace extends ACSReader { private async updateKey(args: { options: AcsOptions; key: ACSKey }) { const cache = this.getCache(args.key) - // if (!cache) { - // } - await cache.update({ - offset: args.options.offset, - key: args.key, - }) + await cache.update(args.options) return await cache.calculateAt(args.options.offset) } diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/reader.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/reader.ts index 95145b2ba..f95dba275 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/reader.ts +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/reader.ts @@ -45,7 +45,6 @@ export class ACSReader { */ public async read(options: AcsRequestOptions) { return (await this.readRaw(options)) - .filter( (acs) => acs.contractEntry != null && diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/namespace.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/namespace.ts index 0436fd164..230356d47 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/ledger/namespace.ts +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/namespace.ts @@ -43,7 +43,7 @@ export class LedgerNamespace { * Performs the prepare step of the interactive submission flow. * @returns PreparedTransaction which includes the response from the ledger and an execute function that can be called with a SignedTransaction to perform the execute step of the interactive submission flow. */ - prepare(options: PrepareOptions): PreparedTransaction { + public prepare(options: PrepareOptions): PreparedTransaction { const preparePromise = async () => { const synchronizerId = options.synchronizerId || this.sdkContext.defaultSynchronizerId @@ -79,7 +79,7 @@ export class LedgerNamespace { * @param options The options for executing the transaction, including userId, partyId, and an optional submissionId. * @returns The submissionId of the executed transaction. */ - async execute( + public async execute( signed: SignedTransaction, options: ExecuteOptions ): Promise< @@ -149,7 +149,7 @@ export class LedgerNamespace { * @param signature The externally produced signature * @returns A SignedTransaction that can be passed to execute() */ - fromSignature( + public fromSignature( response: Ops.PostV2InteractiveSubmissionPrepare['ledgerApi']['result'], signature: string ): SignedTransaction { diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/types.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/types.ts index fafd52fca..9f4c9c3ff 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/ledger/types.ts +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/types.ts @@ -29,9 +29,6 @@ export type WrappedCommand< [P in K]: { [Q in P]: RawCommandMap[P] } }[K] -export type AcsRequestOptions = Omit< - AcsOptions, - 'offset' | 'continueUntilCompletion' -> & { +export type AcsRequestOptions = Omit & { offset?: number } From 349114515a3f9e0e71589072258c5d0ee0a94745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Pi=C4=85tkowski?= Date: Tue, 28 Apr 2026 13:41:56 +0200 Subject: [PATCH 09/12] feat: finalize stress test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mateusz Piątkowski --- .../examples/scripts/stress/01-acs-cache.ts | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/docs/wallet-integration-guide/examples/scripts/stress/01-acs-cache.ts b/docs/wallet-integration-guide/examples/scripts/stress/01-acs-cache.ts index d8424ce0b..e87b234c4 100644 --- a/docs/wallet-integration-guide/examples/scripts/stress/01-acs-cache.ts +++ b/docs/wallet-integration-guide/examples/scripts/stress/01-acs-cache.ts @@ -6,9 +6,6 @@ const logger = pino({ name: 'stress-01-acs-cache', level: 'info' }) const partiesToCreate = parseInt(process.env.PARTIES_AMOUNT ?? '') || 25 -const acsQueries = - parseInt(process.env.ACS_QUERIES ?? '') || partiesToCreate * 5 - const sdk = await SDK.create({ auth: TOKEN_PROVIDER_CONFIG_DEFAULT, ledgerClientUrl: localNetStaticConfig.LOCALNET_APP_USER_LEDGER_URL, @@ -66,30 +63,41 @@ for (let i = 0; i < partiesToCreate; i += batchSize) { logger.info(`All ${parties.length} parties created successfully!`) -logger.info(`Executing ${acsQueries} ACS queries...`) -const startTime = Date.now() - -for (let i = 0; i < acsQueries; ++i) { - const randomParty = parties[Math.floor(Math.random() * parties.length)] +const timeResults: [number, number][] = [] +for (const party of parties) { + const firstStartTime = performance.now() + await sdk.ledger.acs.read({ + parties: [party.partyId], + filterByParty: true, + templateIds: [ + '#canton-builtin-admin-workflow-ping:Canton.Internal.Ping:Ping', + ], + }) + const secondStartTime = performance.now() await sdk.ledger.acs.read({ - parties: [randomParty.partyId], + parties: [party.partyId], filterByParty: true, templateIds: [ '#canton-builtin-admin-workflow-ping:Canton.Internal.Ping:Ping', ], }) + const endTime = performance.now() - if ((i + 1) % 10 === 0) { - logger.info(`Completed ${i + 1}/${acsQueries} queries...`) - } + timeResults.push([ + secondStartTime - firstStartTime, + endTime - secondStartTime, + ]) } -const endTime = Date.now() -const totalTime = (endTime - startTime) / 1000 -const avgTime = totalTime / acsQueries +const isCacheFaster = timeResults.map( + ([beforeCacheTime, afterCacheTime]) => beforeCacheTime >= afterCacheTime +) + +const timesFaster = isCacheFaster.reduce((acc, value) => +value + acc, 0) +const timesSlower = isCacheFaster.length - timesFaster -logger.info(`Completed ${acsQueries} ACS queries`) -logger.info(`Total time: ${totalTime.toFixed(2)}s`) -logger.info(`Average time per query: ${avgTime.toFixed(3)}s`) -logger.info(`Queries per second: ${(acsQueries / totalTime).toFixed(2)}`) +logger.info( + { timesFaster, timesSlower }, + 'Times when caching mechanism was faster' +) From aa4f413db37af1cc0d40f7d688058058a49c506b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Pi=C4=85tkowski?= Date: Wed, 29 Apr 2026 16:15:28 +0200 Subject: [PATCH 10/12] feat(wallet-sdk): update jsdoc, update ts type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot Signed-off-by: Mateusz Piątkowski --- .../src/wallet/namespace/ledger/acs/cache.ts | 46 +++++++++++++++++-- .../wallet/namespace/ledger/acs/namespace.ts | 23 +++++++++- .../src/wallet/namespace/ledger/acs/reader.ts | 4 +- 3 files changed, 66 insertions(+), 7 deletions(-) diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/cache.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/cache.ts index 009b07793..b4c0e7cf7 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/cache.ts +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/cache.ts @@ -32,14 +32,27 @@ export class ACSCacheNamespace { this.ledger = new LedgerNamespace(sdkContext) } + /** + * Returns the initial snapshot of the active contract set. + * Contains the base state captured at a specific ledger offset. + */ private get initial() { return this.state.initial } + /** + * Returns the incremental updates applied after the initial snapshot. + * Contains created and archived events that occurred since the initial state. + */ private get updates() { return this.state.updates } + /** + * Updates the cache to include ledger changes up to the specified offset. + * Fetches and applies incremental updates from the ledger, initializing the cache if needed. + * Automatically prunes old events when the update buffer exceeds configured thresholds. + */ public async update(options: AcsOptions) { if (!this.initial.acs.length || this.initial.offset > options.offset) { await this.initState(options) @@ -47,7 +60,7 @@ export class ACSCacheNamespace { const builtFilter = buildActiveContractFilter(options) - const updates = await this.updateContracts({ + const updates = await this.fetchUpdates({ beginExclusive: this.updates.offset, endInclusive: options.offset, eventFormat: { @@ -75,6 +88,11 @@ export class ACSCacheNamespace { } } + /** + * Calculates the active contract set at a specific ledger offset. + * Applies cached updates to the initial snapshot and filters out archived contracts. + * Throws an error if the cache is not initialized or the requested offset is too old. + */ public calculateAt(offset: number) { if (!this.initial.acs) this.sdkContext.error.throw({ @@ -127,6 +145,10 @@ export class ACSCacheNamespace { }) } + /** + * Initializes the cache state by fetching the active contract set at the specified offset. + * Clears any existing updates and archived contract tracking. + */ private async initState(options: AcsOptions) { const initialAcs = await this.acsReader.readRaw(options) this.state.initial = { @@ -140,6 +162,11 @@ export class ACSCacheNamespace { this.state.archivedACs = new Set() } + /** + * Compacts the cache by moving the initial snapshot forward to a more recent offset. + * Applies accumulated updates to create a new initial state and discards old events. + * Improves performance by reducing the number of updates to process on each query. + */ private prune() { const newOffset = Math.max( this.initial.offset, @@ -160,7 +187,11 @@ export class ACSCacheNamespace { } } - private async updateContracts(args: { + /** + * Fetches ledger updates between two offsets. + * Queries the ledger API for transactions containing contract create and archive events. + */ + private async fetchUpdates(args: { beginExclusive: number endInclusive: number eventFormat: LedgerTypes['EventFormat'] @@ -180,8 +211,13 @@ export class ACSCacheNamespace { }) } + /** + * Extracts contract creation and archival events from raw ledger updates. + * Processes transaction and checkpoint updates to build a list of relevant contract events. + * Tracks the highest offset seen across all updates. + */ private extractEvents(args: { - updates: Awaited> + updates: Awaited> offset: number }) { const { updates, offset } = args @@ -245,6 +281,10 @@ export class ACSCacheNamespace { } } +/** + * Checks if an event represents a contract creation. + * Used to distinguish between created and archived events when processing cache updates. + */ function isCreatedEvent( event: ACEvent ): event is ACEvent & { archived: true; event: LedgerTypes['CreatedEvent'] } { diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/namespace.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/namespace.ts index 786be02e1..2ef0e1d51 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/namespace.ts +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/namespace.ts @@ -16,6 +16,7 @@ export type ACSNamespaceOptions = Pick< export class ACSNamespace extends ACSReader { private readonly caches: LRUCache + constructor( protected readonly sdkContext: SDKContext, private readonly options: ACSNamespaceOptions = { @@ -27,6 +28,14 @@ export class ACSNamespace extends ACSReader { this.caches = new LRUCache(options) } + /** + * Reads the active contract set from the ledger with caching. + * Resolves party references and constructs cache keys from the provided template and interface IDs. + * Queries are deduplicated and cached per party-template-interface combination. + * + * @override + * @see {@link ACSReader.readRaw} + */ public override async readRaw( options: AcsRequestOptions ): Promise> { @@ -59,12 +68,20 @@ export class ACSNamespace extends ACSReader { return newCache } - private async updateKey(args: { options: AcsOptions; key: ACSKey }) { + /** + * Updates the cached active contract set for a specific key and returns contracts at the requested offset. + * If the cache is outdated, fetches updates from the ledger and applies them incrementally. + */ + private async updateCache(args: { options: AcsOptions; key: ACSKey }) { const cache = this.getCache(args.key) await cache.update(args.options) return await cache.calculateAt(args.options.offset) } + /** + * Queries multiple cache keys in parallel and combines the results. + * Each key represents a unique party-template-interface combination to be queried independently. + */ private async query(args: { options: AcsOptions keys: ACSKey[] @@ -72,7 +89,9 @@ export class ACSNamespace extends ACSReader { const { options, keys } = args return ( await Promise.all( - keys.map(async (key) => await this.updateKey({ options, key })) + keys.map( + async (key) => await this.updateCache({ options, key }) + ) ) ).flat() } diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/reader.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/reader.ts index f95dba275..6a7f2b127 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/reader.ts +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/reader.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { AcsOptions } from '@canton-network/core-acs-reader' -import { v3_4 } from '@canton-network/core-ledger-client-types' +import { v3_5 } from '@canton-network/core-ledger-client-types' import { Ops } from '@canton-network/core-provider-ledger' import { LedgerTypes, SDKContext } from '../../../sdk.js' import { AcsRequestOptions } from '../types.js' @@ -53,7 +53,7 @@ export class ACSReader { .map((acs) => { const jsActiveContract = ( acs.contractEntry as { - JsActiveContract: v3_4.components['schemas']['JsActiveContract'] + JsActiveContract: v3_5.components['schemas']['JsActiveContract'] } ).JsActiveContract From be357df23e36c98539dea69b24367b3c9eb36073 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Pi=C4=85tkowski?= Date: Thu, 14 May 2026 14:10:54 +0200 Subject: [PATCH 11/12] feat(wallet-sdk,core-acs-reader): move acs logic to acs-reader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mateusz Piątkowski --- core/acs-reader/package.json | 1 + .../acs-reader/src/cache}/cache.ts | 94 +++++++++------- .../acs-reader/src/cache/collection.ts | 55 +++++---- core/acs-reader/src/index.ts | 7 +- core/acs-reader/src/reader.ts | 104 ++++++++++++++++++ .../src/{acs-reader.ts => service.ts} | 14 +-- .../acs => core/acs-reader/src}/types.ts | 10 +- .../src/token-standard-service.ts | 6 +- .../src/index.ts | 4 +- .../src/openrpc.json | 2 +- core/wallet-dapp-rpc-client/src/index.ts | 4 +- core/wallet-dapp-rpc-client/src/openrpc.json | 2 +- core/wallet-test-utils/src/otc-trade.ts | 20 ++-- core/wallet-user-rpc-client/src/index.ts | 4 +- core/wallet-user-rpc-client/src/openrpc.json | 2 +- .../scripts/04-token-standard-allocation.ts | 6 +- .../examples/scripts/05-preapproval.ts | 2 +- .../13-rewards-for-deposits/_withdraw.ts | 15 +-- .../scripts/13-rewards-for-deposits/index.ts | 15 +-- .../examples/scripts/stress/03-acs-cache.ts | 4 +- nx.json | 4 +- .../src/wallet/namespace/ledger/acs/index.ts | 6 - .../src/wallet/namespace/ledger/acs/reader.ts | 86 --------------- .../namespace/ledger/internal/namespace.ts | 26 ----- .../wallet/namespace/ledger/internal/types.ts | 9 +- .../src/wallet/namespace/ledger/namespace.ts | 6 +- .../namespace/token/utxos/mergeDelegation.ts | 36 +++--- sdk/wallet-sdk/src/wallet/sdk.ts | 5 - yarn.lock | 1 + 29 files changed, 275 insertions(+), 275 deletions(-) rename {sdk/wallet-sdk/src/wallet/namespace/ledger/acs => core/acs-reader/src/cache}/cache.ts (80%) rename sdk/wallet-sdk/src/wallet/namespace/ledger/acs/namespace.ts => core/acs-reader/src/cache/collection.ts (60%) create mode 100644 core/acs-reader/src/reader.ts rename core/acs-reader/src/{acs-reader.ts => service.ts} (98%) rename {sdk/wallet-sdk/src/wallet/namespace/ledger/acs => core/acs-reader/src}/types.ts (83%) delete mode 100644 sdk/wallet-sdk/src/wallet/namespace/ledger/acs/index.ts delete mode 100644 sdk/wallet-sdk/src/wallet/namespace/ledger/acs/reader.ts diff --git a/core/acs-reader/package.json b/core/acs-reader/package.json index 84991aeac..8cdd74d3b 100644 --- a/core/acs-reader/package.json +++ b/core/acs-reader/package.json @@ -38,6 +38,7 @@ "dependencies": { "@canton-network/core-ledger-client-types": "workspace:^", "@canton-network/core-provider-ledger": "workspace:^", + "@canton-network/core-token-standard": "workspace:^", "@canton-network/core-types": "workspace:^", "bignumber.js": "^9.3.1", "dayjs": "^1.11.19", diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/cache.ts b/core/acs-reader/src/cache/cache.ts similarity index 80% rename from sdk/wallet-sdk/src/wallet/namespace/ledger/acs/cache.ts rename to core/acs-reader/src/cache/cache.ts index b4c0e7cf7..d7d6fbe60 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/cache.ts +++ b/core/acs-reader/src/cache/cache.ts @@ -2,17 +2,28 @@ // SPDX-License-Identifier: Apache-2.0 import { ContractId } from '@canton-network/core-token-standard' -import { LedgerTypes, SDKContext } from '../../../sdk.js' -import { LedgerNamespace } from '../namespace.js' -import { ACSReader } from './reader.js' -import { ACEvent, ACS_UPDATE_CONFIG, ACSState } from './types.js' -import { Ops } from '@canton-network/core-provider-ledger' +import { ACEvent, ACS_UPDATE_CONFIG, ACSState } from '../types' import { - AcsOptions, + AbstractLedgerProvider, + Ops, +} from '@canton-network/core-provider-ledger' +import { LRUCacheOptions } from 'typescript-lru-cache' +import { LedgerCommonSchemas } from '@canton-network/core-ledger-client-types' +import pino from 'pino' +import { + ResolvedAcsOptions, + AcsService, buildActiveContractFilter, -} from '@canton-network/core-acs-reader' +} from '../service' + +export type ACSCacheOptions = Pick< + LRUCacheOptions, + 'maxSize' | 'entryExpirationTimeInMS' +> + +const logger = pino({ name: 'acs-reader/cache' }) -export class ACSCacheNamespace { +export class ACSCache { private readonly state: ACSState = { initial: { offset: 0, @@ -24,12 +35,10 @@ export class ACSCacheNamespace { }, archivedACs: new Set(), } - private readonly acsReader: ACSReader - private readonly ledger: LedgerNamespace + private readonly service: AcsService - constructor(private readonly sdkContext: SDKContext) { - this.acsReader = new ACSReader(sdkContext) - this.ledger = new LedgerNamespace(sdkContext) + constructor(private readonly ledger: AbstractLedgerProvider) { + this.service = new AcsService(ledger) } /** @@ -53,7 +62,7 @@ export class ACSCacheNamespace { * Fetches and applies incremental updates from the ledger, initializing the cache if needed. * Automatically prunes old events when the update buffer exceeds configured thresholds. */ - public async update(options: AcsOptions) { + public async update(options: ResolvedAcsOptions) { if (!this.initial.acs.length || this.initial.offset > options.offset) { await this.initState(options) } @@ -95,17 +104,12 @@ export class ACSCacheNamespace { */ public calculateAt(offset: number) { if (!this.initial.acs) - this.sdkContext.error.throw({ - message: 'No ACS initialized. Call `.update()` first', - type: 'Unexpected', - }) + throw Error('No ACS initialized. Call `.update()` first') if (this.initial.offset > offset) - this.sdkContext.error.throw({ - message: 'Provided offset cannot be smaller than ACS offset', - type: 'Unexpected', - }) + throw Error('Provided offset cannot be smaller than ACS offset') - const newContracts: LedgerTypes['JsGetActiveContractsResponse'][] = [] + const newContracts: LedgerCommonSchemas['JsGetActiveContractsResponse'][] = + [] const newArchivedContracts: Set> = new Set() this.updates.acs @@ -149,8 +153,8 @@ export class ACSCacheNamespace { * Initializes the cache state by fetching the active contract set at the specified offset. * Clears any existing updates and archived contract tracking. */ - private async initState(options: AcsOptions) { - const initialAcs = await this.acsReader.readRaw(options) + private async initState(options: ResolvedAcsOptions) { + const initialAcs = await this.service.getActiveContracts(options) this.state.initial = { offset: options.offset, acs: initialAcs, @@ -194,9 +198,10 @@ export class ACSCacheNamespace { private async fetchUpdates(args: { beginExclusive: number endInclusive: number - eventFormat: LedgerTypes['EventFormat'] + eventFormat: LedgerCommonSchemas['EventFormat'] + filter?: LedgerCommonSchemas['TransactionFilter'] }) { - const { beginExclusive, endInclusive, eventFormat } = args + const { beginExclusive, endInclusive, eventFormat, filter } = args const updateFormat: Ops.PostV2UpdatesFlats['ledgerApi']['params']['body']['updateFormat'] = { includeTransactions: { @@ -204,10 +209,24 @@ export class ACSCacheNamespace { transactionShape: 'TRANSACTION_SHAPE_ACS_DELTA', }, } - return await this.ledger.internal.updates({ - beginExclusive, - endInclusive, - updateFormat, + + return await this.ledger.request({ + method: 'ledgerApi', + params: { + resource: '/v2/updates', + requestMethod: 'post', + body: { + beginExclusive, + endInclusive, + updateFormat, + verbose: false, + ...(filter ? { filter } : {}), + }, + query: { + limit: ACS_UPDATE_CONFIG.maxUpdatesToFetch, + stream_idle_timeout_ms: 1000, + }, + }, }) } @@ -217,7 +236,7 @@ export class ACSCacheNamespace { * Tracks the highest offset seen across all updates. */ private extractEvents(args: { - updates: Awaited> + updates: Awaited> offset: number }) { const { updates, offset } = args @@ -231,7 +250,7 @@ export class ACSCacheNamespace { const transaction = update.update.Transaction const trOffset = transaction?.value?.offset if (trOffset && trOffset > newOffset) { - const events: Array = + const events: Array = transaction?.value?.events ?? [] events.forEach((event) => { if (!event) { @@ -269,7 +288,7 @@ export class ACSCacheNamespace { newOffset = offset } } else { - this.sdkContext.logger.warn( + logger.warn( { value: JSON.stringify(update.update), }, @@ -285,8 +304,9 @@ export class ACSCacheNamespace { * Checks if an event represents a contract creation. * Used to distinguish between created and archived events when processing cache updates. */ -function isCreatedEvent( - event: ACEvent -): event is ACEvent & { archived: true; event: LedgerTypes['CreatedEvent'] } { +function isCreatedEvent(event: ACEvent): event is ACEvent & { + archived: true + event: LedgerCommonSchemas['CreatedEvent'] +} { return !event.archived } diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/namespace.ts b/core/acs-reader/src/cache/collection.ts similarity index 60% rename from sdk/wallet-sdk/src/wallet/namespace/ledger/acs/namespace.ts rename to core/acs-reader/src/cache/collection.ts index 2ef0e1d51..e95d20e67 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/namespace.ts +++ b/core/acs-reader/src/cache/collection.ts @@ -1,31 +1,24 @@ // Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { LRUCache, LRUCacheOptions } from 'typescript-lru-cache' -import { LedgerTypes, SDKContext } from '../../../sdk.js' -import { ACSCacheNamespace } from './cache.js' -import { ACSReader } from './reader.js' -import { ACSKey } from './types.js' -import { AcsRequestOptions } from '../index.js' -import { AcsOptions } from '@canton-network/core-acs-reader' +import { LRUCache } from 'typescript-lru-cache' +import { ACSCache, ACSCacheOptions } from './cache' +import { ACSKey } from '../types' +import { ResolvedAcsOptions } from '../service' +import { LedgerCommonSchemas } from '@canton-network/core-ledger-client-types' +import { AbstractLedgerProvider } from '@canton-network/core-provider-ledger' -export type ACSNamespaceOptions = Pick< - LRUCacheOptions, - 'maxSize' | 'entryExpirationTimeInMS' -> - -export class ACSNamespace extends ACSReader { - private readonly caches: LRUCache +export class ACSCacheCollection { + private readonly collection: LRUCache constructor( - protected readonly sdkContext: SDKContext, - private readonly options: ACSNamespaceOptions = { + private readonly ledger: AbstractLedgerProvider, + private readonly options: ACSCacheOptions = { maxSize: 100, entryExpirationTimeInMS: 10 * 60 * 1000, } ) { - super(sdkContext) - this.caches = new LRUCache(options) + this.collection = new LRUCache(options) } /** @@ -36,11 +29,10 @@ export class ACSNamespace extends ACSReader { * @override * @see {@link ACSReader.readRaw} */ - public override async readRaw( - options: AcsRequestOptions - ): Promise> { - const resolvedOptions = await this.resolveAcsOptions(options) - const { parties, interfaceIds, templateIds } = resolvedOptions + public async readFromCache( + options: ResolvedAcsOptions + ): Promise> { + const { parties, interfaceIds, templateIds } = options const keys: ACSKey[] = parties?.flatMap((party) => { const withTemplateIds = @@ -54,16 +46,16 @@ export class ACSNamespace extends ACSReader { return [...withInterfaceIds, ...withTemplateIds] }) ?? [] - return await this.query({ options: resolvedOptions, keys }) + return await this.query({ options, keys }) } private getCache(key: ACSKey) { const serializedKey = this.serializeKey(key) - const existingCache = this.caches.get(serializedKey) + const existingCache = this.collection.get(serializedKey) if (existingCache) return existingCache - const newCache = new ACSCacheNamespace(this.sdkContext) - this.caches.set(serializedKey, newCache) + const newCache = new ACSCache(this.ledger) + this.collection.set(serializedKey, newCache) return newCache } @@ -72,7 +64,10 @@ export class ACSNamespace extends ACSReader { * Updates the cached active contract set for a specific key and returns contracts at the requested offset. * If the cache is outdated, fetches updates from the ledger and applies them incrementally. */ - private async updateCache(args: { options: AcsOptions; key: ACSKey }) { + private async updateCache(args: { + options: ResolvedAcsOptions + key: ACSKey + }) { const cache = this.getCache(args.key) await cache.update(args.options) return await cache.calculateAt(args.options.offset) @@ -83,9 +78,9 @@ export class ACSNamespace extends ACSReader { * Each key represents a unique party-template-interface combination to be queried independently. */ private async query(args: { - options: AcsOptions + options: ResolvedAcsOptions keys: ACSKey[] - }): Promise> { + }): Promise> { const { options, keys } = args return ( await Promise.all( diff --git a/core/acs-reader/src/index.ts b/core/acs-reader/src/index.ts index 5fb53e86c..0950034ad 100644 --- a/core/acs-reader/src/index.ts +++ b/core/acs-reader/src/index.ts @@ -1,8 +1,5 @@ // Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -export { - AcsReader, - AcsOptions, - buildActiveContractFilter, -} from './acs-reader.js' +export * from './reader.js' +export * from './service.js' diff --git a/core/acs-reader/src/reader.ts b/core/acs-reader/src/reader.ts new file mode 100644 index 000000000..6f9e3618a --- /dev/null +++ b/core/acs-reader/src/reader.ts @@ -0,0 +1,104 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { + AbstractLedgerProvider, + Ops, +} from '@canton-network/core-provider-ledger' +import { ACSCacheCollection } from './cache/collection' +import { AcsOptions, AcsService, ResolvedAcsOptions } from './service' +import { ACSCacheOptions } from './cache/cache' +import { LedgerCommonSchemas } from '@canton-network/core-ledger-client-types' + +type Reader = { + read: ( + options: AcsOptions + ) => Promise> + readJsContracts: (options: AcsOptions) => Promise< + Array< + LedgerCommonSchemas['JsActiveContract']['createdEvent'] & { + synchronizerId: LedgerCommonSchemas['JsActiveContract']['synchronizerId'] + } + > + > +} + +export class ACSReader implements Reader { + private cacheCollection: ACSCacheCollection + private service: AcsService + + constructor( + private readonly ledger: AbstractLedgerProvider, + private readonly cacheOptions?: ACSCacheOptions + ) { + this.cacheCollection = new ACSCacheCollection(ledger, cacheOptions) + this.service = new AcsService(ledger) + } + + /** + * Provides direct access to the ACS without using the cache. + */ + public raw: Reader = { + read: async (options: AcsOptions) => { + const resolvedOptions = await this.resolveAcsOptions(options) + return await this.service.getActiveContracts(resolvedOptions) + }, + readJsContracts: async (options: AcsOptions) => { + return this.readJsContractsWith(await this.raw.read(options)) + }, + } + + /** + * Reads active contracts from the cache. + */ + public async read(options: AcsOptions) { + const resolvedOptions = await this.resolveAcsOptions(options) + return await this.cacheCollection.readFromCache(resolvedOptions) + } + + /** + * Convenience method that returns active contracts as JS contract objects. + */ + public async readJsContracts(options: AcsOptions) { + return this.readJsContractsWith(await this.read(options)) + } + + private readJsContractsWith(output: Awaited>) { + return output + .filter( + (acs) => + acs.contractEntry != null && + 'JsActiveContract' in acs.contractEntry + ) + .map((acs) => { + const jsActiveContract = ( + acs.contractEntry as { + JsActiveContract: LedgerCommonSchemas['JsActiveContract'] + } + ).JsActiveContract + + return { + ...jsActiveContract.createdEvent, + synchronizerId: jsActiveContract.synchronizerId, + } + }) + } + + private async resolveAcsOptions( + options: AcsOptions + ): Promise { + const offset = + options.offset ?? + ( + await this.ledger.request({ + method: 'ledgerApi', + params: { + resource: '/v2/state/ledger-end', + requestMethod: 'get', + }, + }) + ).offset! + + return { ...options, offset } + } +} diff --git a/core/acs-reader/src/acs-reader.ts b/core/acs-reader/src/service.ts similarity index 98% rename from core/acs-reader/src/acs-reader.ts rename to core/acs-reader/src/service.ts index ce8d9d869..bd3abb466 100644 --- a/core/acs-reader/src/acs-reader.ts +++ b/core/acs-reader/src/service.ts @@ -19,7 +19,7 @@ const COMPLETIONS_LIMIT = '100' const COMPLETIONS_STREAM_IDLE_TIMEOUT_MS = '1000' export type AcsOptions = { - offset: number + offset?: number templateIds?: string[] parties?: string[] //TODO: Figure out if this should use this.partyId by default and not allow cross party filtering filterByParty?: boolean @@ -28,15 +28,13 @@ export type AcsOptions = { continueUntilCompletion?: boolean } -export class AcsReader { - private readonly ledgerProvider: AbstractLedgerProvider +export type ResolvedAcsOptions = Omit & { offset: number } - constructor(ledgerProvider: AbstractLedgerProvider) { - this.ledgerProvider = ledgerProvider - } +export class AcsService { + constructor(private readonly ledgerProvider: AbstractLedgerProvider) {} public async getActiveContracts( - options: AcsOptions + options: ResolvedAcsOptions ): Promise> { const { limit, continueUntilCompletion } = options @@ -50,8 +48,6 @@ export class AcsReader { ) } - //TODO: add back caching later - return await this.ledgerProvider.request( { method: 'ledgerApi', diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/types.ts b/core/acs-reader/src/types.ts similarity index 83% rename from sdk/wallet-sdk/src/wallet/namespace/ledger/acs/types.ts rename to core/acs-reader/src/types.ts index 8a82a95fb..b4a5381ee 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/types.ts +++ b/core/acs-reader/src/types.ts @@ -1,7 +1,7 @@ // Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import { LedgerTypes } from '../../../sdk.js' +import { LedgerCommonSchemas } from '@canton-network/core-ledger-client-types' import { ContractId } from '@canton-network/core-token-standard' import { PartyId } from '@canton-network/core-types' @@ -13,7 +13,9 @@ export type ACSKey = Partial<{ export type ACEvent = { offset: number - event: LedgerTypes['CreatedEvent'] | LedgerTypes['ArchivedEvent'] + event: + | LedgerCommonSchemas['CreatedEvent'] + | LedgerCommonSchemas['ArchivedEvent'] workflowId: string | null synchronizerId: string | null archived?: boolean @@ -25,7 +27,9 @@ export type ACSComponentState = { } export type ACSState = { - initial: ACSComponentState + initial: ACSComponentState< + LedgerCommonSchemas['JsGetActiveContractsResponse'] + > updates: ACSComponentState archivedACs: Set> } diff --git a/core/token-standard-service/src/token-standard-service.ts b/core/token-standard-service/src/token-standard-service.ts index 59b0920b6..67c13c78d 100644 --- a/core/token-standard-service/src/token-standard-service.ts +++ b/core/token-standard-service/src/token-standard-service.ts @@ -28,7 +28,7 @@ import { type LedgerCommonSchemas, } from '@canton-network/core-ledger-client-types' import { Logger, PartyId } from '@canton-network/core-types' -import { AcsReader, AcsOptions } from '@canton-network/core-acs-reader' +import { ACSReader, AcsOptions } from '@canton-network/core-acs-reader' import { TokenStandardTransactionInterfaces, ensureInterfaceViewIsPresent, @@ -246,10 +246,10 @@ export class CoreService { `continue to completion: ${Boolean(continueUntilCompletion)}` ) - const reader = new AcsReader(this.ledgerProvider) + const reader = new ACSReader(this.ledgerProvider) const acsResponses: JsGetActiveContractsResponse[] = - (await reader.getActiveContracts( + (await reader.raw.read( options ))! as JsGetActiveContractsResponse[] diff --git a/core/wallet-dapp-remote-rpc-client/src/index.ts b/core/wallet-dapp-remote-rpc-client/src/index.ts index 9188e7e0d..bac3d3294 100644 --- a/core/wallet-dapp-remote-rpc-client/src/index.ts +++ b/core/wallet-dapp-remote-rpc-client/src/index.ts @@ -608,7 +608,7 @@ export type RpcTypes = { } } -export class SpliceWalletJSONRPCRemoteDAppAPI { +export class WalletJSONRPCRemoteDAppAPI { public transport: RpcTransport constructor(transport: RpcTransport) { @@ -632,4 +632,4 @@ export class SpliceWalletJSONRPCRemoteDAppAPI { } } } -export default SpliceWalletJSONRPCRemoteDAppAPI +export default WalletJSONRPCRemoteDAppAPI diff --git a/core/wallet-dapp-remote-rpc-client/src/openrpc.json b/core/wallet-dapp-remote-rpc-client/src/openrpc.json index 44df6bb7a..987956149 100644 --- a/core/wallet-dapp-remote-rpc-client/src/openrpc.json +++ b/core/wallet-dapp-remote-rpc-client/src/openrpc.json @@ -1,7 +1,7 @@ { "openrpc": "1.2.6", "info": { - "title": "Splice Wallet JSON-RPC Remote dApp API", + "title": "Wallet JSON-RPC Remote dApp API", "version": "0.1.0", "description": "An OpenRPC specification for remotely hosted Wallet Providers. Due to the remote nature, an implementing provider must bridge certain functionality on the client-side to satisfy the general dApp API spec." }, diff --git a/core/wallet-dapp-rpc-client/src/index.ts b/core/wallet-dapp-rpc-client/src/index.ts index c41bd3567..283a157bb 100644 --- a/core/wallet-dapp-rpc-client/src/index.ts +++ b/core/wallet-dapp-rpc-client/src/index.ts @@ -594,7 +594,7 @@ export type RpcTypes = { } } -export class SpliceWalletJSONRPCDAppAPI { +export class WalletJSONRPCDAppAPI { public transport: RpcTransport constructor(transport: RpcTransport) { @@ -618,4 +618,4 @@ export class SpliceWalletJSONRPCDAppAPI { } } } -export default SpliceWalletJSONRPCDAppAPI +export default WalletJSONRPCDAppAPI diff --git a/core/wallet-dapp-rpc-client/src/openrpc.json b/core/wallet-dapp-rpc-client/src/openrpc.json index ea99511e6..e8a7afdcd 100644 --- a/core/wallet-dapp-rpc-client/src/openrpc.json +++ b/core/wallet-dapp-rpc-client/src/openrpc.json @@ -1,7 +1,7 @@ { "openrpc": "1.2.6", "info": { - "title": "Splice Wallet JSON-RPC dApp API", + "title": "Wallet JSON-RPC dApp API", "version": "0.5.0", "description": "An OpenRPC specification for the dapp to interact with a Wallet Provider." }, diff --git a/core/wallet-test-utils/src/otc-trade.ts b/core/wallet-test-utils/src/otc-trade.ts index 0f912b27b..d3c8ed989 100644 --- a/core/wallet-test-utils/src/otc-trade.ts +++ b/core/wallet-test-utils/src/otc-trade.ts @@ -151,13 +151,15 @@ export class OTCTrade { await this.acceptProposal(this.charlie, 'Charlie') // Venue initiates settlement of OTCTradeProposal - const activeTradeProposals2 = await this.sdk.ledger.acs.read({ - templateIds: [ - '#splice-token-test-trading-app:Splice.Testing.Apps.TradingApp:OTCTradeProposal', - ], - parties: [this.venue], - filterByParty: true, - }) + const activeTradeProposals2 = await this.sdk.ledger.acs.readJsContracts( + { + templateIds: [ + '#splice-token-test-trading-app:Splice.Testing.Apps.TradingApp:OTCTradeProposal', + ], + parties: [this.venue], + filterByParty: true, + } + ) const now = new Date() const prepareUntil = new Date( now.getTime() + 60 * 60 * 1000 @@ -187,7 +189,7 @@ export class OTCTrade { this.logger.info('Venue initated settlement of OTCTradeProposal') - const otcTrades = await this.sdk.ledger.acs.read({ + const otcTrades = await this.sdk.ledger.acs.readJsContracts({ templateIds: [ '#splice-token-test-trading-app:Splice.Testing.Apps.TradingApp:OTCTrade', ], @@ -212,7 +214,7 @@ export class OTCTrade { ): Promise { if (!this.sdk) throw new Error('SDK not initialized') - const activeTradeProposals = await this.sdk.ledger.acs.read({ + const activeTradeProposals = await this.sdk.ledger.acs.readJsContracts({ templateIds: [ '#splice-token-test-trading-app:Splice.Testing.Apps.TradingApp:OTCTradeProposal', ], diff --git a/core/wallet-user-rpc-client/src/index.ts b/core/wallet-user-rpc-client/src/index.ts index 6fb1b1018..cbe1a7ece 100644 --- a/core/wallet-user-rpc-client/src/index.ts +++ b/core/wallet-user-rpc-client/src/index.ts @@ -679,7 +679,7 @@ export type RpcTypes = { } } -export class SpliceWalletJSONRPCUserAPI { +export class WalletJSONRPCUserAPI { public transport: RpcTransport constructor(transport: RpcTransport) { @@ -703,4 +703,4 @@ export class SpliceWalletJSONRPCUserAPI { } } } -export default SpliceWalletJSONRPCUserAPI +export default WalletJSONRPCUserAPI diff --git a/core/wallet-user-rpc-client/src/openrpc.json b/core/wallet-user-rpc-client/src/openrpc.json index 866139881..3991eb616 100644 --- a/core/wallet-user-rpc-client/src/openrpc.json +++ b/core/wallet-user-rpc-client/src/openrpc.json @@ -1,7 +1,7 @@ { "openrpc": "1.2.6", "info": { - "title": "Splice Wallet JSON-RPC User API", + "title": "Wallet JSON-RPC User API", "version": "0.1.0", "description": "An OpenRPC specification for the user to interact with the Wallet Gateway." }, diff --git a/docs/wallet-integration-guide/examples/scripts/04-token-standard-allocation.ts b/docs/wallet-integration-guide/examples/scripts/04-token-standard-allocation.ts index 4ea8a3847..d4f81b546 100644 --- a/docs/wallet-integration-guide/examples/scripts/04-token-standard-allocation.ts +++ b/docs/wallet-integration-guide/examples/scripts/04-token-standard-allocation.ts @@ -159,7 +159,7 @@ logger.info( // Bob accepts OTCTradeProposal -const activeTradeProposals = await sdk.ledger.acs.read({ +const activeTradeProposals = await sdk.ledger.acs.readJsContracts({ templateIds: [ '#splice-token-test-trading-app:Splice.Testing.Apps.TradingApp:OTCTradeProposal', ], @@ -197,7 +197,7 @@ logger.info('Bob accepted OTCTradeProposal') //Venue initiates settlement of OTCTradeProposal -const activeTradeProposals2 = await sdk.ledger.acs.read({ +const activeTradeProposals2 = await sdk.ledger.acs.readJsContracts({ templateIds: [ '#splice-token-test-trading-app:Splice.Testing.Apps.TradingApp:OTCTradeProposal', ], @@ -234,7 +234,7 @@ await sdk.ledger logger.info('Venue initated settlement of OTCTradeProposal') -const otcTrades = await sdk.ledger.acs.read({ +const otcTrades = await sdk.ledger.acs.readJsContracts({ templateIds: [ '#splice-token-test-trading-app:Splice.Testing.Apps.TradingApp:OTCTrade', ], diff --git a/docs/wallet-integration-guide/examples/scripts/05-preapproval.ts b/docs/wallet-integration-guide/examples/scripts/05-preapproval.ts index f072a2c1f..954997465 100644 --- a/docs/wallet-integration-guide/examples/scripts/05-preapproval.ts +++ b/docs/wallet-integration-guide/examples/scripts/05-preapproval.ts @@ -201,7 +201,7 @@ const cancelled = await sdk.amulet.preapproval.fetchStatus(bob.partyId, { cancelled: true, }) -const preapprovalACS = await sdk.ledger.acs.read({ +const preapprovalACS = await sdk.ledger.acs.readJsContracts({ parties: [bob.partyId], filterByParty: true, }) diff --git a/docs/wallet-integration-guide/examples/scripts/13-rewards-for-deposits/_withdraw.ts b/docs/wallet-integration-guide/examples/scripts/13-rewards-for-deposits/_withdraw.ts index fe639549b..0545a0d0f 100644 --- a/docs/wallet-integration-guide/examples/scripts/13-rewards-for-deposits/_withdraw.ts +++ b/docs/wallet-integration-guide/examples/scripts/13-rewards-for-deposits/_withdraw.ts @@ -52,13 +52,14 @@ export default async ( partyId: treasury.partyId, }) - const activeContractsForDelegateTreasuryProxy = sdk.ledger.acs.read({ - parties: [treasury.partyId], - templateIds: [ - '#splice-util-featured-app-proxies:Splice.Util.FeaturedApp.DelegateProxy:DelegateProxy', - ], - filterByParty: true, - }) + const activeContractsForDelegateTreasuryProxy = + sdk.ledger.acs.readJsContracts({ + parties: [treasury.partyId], + templateIds: [ + '#splice-util-featured-app-proxies:Splice.Util.FeaturedApp.DelegateProxy:DelegateProxy', + ], + filterByParty: true, + }) const proxyCid = await activeContractsForDelegateTreasuryProxy.then( (list) => list[0].contractId diff --git a/docs/wallet-integration-guide/examples/scripts/13-rewards-for-deposits/index.ts b/docs/wallet-integration-guide/examples/scripts/13-rewards-for-deposits/index.ts index 3882c4ae6..e821ef5f5 100644 --- a/docs/wallet-integration-guide/examples/scripts/13-rewards-for-deposits/index.ts +++ b/docs/wallet-integration-guide/examples/scripts/13-rewards-for-deposits/index.ts @@ -141,13 +141,14 @@ const setupIteration = partyId: alice.partyId, }) - const activeContractsForDelegateTreasuryProxy = sdk.ledger.acs.read({ - parties: [treasury.partyId], - templateIds: [ - '#splice-util-featured-app-proxies:Splice.Util.FeaturedApp.DelegateProxy:DelegateProxy', - ], - filterByParty: true, - }) + const activeContractsForDelegateTreasuryProxy = + sdk.ledger.acs.readJsContracts({ + parties: [treasury.partyId], + templateIds: [ + '#splice-util-featured-app-proxies:Splice.Util.FeaturedApp.DelegateProxy:DelegateProxy', + ], + filterByParty: true, + }) const proxyCid = await activeContractsForDelegateTreasuryProxy.then( (list) => list[0].contractId diff --git a/docs/wallet-integration-guide/examples/scripts/stress/03-acs-cache.ts b/docs/wallet-integration-guide/examples/scripts/stress/03-acs-cache.ts index d9fe253f9..353b8f194 100644 --- a/docs/wallet-integration-guide/examples/scripts/stress/03-acs-cache.ts +++ b/docs/wallet-integration-guide/examples/scripts/stress/03-acs-cache.ts @@ -67,7 +67,7 @@ const timeResults: [number, number][] = [] for (const party of parties) { const firstStartTime = performance.now() - await sdk.ledger.acs.read({ + await sdk.ledger.acs.readJsContracts({ parties: [party.partyId], filterByParty: true, templateIds: [ @@ -75,7 +75,7 @@ for (const party of parties) { ], }) const secondStartTime = performance.now() - await sdk.ledger.acs.read({ + await sdk.ledger.acs.readJsContracts({ parties: [party.partyId], filterByParty: true, templateIds: [ diff --git a/nx.json b/nx.json index c21316c00..84b4f21a4 100644 --- a/nx.json +++ b/nx.json @@ -104,5 +104,7 @@ "staticStorybookTargetName": "static-storybook" } } - ] + ], + "analytics": false, + "nxCloudId": "6a05b93522efcb8f608e1a3d" } diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/index.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/index.ts deleted file mode 100644 index 3f20a1755..000000000 --- a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -export * from './namespace.js' -export type * from './cache.js' -export * from './types.js' diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/reader.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/reader.ts deleted file mode 100644 index 6a7f2b127..000000000 --- a/sdk/wallet-sdk/src/wallet/namespace/ledger/acs/reader.ts +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -import { AcsOptions } from '@canton-network/core-acs-reader' -import { v3_5 } from '@canton-network/core-ledger-client-types' -import { Ops } from '@canton-network/core-provider-ledger' -import { LedgerTypes, SDKContext } from '../../../sdk.js' -import { AcsRequestOptions } from '../types.js' - -export class ACSReader { - constructor(protected readonly sdkContext: SDKContext) {} - - /** - * - * @param options AcsOptions for querying the Active Contract Set (ACS). - * offset: The ledger offset at which to query the ACS. If not provided, will fetch the ledgerEnd. - * templateIds: An optional array of template IDs to filter the ACS by. If not provided, no filtering by template ID will be applied. - * parties: An optional array of party IDs to filter the ACS by. If not provided, no filtering by party will be applied. - * filterByParty: A boolean flag indicating whether to apply party-based filtering. If true, the query will filter contracts based on the specified parties. If false or not provided, party-based filtering will not be applied. - * interfaceIds: An optional array of interface IDs to filter the ACS by. If not provided, no filtering by interface ID will be applied. - * limit: An optional number specifying the maximum number of active contracts to return in a single query. If not provided, the default limit will be determined by the ledger API. - * continueUntilCompletion: A boolean flag indicating whether to continue polling the ledger until the query is complete. If true, the method will repeatedly query the ledger until all matching active contracts have been retrieved. If false or not provided, the method will return after a single query, which may return a - * @returns Active contracts matching the provided query options. - */ - public async readRaw( - options: AcsRequestOptions - ): Promise> { - const resolvedOptions = await this.resolveAcsOptions(options) - - this.sdkContext.logger.debug( - resolvedOptions, - `Querying acs with options:` - ) - - const activeContracts = - await this.sdkContext.acsReader.getActiveContracts(resolvedOptions) - - return activeContracts - } - - /** - * Queries the ACS and filters for JsActiveContracts - * @param options AcsOptions for querying the Active Contract Set (ACS). - * returns the createdEvent and synchronizerId - */ - public async read(options: AcsRequestOptions) { - return (await this.readRaw(options)) - .filter( - (acs) => - acs.contractEntry != null && - 'JsActiveContract' in acs.contractEntry - ) - .map((acs) => { - const jsActiveContract = ( - acs.contractEntry as { - JsActiveContract: v3_5.components['schemas']['JsActiveContract'] - } - ).JsActiveContract - - return { - ...jsActiveContract.createdEvent, - synchronizerId: jsActiveContract.synchronizerId, - } - }) - } - - protected async resolveAcsOptions( - options: AcsRequestOptions - ): Promise { - const offset = - options.offset ?? - ( - await this.sdkContext.ledgerProvider.request( - { - method: 'ledgerApi', - params: { - resource: '/v2/state/ledger-end', - requestMethod: 'get', - }, - } - ) - ).offset! - - return { ...options, offset } - } -} diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/internal/namespace.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/internal/namespace.ts index 85b4e4fa8..813c38408 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/ledger/internal/namespace.ts +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/internal/namespace.ts @@ -5,7 +5,6 @@ import { SDKContext } from '../../../sdk.js' import { v4 } from 'uuid' import { Ops } from '@canton-network/core-provider-ledger' import { InternalOperationParams } from './types.js' -import { ACS_UPDATE_CONFIG } from '../acs/index.js' export class InternalLedgerNamespace { constructor(private readonly ctx: SDKContext) {} @@ -81,29 +80,4 @@ export class InternalLedgerNamespace { } ) } - - public async updates(args: InternalOperationParams) { - const { beginExclusive, endInclusive, filter, updateFormat } = args - - const request = { - beginExclusive, - endInclusive, - updateFormat, - verbose: false, - ...(filter ? { filter } : {}), - } - - return await this.ctx.ledgerProvider.request({ - method: 'ledgerApi', - params: { - resource: '/v2/updates', - requestMethod: 'post', - body: request, - query: { - limit: ACS_UPDATE_CONFIG.maxUpdatesToFetch, - stream_idle_timeout_ms: 1000, - }, - }, - }) - } } diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/internal/types.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/internal/types.ts index 3b2db5f04..6a85b6bc9 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/ledger/internal/types.ts +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/internal/types.ts @@ -6,7 +6,6 @@ import { Ops } from '@canton-network/core-provider-ledger' type AllowedOperation = | Ops.PostV2CommandsSubmitAndWait | Ops.PostV2InteractiveSubmissionPrepare - | Ops.PostV2Updates type OperationBodyRequest = Operation['ledgerApi']['params']['body'] @@ -17,9 +16,7 @@ type RequiredParamsFor = Extract< | Ops.PostV2CommandsSubmitAndWait | Ops.PostV2InteractiveSubmissionPrepare ? 'commands' | 'actAs' - : Operation extends Ops.PostV2Updates - ? 'beginExclusive' | 'endInclusive' | 'updateFormat' - : never + : never > type UnusedParams = Extract< keyof OperationBodyRequest, @@ -27,9 +24,7 @@ type UnusedParams = Extract< | Ops.PostV2CommandsSubmitAndWait | Ops.PostV2InteractiveSubmissionPrepare ? 'userId' - : Operation extends Ops.PostV2Updates - ? 'verbose' - : never + : never > export type InternalOperationParams = diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/namespace.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/namespace.ts index 343b6103a..21e6a08d8 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/ledger/namespace.ts +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/namespace.ts @@ -9,20 +9,20 @@ import { SignedTransaction } from '../transactions/signed.js' import { Ops } from '@canton-network/core-provider-ledger' import { DarNamespace } from './dar/client.js' import { InternalLedgerNamespace } from './internal/index.js' -import { ACSNamespace } from './acs/namespace.js' import { PreparedTransactionNamespace } from './hash/namespace.js' +import { ACSReader } from '@canton-network/core-acs-reader' export class LedgerNamespace { public readonly dar: DarNamespace public readonly internal: InternalLedgerNamespace public readonly preparedTransaction: PreparedTransactionNamespace - public readonly acs: ACSNamespace + public readonly acs: ACSReader constructor(private readonly sdkContext: SDKContext) { this.dar = new DarNamespace(sdkContext) this.internal = new InternalLedgerNamespace(sdkContext) this.preparedTransaction = new PreparedTransactionNamespace(sdkContext) - this.acs = new ACSNamespace(sdkContext) + this.acs = new ACSReader(sdkContext.ledgerProvider) } public async ledgerEnd() { diff --git a/sdk/wallet-sdk/src/wallet/namespace/token/utxos/mergeDelegation.ts b/sdk/wallet-sdk/src/wallet/namespace/token/utxos/mergeDelegation.ts index 795eb828d..d669107fb 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/token/utxos/mergeDelegation.ts +++ b/sdk/wallet-sdk/src/wallet/namespace/token/utxos/mergeDelegation.ts @@ -45,7 +45,7 @@ export class MergeDelegationNamespace { async approve(args: { owner: PartyId; synchronizerId?: string }) { const { owner, synchronizerId = '' } = args - const mergeDelegationProposals = await this.ledger.acs.read({ + const mergeDelegationProposals = await this.ledger.acs.readJsContracts({ parties: [owner], templateIds: [ '#splice-util-token-standard-wallet:Splice.Util.Token.Wallet.MergeDelegation:MergeDelegationProposal', @@ -107,26 +107,28 @@ export class MergeDelegationNamespace { const allMergeDelegationChoices: WrappedCommand<'ExerciseCommand'>[] = [] - const mergeDelegationContractsForUser = await this.ledger.acs.read({ - parties: [party], - templateIds: [ - '#splice-util-token-standard-wallet:Splice.Util.Token.Wallet.MergeDelegation:MergeDelegation', - ], - filterByParty: true, - }) + const mergeDelegationContractsForUser = + await this.ledger.acs.readJsContracts({ + parties: [party], + templateIds: [ + '#splice-util-token-standard-wallet:Splice.Util.Token.Wallet.MergeDelegation:MergeDelegation', + ], + filterByParty: true, + }) const mergeDelegationDisclosedContract = this.activeContractToDisclosedContract( mergeDelegationContractsForUser[0] ) - const batchMergeUtilityContracts = await this.ledger.acs.read({ - parties: [this.ctx.validatorParty], - templateIds: [ - '#splice-util-token-standard-wallet:Splice.Util.Token.Wallet.MergeDelegation:BatchMergeUtility', - ], - filterByParty: true, - }) + const batchMergeUtilityContracts = + await this.ledger.acs.readJsContracts({ + parties: [this.ctx.validatorParty], + templateIds: [ + '#splice-util-token-standard-wallet:Splice.Util.Token.Wallet.MergeDelegation:BatchMergeUtility', + ], + filterByParty: true, + }) const batchMergeUtilityDisclosedContract = this.activeContractToDisclosedContract( @@ -226,7 +228,9 @@ export class MergeDelegationNamespace { } private activeContractToDisclosedContract( - data: Awaited>[number] + data: Awaited< + ReturnType + >[number] ) { return { templateId: data.templateId, diff --git a/sdk/wallet-sdk/src/wallet/sdk.ts b/sdk/wallet-sdk/src/wallet/sdk.ts index 102f45c28..0e4c73150 100644 --- a/sdk/wallet-sdk/src/wallet/sdk.ts +++ b/sdk/wallet-sdk/src/wallet/sdk.ts @@ -8,7 +8,6 @@ import { LedgerProvider, Ops, } from '@canton-network/core-provider-ledger' -import { AcsReader } from '@canton-network/core-acs-reader' import { EXTENDED_SDK_OPTION_KEYS, ExtendedSDKOptions, @@ -41,7 +40,6 @@ export type LedgerTypes = LedgerCommonSchemas export type SDKContext = { ledgerProvider: AbstractLedgerProvider - acsReader: AcsReader userId: string logger: SDKLogger error: SDKErrorHandler @@ -104,11 +102,8 @@ export class SDK { logger ) - const acsReader = new AcsReader(ledgerProvider) - const ctx: SDKContext = { ledgerProvider, - acsReader, userId, logger, error, diff --git a/yarn.lock b/yarn.lock index f382a86f3..3ea9ef3d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1457,6 +1457,7 @@ __metadata: dependencies: "@canton-network/core-ledger-client-types": "workspace:^" "@canton-network/core-provider-ledger": "workspace:^" + "@canton-network/core-token-standard": "workspace:^" "@canton-network/core-types": "workspace:^" "@types/node": "npm:^25.0.10" "@vitest/browser-playwright": "npm:^4.1.2" From aa3aa2315c3fb80e7937ca3a1e75e6b9ad2f899a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Pi=C4=85tkowski?= Date: Fri, 15 May 2026 08:57:45 +0200 Subject: [PATCH 12/12] fix: fix build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mateusz Piątkowski --- scripts/src/clean-coding.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/src/clean-coding.ts b/scripts/src/clean-coding.ts index 5d6afdafc..b5cf5be86 100644 --- a/scripts/src/clean-coding.ts +++ b/scripts/src/clean-coding.ts @@ -147,9 +147,9 @@ function main(): void { let errorCount = 0 traverseDirectory(rootDir, (filePath) => { if ( - filePath.includes('.canton') || - filePath.includes('.yarn') || - filePath.includes('.splice') + ['.canton', '.yarn', '.splice', '.nx'].some((dirName) => + filePath.includes(dirName) + ) ) { return // Skip directories }