// SPDX-FileCopyrightText: © 2026 LEDGER SAS // SPDX-License-Identifier: Apache-2.0 import type { StellarContext } from '../../config' import type { DeepPartialReturn } from '@ledgerhq/coin-module-framework/test/utils' import { NetworkDown } from '@ledgerhq/live-network/errors' import { Memo } from '@stellar/stellar-sdk' import { getRecipientAccount, loadAccount } from '../../network' import { StellarAssetRequired, StellarMuxedAccountNotExist } from '../../types' import { craftTransaction } from '../craftTransaction' import { buildChangeTrustOperation, buildCreateAccountOperation, buildPaymentOperation, buildTransactionBuilder, } from '../sdkWrapper' jest.mock('../../network', () => ({ loadAccount: jest.fn(), getRecipientAccount: jest.fn(), })) jest.mock('../sdkWrapper', () => ({ buildChangeTrustOperation: jest.fn(), buildCreateAccountOperation: jest.fn(), buildPaymentOperation: jest.fn(), buildTransactionBuilder: jest.fn(), })) const context: StellarContext = { config: async () => ({ name: 'Stellar', unit: { name: 'Lumen', code: 'XLM', magnitude: 7 }, explorer: { url: 'https://horizon.example.com' }, status: { type: 'active' }, }), logger: jest.fn(), } describe('craftTransaction', () => { const account = { address: 'GABC' } const transaction = { type: 'payment', recipient: 'GRECIPIENT', amount: 10000000n, fee: 100n, } const craftedTransaction = { toXDR: jest.fn().mockReturnValue('tx-xdr'), signatureBase: jest.fn().mockReturnValue(Buffer.from('signature-base')), } const txBuilder = { addOperation: jest.fn(), addMemo: jest.fn(), setTimeout: jest.fn(), build: jest.fn().mockReturnValue(craftedTransaction), } beforeEach(() => { jest.clearAllMocks() txBuilder.setTimeout.mockReturnValue(txBuilder) jest .mocked>(buildTransactionBuilder) .mockReturnValue(txBuilder) jest .mocked>(loadAccount) .mockResolvedValue({ id: 'source-account' }) jest.mocked(getRecipientAccount).mockResolvedValue({ id: 'GRECIPIENT', isMuxedAccount: false, assetIds: [], }) jest .mocked>(buildPaymentOperation) .mockReturnValue({}) jest .mocked>(buildCreateAccountOperation) .mockReturnValue({}) jest .mocked>(buildChangeTrustOperation) .mockReturnValue({}) }) it('throws NetworkDown when source account cannot be loaded', async () => { jest.mocked(loadAccount).mockResolvedValue(null) await expect(craftTransaction(context, account, transaction)).rejects.toThrow(NetworkDown) }) it('throws StellarAssetRequired for changeTrust without asset code and issuer', async () => { await expect( craftTransaction(context, account, { ...transaction, type: 'changeTrust', assetCode: undefined, assetIssuer: undefined, }) ).rejects.toThrow(StellarAssetRequired) }) it('builds a changeTrust operation when type is changeTrust', async () => { const result = await craftTransaction(context, account, { ...transaction, type: 'changeTrust', assetCode: 'USDC', assetIssuer: 'GISSUER', }) expect(buildChangeTrustOperation).toHaveBeenCalledWith('USDC', 'GISSUER') expect(txBuilder.addOperation).toHaveBeenCalledWith({}) expect(getRecipientAccount).not.toHaveBeenCalled() expect(result.xdr).toBe('tx-xdr') expect(result.signatureBase).toBe('c2lnbmF0dXJlLWJhc2U=') }) it('builds a payment operation and memo when recipient exists', async () => { const memoTextSpy = jest.spyOn(Memo, 'text') await craftTransaction(context, account, { ...transaction, memoType: 'MEMO_TEXT', memoValue: 'hello', }) expect(buildPaymentOperation).toHaveBeenCalledWith({ unit: { name: 'Lumen', code: 'XLM', magnitude: 7 }, destination: 'GRECIPIENT', amount: 10000000n, assetCode: undefined, assetIssuer: undefined, }) expect(txBuilder.addOperation).toHaveBeenCalledWith({}) expect(memoTextSpy).toHaveBeenCalledWith('hello') expect(txBuilder.addMemo).toHaveBeenCalledTimes(1) }) it('throws StellarMuxedAccountNotExist when missing muxed recipient account', async () => { jest .mocked(getRecipientAccount) .mockResolvedValue({ id: null, isMuxedAccount: true, assetIds: [] }) await expect(craftTransaction(context, account, transaction)).rejects.toThrow( StellarMuxedAccountNotExist ) }) it('builds createAccount operation when recipient does not exist and is not muxed', async () => { jest .mocked(getRecipientAccount) .mockResolvedValue({ id: null, isMuxedAccount: false, assetIds: [] }) await craftTransaction(context, account, transaction) expect(buildCreateAccountOperation).toHaveBeenCalledWith( { name: 'Lumen', code: 'XLM', magnitude: 7 }, 'GRECIPIENT', 10000000n ) expect(txBuilder.addOperation).toHaveBeenCalledWith({}) }) })