// SPDX-FileCopyrightText: © 2024 LEDGER SAS // SPDX-License-Identifier: Apache-2.0 import type { CurrencyConfig } from '../config' import type { Context } from '../config' import type { CoinModuleImpl } from './index' import type { Balance, BlockInfo, CraftedTransaction, FeeEstimation, ListOperationsOptions, TransactionIntent, } from './types' import { optionalApiKeys, requiredApiKeys } from './impl' type Cfg = CurrencyConfig type Ctx = Context /** * The shape a module carrying exactly `names` as methods must equal. Asserting against the * whole object rather than attribute by attribute states both halves at once: the listed * methods are present and callable, and nothing else is there. */ const exactlyMethods = (names: readonly string[]): Record => Object.fromEntries(names.map((name) => [name, expect.any(Function)])) // Required set: lastBlock, getBalance, listOperations, craftTransaction, // estimateFees, combine, broadcast, craftTransactionData. 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('CoinModuleImpl', () => { it('a module with Required methods only is a valid CoinModuleImpl', () => { // Exactly the required methods, all of them callable, and no optional capability. The // type enforces the required half at compile time; asserting it pins the set the // authoring type demands, so a method leaving that set is a visible change. expect(minimalModule).toEqual(exactlyMethods(requiredApiKeys)) }) it('getStakes may be present without getValidators (per-method staking optionality)', () => { const partial: CoinModuleImpl = { ...minimalModule, getStakes: (_ctx: Ctx, _addr: string) => Promise.resolve({ items: [] }), // getValidators intentionally absent } expect(partial).toEqual(exactlyMethods([...requiredApiKeys, 'getStakes'])) }) it('a fully-populated CoinModuleImpl exposes all CoinModuleApi optional methods', () => { const full: CoinModuleImpl = { ...minimalModule, getBlock: (_ctx: Ctx, _h: number) => Promise.reject(new Error('stub')), getBlockInfo: (_ctx: Ctx, _h: number) => Promise.reject(new Error('stub')), getStakes: (_ctx: Ctx, _addr: string) => Promise.resolve({ items: [] }), getRewards: (_ctx: Ctx, _addr: string) => Promise.resolve({ items: [] }), getValidators: (_ctx: Ctx) => Promise.resolve({ items: [] }), call: (_ctx: Ctx, _params: unknown) => Promise.resolve(null), craftRawTransaction: (_ctx: Ctx, _tx: string, _s: string, _pk: string, _seq: bigint) => Promise.reject(new Error('stub')), register: (_ctx: Ctx, _addr: string) => Promise.reject(new Error('stub')), getNextSequence: (_ctx: Ctx, _addr: string) => Promise.resolve(0n), validateIntent: (_ctx: Ctx, _intent: TransactionIntent, _balances: Balance[]) => Promise.reject(new Error('stub')), validateAddress: (_ctx: Ctx, _addr: string, _params: object) => Promise.resolve(true), getAccountInfo: (_ctx: Ctx, _addr: string) => Promise.resolve({ type: 'none' } as const), } // Runtime guard: all optional capability methods must be present when explicitly set, // confirming that withDefaults can widen this impl to a full CoinModuleApi at runtime. expect(full).toEqual(exactlyMethods([...requiredApiKeys, ...optionalApiKeys])) }) })