UNPKG

@ledgerhq/coin-stellar

Version:
177 lines (165 loc) 5.7 kB
// SPDX-FileCopyrightText: © 2026 LEDGER SAS // SPDX-License-Identifier: Apache-2.0 import { rejectBalanceOptions } from '@ledgerhq/coin-module-framework/api/getBalance/rejectBalanceOptions' import { CoinModuleApi, BalanceOptions, CraftedTransaction, Cursor, FeeEstimation, ListOperationsOptions, Operation, Page, Reward, Stake, TransactionIntent, Validator, } from '@ledgerhq/coin-module-framework/api/index' import { craftTransactionData } from '@ledgerhq/coin-module-framework/logic/craftTransactionData' import { xdr } from '@stellar/stellar-sdk' import coinConfig, { type StellarConfig } from '../config' import { broadcast, combine, craftTransaction, estimateFees, getBalance, getBlock, getBlockInfo, lastBlock, validateIntent, } from '../logic' import { listOperations } from '../logic' import { validateAddress } from '../logic/validateAddress' import { fetchSequence, registerHorizonInterceptors } from '../network' import { StellarMemo } from '../types' export function createApi(config: StellarConfig): CoinModuleApi<StellarMemo> { coinConfig.setCoinConfig(() => ({ ...config, status: { type: 'active' } })) registerHorizonInterceptors() return { broadcast, combine: compose, craftTransaction: craft, craftRawTransaction: ( _transaction: string, _sender: string, _publicKey: string, _sequence: bigint ): Promise<CraftedTransaction> => { throw new Error('craftRawTransaction is not supported') }, estimateFees: estimate, getBalance: (address: string, options?: BalanceOptions) => rejectBalanceOptions(() => getBalance(address), options), lastBlock, listOperations: operations, getBlock, getBlockInfo, getStakes(_address: string, _cursor?: Cursor): Promise<Page<Stake>> { throw new Error('getStakes is not supported') }, getRewards(_address: string, _cursor?: Cursor): Promise<Page<Reward>> { throw new Error('getRewards is not supported') }, validateIntent, getNextSequence: async (address: string) => { const sequence = await fetchSequence(address) return BigInt(sequence.plus(1).toFixed()) }, getValidators(_cursor?: Cursor): Promise<Page<Validator>> { throw new Error('getValidators is not supported') }, validateAddress, craftTransactionData, } } async function craft( transactionIntent: TransactionIntent<StellarMemo>, customFees?: FeeEstimation ): Promise<CraftedTransaction> { const fees = customFees?.value || (await estimateFees()) // NOTE: check how many memos, throw if more than one? // if (transactionIntent.memos && transactionIntent.memos.length > 1) { // throw new Error("Stellar only supports one memo per transaction."); // } const memo = 'memo' in transactionIntent ? transactionIntent.memo : undefined const hasMemoValue = memo && memo.type !== 'NO_MEMO' const tx = await craftTransaction( { address: transactionIntent.sender }, { type: transactionIntent.type, recipient: transactionIntent.recipient, amount: transactionIntent.amount, fee: fees, ...(transactionIntent.asset.type !== 'native' && 'assetReference' in transactionIntent.asset ? { assetCode: transactionIntent.asset.assetReference, assetIssuer: transactionIntent.asset.assetOwner, } : {}), memoType: memo?.type, ...(hasMemoValue ? { memoValue: (memo as { value: string }).value } : {}), } ) // Note: the API returns the signature base, not the full XDR, see BACK-8727 for more context return { transaction: tx.signatureBase } } function compose(tx: string, signature: string, pubkey?: string): string { if (!pubkey) { throw new Error('Missing pubkey') } // note: accept here `TransactionEnvelope` or `TransactionSignaturePayload`, see BACK-8727 for more context return combine(envelopeFromAnyXDR(tx, 'base64'), signature, pubkey) } async function estimate(_transactionIntent: TransactionIntent): Promise<FeeEstimation> { const value = await estimateFees() return { value } } async function operations( address: string, options: ListOperationsOptions ): Promise<Page<Operation>> { const { items, next } = await listOperations(address, options) return { items, next: next || undefined } } /** * Deserialize a transaction envelope, also accepting transaction signature payload form. * * @param input serialized `TransactionEnvelope` or `TransactionSignaturePayload` * @param format serialization encoding */ export function envelopeFromAnyXDR( input: string, format: 'hex' | 'base64' ): xdr.TransactionEnvelope { try { return xdr.TransactionEnvelope.fromXDR(input, format) } catch (envelopeError) { try { return signatureBaseToEnvelope(xdr.TransactionSignaturePayload.fromXDR(input, format)) } catch (signatureBaseError) { throw new Error( `Failed decoding transaction as an envelope (${envelopeError}) or as a signature base: (${signatureBaseError})` ) } } } /** * Convert a `TransactionSignaturePayload` into a `TransactionEnvelope`. * * @param signatureBase deserialized `TransactionSignaturePayload` */ function signatureBaseToEnvelope( signatureBase: xdr.TransactionSignaturePayload ): xdr.TransactionEnvelope { const tx = signatureBase.taggedTransaction().value() if (tx instanceof xdr.Transaction) { return xdr.TransactionEnvelope.envelopeTypeTx( new xdr.TransactionV1Envelope({ tx, signatures: [] }) ) } else { return xdr.TransactionEnvelope.envelopeTypeTxFeeBump( new xdr.FeeBumpTransactionEnvelope({ tx, signatures: [] }) ) } }