// SPDX-FileCopyrightText: © 2026 LEDGER SAS // SPDX-License-Identifier: Apache-2.0 import type { CoinModuleImpl } from '../api/impl' import type { BlockInfo, ListOperationsOptions, TransactionIntent } from '../api/types' import type { Context, CurrencyConfig } from '../config' import { notSupported } from '../api/notSupported' import { notSupportedApi } from '../api/notSupportedApi' import { capabilityReport } from './capabilities' type Cfg = CurrencyConfig type Ctx = Context const ctx: Ctx = { config: () => Promise.reject(new Error('stub')), logger: () => undefined } const minimalModule: CoinModuleImpl = { lastBlock: (_ctx: Ctx): Promise => Promise.reject(new Error('stub')), getBalance: (_ctx: Ctx, _addr: string) => Promise.resolve([]), listOperations: (_ctx: Ctx, _addr: string, _opts: ListOperationsOptions) => Promise.resolve({ items: [] }), craftTransaction: (_ctx: Ctx, _intent: TransactionIntent) => Promise.reject(new Error('stub')), estimateFees: (_ctx: Ctx, _intent: TransactionIntent) => Promise.reject(new Error('stub')), combine: (_ctx: Ctx, tx: string, _sig: string[]): string => tx, broadcast: (_ctx: Ctx, _tx: string) => Promise.reject(new Error('stub')), craftTransactionData: (_ctx: Ctx, _intent: TransactionIntent) => ({ type: 'none' }) as const, } // Every optional capability but getAccountInfo, which ADR-045 answers with a sentinel rather // than an error and which the report therefore leaves out. const ALL_CAPABILITIES = [ 'call', 'craftRawTransaction', 'getBlock', 'getBlockInfo', 'getNextSequence', 'getRewards', 'getStakes', 'getValidators', 'register', 'validateAddress', 'validateIntent', ] describe('capabilityReport', () => { it('lists every capability a minimal module leaves out', async () => { await expect(capabilityReport(minimalModule, ctx)).resolves.toEqual({ unsupported: ALL_CAPABILITIES, inconsistent: [], }) }) it('drops a capability from the list once the module implements it', async () => { const withStaking: CoinModuleImpl = { ...minimalModule, getStakes: (_ctx: Ctx, _addr: string) => Promise.resolve({ items: [] }), } const report = await capabilityReport(withStaking, ctx) expect(report.unsupported).not.toContain('getStakes') expect(report.unsupported).toContain('getRewards') expect(report.inconsistent).toEqual([]) }) // A module built by spreading `notSupportedApi()` carries a stub for everything, where a // `CoinModuleImpl` omits it. The report reads both the same way, since it observes what a // consumer gets rather than the shape of the authored object. it('reads a module built by spreading notSupportedApi the same way', async () => { const spread = { ...notSupportedApi(), lastBlock: minimalModule.lastBlock, getBalance: minimalModule.getBalance, getStakes: (_ctx: Ctx, _addr: string) => Promise.resolve({ items: [] }), } const report = await capabilityReport(spread, ctx) expect(report.unsupported).not.toContain('getStakes') expect(report.unsupported).toContain('getValidators') expect(report.inconsistent).toEqual([]) }) it('never invokes an implemented capability', async () => { const network = jest.fn(() => Promise.reject(new Error('should not be reached'))) await capabilityReport({ ...minimalModule, getStakes: network }, ctx) expect(network).not.toHaveBeenCalled() }) it('counts a stub that rejects asynchronously as raising the error', async () => { const asyncStub = { ...minimalModule, getRewards: (_ctx: Ctx, _addr: string) => Promise.reject(new Error('getRewards is not supported')), } const report = await capabilityReport(asyncStub, ctx) // The module fills the slot, so `supports()` calls it implemented and the report skips it — // the tagged-stub check is what tells a placeholder apart, and an inline throw cannot be // told apart from an implementation. expect(report.unsupported).not.toContain('getRewards') expect(report.inconsistent).toEqual([]) }) it('re-labels a tagged stub whose error names another method', async () => { const mislabelled = { ...minimalModule, getValidators: notSupported('getStakes') } const report = await capabilityReport(mislabelled, ctx) // A tagged stub counts as omitted, so `withDefaults` replaces it with its own — which names // the method it stands for. The mislabelling `coin-canton` shipped cannot survive the // resolver, and `inconsistent` stays empty: it only fills if a backfill goes missing. expect(report.unsupported).toContain('getValidators') expect(report.inconsistent).toEqual([]) }) })