// SPDX-FileCopyrightText: © 2024 LEDGER SAS // SPDX-License-Identifier: Apache-2.0 import type { CurrencyConfig } from '../../config' import type { CoinModuleApi, Memo, TxData } from '../types' import { apiMethodKeys } from '../impl' /** Narrows a copied member to a callable without asserting its type. */ const isMethod = (value: unknown): value is (...args: unknown[]) => unknown => typeof value === 'function' /** * Returns a copy of a {@link CoinModuleApi} whose contract methods route through the * provided `instrument` function. Everything else the object carries — non-API members * such as the legacy bridge surface, or `supports` — is copied over untouched. * * Exported so other wrappers (withLogging, withTracing, withMetrics, …) can reuse the * same interception mechanism without duplicating the boilerplate. * * The wrapped set is {@link apiMethodKeys}, the contract's own method list, rather than * "every property that happens to be a function": what gets instrumented is then readable * from the source, and a method added to the contract cannot be silently skipped — the * exhaustiveness guard next to that list fails to compile first. * * Each method is wrapped and bound once, eagerly, so repeated reads are referentially * stable (`api.getBalance === api.getBalance`) — a consumer using a method as a dependency * key, a React effect or a memo cache, would otherwise see a new function every time — and * no allocation happens on the call path. * * Expects a plain object, which is what a `createApi()` result and a `withDefaults` result * both are. Methods living on a prototype rather than on the object itself are copied by * value here, so a class instance that reassigns its own methods after wrapping would not * see the change. */ export function instrumentApi< C extends CurrencyConfig, M extends Memo, T extends TxData, Extra extends object = Record, >( api: CoinModuleApi & Extra, instrument: (name: string, fn: (...args: unknown[]) => unknown, args: unknown[]) => unknown ): CoinModuleApi & Extra { const instrumented: Record = { ...api } for (const name of apiMethodKeys) { const method = instrumented[name] // A contract method the object does not carry: `listFeeOptions`, which withDefaults // leaves absent, or any method on a value that never went through withDefaults. if (!isMethod(method)) continue const bound = method.bind(api) instrumented[name] = (...args: unknown[]): unknown => instrument(name, bound, args) } // oxlint-disable-next-line consistent-type-assertions -- every contract method was replaced by a wrapper of the same arity and the rest was copied verbatim, so the shape is unchanged; TS cannot follow that through an index-signature write return instrumented as CoinModuleApi & Extra }