// SPDX-FileCopyrightText: © 2024 LEDGER SAS // SPDX-License-Identifier: Apache-2.0 import type { CurrencyConfig, Context } from '../config' import type { CoinModuleImpl } from './impl' import type { Balance, BlockInfo, CraftedTransaction, FeeEstimation, ListOperationsOptions, TransactionIntent, } from './types' import { notSupported } from './notSupported' import { withDefaults } from './withDefaults' const stubIntent: TransactionIntent = { intentType: 'transaction', type: 'send', sender: 'addr', recipient: 'addr', amount: 0n, asset: { type: 'native' }, } 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 => Promise.resolve([]), listOperations: (_ctx: Ctx, _addr: string, _opts: ListOperationsOptions) => Promise.resolve({ items: [] }), craftTransaction: (_ctx: Ctx, _intent: TransactionIntent): Promise => Promise.reject(new Error('stub')), estimateFees: (_ctx: Ctx, _intent: TransactionIntent): Promise => Promise.reject(new Error('stub')), combine: (_ctx: Ctx, tx: string, _sig: string[]): string => tx, broadcast: (_ctx: Ctx, _tx: string): Promise => Promise.reject(new Error('stub')), craftTransactionData: (_ctx: Ctx, _intent: TransactionIntent) => ({ type: 'none' }) as const, } describe('withDefaults', () => { it('widens a minimal impl to the full CoinModuleApi shape', () => { const api = withDefaults(minimalModule) // Required methods forwarded from impl expect(api.lastBlock).not.toBe(undefined) expect(api.getBalance).not.toBe(undefined) expect(api.listOperations).not.toBe(undefined) expect(api.craftTransaction).not.toBe(undefined) expect(api.estimateFees).not.toBe(undefined) expect(api.combine).not.toBe(undefined) expect(api.broadcast).not.toBe(undefined) expect(api.craftTransactionData).not.toBe(undefined) // Capability methods are backfilled (never absent on result) expect(api.getBlock).not.toBe(undefined) expect(api.getBlockInfo).not.toBe(undefined) expect(api.getStakes).not.toBe(undefined) expect(api.getRewards).not.toBe(undefined) expect(api.getValidators).not.toBe(undefined) expect(api.call).not.toBe(undefined) expect(api.craftRawTransaction).not.toBe(undefined) expect(api.register).not.toBe(undefined) expect(api.getNextSequence).not.toBe(undefined) expect(api.validateIntent).not.toBe(undefined) expect(api.validateAddress).not.toBe(undefined) expect(api.getAccountInfo).not.toBe(undefined) // Introspection helper present expect(api.supports).not.toBe(undefined) }) it('backfills capability methods that throw " is not supported"', () => { const api = withDefaults(minimalModule) expect(() => api.getBlock(ctx, 1)).toThrow('getBlock is not supported') expect(() => api.getBlockInfo(ctx, 1)).toThrow('getBlockInfo is not supported') expect(() => api.getStakes(ctx, 'addr')).toThrow('getStakes is not supported') expect(() => api.call(ctx, {})).toThrow('call is not supported') expect(() => api.getNextSequence(ctx, 'addr')).toThrow('getNextSequence is not supported') expect(() => api.validateIntent(ctx, stubIntent, [])).toThrow('validateIntent is not supported') }) it('backfills validateAddress with a throwing stub rather than a permissive default', () => { const api = withDefaults(minimalModule) // A validator that silently answers "valid" for every address would let a bad // recipient through, so an omitted validateAddress fails loud like any other capability. expect(() => api.validateAddress(ctx, 'someaddr', {})).toThrow( 'validateAddress is not supported' ) }) it('backfills an omitted getAccountInfo with the {type:none} sentinel', async () => { const api = withDefaults(minimalModule) // ADR-045: no extra account metadata is an answer, not a missing capability, so this // resolves rather than throwing like the other backfilled capabilities. await expect(api.getAccountInfo(ctx, 'addr')).resolves.toEqual({ type: 'none' }) }) it('uses the impl getAccountInfo when it is provided', async () => { const implWithInfo: CoinModuleImpl = { ...minimalModule, getAccountInfo: (_ctx: Ctx, _addr: string) => Promise.resolve({ type: 'tezos', revealed: true }), } const api = withDefaults(implWithInfo) await expect(api.getAccountInfo(ctx, 'addr')).resolves.toEqual({ type: 'tezos', revealed: true, }) expect(api.supports('getAccountInfo')).toBe(true) }) it('reports an omitted getAccountInfo as unsupported', () => { const api = withDefaults(minimalModule) expect(api.supports('getAccountInfo')).toBe(false) }) it('preserves properties the impl carries beyond the API surface', () => { const implWithExtra = { ...minimalModule, extraHelper: () => 'kept' } const api = withDefaults(implWithExtra) expect(api.extraHelper()).toBe('kept') }) it('uses the impl method when it is provided instead of the backfilled default', async () => { let called = false const implWithStakes: CoinModuleImpl = { ...minimalModule, getStakes: (_ctx: Ctx, _addr: string) => { called = true return Promise.resolve({ items: [] }) }, } const api = withDefaults(implWithStakes) await api.getStakes(ctx, 'addr') expect(called).toBe(true) }) describe('supports()', () => { it('returns false for capability methods absent from the impl', () => { const api = withDefaults(minimalModule) expect(api.supports('getStakes')).toBe(false) expect(api.supports('getBlock')).toBe(false) expect(api.supports('call')).toBe(false) expect(api.supports('getNextSequence')).toBe(false) expect(api.supports('validateAddress')).toBe(false) }) it('returns true for capability methods present in the impl', () => { const implWithStakes: CoinModuleImpl = { ...minimalModule, getStakes: (_ctx: Ctx, _addr: string) => Promise.resolve({ items: [] }), } const api = withDefaults(implWithStakes) expect(api.supports('getStakes')).toBe(true) // Other optionals remain absent expect(api.supports('getBlock')).toBe(false) expect(api.supports('validateAddress')).toBe(false) }) it('returns false for a method filled with a notSupported() stub', () => { // The shape every not-yet-migrated module ships: the key is present, but it is a // placeholder that throws. Reporting it as supported would make supports() lie. const implWithStub: CoinModuleImpl = { ...minimalModule, getStakes: notSupported('getStakes'), getBlock: notSupported('getBlock'), } const api = withDefaults(implWithStub) expect(api.supports('getStakes')).toBe(false) expect(api.supports('getBlock')).toBe(false) }) it('keeps the "not supported" behavior when a stub is normalized away', () => { const implWithStub: CoinModuleImpl = { ...minimalModule, getStakes: notSupported('getStakes'), } const api = withDefaults(implWithStub) expect(() => api.getStakes(ctx, 'addr')).toThrow('getStakes is not supported') }) }) })