// SPDX-FileCopyrightText: © 2024 LEDGER SAS // SPDX-License-Identifier: Apache-2.0 import type { CurrencyConfig } from '../config' import type { CoinModuleImpl, OptionalApiKey } from './impl' import type { CoinModuleApi, Memo, MemoNotSupported, TxData, TxDataNotSupported } from './types' import { optionalApiKeys } from './impl' import { isNotSupportedStub, notSupported } from './notSupported' /** * What {@link withDefaults} returns: the full consumer contract, plus `supports()`. * * `getAccountInfo` is narrowed to required. It is the one managed capability that * {@link CoinModuleApi} itself declares optional, and `withDefaults` always supplies it — so * callers can invoke it without a non-null assertion, and read `type` to tell a real answer from * the `{ type: 'none' }` sentinel. */ export type WithDefaultsResult< ConfigType extends CurrencyConfig, MemoType extends Memo = MemoNotSupported, TxDataType extends TxData = TxDataNotSupported, > = CoinModuleApi & Required, 'getAccountInfo'>> & { supports: (method: OptionalApiKey) => boolean } /** * Normalizes an authored capability method to "present" or "absent". * * A `notSupported()` stub counts as absent: it is a placeholder, not an implementation, * so the framework's own default takes over. Behavior is unchanged either way (both throw * the same `" is not supported"`), but `supports()` then reports the truth. */ function implemented(value: T | undefined): T | undefined { return value === undefined || isNotSupportedStub(value) ? undefined : value } /** * Widens a {@link CoinModuleImpl} authoring value to the full {@link CoinModuleApi} * consumer contract by backfilling the capability methods the author omitted, and attaches * a `supports()` introspection helper. * * Default per omitted method: * - `getAccountInfo` → a `{ type: 'none' }` sentinel (ADR-045): having no extra account * metadata is an answer, so callers get one instead of an exception. * - every other capability → `notSupported(name)`, which throws `" is not supported"`. * * Properties the authoring object carries beyond the API surface are preserved. * * A capability method is considered implemented only when the author supplied a real * function — a `notSupported()` stub is treated as if the method had been omitted. */ export function withDefaults< ConfigType extends CurrencyConfig, MemoType extends Memo = MemoNotSupported, TxDataType extends TxData = TxDataNotSupported, Extra extends object = Record, >( impl: CoinModuleImpl & Extra ): WithDefaultsResult & Extra { const implementedMethods = new Set( optionalApiKeys.filter((k) => implemented(impl[k]) !== undefined) ) // Spread first so anything the module exposes beyond the API surface survives, then // override every capability method with either the real implementation or the default. return { ...impl, getBlock: implemented(impl.getBlock) ?? notSupported('getBlock'), getBlockInfo: implemented(impl.getBlockInfo) ?? notSupported('getBlockInfo'), getStakes: implemented(impl.getStakes) ?? notSupported('getStakes'), getRewards: implemented(impl.getRewards) ?? notSupported('getRewards'), getValidators: implemented(impl.getValidators) ?? notSupported('getValidators'), call: implemented(impl.call) ?? notSupported('call'), craftRawTransaction: implemented(impl.craftRawTransaction) ?? notSupported('craftRawTransaction'), register: implemented(impl.register) ?? notSupported('register'), getNextSequence: implemented(impl.getNextSequence) ?? notSupported('getNextSequence'), validateIntent: implemented(impl.validateIntent) ?? notSupported('validateIntent'), validateAddress: implemented(impl.validateAddress) ?? notSupported('validateAddress'), // ADR-045: an absent getAccountInfo means the chain has no extra account metadata to // report, which is an answer rather than a missing capability — hence a sentinel instead // of a throwing stub. Callers read `type` and branch on it. getAccountInfo: implemented(impl.getAccountInfo) ?? (() => Promise.resolve({ type: 'none' })), supports: (method: OptionalApiKey): boolean => implementedMethods.has(method), } }