diff --git a/core/acs-reader/package.json b/core/acs-reader/package.json index 3345eb666..2b722f68f 100644 --- a/core/acs-reader/package.json +++ b/core/acs-reader/package.json @@ -21,8 +21,8 @@ "dev": "tsup --watch --onSuccess \"tsc\"", "clean": "tsc -b --clean; rm -rf dist", "flatpack": "yarn pack --out \"$FLATPACK_OUTDIR\"", - "test": "vitest run --project node --project browser --passWithNoTests", - "test:coverage": "vitest run --project node --project browser --coverage --passWithNoTests" + "test": "vitest run --project node --project browser", + "test:coverage": "vitest run --project node --project browser --coverage" }, "devDependencies": { "@types/node": "^25.0.10", diff --git a/core/acs-reader/src/__test__/cache/collection.test.ts b/core/acs-reader/src/__test__/cache/collection.test.ts new file mode 100644 index 000000000..68cdb4acc --- /dev/null +++ b/core/acs-reader/src/__test__/cache/collection.test.ts @@ -0,0 +1,226 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { + ACSCacheCollection, + PaginatedACSCacheCollection, +} from '../../cache/collection' + +const { mockCache, MockACSCache } = vi.hoisted(() => { + const update = vi.fn() + const calculateAt = vi.fn() + + const mockCache = { + update, + calculateAt, + } + + const MockACSCache = vi.fn( + class { + update = update + calculateAt = calculateAt + } + ) + + return { mockCache, MockACSCache } +}) + +vi.mock('../../cache/item', () => { + return { + ACSCache: MockACSCache, + PaginatedACSCache: MockACSCache, + } +}) + +const ledgerProvider = vi.hoisted(() => ({ + request: vi.fn(), +})) + +describe('cache collection', () => { + ;[ACSCacheCollection, PaginatedACSCacheCollection].forEach( + (cacheConstructor) => { + describe(`using ${cacheConstructor.name}`, () => { + let collection: ACSCacheCollection | PaginatedACSCacheCollection + + beforeEach(() => { + vi.clearAllMocks() + mockCache.calculateAt.mockReturnValue([ + { + workflowId: 'test-workflow', + contractEntry: { + JsActiveContract: { + createdEvent: { + contractId: 'contract-1', + templateId: 'template1', + }, + synchronizerId: 'sync1', + reassignmentCounter: 0, + }, + }, + }, + ]) + + collection = new cacheConstructor(ledgerProvider) + }) + + it('should create cache collection with custom options', () => { + expect(collection).toBeDefined() + + const customCollection = new cacheConstructor( + ledgerProvider, + { + maxSize: 50, + entryExpirationTimeInMS: 5 * 60 * 1000, + } + ) + expect(customCollection).toBeDefined() + }) + + describe('readFromCache', () => { + it('should read from cache with single party and template', async () => { + const options = { + offset: 100, + parties: ['party1'], + templateIds: ['template1'], + } + + const result = await collection.readFromCache(options) + + expect(MockACSCache).toHaveBeenCalledWith( + ledgerProvider + ) + expect(mockCache.update).toHaveBeenCalledWith(options) + expect(mockCache.calculateAt).toHaveBeenCalledWith(100) + expect(result).toHaveLength(1) + }) + + it('should read from cache with multiple parties and templates', async () => { + const options = { + offset: 100, + parties: ['party1', 'party2'], + templateIds: [ + 'template1', + 'template2', + 'template3', + ], + } + + const result = await collection.readFromCache(options) + + // Should create 6 cache instances (2 parties × 3 templates) + expect(MockACSCache).toHaveBeenCalledTimes(6) + expect(mockCache.update).toHaveBeenCalledTimes(6) + expect(mockCache.calculateAt).toHaveBeenCalledTimes(6) + for (let i = 0; i < 6; ++i) { + expect(mockCache.update).toHaveBeenNthCalledWith( + i + 1, + options + ) + expect( + mockCache.calculateAt + ).toHaveBeenNthCalledWith(i + 1, options.offset) + } + expect(result).toHaveLength(6) + }) + + it('should read from cache with parties and interfaces', async () => { + const options = { + offset: 100, + parties: ['party1'], + interfaceIds: ['interface1', 'interface2'], + } + + const result = await collection.readFromCache(options) + + // Should create 2 cache instances (1 party × 2 interfaces) + expect(MockACSCache).toHaveBeenCalledTimes(2) + expect(mockCache.update).toHaveBeenCalledTimes(2) + expect(result).toHaveLength(2) + }) + + it('should read from cache with parties, templates, and interfaces', async () => { + const options = { + offset: 150, + parties: ['party1'], + templateIds: ['template1'], + interfaceIds: ['interface1'], + } + + const result = await collection.readFromCache(options) + + // Should create 2 cache instances (1 for interface, 1 for template) + expect(MockACSCache).toHaveBeenCalledTimes(2) + expect(mockCache.update).toHaveBeenCalledTimes(2) + expect(mockCache.calculateAt).toHaveBeenCalledWith(150) + expect(result).toHaveLength(2) + }) + + it('should reuse existing cache for same key', async () => { + const options = { + offset: 100, + parties: ['party1'], + templateIds: ['template1'], + } + + await collection.readFromCache(options) + await collection.readFromCache(options) + + // Should only create cache once + expect(MockACSCache).toHaveBeenCalledOnce() + // But should update and calculate twice + expect(mockCache.update).toHaveBeenCalledTimes(2) + expect(mockCache.calculateAt).toHaveBeenCalledTimes(2) + }) + + it('should create different caches for different keys', async () => { + await collection.readFromCache({ + offset: 100, + parties: ['party1'], + templateIds: ['template1'], + }) + + await collection.readFromCache({ + offset: 100, + parties: ['party2'], + templateIds: ['template1'], + }) + + // Should create two different caches + expect(MockACSCache).toHaveBeenCalledTimes(2) + }) + + it('should flatten results from multiple queries', async () => { + mockCache.calculateAt.mockReturnValue([ + { + contractEntry: { + JsActiveContract: { + createdEvent: { contractId: 'c1' }, + }, + }, + }, + { + contractEntry: { + JsActiveContract: { + createdEvent: { contractId: 'c2' }, + }, + }, + }, + ]) + + const options = { + offset: 100, + parties: ['party1'], + templateIds: ['template1', 'template2'], + } + + const result = await collection.readFromCache(options) + + // 2 templates × 2 contracts per template = 4 total contracts + expect(result).toHaveLength(4) + }) + }) + }) + } + ) +}) diff --git a/core/acs-reader/src/__test__/cache/item/item.test.ts b/core/acs-reader/src/__test__/cache/item/item.test.ts new file mode 100644 index 000000000..99aa38d77 --- /dev/null +++ b/core/acs-reader/src/__test__/cache/item/item.test.ts @@ -0,0 +1,459 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { ACSCache } from '../../../cache/item' + +const { getActiveContracts, MockACSService, mockBuildActiveContractFilter } = + vi.hoisted(() => { + const getActiveContracts = vi.fn() + const mockBuildActiveContractFilter = vi.fn((options) => ({ + filter: { filtersByParty: {} }, + verbose: false, + activeAtOffset: options.offset, + })) + + const MockACSService = vi.fn( + class { + getActiveContracts = getActiveContracts + } + ) + + return { + getActiveContracts, + MockACSService, + mockBuildActiveContractFilter, + } + }) + +vi.mock('../../../service.ts', () => { + return { + AcsService: MockACSService, + buildActiveContractFilter: mockBuildActiveContractFilter, + } +}) + +const ledgerProvider = vi.hoisted(() => ({ + request: vi.fn(), +})) + +describe('cache - item', () => { + let cache: ACSCache + + beforeEach(() => { + vi.clearAllMocks() + + getActiveContracts.mockReturnValue([ + { + workflowId: 'id1', + contractEntry: { + JsActiveContract: { + createdEvent: { + contractId: 'initial-contract-1', + templateId: 'template1', + }, + synchronizerId: 'sync1', + reassignmentCounter: 0, + }, + }, + }, + { + workflowId: 'id2', + contractEntry: { + JsActiveContract: { + createdEvent: { + contractId: 'initial-contract-2', + templateId: 'template2', + }, + synchronizerId: 'sync2', + reassignmentCounter: 0, + }, + }, + }, + ]) + + ledgerProvider.request.mockResolvedValue([]) + + cache = new ACSCache(ledgerProvider) + }) + + it('should call ACSService upon init', () => { + expect(MockACSService).toHaveBeenCalledOnce() + }) + + describe('update', () => { + it('should init state when cache is empty', async () => { + const updateOptions = { + offset: 100, + } + await cache.update(updateOptions) + expect(getActiveContracts).toHaveBeenCalledExactlyOnceWith( + updateOptions + ) + }) + + it('should init state when initial.offset > options.offset', async () => { + await cache.update({ offset: 200 }) + + // Second update with lower offset should trigger initState again + await cache.update({ offset: 100 }) + + expect(getActiveContracts).toHaveBeenCalledWith({ offset: 100 }) + }) + + it('should fetch updates with correct parameters', async () => { + const mockUpdates = [ + { + update: { + OffsetCheckpoint: { + value: { offset: 150 }, + }, + }, + }, + ] + ledgerProvider.request.mockResolvedValueOnce(mockUpdates) + + await cache.update({ offset: 100 }) + + await cache.update({ offset: 200 }) + + expect(ledgerProvider.request).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'ledgerApi', + params: expect.objectContaining({ + resource: '/v2/updates', + requestMethod: 'post', + body: expect.objectContaining({ + beginExclusive: 150, + endInclusive: 200, + }), + }), + }) + ) + }) + + it('should recursively call update when reaching maxUpdatesToFetch', async () => { + // Create exactly 100 updates (the maxUpdatesToFetch limit) + const mockUpdates = Array.from({ length: 100 }, (_, i) => ({ + update: { + OffsetCheckpoint: { + value: { offset: 100 + i }, + }, + }, + })) + + ledgerProvider.request.mockResolvedValueOnce(mockUpdates) + + await cache.update({ offset: 100 }) + + // Should have made multiple requests due to recursive call + expect(ledgerProvider.request).toHaveBeenCalledTimes(2) + }) + + it('should handle archived events correctly', async () => { + const mockUpdates = [ + { + update: { + Transaction: { + value: { + offset: 150, + workflowId: 'wf1', + synchronizerId: 'sync1', + events: [ + { + ArchivedEvent: { + contractId: 'initial-contract-1', + templateId: { value: 'template1' }, + }, + }, + ], + }, + }, + }, + }, + ] + ledgerProvider.request.mockResolvedValue(mockUpdates) + + await cache.update({ offset: 100 }) + + await cache.update({ offset: 200 }) + + // Should process archived events without errors + expect(ledgerProvider.request).toHaveBeenCalledTimes(2) + }) + }) + + describe('calculateAt', () => { + it('should throw error when offset is smaller than initial offset', async () => { + await cache.update({ offset: 200 }) + + expect(() => cache.calculateAt(100)).toThrow() + }) + + it('should return initial contracts when no updates have been applied', async () => { + await cache.update({ offset: 100 }) + + const result = cache.calculateAt(100) + + const expectedResult = getActiveContracts() + + expect(result).toHaveLength(2) + expect(result[0]).toMatchObject(expectedResult[0]) + expect(result[1]).toMatchObject(expectedResult[1]) + }) + + it('should include new created contracts in the result', async () => { + await cache.update({ offset: 100 }) + + const mockUpdates = [ + { + update: { + Transaction: { + value: { + offset: 150, + workflowId: 'wf1', + synchronizerId: 'sync1', + events: [ + { + CreatedEvent: { + contractId: 'new-contract-1', + templateId: { value: 'template3' }, + contractKey: null, + createArguments: {}, + createdAt: '2024-01-01T00:00:00Z', + signatories: [], + observers: [], + }, + }, + ], + }, + }, + }, + }, + ] + ledgerProvider.request.mockResolvedValueOnce(mockUpdates) + + await cache.update({ offset: 200 }) + + const result = cache.calculateAt(200) + + expect(result).toHaveLength(3) + expect( + result.some( + (c) => + c.contractEntry && + 'JsActiveContract' in c.contractEntry && + c.contractEntry.JsActiveContract.createdEvent + .contractId === 'new-contract-1' + ) + ).toBe(true) + }) + + it('should handle multiple created and archived events correctly', async () => { + await cache.update({ offset: 100 }) + + const mockUpdates = [ + { + update: { + Transaction: { + value: { + offset: 150, + workflowId: 'wf1', + synchronizerId: 'sync1', + events: [ + { + CreatedEvent: { + contractId: 'new-contract-1', + templateId: { value: 'template3' }, + contractKey: null, + createArguments: {}, + createdAt: '2024-01-01T00:00:00Z', + signatories: [], + observers: [], + }, + }, + ], + }, + }, + }, + }, + { + update: { + Transaction: { + value: { + offset: 160, + workflowId: 'wf2', + synchronizerId: 'sync1', + events: [ + { + CreatedEvent: { + contractId: 'new-contract-2', + templateId: { value: 'template4' }, + contractKey: null, + createArguments: {}, + createdAt: '2024-01-01T00:00:00Z', + signatories: [], + observers: [], + }, + }, + ], + }, + }, + }, + }, + { + update: { + Transaction: { + value: { + offset: 170, + workflowId: 'wf3', + synchronizerId: 'sync1', + events: [ + { + ArchivedEvent: { + contractId: 'new-contract-1', + templateId: { value: 'template3' }, + }, + }, + ], + }, + }, + }, + }, + ] + ledgerProvider.request.mockResolvedValueOnce(mockUpdates) + + await cache.update({ offset: 200 }) + + const result = cache.calculateAt(200) + + // Should have: initial-contract-1, initial-contract-2, new-contract-2 + // new-contract-1 was created and then archived + expect(result).toHaveLength(3) + expect( + result.some( + (c) => + c.contractEntry && + 'JsActiveContract' in c.contractEntry && + c.contractEntry.JsActiveContract.createdEvent + .contractId === 'new-contract-1' + ) + ).toBe(false) + expect( + result.some( + (c) => + c.contractEntry && + 'JsActiveContract' in c.contractEntry && + c.contractEntry.JsActiveContract.createdEvent + .contractId === 'new-contract-2' + ) + ).toBe(true) + }) + + it('should work correctly when called multiple times with different offsets', async () => { + await cache.update({ offset: 100 }) + + const mockUpdates = [ + { + update: { + Transaction: { + value: { + offset: 150, + workflowId: 'wf1', + synchronizerId: 'sync1', + events: [ + { + CreatedEvent: { + contractId: 'contract-150', + templateId: { value: 'template3' }, + contractKey: null, + createArguments: {}, + createdAt: '2024-01-01T00:00:00Z', + signatories: [], + observers: [], + }, + }, + ], + }, + }, + }, + }, + { + update: { + Transaction: { + value: { + offset: 200, + workflowId: 'wf2', + synchronizerId: 'sync1', + events: [ + { + CreatedEvent: { + contractId: 'contract-200', + templateId: { value: 'template4' }, + contractKey: null, + createArguments: {}, + createdAt: '2024-01-01T00:00:00Z', + signatories: [], + observers: [], + }, + }, + ], + }, + }, + }, + }, + ] + ledgerProvider.request.mockResolvedValueOnce(mockUpdates) + + await cache.update({ offset: 250 }) + + const resultAt150 = cache.calculateAt(150) + expect(resultAt150).toHaveLength(3) // 2 initial + 1 at 150 + + const resultAt180 = cache.calculateAt(180) + expect(resultAt180).toHaveLength(3) // 2 initial + 1 at 150 + + const resultAt250 = cache.calculateAt(250) + expect(resultAt250).toHaveLength(4) // 2 initial + 1 at 150 + 1 at 200 + }) + + it('should handle empty contract entries gracefully', async () => { + const mockUpdates = [ + { + update: { + Transaction: { + value: { + offset: 150, + workflowId: 'wf1', + synchronizerId: 'sync1', + events: [ + { + CreatedEvent: { + contractId: 'new-contract', + templateId: { value: 'template3' }, + contractKey: null, + createArguments: {}, + createdAt: '2024-01-01T00:00:00Z', + signatories: [], + observers: [], + }, + }, + ], + }, + }, + }, + }, + ] + ledgerProvider.request.mockResolvedValue(mockUpdates) + + await cache.update({ offset: 200 }) + + const result = cache.calculateAt(200) + + // Should filter out any entries without contractEntry + expect( + result.every(({ contractEntry }) => Boolean(contractEntry)) + ).toBe(true) + }) + }) +}) diff --git a/core/acs-reader/src/__test__/cache/item/paginatedItem.test.ts b/core/acs-reader/src/__test__/cache/item/paginatedItem.test.ts new file mode 100644 index 000000000..fc5f36b0c --- /dev/null +++ b/core/acs-reader/src/__test__/cache/item/paginatedItem.test.ts @@ -0,0 +1,485 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { PaginatedACSCache } from '../../../cache/item' + +const { + getPaginatedActiveContracts, + MockACSService, + mockBuildActiveContractFilter, +} = vi.hoisted(() => { + const getPaginatedActiveContracts = vi.fn() + const mockBuildActiveContractFilter = vi.fn((options) => ({ + filter: { filtersByParty: {} }, + verbose: false, + activeAtOffset: options.offset, + })) + + const MockACSService = vi.fn( + class { + getPaginatedActiveContracts = getPaginatedActiveContracts + } + ) + + return { + getPaginatedActiveContracts, + MockACSService, + mockBuildActiveContractFilter, + } +}) + +vi.mock('../../../service.ts', () => { + return { + AcsService: MockACSService, + buildActiveContractFilter: mockBuildActiveContractFilter, + } +}) + +const ledgerProvider = vi.hoisted(() => ({ + request: vi.fn(), +})) + +describe('cache - paginated item', () => { + let cache: PaginatedACSCache + + beforeEach(() => { + vi.clearAllMocks() + + getPaginatedActiveContracts.mockResolvedValue({ + activeContracts: [ + { + workflowId: 'id1', + contractEntry: { + JsActiveContract: { + createdEvent: { + contractId: 'initial-contract-1', + templateId: 'template1', + offset: 99, + }, + synchronizerId: 'sync1', + reassignmentCounter: 0, + }, + }, + }, + { + workflowId: 'id2', + contractEntry: { + JsActiveContract: { + createdEvent: { + contractId: 'initial-contract-2', + templateId: 'template2', + offset: 100, + }, + synchronizerId: 'sync2', + reassignmentCounter: 0, + }, + }, + }, + ], + activeAtOffset: 100, + nextPageToken: '', + }) + + cache = new PaginatedACSCache(ledgerProvider) + }) + + it('should call ACSService upon init', () => { + expect(MockACSService).toHaveBeenCalledOnce() + }) + + describe('update', () => { + it('should init state when cache is empty', async () => { + const updateOptions = { + offset: 100, + } + await cache.update(updateOptions) + expect(getPaginatedActiveContracts).toHaveBeenCalledExactlyOnceWith( + updateOptions + ) + }) + + it('should loop through pages until offset is reached', async () => { + getPaginatedActiveContracts + .mockResolvedValueOnce({ + activeContracts: [ + { + workflowId: 'id1', + contractEntry: { + JsActiveContract: { + createdEvent: { + contractId: 'contract-page1', + templateId: 'template1', + offset: 100, + }, + synchronizerId: 'sync1', + reassignmentCounter: 0, + }, + }, + }, + ], + activeAtOffset: 100, + nextPageToken: 'page2', + }) + .mockResolvedValueOnce({ + activeContracts: [ + { + workflowId: 'id2', + contractEntry: { + JsActiveContract: { + createdEvent: { + contractId: 'contract-page2', + templateId: 'template2', + offset: 200, + }, + synchronizerId: 'sync1', + reassignmentCounter: 0, + }, + }, + }, + ], + activeAtOffset: 200, + nextPageToken: 'page3', + }) + .mockResolvedValueOnce({ + activeContracts: [ + { + workflowId: 'id3', + contractEntry: { + JsActiveContract: { + createdEvent: { + contractId: 'contract-page3', + templateId: 'template3', + offset: 300, + }, + synchronizerId: 'sync1', + reassignmentCounter: 0, + }, + }, + }, + ], + activeAtOffset: 300, + nextPageToken: '', + }) + + await cache.update({ offset: 300 }) + + // Should have called getPaginatedActiveContracts 3 times (initial + 2 more pages) + expect(getPaginatedActiveContracts).toHaveBeenCalledTimes(3) + expect(getPaginatedActiveContracts).toHaveBeenNthCalledWith(1, { + offset: 300, + }) + expect(getPaginatedActiveContracts).toHaveBeenNthCalledWith(2, { + offset: 300, + pageToken: 'page2', + }) + expect(getPaginatedActiveContracts).toHaveBeenNthCalledWith(3, { + offset: 300, + pageToken: 'page3', + }) + }) + + it('should stop pagination when nextPageToken is empty', async () => { + await cache.update({ offset: 100 }) + + // Try to update with higher offset, but no more pages available + await cache.update({ offset: 200 }) + + // Should only have called once (initial), not loop since nextPageToken is empty + expect(getPaginatedActiveContracts).toHaveBeenCalledOnce() + }) + + it('should stop pagination when offset is reached', async () => { + getPaginatedActiveContracts + .mockResolvedValueOnce({ + activeContracts: [], + activeAtOffset: 100, + nextPageToken: 'page2', + }) + .mockResolvedValueOnce({ + activeContracts: [], + activeAtOffset: 200, + nextPageToken: 'page3', + }) + + await cache.update({ offset: 150 }) + + // Should have called twice: initial fetch reaches offset 100, + // then fetches page2 which reaches offset 200 (> 150), so stops + expect(getPaginatedActiveContracts).toHaveBeenCalledTimes(2) + }) + }) + + describe('calculateAt', () => { + it('should throw error when no ACS is initialized', () => { + const emptyCache = new PaginatedACSCache(ledgerProvider) + expect(() => emptyCache.calculateAt(100)).toThrow() + }) + + it('should return contracts at specified offset', async () => { + await cache.update({ offset: 100 }) + + const result = cache.calculateAt(100) + + expect(result).toHaveLength(2) + expect(result[0].contractEntry).toBeDefined() + expect(result[1].contractEntry).toBeDefined() + }) + + it("shouldn't return contracts for wrong offset", async () => { + await cache.update({ offset: 100 }) + + const emptyResult = cache.calculateAt(0) + expect(emptyResult).toHaveLength(0) + }) + + it('should filter contracts by offset from createdEvent', async () => { + getPaginatedActiveContracts.mockResolvedValueOnce({ + activeContracts: [ + { + workflowId: 'id1', + contractEntry: { + JsActiveContract: { + createdEvent: { + contractId: 'contract-100', + templateId: 'template1', + offset: 100, + }, + synchronizerId: 'sync1', + reassignmentCounter: 0, + }, + }, + }, + { + workflowId: 'id2', + contractEntry: { + JsActiveContract: { + createdEvent: { + contractId: 'contract-150', + templateId: 'template2', + offset: 150, + }, + synchronizerId: 'sync1', + reassignmentCounter: 0, + }, + }, + }, + { + workflowId: 'id3', + contractEntry: { + JsActiveContract: { + createdEvent: { + contractId: 'contract-200', + templateId: 'template3', + offset: 200, + }, + synchronizerId: 'sync1', + reassignmentCounter: 0, + }, + }, + }, + ], + activeAtOffset: 200, + nextPageToken: '', + }) + + await cache.update({ offset: 200 }) + + const resultAt100 = cache.calculateAt(100) + expect(resultAt100).toHaveLength(1) // Only contract-100 + + const resultAt150 = cache.calculateAt(150) + expect(resultAt150).toHaveLength(2) // contract-100 and contract-150 + + const resultAt200 = cache.calculateAt(200) + expect(resultAt200).toHaveLength(3) // All three contracts + }) + + it('should aggregate contracts from multiple pages', async () => { + getPaginatedActiveContracts + .mockResolvedValueOnce({ + activeContracts: [ + { + workflowId: 'id1', + contractEntry: { + JsActiveContract: { + createdEvent: { + contractId: 'page1-contract', + templateId: 'template1', + offset: 100, + }, + synchronizerId: 'sync1', + reassignmentCounter: 0, + }, + }, + }, + ], + activeAtOffset: 100, + nextPageToken: 'page2', + }) + .mockResolvedValueOnce({ + activeContracts: [ + { + workflowId: 'id2', + contractEntry: { + JsActiveContract: { + createdEvent: { + contractId: 'page2-contract', + templateId: 'template2', + offset: 200, + }, + synchronizerId: 'sync1', + reassignmentCounter: 0, + }, + }, + }, + ], + activeAtOffset: 200, + nextPageToken: '', + }) + + await cache.update({ offset: 200 }) + + const result = cache.calculateAt(200) + + // Should have contracts from both pages + expect(result).toHaveLength(2) + expect( + result.some( + (c) => + c.contractEntry && + 'JsActiveContract' in c.contractEntry && + c.contractEntry.JsActiveContract.createdEvent + .contractId === 'page1-contract' + ) + ).toBe(true) + expect( + result.some( + (c) => + c.contractEntry && + 'JsActiveContract' in c.contractEntry && + c.contractEntry.JsActiveContract.createdEvent + .contractId === 'page2-contract' + ) + ).toBe(true) + }) + + it('should handle contracts without JsActiveContract gracefully', async () => { + getPaginatedActiveContracts.mockResolvedValueOnce({ + activeContracts: [ + { + workflowId: 'id1', + contractEntry: { + JsActiveContract: { + createdEvent: { + contractId: 'valid-contract', + templateId: 'template1', + offset: 100, + }, + synchronizerId: 'sync1', + reassignmentCounter: 0, + }, + }, + }, + { + workflowId: 'id2', + contractEntry: undefined, + }, + ], + activeAtOffset: 100, + nextPageToken: '', + }) + + await cache.update({ offset: 100 }) + + const result = cache.calculateAt(100) + + // Should filter out entries without JsActiveContract + expect(result).toHaveLength(1) + expect( + result[0].contractEntry && + 'JsActiveContract' in result[0].contractEntry && + result[0].contractEntry.JsActiveContract.createdEvent + .contractId + ).toBe('valid-contract') + }) + }) + + describe('getPage', () => { + it('should retrieve a specific page by token', async () => { + getPaginatedActiveContracts.mockResolvedValueOnce({ + activeContracts: [ + { + workflowId: 'id1', + contractEntry: { + JsActiveContract: { + createdEvent: { + contractId: 'contract-1', + templateId: 'template1', + offset: 100, + }, + synchronizerId: 'sync1', + reassignmentCounter: 0, + }, + }, + }, + ], + activeAtOffset: 100, + nextPageToken: 'page2', + }) + + await cache.update({ offset: 100 }) + + const firstPage = cache.getPage(PaginatedACSCache.FIRST_PAGE_TOKEN) + expect(firstPage).toBeDefined() + expect(firstPage.activeContracts).toHaveLength(1) + expect(firstPage.activeAtOffset).toBe(100) + expect(firstPage.nextPageToken).toBe('page2') + }) + + it('should return undefined for non-existent page token', async () => { + await cache.update({ offset: 100 }) + + const nonExistentPage = cache.getPage('non-existent-token') + expect(nonExistentPage).toBeUndefined() + }) + + it('should retrieve page after fetching it with pageToken option', async () => { + await cache.update({ offset: 100 }) + + getPaginatedActiveContracts.mockResolvedValueOnce({ + activeContracts: [ + { + workflowId: 'id3', + contractEntry: { + JsActiveContract: { + createdEvent: { + contractId: 'page2-contract', + templateId: 'template3', + offset: 200, + }, + synchronizerId: 'sync1', + reassignmentCounter: 0, + }, + }, + }, + ], + activeAtOffset: 200, + nextPageToken: '', + }) + + await cache.update({ offset: 200, pageToken: 'page2Token' }) + + const page2 = cache.getPage('page2Token') + expect(page2).toBeDefined() + expect(page2.activeContracts).toHaveLength(1) + expect( + page2.activeContracts[0].contractEntry && + 'JsActiveContract' in page2.activeContracts[0].contractEntry + ? page2.activeContracts[0].contractEntry.JsActiveContract + .createdEvent.contractId + : undefined + ).toBe('page2-contract') + }) + }) +}) diff --git a/core/acs-reader/src/__test__/reader.test.ts b/core/acs-reader/src/__test__/reader.test.ts new file mode 100644 index 000000000..8bcb84ead --- /dev/null +++ b/core/acs-reader/src/__test__/reader.test.ts @@ -0,0 +1,672 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { ACSReader } from '../reader' + +const { mockCacheCollection, MockACSCacheCollection } = vi.hoisted(() => { + const readFromCache = vi.fn() + + const mockCacheCollection = { + readFromCache, + } + + const MockACSCacheCollection = vi.fn( + class { + readFromCache = readFromCache + } + ) + + return { mockCacheCollection, MockACSCacheCollection } +}) + +const { mockService, MockAcsService } = vi.hoisted(() => { + const getActiveContracts = vi.fn() + const getPaginatedActiveContracts = vi.fn() + + const mockService = { + getActiveContracts, + getPaginatedActiveContracts, + } + + const MockAcsService = vi.fn( + class { + getActiveContracts = getActiveContracts + getPaginatedActiveContracts = getPaginatedActiveContracts + } + ) + + return { mockService, MockAcsService } +}) + +vi.mock('../cache/collection', () => { + return { + ACSCacheCollection: MockACSCacheCollection, + PaginatedACSCacheCollection: MockACSCacheCollection, + } +}) + +vi.mock('../service', () => { + return { + AcsService: MockAcsService, + } +}) + +const ledgerProvider = vi.hoisted(() => ({ + request: vi.fn(), +})) + +describe('reader', () => { + let reader: ACSReader + + const createMockContract = (id: string, party: string, syncId: string) => ({ + workflowId: `wf-${id}`, + contractEntry: { + JsActiveContract: { + createdEvent: { + contractId: `contract-${id}`, + templateId: `template${id}`, + contractKey: null, + createArguments: {}, + createdAt: '2024-01-01T00:00:00Z', + signatories: [party], + observers: [], + }, + synchronizerId: syncId, + reassignmentCounter: 0, + }, + }, + }) + + const mockActiveContracts = [ + createMockContract('1', 'party1', 'sync1'), + createMockContract('2', 'party2', 'sync2'), + ] + + const expectLedgerEndCalled = () => { + expect(ledgerProvider.request).toHaveBeenCalledWith({ + method: 'ledgerApi', + params: { resource: '/v2/state/ledger-end', requestMethod: 'get' }, + }) + } + + beforeEach(() => { + vi.clearAllMocks() + mockService.getActiveContracts.mockResolvedValue(mockActiveContracts) + mockService.getPaginatedActiveContracts.mockResolvedValue({ + activeContracts: mockActiveContracts, + activeAtOffset: 100, + nextPageToken: '', + }) + mockCacheCollection.readFromCache.mockResolvedValue(mockActiveContracts) + ledgerProvider.request.mockResolvedValue({ offset: 1000 }) + reader = new ACSReader(ledgerProvider) + }) + + it('should initialize cache collection and service', () => { + expect(MockACSCacheCollection).toHaveBeenCalledWith( + ledgerProvider, + undefined + ) + expect(MockAcsService).toHaveBeenCalledWith(ledgerProvider) + }) + + it('should initialize with custom cache options', () => { + const cacheOptions = { + maxSize: 50, + entryExpirationTimeInMS: 5 * 60 * 1000, + } + + new ACSReader(ledgerProvider, cacheOptions) + + expect(MockACSCacheCollection).toHaveBeenCalledWith( + ledgerProvider, + cacheOptions + ) + }) + + describe('raw.read', () => { + it('should read active contracts directly without cache', async () => { + const options = { + offset: 100, + parties: ['party1'], + templateIds: ['template1'], + } + const result = await reader.raw.read(options) + + expect(mockService.getActiveContracts).toHaveBeenCalledWith(options) + expect(result).toEqual(mockActiveContracts) + expect(mockCacheCollection.readFromCache).not.toHaveBeenCalled() + }) + + it('should resolve offset when not provided', async () => { + ledgerProvider.request.mockResolvedValue({ offset: 500 }) + await reader.raw.read({ + parties: ['party1'], + templateIds: ['template1'], + }) + + expectLedgerEndCalled() + expect(mockService.getActiveContracts).toHaveBeenCalledWith( + expect.objectContaining({ offset: 500 }) + ) + }) + + it('should handle empty results', async () => { + mockService.getActiveContracts.mockResolvedValue([]) + expect( + await reader.raw.read({ offset: 100, parties: ['party1'] }) + ).toEqual([]) + }) + }) + + describe('raw.readJsContracts', () => { + it('should read and transform to JS contracts', async () => { + const result = await reader.raw.readJsContracts({ + offset: 100, + parties: ['party1'], + templateIds: ['template1'], + }) + + expect(result).toHaveLength(2) + expect(result[0]).toMatchObject({ + contractId: 'contract-1', + templateId: 'template1', + synchronizerId: 'sync1', + }) + }) + + it('should filter out contracts without JsActiveContract', async () => { + mockService.getActiveContracts.mockResolvedValue([ + ...mockActiveContracts, + { workflowId: 'wf3', contractEntry: null }, + { workflowId: 'wf4', contractEntry: { OtherType: {} } }, + ]) + + const result = await reader.raw.readJsContracts({ + offset: 100, + parties: ['party1'], + }) + expect(result).toHaveLength(2) + }) + + it('should return empty array when no contracts', async () => { + mockService.getActiveContracts.mockResolvedValue([]) + expect( + await reader.raw.readJsContracts({ + offset: 100, + parties: ['party1'], + }) + ).toEqual([]) + }) + }) + + describe('read', () => { + it('should read active contracts from cache', async () => { + const options = { + offset: 100, + parties: ['party1'], + templateIds: ['template1'], + } + const result = await reader.read(options) + + expect(mockCacheCollection.readFromCache).toHaveBeenCalledWith( + options + ) + expect(result).toEqual(mockActiveContracts) + expect(mockService.getActiveContracts).not.toHaveBeenCalled() + }) + + it('should resolve offset when not provided', async () => { + ledgerProvider.request.mockResolvedValue({ offset: 750 }) + await reader.read({ + parties: ['party1'], + templateIds: ['template1'], + }) + + expectLedgerEndCalled() + expect(mockCacheCollection.readFromCache).toHaveBeenCalledWith( + expect.objectContaining({ offset: 750 }) + ) + }) + + it('should handle empty cache results', async () => { + mockCacheCollection.readFromCache.mockResolvedValue([]) + expect( + await reader.read({ offset: 100, parties: ['party1'] }) + ).toEqual([]) + }) + + it.each([ + [ + 'multiple parties and templates', + { + offset: 200, + parties: ['party1', 'party2'], + templateIds: ['template1', 'template2'], + }, + ], + [ + 'interface IDs', + { + offset: 200, + parties: ['party1'], + interfaceIds: ['interface1'], + }, + ], + ])('should work with %s', async (_, options) => { + await reader.read(options) + expect(mockCacheCollection.readFromCache).toHaveBeenCalledWith( + options + ) + }) + }) + + describe('readJsContracts', () => { + it('should read from cache and transform to JS contracts', async () => { + const result = await reader.readJsContracts({ + offset: 100, + parties: ['party1'], + templateIds: ['template1'], + }) + + expect(result).toHaveLength(2) + expect(result[0]).toMatchObject({ + contractId: 'contract-1', + synchronizerId: 'sync1', + }) + expect(result[1]).toMatchObject({ + contractId: 'contract-2', + synchronizerId: 'sync2', + }) + }) + + it('should filter out contracts without JsActiveContract', async () => { + mockCacheCollection.readFromCache.mockResolvedValue([ + mockActiveContracts[0], + { workflowId: 'wf3', contractEntry: null }, + mockActiveContracts[1], + { workflowId: 'wf4', contractEntry: { OtherType: {} } }, + ]) + + const result = await reader.readJsContracts({ + offset: 100, + parties: ['party1'], + }) + expect(result).toHaveLength(2) + expect(result.map((c) => c.contractId)).toEqual([ + 'contract-1', + 'contract-2', + ]) + }) + + it('should return empty array when cache is empty', async () => { + mockCacheCollection.readFromCache.mockResolvedValue([]) + expect( + await reader.readJsContracts({ + offset: 100, + parties: ['party1'], + }) + ).toEqual([]) + }) + + it('should resolve offset before reading from cache', async () => { + ledgerProvider.request.mockResolvedValue({ offset: 999 }) + await reader.readJsContracts({ + parties: ['party1'], + templateIds: ['template1'], + }) + + expectLedgerEndCalled() + expect(mockCacheCollection.readFromCache).toHaveBeenCalledWith( + expect.objectContaining({ offset: 999 }) + ) + }) + }) + + describe('error handling', () => { + it.each([ + [ + 'service', + () => + mockService.getActiveContracts.mockRejectedValue( + new Error('Service error') + ), + () => reader.raw.read({ offset: 100, parties: ['party1'] }), + 'Service error', + ], + [ + 'cache', + () => + mockCacheCollection.readFromCache.mockRejectedValue( + new Error('Cache error') + ), + () => reader.read({ offset: 100, parties: ['party1'] }), + 'Cache error', + ], + [ + 'ledger-end', + () => + ledgerProvider.request.mockRejectedValue( + new Error('Ledger error') + ), + () => reader.read({ parties: ['party1'] }), + 'Ledger error', + ], + ])( + 'should propagate errors from %s', + async (_, setupError, action, expectedError) => { + setupError() + await expect(action()).rejects.toThrow(expectedError) + } + ) + }) + + describe('paginated', () => { + const createPagedMocks = (pages: number = 1) => { + if (pages === 1) { + return { + activeContracts: mockActiveContracts, + activeAtOffset: 100, + nextPageToken: '', + } + } + return Array.from({ length: pages }, (_, i) => ({ + activeContracts: [mockActiveContracts[i]], + activeAtOffset: 100 * (i + 1), + nextPageToken: i < pages - 1 ? `page${i + 2}` : '', + })) + } + + describe('raw.read', () => { + it('should read paginated active contracts directly without cache', async () => { + mockService.getPaginatedActiveContracts.mockResolvedValue( + createPagedMocks() + ) + const options = { + offset: 100, + parties: ['party1'], + templateIds: ['template1'], + } + + const result = await reader.paginated.raw.read(options) + + expect( + mockService.getPaginatedActiveContracts + ).toHaveBeenCalledWith(options) + expect(result).toEqual(mockActiveContracts) + expect(mockCacheCollection.readFromCache).not.toHaveBeenCalled() + }) + + it('should handle multiple pages when continueUntilCompletion is true', async () => { + mockService.getPaginatedActiveContracts.mockResolvedValue( + createPagedMocks(2) + ) + + const result = await reader.paginated.raw.read({ + offset: 100, + parties: ['party1'], + continueUntilCompletion: true, + }) + + expect(result).toEqual(mockActiveContracts) + }) + + it('should resolve offset when not provided', async () => { + ledgerProvider.request.mockResolvedValue({ offset: 500 }) + await reader.paginated.raw.read({ + parties: ['party1'], + templateIds: ['template1'], + }) + + expectLedgerEndCalled() + expect( + mockService.getPaginatedActiveContracts + ).toHaveBeenCalledWith(expect.objectContaining({ offset: 500 })) + }) + + it('should handle empty page results', async () => { + mockService.getPaginatedActiveContracts.mockResolvedValue({ + activeContracts: [], + activeAtOffset: 100, + nextPageToken: '', + }) + expect( + await reader.paginated.raw.read({ + offset: 100, + parties: ['party1'], + }) + ).toEqual([]) + }) + + it.each([ + ['pageToken', { pageToken: 'customToken' }], + ['maxPageSize', { maxPageSize: 50 }], + ])('should include %s when provided', async (_, extraOptions) => { + const options = { + offset: 100, + parties: ['party1'], + ...extraOptions, + } + await reader.paginated.raw.read(options) + expect( + mockService.getPaginatedActiveContracts + ).toHaveBeenCalledWith(options) + }) + }) + + describe('raw.readJsContracts', () => { + it('should read and transform paginated contracts to JS contracts', async () => { + mockService.getPaginatedActiveContracts.mockResolvedValue( + createPagedMocks() + ) + + const result = await reader.paginated.raw.readJsContracts({ + offset: 100, + parties: ['party1'], + templateIds: ['template1'], + }) + + expect(result).toHaveLength(2) + expect(result[0]).toMatchObject({ + contractId: 'contract-1', + synchronizerId: 'sync1', + }) + }) + + it('should handle multiple pages and flatten results', async () => { + mockService.getPaginatedActiveContracts.mockResolvedValue( + createPagedMocks(2) + ) + + const result = await reader.paginated.raw.readJsContracts({ + offset: 100, + parties: ['party1'], + continueUntilCompletion: true, + }) + + expect(result).toHaveLength(2) + expect(result.map((c) => c.contractId)).toEqual([ + 'contract-1', + 'contract-2', + ]) + }) + + it('should filter out contracts without JsActiveContract', async () => { + mockService.getPaginatedActiveContracts.mockResolvedValue({ + activeContracts: [ + ...mockActiveContracts, + { workflowId: 'wf3', contractEntry: null }, + ], + activeAtOffset: 100, + nextPageToken: '', + }) + + const result = await reader.paginated.raw.readJsContracts({ + offset: 100, + parties: ['party1'], + }) + expect(result).toHaveLength(2) + }) + }) + + describe('read', () => { + it('should read paginated active contracts from cache', async () => { + const options = { + offset: 100, + parties: ['party1'], + templateIds: ['template1'], + } + const result = await reader.paginated.read(options) + + expect(mockCacheCollection.readFromCache).toHaveBeenCalledWith( + options + ) + expect(result).toEqual(mockActiveContracts) + expect( + mockService.getPaginatedActiveContracts + ).not.toHaveBeenCalled() + }) + + it('should resolve offset when not provided', async () => { + ledgerProvider.request.mockResolvedValue({ offset: 750 }) + await reader.paginated.read({ + parties: ['party1'], + templateIds: ['template1'], + }) + + expectLedgerEndCalled() + expect(mockCacheCollection.readFromCache).toHaveBeenCalledWith( + expect.objectContaining({ offset: 750 }) + ) + }) + + it.each([ + ['pageToken', { pageToken: 'token123' }], + ['maxPageSize', { maxPageSize: 100 }], + ])('should work with %s option', async (_, extraOptions) => { + const options = { + offset: 200, + parties: ['party1'], + ...extraOptions, + } + await reader.paginated.read(options) + expect(mockCacheCollection.readFromCache).toHaveBeenCalledWith( + options + ) + }) + + it('should handle empty cache results', async () => { + mockCacheCollection.readFromCache.mockResolvedValue([]) + expect( + await reader.paginated.read({ + offset: 100, + parties: ['party1'], + }) + ).toEqual([]) + }) + }) + + describe('readJsContracts', () => { + it('should read paginated from cache and transform to JS contracts', async () => { + const result = await reader.paginated.readJsContracts({ + offset: 100, + parties: ['party1'], + templateIds: ['template1'], + }) + + expect(result).toHaveLength(2) + expect(result[0]).toMatchObject({ + contractId: 'contract-1', + synchronizerId: 'sync1', + }) + }) + + it('should filter out contracts without JsActiveContract from cache', async () => { + mockCacheCollection.readFromCache.mockResolvedValue([ + mockActiveContracts[0], + { workflowId: 'wf3', contractEntry: null }, + mockActiveContracts[1], + ]) + + const result = await reader.paginated.readJsContracts({ + offset: 100, + parties: ['party1'], + }) + expect(result.map((c) => c.contractId)).toEqual([ + 'contract-1', + 'contract-2', + ]) + }) + + it('should resolve offset before reading from cache', async () => { + ledgerProvider.request.mockResolvedValue({ offset: 999 }) + await reader.paginated.readJsContracts({ + parties: ['party1'], + templateIds: ['template1'], + }) + + expectLedgerEndCalled() + expect(mockCacheCollection.readFromCache).toHaveBeenCalledWith( + expect.objectContaining({ offset: 999 }) + ) + }) + + it('should return empty array when cache is empty', async () => { + mockCacheCollection.readFromCache.mockResolvedValue([]) + expect( + await reader.paginated.readJsContracts({ + offset: 100, + parties: ['party1'], + }) + ).toEqual([]) + }) + }) + + describe('error handling', () => { + it.each([ + [ + 'paginated service', + () => + mockService.getPaginatedActiveContracts.mockRejectedValue( + new Error('Paginated service error') + ), + () => + reader.paginated.raw.read({ + offset: 100, + parties: ['party1'], + }), + 'Paginated service error', + ], + [ + 'paginated cache', + () => + mockCacheCollection.readFromCache.mockRejectedValue( + new Error('Paginated cache error') + ), + () => + reader.paginated.read({ + offset: 100, + parties: ['party1'], + }), + 'Paginated cache error', + ], + [ + 'ledger-end in paginated mode', + () => + ledgerProvider.request.mockRejectedValue( + new Error('Ledger error in paginated') + ), + () => reader.paginated.read({ parties: ['party1'] }), + 'Ledger error in paginated', + ], + ])( + 'should propagate errors from %s', + async (_, setupError, action, expectedError) => { + setupError() + await expect(action()).rejects.toThrow(expectedError) + } + ) + }) + }) +}) diff --git a/core/acs-reader/src/__test__/service.test.ts b/core/acs-reader/src/__test__/service.test.ts new file mode 100644 index 000000000..329650ee7 --- /dev/null +++ b/core/acs-reader/src/__test__/service.test.ts @@ -0,0 +1,1218 @@ +// Copyright (c) 2025-2026 Digital Asset (Switzerland) GmbH and/or its affiliates. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { + AcsService, + awaitCompletion, + buildActiveContractFilter, + promiseWithTimeout, +} from '../service' +import { PaginatedACSCache } from '../cache/item' + +const ledgerProvider = vi.hoisted(() => ({ + request: vi.fn(), +})) + +describe('service', () => { + let service: AcsService + + const mockActiveContracts = [ + { + workflowId: 'wf1', + contractEntry: { + JsActiveContract: { + createdEvent: { + contractId: 'contract-1', + templateId: 'template1', + contractKey: null, + createArguments: {}, + createdAt: '2024-01-01T00:00:00Z', + signatories: ['party1'], + observers: [], + }, + synchronizerId: 'sync1', + reassignmentCounter: 0, + }, + }, + }, + ] + + beforeEach(() => { + vi.clearAllMocks() + ledgerProvider.request.mockResolvedValue(mockActiveContracts) + + service = new AcsService(ledgerProvider) + }) + + describe('getActiveContracts', () => { + it('should fetch active contracts with basic options', async () => { + const options = { + offset: 100, + parties: ['party1'], + templateIds: ['template1'], + } + + const result = await service.getActiveContracts(options) + + expect(ledgerProvider.request).toHaveBeenCalledWith({ + method: 'ledgerApi', + params: { + resource: '/v2/state/active-contracts', + requestMethod: 'post', + body: expect.objectContaining({ + activeAtOffset: 100, + verbose: false, + }), + query: {}, + }, + }) + expect(result).toEqual(mockActiveContracts) + }) + + it('should include limit in query when provided', async () => { + const options = { + offset: 100, + parties: ['party1'], + limit: 50, + } + + await service.getActiveContracts(options) + + expect(ledgerProvider.request).toHaveBeenCalledWith({ + method: 'ledgerApi', + params: { + resource: '/v2/state/active-contracts', + requestMethod: 'post', + body: expect.anything(), + query: { limit: 50 }, + }, + }) + }) + + it('should use default limit of 200 when continueUntilCompletion is true', async () => { + ledgerProvider.request.mockResolvedValueOnce({ offset: 1000 }) + + const options = { + offset: 100, + parties: ['party1'], + continueUntilCompletion: true, + } + + await service.getActiveContracts(options) + + expect(ledgerProvider.request).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'ledgerApi', + params: expect.objectContaining({ + resource: '/v2/updates', + query: { limit: 200 }, + }), + }) + ) + }) + + it('should use custom limit when continueUntilCompletion is true', async () => { + ledgerProvider.request.mockResolvedValueOnce({ offset: 1000 }) + + const options = { + offset: 100, + parties: ['party1'], + continueUntilCompletion: true, + limit: 150, + } + + await service.getActiveContracts(options) + + expect(ledgerProvider.request).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'ledgerApi', + params: expect.objectContaining({ + resource: '/v2/updates', + query: { limit: 150 }, + }), + }) + ) + }) + + it('should scan whole ledger when continueUntilCompletion is true', async () => { + ledgerProvider.request + .mockResolvedValueOnce({ offset: 200 }) + .mockResolvedValueOnce([ + { + update: { + Transaction: { + value: { + offset: 200, + workflowId: 'wf1', + synchronizerId: 'sync1', + events: [ + { + CreatedEvent: { + contractId: 'contract-1', + templateId: { + value: 'template1', + }, + contractKey: null, + createArgument: { + owner: 'party1', + }, + createdAt: + '2024-01-01T00:00:00Z', + signatories: ['party1'], + observers: [], + }, + }, + ], + }, + }, + }, + }, + ]) + + const options = { + offset: 100, + parties: ['party1'], + continueUntilCompletion: true, + filterByParty: true, + } + + const result = await service.getActiveContracts(options) + + expect(ledgerProvider.request).toHaveBeenCalledTimes(2) + expect(result).toHaveLength(1) + expect(result[0].contractEntry).toHaveProperty('JsActiveContract') + }) + + it('should handle archived events when scanning ledger', async () => { + ledgerProvider.request + .mockResolvedValueOnce({ offset: 300 }) + .mockResolvedValueOnce([ + { + update: { + Transaction: { + value: { + offset: 200, + workflowId: 'wf1', + synchronizerId: 'sync1', + events: [ + { + CreatedEvent: { + contractId: 'contract-1', + templateId: { + value: 'template1', + }, + contractKey: null, + createArgument: { + owner: 'party1', + }, + createdAt: + '2024-01-01T00:00:00Z', + signatories: ['party1'], + observers: [], + }, + }, + ], + }, + }, + }, + }, + { + update: { + Transaction: { + value: { + offset: 300, + workflowId: 'wf2', + synchronizerId: 'sync1', + events: [ + { + ArchivedEvent: { + contractId: 'contract-1', + templateId: { + value: 'template1', + }, + }, + }, + ], + }, + }, + }, + }, + ]) + + const options = { + offset: 100, + parties: ['party1'], + continueUntilCompletion: true, + filterByParty: true, + } + + const result = await service.getActiveContracts(options) + + expect(result).toHaveLength(0) + }) + + it('should handle consuming exercised events when scanning ledger', async () => { + ledgerProvider.request + .mockResolvedValueOnce({ offset: 300 }) + .mockResolvedValueOnce([ + { + update: { + Transaction: { + value: { + offset: 200, + workflowId: 'wf1', + synchronizerId: 'sync1', + events: [ + { + CreatedEvent: { + contractId: 'contract-1', + templateId: { + value: 'template1', + }, + contractKey: null, + createArgument: { + owner: 'party1', + }, + createdAt: + '2024-01-01T00:00:00Z', + signatories: ['party1'], + observers: [], + }, + }, + ], + }, + }, + }, + }, + { + update: { + Transaction: { + value: { + offset: 300, + workflowId: 'wf2', + synchronizerId: 'sync1', + events: [ + { + ExercisedEvent: { + contractId: 'contract-1', + templateId: { + value: 'template1', + }, + consuming: true, + }, + }, + ], + }, + }, + }, + }, + ]) + + const options = { + offset: 100, + parties: ['party1'], + continueUntilCompletion: true, + filterByParty: true, + } + + const result = await service.getActiveContracts(options) + + expect(result).toHaveLength(0) + }) + + it('should not remove contracts with non-consuming exercised events', async () => { + ledgerProvider.request + .mockResolvedValueOnce({ offset: 300 }) + .mockResolvedValueOnce([ + { + update: { + Transaction: { + value: { + offset: 200, + workflowId: 'wf1', + synchronizerId: 'sync1', + events: [ + { + CreatedEvent: { + contractId: 'contract-1', + templateId: { + value: 'template1', + }, + contractKey: null, + createArgument: { + owner: 'party1', + }, + createdAt: + '2024-01-01T00:00:00Z', + signatories: ['party1'], + observers: [], + }, + }, + ], + }, + }, + }, + }, + { + update: { + Transaction: { + value: { + offset: 300, + workflowId: 'wf2', + synchronizerId: 'sync1', + events: [ + { + ExercisedEvent: { + contractId: 'contract-1', + templateId: { + value: 'template1', + }, + consuming: false, + }, + }, + ], + }, + }, + }, + }, + ]) + + const options = { + offset: 100, + parties: ['party1'], + continueUntilCompletion: true, + filterByParty: true, + } + + const result = await service.getActiveContracts(options) + + expect(result).toHaveLength(1) + }) + + it('should handle multiple batches when scanning ledger', async () => { + ledgerProvider.request + .mockResolvedValueOnce({ offset: 400 }) + .mockResolvedValueOnce([ + { + update: { + Transaction: { + value: { + offset: 200, + workflowId: 'wf1', + synchronizerId: 'sync1', + events: [ + { + CreatedEvent: { + contractId: 'contract-1', + templateId: { + value: 'template1', + }, + contractKey: null, + createArgument: { + owner: 'party1', + }, + createdAt: + '2024-01-01T00:00:00Z', + signatories: ['party1'], + observers: [], + }, + }, + ], + }, + }, + }, + }, + ]) + .mockResolvedValueOnce([ + { + update: { + Transaction: { + value: { + offset: 400, + workflowId: 'wf2', + synchronizerId: 'sync1', + events: [ + { + CreatedEvent: { + contractId: 'contract-2', + templateId: { + value: 'template1', + }, + contractKey: null, + createArgument: { + owner: 'party1', + }, + createdAt: + '2024-01-01T00:00:00Z', + signatories: ['party1'], + observers: [], + }, + }, + ], + }, + }, + }, + }, + ]) + + const options = { + offset: 100, + parties: ['party1'], + continueUntilCompletion: true, + filterByParty: true, + } + + const result = await service.getActiveContracts(options) + + expect(ledgerProvider.request).toHaveBeenCalledTimes(3) + expect(result).toHaveLength(2) + }) + }) + + describe('getPaginatedActiveContracts', () => { + const mockPageResponse = { + activeContracts: [ + { + workflowId: 'wf1', + contractEntry: { + JsActiveContract: { + createdEvent: { + contractId: 'contract-1', + templateId: 'template1', + contractKey: null, + createArguments: {}, + createdAt: '2024-01-01T00:00:00Z', + signatories: ['party1'], + observers: [], + offset: 100, + }, + synchronizerId: 'sync1', + reassignmentCounter: 0, + }, + }, + }, + ], + activeAtOffset: 100, + nextPageToken: '', + } + + it('should properly construct request body when specific args are provided', async () => { + ledgerProvider.request.mockResolvedValue(mockPageResponse) + + const options = { + offset: 100, + parties: ['party1'], + pageToken: 'page2Token', + maxPageSize: 50, + } + + await service.getPaginatedActiveContracts(options) + + expect(ledgerProvider.request).toHaveBeenCalledWith({ + method: 'ledgerApi', + params: { + resource: '/v2/state/active-contracts-page', + requestMethod: 'get', + body: expect.objectContaining({ + pageToken: 'page2Token', + maxPageSize: 50, + activeAtOffset: 100, + eventFormat: expect.any(Object), + }), + }, + }) + }) + + it('should include filtersByParty when filterByParty is true', async () => { + ledgerProvider.request.mockResolvedValue(mockPageResponse) + + const options = { + offset: 100, + parties: ['party1'], + templateIds: ['template1'], + filterByParty: true, + } + + await service.getPaginatedActiveContracts(options) + + expect(ledgerProvider.request).toHaveBeenCalledWith({ + method: 'ledgerApi', + params: { + resource: '/v2/state/active-contracts-page', + requestMethod: 'get', + body: expect.objectContaining({ + eventFormat: expect.objectContaining({ + filtersByParty: expect.any(Object), + }), + }), + }, + }) + }) + + it('should include filtersForAnyParty when filterByParty is false', async () => { + ledgerProvider.request.mockResolvedValue(mockPageResponse) + + const options = { + offset: 100, + templateIds: ['template1'], + } + + await service.getPaginatedActiveContracts(options) + + expect(ledgerProvider.request).toHaveBeenCalledWith({ + method: 'ledgerApi', + params: { + resource: '/v2/state/active-contracts-page', + requestMethod: 'get', + body: expect.objectContaining({ + eventFormat: expect.objectContaining({ + filtersForAnyParty: expect.any(Object), + }), + }), + }, + }) + }) + + it('should fetch all pages when continueUntilCompletion is true', async () => { + ledgerProvider.request + .mockResolvedValueOnce({ + activeContracts: [mockPageResponse.activeContracts[0]], + activeAtOffset: 100, + nextPageToken: 'page2', + }) + .mockResolvedValueOnce({ + activeContracts: [ + { + workflowId: 'wf2', + contractEntry: { + JsActiveContract: { + createdEvent: { + contractId: 'contract-2', + templateId: 'template1', + contractKey: null, + createArguments: {}, + createdAt: '2024-01-01T00:00:00Z', + signatories: ['party1'], + observers: [], + offset: 200, + }, + synchronizerId: 'sync1', + reassignmentCounter: 0, + }, + }, + }, + ], + activeAtOffset: 200, + nextPageToken: 'page3', + }) + .mockResolvedValueOnce({ + activeContracts: [ + { + workflowId: 'wf3', + contractEntry: { + JsActiveContract: { + createdEvent: { + contractId: 'contract-3', + templateId: 'template1', + contractKey: null, + createArguments: {}, + createdAt: '2024-01-01T00:00:00Z', + signatories: ['party1'], + observers: [], + offset: 300, + }, + synchronizerId: 'sync1', + reassignmentCounter: 0, + }, + }, + }, + ], + activeAtOffset: 300, + nextPageToken: '', + }) + + const options = { + offset: 100, + parties: ['party1'], + templateIds: ['template1'], + continueUntilCompletion: true, + } + + const result = await service.getPaginatedActiveContracts(options) + + expect(ledgerProvider.request).toHaveBeenCalledTimes(3) + expect(Array.isArray(result)).toBe(true) + expect(result).toHaveLength(3) + if (Array.isArray(result)) { + expect(result[0].activeAtOffset).toBe(100) + expect(result[1].activeAtOffset).toBe(200) + expect(result[2].activeAtOffset).toBe(300) + } + }) + + it('should stop pagination when nextPageToken is empty', async () => { + ledgerProvider.request + .mockResolvedValueOnce({ + activeContracts: [mockPageResponse.activeContracts[0]], + activeAtOffset: 100, + nextPageToken: 'page2', + }) + .mockResolvedValueOnce({ + activeContracts: [], + activeAtOffset: 200, + nextPageToken: '', + }) + + const options = { + offset: 100, + parties: ['party1'], + continueUntilCompletion: true, + } + + const result = await service.getPaginatedActiveContracts(options) + + expect(ledgerProvider.request).toHaveBeenCalledTimes(2) + expect(Array.isArray(result)).toBe(true) + expect(result).toHaveLength(2) + }) + + it('should use correct pageToken in subsequent requests', async () => { + ledgerProvider.request + .mockResolvedValueOnce({ + activeContracts: [], + activeAtOffset: 100, + nextPageToken: 'customToken1', + }) + .mockResolvedValueOnce({ + activeContracts: [], + activeAtOffset: 200, + nextPageToken: 'customToken2', + }) + .mockResolvedValueOnce({ + activeContracts: [], + activeAtOffset: 300, + nextPageToken: '', + }) + + const options = { + offset: 100, + parties: ['party1'], + continueUntilCompletion: true, + } + + await service.getPaginatedActiveContracts(options) + + expect(ledgerProvider.request).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + params: expect.objectContaining({ + body: expect.not.objectContaining({ + pageToken: PaginatedACSCache.FIRST_PAGE_TOKEN, + }), + }), + }) + ) + expect(ledgerProvider.request).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + params: expect.objectContaining({ + body: expect.objectContaining({ + pageToken: 'customToken1', + }), + }), + }) + ) + expect(ledgerProvider.request).toHaveBeenNthCalledWith( + 3, + expect.objectContaining({ + params: expect.objectContaining({ + body: expect.objectContaining({ + pageToken: 'customToken2', + }), + }), + }) + ) + }) + }) + + describe('buildActiveContractFilter', () => { + it('should build filter with template IDs', () => { + const filter = buildActiveContractFilter({ + offset: 100, + templateIds: ['template1', 'template2'], + }) + + expect(filter.activeAtOffset).toBe(100) + expect(filter.verbose).toBe(false) + expect(filter.filter?.filtersForAnyParty?.cumulative).toHaveLength( + 2 + ) + expect( + filter.filter?.filtersForAnyParty?.cumulative?.[0] + ?.identifierFilter + ).toHaveProperty('TemplateFilter') + }) + + it('should build filter with interface IDs', () => { + const filter = buildActiveContractFilter({ + offset: 100, + interfaceIds: ['interface1', 'interface2'], + }) + + expect(filter.activeAtOffset).toBe(100) + expect(filter.filter?.filtersForAnyParty?.cumulative).toHaveLength( + 2 + ) + expect( + filter.filter?.filtersForAnyParty?.cumulative?.[0] + ?.identifierFilter + ).toHaveProperty('InterfaceFilter') + }) + + it('should build filter by party with template IDs', () => { + const filter = buildActiveContractFilter({ + offset: 100, + parties: ['party1', 'party2'], + templateIds: ['template1'], + filterByParty: true, + }) + + expect(filter.filter?.filtersByParty).toHaveProperty('party1') + expect(filter.filter?.filtersByParty).toHaveProperty('party2') + expect( + filter.filter?.filtersByParty?.['party1']?.cumulative + ).toHaveLength(1) + }) + + it('should build filter by party with interface IDs', () => { + const filter = buildActiveContractFilter({ + offset: 100, + parties: ['party1'], + interfaceIds: ['interface1'], + filterByParty: true, + }) + + expect(filter.filter?.filtersByParty).toHaveProperty('party1') + expect( + filter.filter?.filtersByParty?.['party1']?.cumulative?.[0] + ?.identifierFilter + ).toHaveProperty('InterfaceFilter') + }) + + it('should use empty cumulative filter when filterByParty is true without templates or interfaces', () => { + const filter = buildActiveContractFilter({ + offset: 100, + parties: ['party1'], + filterByParty: true, + }) + + expect(filter.filter?.filtersByParty).toHaveProperty('party1') + expect( + filter.filter?.filtersByParty?.['party1']?.cumulative + ).toHaveLength(0) + }) + + it('should include template metadata in filter', () => { + const filter = buildActiveContractFilter({ + offset: 100, + templateIds: ['template1'], + }) + + const identifierFilter = + filter.filter?.filtersForAnyParty?.cumulative?.[0] + ?.identifierFilter + + expect(identifierFilter).toBeDefined() + if (identifierFilter && 'TemplateFilter' in identifierFilter) { + expect(identifierFilter.TemplateFilter.value.templateId).toBe( + 'template1' + ) + expect( + identifierFilter.TemplateFilter.value + .includeCreatedEventBlob + ).toBe(true) + } else { + throw new Error('Expected TemplateFilter in identifierFilter') + } + }) + + it('should include interface metadata in filter', () => { + const filter = buildActiveContractFilter({ + offset: 100, + interfaceIds: ['interface1'], + }) + + const identifierFilter = + filter.filter?.filtersForAnyParty?.cumulative?.[0] + ?.identifierFilter + + expect(identifierFilter).toBeDefined() + if (identifierFilter && 'InterfaceFilter' in identifierFilter) { + expect(identifierFilter.InterfaceFilter.value.interfaceId).toBe( + 'interface1' + ) + expect( + identifierFilter.InterfaceFilter.value + .includeCreatedEventBlob + ).toBe(true) + expect( + identifierFilter.InterfaceFilter.value.includeInterfaceView + ).toBe(true) + } else { + throw new Error('Expected InterfaceFilter in identifierFilter') + } + }) + + it('should handle offset 0', () => { + const filter = buildActiveContractFilter({ + offset: 0, + templateIds: ['template1'], + }) + + expect(filter.activeAtOffset).toBe(0) + }) + + it('should set activeAtOffset correctly', () => { + const filter = buildActiveContractFilter({ + offset: 12345, + parties: ['party1'], + filterByParty: true, + }) + + expect(filter.activeAtOffset).toBe(12345) + }) + }) + + describe('awaitCompletion', () => { + it('should return completion when found immediately', async () => { + const mockCompletion = { + completionResponse: { + Completion: { + value: { + userId: 'user1', + commandId: 'cmd1', + submissionId: 'sub1', + offset: 200, + status: { code: 0, message: 'Success' }, + updateId: 'update1', + synchronizerId: 'sync1', + recordTime: '2024-01-01T00:00:00Z', + }, + }, + }, + } + + ledgerProvider.request.mockResolvedValue([mockCompletion]) + + const result = await awaitCompletion( + ledgerProvider, + 100, + 'party1', + 'user1', + 'cmd1' + ) + + expect(ledgerProvider.request).toHaveBeenCalledWith({ + method: 'ledgerApi', + params: { + resource: '/v2/commands/completions', + requestMethod: 'post', + body: { + userId: 'user1', + parties: ['party1'], + beginExclusive: 100, + }, + query: { + limit: 100, + stream_idle_timeout_ms: 1000, + }, + }, + }) + expect(result.commandId).toBe('cmd1') + }) + + it('should match by submissionId when provided', async () => { + const mockCompletion = { + completionResponse: { + Completion: { + value: { + userId: 'user1', + commandId: 'cmd1', + submissionId: 'sub1', + offset: 200, + status: { code: 0, message: 'Success' }, + updateId: 'update1', + synchronizerId: 'sync1', + recordTime: '2024-01-01T00:00:00Z', + }, + }, + }, + } + + ledgerProvider.request.mockResolvedValue([mockCompletion]) + + const result = await awaitCompletion( + ledgerProvider, + 100, + 'party1', + 'user1', + 'sub1' + ) + + expect(result.submissionId).toBe('sub1') + }) + + it('should retry when completion not found', async () => { + const otherCompletion = { + completionResponse: { + Completion: { + value: { + userId: 'user1', + commandId: 'other-cmd', + submissionId: 'other-sub', + offset: 150, + status: { code: 0, message: 'Success' }, + updateId: 'update1', + synchronizerId: 'sync1', + recordTime: '2024-01-01T00:00:00Z', + }, + }, + }, + } + + const wantedCompletion = { + completionResponse: { + Completion: { + value: { + userId: 'user1', + commandId: 'cmd1', + submissionId: 'sub1', + offset: 200, + status: { code: 0, message: 'Success' }, + updateId: 'update1', + synchronizerId: 'sync1', + recordTime: '2024-01-01T00:00:00Z', + }, + }, + }, + } + + ledgerProvider.request + .mockResolvedValueOnce([otherCompletion]) + .mockResolvedValueOnce([wantedCompletion]) + + const result = await awaitCompletion( + ledgerProvider, + 100, + 'party1', + 'user1', + 'cmd1' + ) + + expect(ledgerProvider.request).toHaveBeenCalledTimes(2) + expect(result.commandId).toBe('cmd1') + }) + + it('should throw error when completion status code is not 0', async () => { + const failedCompletion = { + completionResponse: { + Completion: { + value: { + userId: 'user1', + commandId: 'cmd1', + submissionId: 'sub1', + offset: 200, + status: { code: 1, message: 'Command failed' }, + updateId: 'update1', + synchronizerId: 'sync1', + recordTime: '2024-01-01T00:00:00Z', + }, + }, + }, + } + + ledgerProvider.request.mockResolvedValue([failedCompletion]) + + await expect( + awaitCompletion(ledgerProvider, 100, 'party1', 'user1', 'cmd1') + ).rejects.toThrow('Command failed with status code 1') + }) + + it('should use last completion offset for retry when no match found', async () => { + const completion1 = { + completionResponse: { + Completion: { + value: { + userId: 'user1', + commandId: 'other1', + submissionId: 'other1', + offset: 150, + status: { code: 0 }, + }, + }, + }, + } + + const completion2 = { + completionResponse: { + Completion: { + value: { + userId: 'user1', + commandId: 'other2', + submissionId: 'other2', + offset: 175, + status: { code: 0 }, + }, + }, + }, + } + + const wantedCompletion = { + completionResponse: { + Completion: { + value: { + userId: 'user1', + commandId: 'cmd1', + submissionId: 'sub1', + offset: 200, + status: { code: 0 }, + }, + }, + }, + } + + ledgerProvider.request + .mockResolvedValueOnce([completion1, completion2]) + .mockResolvedValueOnce([wantedCompletion]) + + await awaitCompletion( + ledgerProvider, + 100, + 'party1', + 'user1', + 'cmd1' + ) + + expect(ledgerProvider.request).toHaveBeenNthCalledWith(2, { + method: 'ledgerApi', + params: { + resource: '/v2/commands/completions', + requestMethod: 'post', + body: { + userId: 'user1', + parties: ['party1'], + beginExclusive: 175, + }, + query: { + limit: 100, + stream_idle_timeout_ms: 1000, + }, + }, + }) + }) + + it('should use original ledger end when response is empty', async () => { + const wantedCompletion = { + completionResponse: { + Completion: { + value: { + userId: 'user1', + commandId: 'cmd1', + submissionId: 'sub1', + offset: 200, + status: { code: 0 }, + }, + }, + }, + } + + ledgerProvider.request + .mockResolvedValueOnce([]) + .mockResolvedValueOnce([wantedCompletion]) + + await awaitCompletion( + ledgerProvider, + 100, + 'party1', + 'user1', + 'cmd1' + ) + + expect(ledgerProvider.request).toHaveBeenNthCalledWith(2, { + method: 'ledgerApi', + params: { + resource: '/v2/commands/completions', + requestMethod: 'post', + body: { + userId: 'user1', + parties: ['party1'], + beginExclusive: 100, + }, + query: { + limit: 100, + stream_idle_timeout_ms: 1000, + }, + }, + }) + }) + }) + + describe('promiseWithTimeout', () => { + it('should resolve when promise completes before timeout', async () => { + const promise = Promise.resolve('success') + + const result = await promiseWithTimeout(promise, 1000, 'Timeout') + + expect(result).toBe('success') + }) + + it('should reject with error message when timeout is reached', async () => { + const promise = new Promise((resolve) => + setTimeout(() => resolve('success'), 100) + ) + + await expect( + promiseWithTimeout(promise, 10, 'Timeout error') + ).rejects.toBe('Timeout error') + }) + + it('should clear timeout when promise resolves', async () => { + const clearTimeoutSpy = vi.spyOn(globalThis, 'clearTimeout') + const promise = Promise.resolve('success') + + await promiseWithTimeout(promise, 1000, 'Timeout') + + expect(clearTimeoutSpy).toHaveBeenCalled() + clearTimeoutSpy.mockRestore() + }) + + it('should clear timeout when promise rejects', async () => { + const clearTimeoutSpy = vi.spyOn(globalThis, 'clearTimeout') + const promise = Promise.reject(new Error('Promise error')) + + await expect( + promiseWithTimeout(promise, 1000, 'Timeout') + ).rejects.toThrow('Promise error') + + expect(clearTimeoutSpy).toHaveBeenCalled() + clearTimeoutSpy.mockRestore() + }) + + it('should handle promise rejection before timeout', async () => { + const promise = Promise.reject(new Error('Failed')) + + await expect( + promiseWithTimeout(promise, 1000, 'Timeout') + ).rejects.toThrow('Failed') + }) + + it('should handle immediate promise resolution', async () => { + const promise = Promise.resolve(42) + + const result = await promiseWithTimeout(promise, 1000, 'Timeout') + + expect(result).toBe(42) + }) + }) +}) diff --git a/core/acs-reader/src/reader/reader.ts b/core/acs-reader/src/reader/reader.ts index 0f3cc3eb9..47423136a 100644 --- a/core/acs-reader/src/reader/reader.ts +++ b/core/acs-reader/src/reader/reader.ts @@ -26,7 +26,7 @@ class RawReader extends BaseReader { super(ledger, cacheOptions) } - public async read( + public override async read( options: AcsOptions ): Promise { const resolvedOptions = await this.resolveAcsOptions(options) diff --git a/core/acs-reader/vitest.config.ts b/core/acs-reader/vitest.config.ts index 75c772c1b..d52fa766e 100644 --- a/core/acs-reader/vitest.config.ts +++ b/core/acs-reader/vitest.config.ts @@ -11,10 +11,10 @@ export default defineConfig({ provider: 'v8', reporter: ['text', 'html', 'lcov', 'json-summary'], thresholds: { - lines: 0, - functions: 0, - branches: 0, - statements: 0, + lines: 80, + functions: 80, + branches: 70, + statements: 80, }, }, environment: 'node', diff --git a/sdk/wallet-sdk/src/wallet/namespace/ledger/namespace.ts b/sdk/wallet-sdk/src/wallet/namespace/ledger/namespace.ts index 43ae6c662..06002fef4 100644 --- a/sdk/wallet-sdk/src/wallet/namespace/ledger/namespace.ts +++ b/sdk/wallet-sdk/src/wallet/namespace/ledger/namespace.ts @@ -10,7 +10,7 @@ import { Ops } from '@canton-network/core-provider-ledger' import { DarNamespace } from './dar/client.js' import { InternalLedgerNamespace } from './internal/index.js' import { PreparedTransactionNamespace } from './hash/namespace.js' -import { AcsOptions, ACSReader } from '@canton-network/core-acs-reader' +import { ACSReader } from '@canton-network/core-acs-reader' export class LedgerNamespace { public readonly dar: DarNamespace @@ -182,14 +182,9 @@ export class LedgerNamespace { readRaw: async ( options: AcsRequestOptions ): Promise> => { - const resolvedOptions = await this.resolveAcsOptions(options) + this.sdkContext.logger.debug(options, `Querying acs with options:`) - this.sdkContext.logger.debug( - resolvedOptions, - `Querying acs with options:` - ) - - return await this.acsReader.raw.read(resolvedOptions) + return await this.acsReader.raw.read(options) }, /** * Queries the ACS and filters for JsActiveContracts @@ -218,27 +213,4 @@ export class LedgerNamespace { }) }, } - - /** - * @deprecated - */ - 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 } - } }