From 61c8012e1abc7cfb4d4b3c7e166e8df1f21ad450 Mon Sep 17 00:00:00 2001 From: Pascal Honegger Date: Wed, 9 Sep 2026 11:33:20 +0200 Subject: [PATCH 1/5] Import v3_5 in tests Signed-off-by: Pascal Honegger --- core/token-standard-service/src/parser.test.ts | 6 +++--- core/tx-parser/src/parser.test.ts | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/core/token-standard-service/src/parser.test.ts b/core/token-standard-service/src/parser.test.ts index 34c9f47bc6..27e6e5007a 100644 --- a/core/token-standard-service/src/parser.test.ts +++ b/core/token-standard-service/src/parser.test.ts @@ -3,7 +3,7 @@ import { vi, describe, it, expect, beforeEach, type Mocked } from 'vitest' -import { v3_4 } from '@canton-network/core-ledger-client-types' +import { v3_5 } from '@canton-network/core-ledger-client-types' import { CoreService } from './token-standard-service.js' import { AccessTokenProvider } from '@canton-network/core-wallet-auth' import { LedgerProvider } from '@canton-network/core-provider-ledger' @@ -13,9 +13,9 @@ import ledgerEffectsMock from './test-data/mock/utility-payload-ledger-effects.j import ledgerEffectsExpected from './test-data/expected/utility-payload-ledger-effects-sender.json' type JsGetEventsByContractIdResponse = - v3_4.components['schemas']['JsGetEventsByContractIdResponse'] + v3_5.components['schemas']['JsGetEventsByContractIdResponse'] -type CreatedEvent = v3_4.components['schemas']['CreatedEvent'] +type CreatedEvent = v3_5.components['schemas']['CreatedEvent'] const makeLedgerProviderMock = ( responses: JsGetEventsByContractIdResponse[] diff --git a/core/tx-parser/src/parser.test.ts b/core/tx-parser/src/parser.test.ts index 01cdcffa03..9b77a41459 100644 --- a/core/tx-parser/src/parser.test.ts +++ b/core/tx-parser/src/parser.test.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { LedgerProvider } from '@canton-network/core-provider-ledger' -import { v3_4 } from '@canton-network/core-ledger-client-types' +import { v3_5 } from '@canton-network/core-ledger-client-types' import { vi, describe, it, expect, beforeEach, Mocked } from 'vitest' import { TransactionParser } from './parser.js' import eventsByContractIdResponses from './test-data/mock/eventsByContractIdResponses.json' @@ -11,11 +11,11 @@ import bobTransferObjectsExpected from './test-data/expected/bob-transfer-object import txsMock from './test-data/mock/txs.json' import txsExpected from './test-data/expected/txs.json' -type JsTransaction = v3_4.components['schemas']['JsTransaction'] -type CreatedEvent = v3_4.components['schemas']['CreatedEvent'] +type JsTransaction = v3_5.components['schemas']['JsTransaction'] +type CreatedEvent = v3_5.components['schemas']['CreatedEvent'] const EVENTS_BY_CID_PATH = '/v2/events/events-by-contract-id' as const type JsGetEventsByContractIdResponse = - v3_4.components['schemas']['JsGetEventsByContractIdResponse'] + v3_5.components['schemas']['JsGetEventsByContractIdResponse'] const makeLedgerProviderMock = ( responses: JsGetEventsByContractIdResponse[] From 424c1ec65b293b8f914e2d2c628a19afc93a786d Mon Sep 17 00:00:00 2001 From: Pascal Honegger Date: Wed, 9 Sep 2026 11:45:50 +0200 Subject: [PATCH 2/5] Improve typing for metadata Signed-off-by: Pascal Honegger --- core/tx-parser/src/types.ts | 6 +++--- core/tx-parser/src/utils.ts | 23 ++++++++++++++--------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/core/tx-parser/src/types.ts b/core/tx-parser/src/types.ts index 0464e259c7..4b7de54698 100644 --- a/core/tx-parser/src/types.ts +++ b/core/tx-parser/src/types.ts @@ -112,7 +112,7 @@ export type Label = type UnknownAction = RawArchive | RawCreate interface BaseLabel { type: string - meta: any + meta: Metadata | undefined } interface KnownLabel extends BaseLabel { mintAmount: string @@ -171,7 +171,7 @@ export interface RawArchive extends BaseLabel { packageName: string actingParties: string[] payload: any - meta: any + meta: Metadata | undefined } export interface RawCreate extends BaseLabel { type: 'Create' | 'Lock' @@ -181,7 +181,7 @@ export interface RawCreate extends BaseLabel { templateId: string payload: any packageName: string - meta: any + meta: Metadata | undefined } export const renderTransaction = (t: Transaction): any => { diff --git a/core/tx-parser/src/utils.ts b/core/tx-parser/src/utils.ts index 18c8220d49..f0e24f14e9 100644 --- a/core/tx-parser/src/utils.ts +++ b/core/tx-parser/src/utils.ts @@ -7,6 +7,7 @@ import { Holding, TransferInstructionView } from './types.js' import { HOLDING_INTERFACE_ID, TRANSFER_INSTRUCTION_INTERFACE_ID, + Metadata, } from '@canton-network/core-token-standard' import { type LedgerCommonSchemas } from '@canton-network/core-ledger-client-types' @@ -94,15 +95,16 @@ export function ensureInterfaceViewIsPresent( return interfaceView } -type Meta = { values: { [key: string]: string } } | undefined - -export function mergeMetas(event: ExercisedEvent, extra?: Meta): Meta { +export function mergeMetas( + event: ExercisedEvent, + extra?: Metadata +): Metadata | undefined { // Add a type assertion to help TypeScript understand the shape of choiceArgument const choiceArgument = event.choiceArgument as | { - transfer?: { meta?: Meta } - extraArgs?: { meta?: Meta } - meta?: Meta + transfer?: { meta?: Metadata } + extraArgs?: { meta?: Metadata } + meta?: Metadata } | undefined @@ -111,7 +113,7 @@ export function mergeMetas(event: ExercisedEvent, extra?: Meta): Meta { choiceArgument?.extraArgs?.meta, choiceArgument?.meta, extra, - (event.exerciseResult as { meta?: Meta } | undefined)?.meta, + (event.exerciseResult as { meta?: Metadata } | undefined)?.meta, ] const result: { [key: string]: string } = {} lastWriteWins.forEach((meta) => { @@ -129,7 +131,10 @@ export function mergeMetas(event: ExercisedEvent, extra?: Meta): Meta { } } -export function getMetaKeyValue(key: string, meta: Meta): string | null { +export function getMetaKeyValue( + key: string, + meta: Metadata | undefined +): string | null { return (meta?.values || {})[key] || null } @@ -138,7 +143,7 @@ export function getMetaKeyValue(key: string, meta: Meta): string | null { * we remove all metadata fields that were fully parsed, and whose content is reflected in the TypeScript structure. * Otherwise, the display code has to do so, overloading the user with superfluous metadata entries. */ -export function removeParsedMetaKeys(meta: Meta): Meta { +export function removeParsedMetaKeys(meta: Metadata | undefined): Metadata { return { values: Object.fromEntries( Object.entries(meta?.values || {}).filter( From ed4d63b8010f4f83a46e3475e0a02324166702db Mon Sep 17 00:00:00 2001 From: Pascal Honegger Date: Wed, 9 Sep 2026 13:54:44 +0200 Subject: [PATCH 3/5] Update holdings to use HoldingView and add missing contractId Signed-off-by: Pascal Honegger --- .../src/test-data/expected/txs.json | 2 + ...utility-payload-ledger-effects-sender.json | 1 + .../src/token-standard-service.ts | 3 +- core/tx-parser/src/parser.ts | 88 +++++++++++-------- .../tx-parser/src/test-data/expected/txs.json | 2 + core/tx-parser/src/types.ts | 20 +---- core/tx-parser/src/utils.ts | 7 +- .../examples/scripts/05-preapproval.ts | 5 +- .../16-amulet-namespace-no-validator-url.ts | 5 +- .../portfolio/src/hooks/useWalletHoldings.ts | 5 +- .../portfolio/src/utils/aggregate-holdings.ts | 16 ++-- examples/portfolio/src/utils/holdings.ts | 21 ++--- .../namespace/token/utxos/mergeDelegation.ts | 6 +- .../wallet/namespace/token/utxos/service.ts | 16 ++-- .../src/wallet/namespace/token/utxos/types.ts | 5 +- 15 files changed, 105 insertions(+), 97 deletions(-) diff --git a/core/token-standard-service/src/test-data/expected/txs.json b/core/token-standard-service/src/test-data/expected/txs.json index 77fe888a28..08cfc2ff62 100644 --- a/core/token-standard-service/src/test-data/expected/txs.json +++ b/core/token-standard-service/src/test-data/expected/txs.json @@ -2667,6 +2667,7 @@ "creates": [ { "amount": "30.0000000000", + "contractId": "4", "instrumentId": { "admin": "alice::normalized", "id": "DummyHolding" @@ -2759,6 +2760,7 @@ "creates": [ { "amount": "40.0000000000", + "contractId": "5", "instrumentId": { "admin": "alice::normalized", "id": "DummyHolding" diff --git a/core/token-standard-service/src/test-data/expected/utility-payload-ledger-effects-sender.json b/core/token-standard-service/src/test-data/expected/utility-payload-ledger-effects-sender.json index f4df8788f6..218881c380 100644 --- a/core/token-standard-service/src/test-data/expected/utility-payload-ledger-effects-sender.json +++ b/core/token-standard-service/src/test-data/expected/utility-payload-ledger-effects-sender.json @@ -644,6 +644,7 @@ "id": "LOAD" }, "amount": "1016.0000000000", + "contractId": "0027bd11771073016844744a664ae407f590c36090602e6ed6fa5c044c6185b911ca111220d8c8c605a8a79276475e4f3e2133cf1ef154669624084fde9fcf7ded8c08dc9a", "lock": { "holders": [ "test-registrar::122002e9e6d1d24cf3fc613d9c6e44919e13f6577b805000f629a773f3817081efc6" diff --git a/core/token-standard-service/src/token-standard-service.ts b/core/token-standard-service/src/token-standard-service.ts index 6f624e764d..367d81ebba 100644 --- a/core/token-standard-service/src/token-standard-service.ts +++ b/core/token-standard-service/src/token-standard-service.ts @@ -34,7 +34,6 @@ import { PrettyContract, renderTransaction, ViewValue, - Holding as TxParseHolding, PrettyTransactions, Transaction, TransferObject, @@ -1723,7 +1722,7 @@ export class TokenStandardService { } static isHoldingLocked( - holding: Holding | TxParseHolding, + holding: HoldingView, currentTime: Date = new Date() ): boolean { const lock = holding.lock diff --git a/core/tx-parser/src/parser.ts b/core/tx-parser/src/parser.ts index 8b1dd48334..26500a2226 100644 --- a/core/tx-parser/src/parser.ts +++ b/core/tx-parser/src/parser.ts @@ -19,9 +19,8 @@ import { TxKindMetaKey, } from './constants.js' import { - Holding, + Holding as HoldingWithContractId, HoldingsChangeSummary, - HoldingLock, HoldingsChange, Label, TokenStandardEvent, @@ -41,6 +40,8 @@ import BigNumber from 'bignumber.js' import { PartyId } from '@canton-network/core-types' import { HOLDING_INTERFACE_ID, + HoldingView, + Lock, TRANSFER_INSTRUCTION_INTERFACE_ID, } from '@canton-network/core-token-standard' @@ -351,11 +352,25 @@ export class TransactionParser { result = { payload: holdingView, unlockedHoldingsChange: { - creates: isLocked ? [] : [holdingView], + creates: isLocked + ? [] + : [ + toHoldingWithContractId( + holdingView, + originalCreate.contractId + ), + ], archives: [], }, lockedHoldingsChange: { - creates: isLocked ? [holdingView] : [], + creates: isLocked + ? [ + toHoldingWithContractId( + holdingView, + originalCreate.contractId + ), + ] + : [], archives: [], }, lockedHoldingsChangeSummaries, @@ -834,15 +849,13 @@ export class TransactionParser { const holdingView = ensureInterfaceViewIsPresent( selfEvent.created.createdEvent, HOLDING_INTERFACE_ID - ).viewValue as Holding - mutatingResult.archives.push({ - amount: holdingView.amount, - instrumentId: holdingView.instrumentId, - contractId: exercisedEvent.contractId, - owner: holdingView.owner, - meta: holdingView.meta, - lock: holdingView.lock, - }) + ).viewValue as HoldingView + mutatingResult.archives.push( + toHoldingWithContractId( + holdingView, + exercisedEvent.contractId + ) + ) } } @@ -861,15 +874,13 @@ export class TransactionParser { interfaceView.interfaceId ) ) { - const holdingView = interfaceView.viewValue as Holding - mutatingResult.creates.push({ - amount: holdingView.amount, - instrumentId: holdingView.instrumentId, - contractId: createdEvent.contractId, - owner: holdingView.owner, - meta: holdingView.meta, - lock: holdingView.lock, - }) + const holdingView = interfaceView.viewValue as HoldingView + mutatingResult.creates.push( + toHoldingWithContractId( + holdingView, + createdEvent.contractId + ) + ) } } else if ( (archivedEvent && @@ -885,17 +896,14 @@ export class TransactionParser { const holdingView = ensureInterfaceViewIsPresent( contractEvents.created?.createdEvent, HOLDING_INTERFACE_ID - ).viewValue as Holding - mutatingResult.archives.push({ - amount: holdingView.amount, - instrumentId: holdingView.instrumentId, - contractId: + ).viewValue as HoldingView + mutatingResult.archives.push( + toHoldingWithContractId( + holdingView, archivedEvent?.contractId || - exercisedEvent!.contractId, - owner: holdingView.owner, - meta: holdingView.meta, - lock: holdingView.lock, - }) + exercisedEvent!.contractId + ) + ) } } } @@ -1037,7 +1045,7 @@ function getNodeIdAndEvent(event: Event): NodeIdAndEvent { * instrument. */ function sumHoldingsChange( change: HoldingsChange, - filter: (owner: string, lock: HoldingLock | null) => boolean + filter: (owner: string, lock: Lock | null) => boolean ): BigNumber { return sumHoldings( change.creates.filter((create) => filter(create.owner, create.lock)) @@ -1050,7 +1058,7 @@ function sumHoldingsChange( ) } -function sumHoldings(holdings: Holding[]): BigNumber { +function sumHoldings(holdings: HoldingView[]): BigNumber { if (holdings.length > 0) { // Sanity check. const instrumentId = holdings[0].instrumentId @@ -1070,6 +1078,16 @@ function sumHoldings(holdings: Holding[]): BigNumber { ) } +function toHoldingWithContractId( + holding: HoldingView, + contractId: string +): HoldingWithContractId { + return { + ...holding, + contractId, + } +} + function computeAmountChanges( children: HoldingsChange, meta: any, @@ -1117,7 +1135,7 @@ function computeSummary( function holdingsChangeByInstrument( changes: HoldingsChange ): InstrumentMap { - const map = new InstrumentMap<{ creates: Holding[]; archives: Holding[] }>() + const map = new InstrumentMap() for (const create of changes.creates) { if (map.has(create.instrumentId)) { map.get(create.instrumentId)!.creates.push(create) diff --git a/core/tx-parser/src/test-data/expected/txs.json b/core/tx-parser/src/test-data/expected/txs.json index d982193f5a..f8cc327b34 100644 --- a/core/tx-parser/src/test-data/expected/txs.json +++ b/core/tx-parser/src/test-data/expected/txs.json @@ -3425,6 +3425,7 @@ "creates": [ { "amount": "30.0000000000", + "contractId": "4", "instrumentId": { "admin": "alice::normalized", "id": "DummyHolding" @@ -3520,6 +3521,7 @@ "creates": [ { "amount": "40.0000000000", + "contractId": "5", "instrumentId": { "admin": "alice::normalized", "id": "DummyHolding" diff --git a/core/tx-parser/src/types.ts b/core/tx-parser/src/types.ts index 4b7de54698..c86a21cb7e 100644 --- a/core/tx-parser/src/types.ts +++ b/core/tx-parser/src/types.ts @@ -3,7 +3,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { Metadata } from '@canton-network/core-token-standard' +import { Metadata, HoldingView } from '@canton-network/core-token-standard' import { type LedgerCommonSchemas } from '@canton-network/core-ledger-client-types' export type ViewValue = LedgerCommonSchemas['JsInterfaceView']['viewValue'] // unknown | undefined @@ -46,22 +46,10 @@ export interface TokenStandardEvent { transferInstruction: TransferInstructionView | null } -// Same definition as HoldingView in Daml -export interface Holding { - contractId: string - owner: string - instrumentId: { admin: string; id: string } - amount: string - lock: HoldingLock | null - meta: any -} +export type Holding = HoldingView & { contractId: string } -export interface HoldingLock { - holders: string[] - expiresAt?: string - expiresAfter?: string - context?: string -} +/** @deprecated use Lock from @canton-network/core-token-standard directly */ +export type HoldingLock = Required export interface HoldingsChange { creates: Holding[] diff --git a/core/tx-parser/src/utils.ts b/core/tx-parser/src/utils.ts index f0e24f14e9..70d7ce82e3 100644 --- a/core/tx-parser/src/utils.ts +++ b/core/tx-parser/src/utils.ts @@ -3,8 +3,9 @@ import { AllKnownMetaKeys, matchInterfaceIds } from './constants.js' -import { Holding, TransferInstructionView } from './types.js' +import { TransferInstructionView } from './types.js' import { + HoldingView, HOLDING_INTERFACE_ID, TRANSFER_INSTRUCTION_INTERFACE_ID, Metadata, @@ -34,7 +35,7 @@ export function getInterfaceView( } export type KnownInterfaceView = - | { type: 'Holding'; viewValue: Holding } + | { type: 'Holding'; viewValue: HoldingView } | { type: 'TransferInstruction'; viewValue: TransferInstructionView } export function getKnownInterfaceView( @@ -48,7 +49,7 @@ export function getKnownInterfaceView( ) { return { type: 'Holding', - viewValue: interfaceView.viewValue as Holding, + viewValue: interfaceView.viewValue as HoldingView, } } else if ( matchInterfaceIds( diff --git a/docs/wallet-integration-guide/examples/scripts/05-preapproval.ts b/docs/wallet-integration-guide/examples/scripts/05-preapproval.ts index 68c31ead3f..f4cac4efa4 100644 --- a/docs/wallet-integration-guide/examples/scripts/05-preapproval.ts +++ b/docs/wallet-integration-guide/examples/scripts/05-preapproval.ts @@ -1,4 +1,5 @@ -import { Holding, PrettyContract } from '@canton-network/core-tx-parser' +import { PrettyContract } from '@canton-network/core-tx-parser' +import { HoldingView } from '@canton-network/core-token-standard' import { localNetStaticConfig, SDK } from '@canton-network/wallet-sdk' import { pino } from 'pino' import { @@ -125,7 +126,7 @@ logger.info({ sentValue }, 'Executed transfer from Alice to Bob with value:') const aliceUtxos = await sdk.token.utxos.list({ partyId: alice.partyId }) const bobUtxos = await sdk.token.utxos.list({ partyId: bob.partyId }) -const partyAmuletValue = (utxos: PrettyContract[]) => +const partyAmuletValue = (utxos: PrettyContract[]) => utxos.reduce( (acc, utxo) => acc + parseFloat(utxo.interfaceViewValue.amount), 0 diff --git a/docs/wallet-integration-guide/examples/scripts/16-amulet-namespace-no-validator-url.ts b/docs/wallet-integration-guide/examples/scripts/16-amulet-namespace-no-validator-url.ts index 39636f39ea..5f0600a9e1 100644 --- a/docs/wallet-integration-guide/examples/scripts/16-amulet-namespace-no-validator-url.ts +++ b/docs/wallet-integration-guide/examples/scripts/16-amulet-namespace-no-validator-url.ts @@ -1,4 +1,5 @@ -import { Holding, PrettyContract } from '@canton-network/core-tx-parser' +import { PrettyContract } from '@canton-network/core-tx-parser' +import { HoldingView } from '@canton-network/core-token-standard' import { getValidatorParty, localNetStaticConfig, @@ -151,7 +152,7 @@ logger.info({ sentValue }, 'Executed transfer from Alice to Bob with value:') const aliceUtxos = await sdk.token.utxos.list({ partyId: alice.partyId }) const bobUtxos = await sdk.token.utxos.list({ partyId: bob.partyId }) -const partyAmuletValue = (utxos: PrettyContract[]) => +const partyAmuletValue = (utxos: PrettyContract[]) => utxos.reduce( (acc, utxo) => acc + parseFloat(utxo.interfaceViewValue.amount), 0 diff --git a/examples/portfolio/src/hooks/useWalletHoldings.ts b/examples/portfolio/src/hooks/useWalletHoldings.ts index 1e5a6a3314..32da385006 100644 --- a/examples/portfolio/src/hooks/useWalletHoldings.ts +++ b/examples/portfolio/src/hooks/useWalletHoldings.ts @@ -3,7 +3,8 @@ import { useMemo } from 'react' import { useQuery } from '@tanstack/react-query' -import type { Holding } from '@canton-network/core-tx-parser' +import type { PrettyContract } from '@canton-network/core-tx-parser' +import type { HoldingView } from '@canton-network/core-token-standard' import { useInstruments } from '@hooks/useInstruments' import { aggregateHoldings, @@ -15,7 +16,7 @@ import { useWalletSdk } from './useWalletSdk' export interface WalletHoldingsResult { instruments: AggregatedHolding[] - holdings: Holding[] + holdings: PrettyContract[] isLoading: boolean isError: boolean error: Error | null diff --git a/examples/portfolio/src/utils/aggregate-holdings.ts b/examples/portfolio/src/utils/aggregate-holdings.ts index d828e604f5..8f88a48b51 100644 --- a/examples/portfolio/src/utils/aggregate-holdings.ts +++ b/examples/portfolio/src/utils/aggregate-holdings.ts @@ -3,7 +3,8 @@ import Decimal from 'decimal.js' import { TokenStandardService } from '@canton-network/core-token-standard-service' -import { type Holding } from '@canton-network/core-tx-parser' +import type { PrettyContract } from '@canton-network/core-tx-parser' +import type { HoldingView } from '@canton-network/core-token-standard' import type { Instruments } from '../types/instruments' export interface AggregatedWalletBalance { @@ -30,19 +31,20 @@ export function getInstrumentKey(instrumentId: { admin: string; id: string }) { } export function aggregateHoldings( - holdings: Holding[], + holdings: PrettyContract[], currentTime: Date = new Date() ): Map { const aggregated = new Map() for (const holding of holdings) { - const key = getInstrumentKey(holding.instrumentId) + const holdingView = holding.interfaceViewValue + const key = getInstrumentKey(holdingView.instrumentId) const existing = aggregated.get(key) const isLocked = TokenStandardService.isHoldingLocked( holding, currentTime ) - const amount = new Decimal(holding.amount) + const amount = new Decimal(holdingView.amount) if (existing) { const newTotal = new Decimal(existing.totalAmount).plus(amount) @@ -54,18 +56,18 @@ export function aggregateHoldings( existing.lockedAmount = newLocked.toString() existing.availableAmount = newTotal.minus(newLocked).toString() existing.numOfHoldings += 1 - addWalletBalance(existing.walletBalances, holding.owner, amount) + addWalletBalance(existing.walletBalances, holdingView.owner, amount) } else { const lockedAmount = isLocked ? amount : new Decimal(0) aggregated.set(key, { - instrumentId: holding.instrumentId, + instrumentId: holdingView.instrumentId, totalAmount: amount.toString(), lockedAmount: lockedAmount.toString(), availableAmount: amount.minus(lockedAmount).toString(), numOfHoldings: 1, walletBalances: [ { - owner: holding.owner, + owner: holdingView.owner, totalAmount: amount.toString(), }, ], diff --git a/examples/portfolio/src/utils/holdings.ts b/examples/portfolio/src/utils/holdings.ts index 543d92c9b9..97763eab7a 100644 --- a/examples/portfolio/src/utils/holdings.ts +++ b/examples/portfolio/src/utils/holdings.ts @@ -1,26 +1,17 @@ // Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -import type { Holding, PrettyContract } from '@canton-network/core-tx-parser' - -export const toPortfolioHolding = ( - contract: PrettyContract -): Holding => ({ - ...contract.interfaceViewValue, - contractId: contract.contractId, -}) +import type { HoldingView } from '@canton-network/core-token-standard' +import type { PrettyContract } from '@canton-network/core-tx-parser' export const toUniquePortfolioHoldings = ( - contracts: PrettyContract[] -): Holding[] => { - const holdingsByContractId = new Map() + contracts: PrettyContract[] +): PrettyContract[] => { + const holdingsByContractId = new Map>() for (const contract of contracts) { if (!holdingsByContractId.has(contract.contractId)) { - holdingsByContractId.set( - contract.contractId, - toPortfolioHolding(contract) - ) + holdingsByContractId.set(contract.contractId, contract) } } 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 c926fcf3e3..2bce42d20d 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/token/utxos/mergeDelegation.ts +++ b/sdk/wallet-sdk/src/wallet/namespace/token/utxos/mergeDelegation.ts @@ -2,12 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 import { TokenNamespaceConfig } from '../../../sdk.js' -import { Metadata } from '@canton-network/core-token-standard' +import { HoldingView, Metadata } from '@canton-network/core-token-standard' import { DisclosedContract, ExerciseCommand, } from '@canton-network/core-token-standard-service' -import { Holding, PrettyContract } from '@canton-network/core-tx-parser' +import { PrettyContract } from '@canton-network/core-tx-parser' import { PartyId } from '@canton-network/core-types' import { LedgerNamespace } from '../../ledger/index.js' import { UtxoNamespace } from './index.js' @@ -103,7 +103,7 @@ export class MergeDelegationNamespace { party: PartyId synchronizerId?: string nodeLimit?: number - inputUtxos?: PrettyContract[] + inputUtxos?: PrettyContract[] validatorParty?: PartyId }) { const providerParty = resolveProviderParty( diff --git a/sdk/wallet-sdk/src/wallet/namespace/token/utxos/service.ts b/sdk/wallet-sdk/src/wallet/namespace/token/utxos/service.ts index 5ca8974114..1c1f0e46e0 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/token/utxos/service.ts +++ b/sdk/wallet-sdk/src/wallet/namespace/token/utxos/service.ts @@ -2,9 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 import { MergeUtxosParams, ListHoldingsParams } from './types.js' -import { HOLDING_INTERFACE_ID } from '@canton-network/core-token-standard' +import { + HOLDING_INTERFACE_ID, + HoldingView, +} from '@canton-network/core-token-standard' import { TokenStandardService } from '@canton-network/core-token-standard-service' -import { Holding, PrettyContract } from '@canton-network/core-tx-parser' +import { PrettyContract } from '@canton-network/core-tx-parser' import { findAsset, LedgerTypes, TokenNamespaceConfig } from '../../../sdk.js' import { Decimal } from 'decimal.js' import { TransferNamespace } from '../transfer/index.js' @@ -47,7 +50,7 @@ export class UtxoNamespace { const utxoGroupedByInstrument: Record< string, - PrettyContract[] | undefined + PrettyContract[] | undefined > = Object.groupBy( utxos, (utxo) => @@ -139,7 +142,7 @@ export class UtxoNamespace { continueUntilCompletion, } = params const utxos = - await this.sdkContext.tokenStandardService.listContractsByInterface( + await this.sdkContext.tokenStandardService.listContractsByInterface( HOLDING_INTERFACE_ID, partyId, limit, @@ -153,10 +156,7 @@ export class UtxoNamespace { ? utxos : utxos.filter( (utxo) => - !TokenStandardService.isHoldingLocked( - utxo.interfaceViewValue, - currentTime - ) + !TokenStandardService.isHoldingLocked(utxo, currentTime) ) return filteredUtxos diff --git a/sdk/wallet-sdk/src/wallet/namespace/token/utxos/types.ts b/sdk/wallet-sdk/src/wallet/namespace/token/utxos/types.ts index 2ebbe5c117..0266eda5aa 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/token/utxos/types.ts +++ b/sdk/wallet-sdk/src/wallet/namespace/token/utxos/types.ts @@ -2,11 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 import { PartyId } from '@canton-network/core-types' -import { Holding, PrettyContract } from '@canton-network/core-tx-parser' +import { PrettyContract } from '@canton-network/core-tx-parser' +import { HoldingView } from '@canton-network/core-token-standard' export type MergeUtxosParams = { partyId: PartyId - inputUtxos?: PrettyContract[] + inputUtxos?: PrettyContract[] nodeLimit?: number memo?: string } From 99daea7b7288fb51c0583835151dba022f1d89d7 Mon Sep 17 00:00:00 2001 From: Pascal Honegger Date: Wed, 9 Sep 2026 13:56:27 +0200 Subject: [PATCH 4/5] Add support for expiresAfter to isHoldingLocked Signed-off-by: Pascal Honegger --- .../src/token-standard-service.test.ts | 106 +++++++++--------- .../src/token-standard-service.ts | 56 ++++++--- 2 files changed, 90 insertions(+), 72 deletions(-) diff --git a/core/token-standard-service/src/token-standard-service.test.ts b/core/token-standard-service/src/token-standard-service.test.ts index b7530909fb..37ca748093 100644 --- a/core/token-standard-service/src/token-standard-service.test.ts +++ b/core/token-standard-service/src/token-standard-service.test.ts @@ -964,61 +964,57 @@ describe('Token standard service', () => { ]) }) - it('holding locked returns correctly', async () => { - const future = new Date(Date.now() + 100_000).toISOString() - const past = new Date(Date.now() - 100_000).toISOString() - - expect( - TokenStandardService.isHoldingLocked({ - lock: null, - owner: '', - instrumentId: { - admin: '', - id: '', - }, - amount: '', - meta: undefined, - } as any) - ).toBe(false) - - expect( - TokenStandardService.isHoldingLocked({ - lock: {}, - owner: '', - instrumentId: { - admin: '', - id: '', - }, - amount: '', - meta: undefined, - } as any) - ).toBe(true) - - expect( - TokenStandardService.isHoldingLocked({ - lock: { expiresAt: future }, - owner: '', - instrumentId: { - admin: '', - id: '', - }, - amount: '', - meta: undefined, - } as any) - ).toBe(true) - - expect( - TokenStandardService.isHoldingLocked({ - lock: { expiresAt: past }, - owner: '', - instrumentId: { - admin: '', - id: '', - }, - amount: '', - meta: undefined, - } as any) - ).toBe(false) + describe('isHoldingLocked', () => { + // Used for relative time + const createdAt = '2026-09-01T10:00:00.000Z' + + const past = '2026-09-01T10:59:59.999Z' + const pastRel = { microseconds: '3599999000' } + + const now = '2026-09-01T11:00:00.000Z' + const nowRel = { microseconds: '3600000000' } + + const future = '2026-09-01T11:00:00.001Z' + const futureRel = { microseconds: '3600001000' } + + it.each([ + [null, false], + [{}, true], + [{ expiresAt: null }, true], + [{ expiresAt: past }, false], + [{ expiresAt: now }, false], + [{ expiresAt: future }, true], + [{ expiresAfter: pastRel }, false], + [{ expiresAfter: nowRel }, false], + [{ expiresAfter: futureRel }, true], + [{ expiresAt: future, expiresAfter: futureRel }, true], + [{ expiresAt: now, expiresAfter: futureRel }, false], + [{ expiresAt: future, expiresAfter: nowRel }, false], + ])('holding locked with lock %o returns %s', (lock, expected) => { + expect( + TokenStandardService.isHoldingLocked( + { + interfaceViewValue: { + lock, + owner: '', + instrumentId: { + admin: '', + id: '', + }, + amount: '', + meta: undefined, + }, + contractId: '', + activeContract: { + createdEvent: { createdAt }, + synchronizerId: '', + reassignmentCounter: 0, + }, + } as any, + new Date(now) + ) + ).toBe(expected) + }) }) it('create delegate proxy transfer', async () => { diff --git a/core/token-standard-service/src/token-standard-service.ts b/core/token-standard-service/src/token-standard-service.ts index 367d81ebba..50170cd5fe 100644 --- a/core/token-standard-service/src/token-standard-service.ts +++ b/core/token-standard-service/src/token-standard-service.ts @@ -125,17 +125,9 @@ export class CoreService { ) } - const unlockedSenderHoldings = senderHoldings.filter((utxo) => { - //filter out locked holdings - const lock = utxo.interfaceViewValue.lock - if (!lock) return true - - const expiresAt = lock.expiresAt - if (!expiresAt) return false - - const expiresAtDate = new Date(expiresAt) - return expiresAtDate <= now - }) + const unlockedSenderHoldings = senderHoldings.filter( + (utxo) => !TokenStandardService.isHoldingLocked(utxo, now) + ) if (unlockedSenderHoldings.length > 100) { this.logger.warn(`Sender has more than 100 unlocked utxos.`) @@ -1722,16 +1714,46 @@ export class TokenStandardService { } static isHoldingLocked( - holding: HoldingView, + holding: PrettyContract, currentTime: Date = new Date() ): boolean { - const lock = holding.lock + const lock = holding.interfaceViewValue.lock if (!lock) return false - const expiresAt = lock.expiresAt - if (!expiresAt) return true + let expiresAtAbsolute: Date | null = null + let expiresAtRelative: Date | null = null + + if (lock.expiresAfter) { + const createdAt = new Date( + holding.activeContract.createdEvent.createdAt + ) + + // 1 microsecond = 0.001 milliseconds + const msToAdd = parseInt(lock.expiresAfter.microseconds) / 1000 + + expiresAtRelative = new Date(createdAt.getTime() + msToAdd) + } + if (lock.expiresAt) { + expiresAtAbsolute = new Date(lock.expiresAt) + } + + let expiresAt: Date + + // If both `expiresAt` and `expiresAfter` are set, the lock expires at the earlier of the two times. + if (expiresAtRelative && expiresAtAbsolute) { + expiresAt = + expiresAtRelative < expiresAtAbsolute + ? expiresAtRelative + : expiresAtAbsolute + } else if (expiresAtRelative) { + expiresAt = expiresAtRelative + } else if (expiresAtAbsolute) { + expiresAt = expiresAtAbsolute + } else { + // No expiration => locked + return true + } - const expiresAtDate = new Date(expiresAt) - return currentTime < expiresAtDate + return currentTime < expiresAt } } From 72e1710af0f0a5a015dee32298f30e045bc2725f Mon Sep 17 00:00:00 2001 From: Pascal Honegger Date: Wed, 9 Sep 2026 18:01:31 +0200 Subject: [PATCH 5/5] Remove HoldingLock interface Signed-off-by: Pascal Honegger --- core/tx-parser/src/types.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/core/tx-parser/src/types.ts b/core/tx-parser/src/types.ts index c86a21cb7e..d650570bf8 100644 --- a/core/tx-parser/src/types.ts +++ b/core/tx-parser/src/types.ts @@ -48,9 +48,6 @@ export interface TokenStandardEvent { export type Holding = HoldingView & { contractId: string } -/** @deprecated use Lock from @canton-network/core-token-standard directly */ -export type HoldingLock = Required - export interface HoldingsChange { creates: Holding[] archives: Holding[]