@ledgerhq/coin-stellar
Version:
Ledger Stellar Coin integration
147 lines (126 loc) • 4.77 kB
text/typescript
// SPDX-FileCopyrightText: © 2026 LEDGER SAS
// SPDX-License-Identifier: Apache-2.0
import { NetworkDown } from '@ledgerhq/errors'
import { Horizon, 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(),
}))
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 as unknown as ReturnType<typeof buildTransactionBuilder>)
jest.mocked(loadAccount).mockResolvedValue({ id: 'source-account' } as Horizon.AccountResponse)
jest.mocked(getRecipientAccount).mockResolvedValue({
id: 'GRECIPIENT',
isMuxedAccount: false,
assetIds: [],
})
jest
.mocked(buildPaymentOperation)
.mockReturnValue('payment-op' as unknown as ReturnType<typeof buildPaymentOperation>)
jest
.mocked(buildCreateAccountOperation)
.mockReturnValue(
'create-account-op' as unknown as ReturnType<typeof buildCreateAccountOperation>
)
jest
.mocked(buildChangeTrustOperation)
.mockReturnValue('change-trust-op' as unknown as ReturnType<typeof buildChangeTrustOperation>)
})
it('throws NetworkDown when source account cannot be loaded', async () => {
jest.mocked(loadAccount).mockResolvedValue(null)
await expect(craftTransaction(account, transaction)).rejects.toThrow(NetworkDown)
})
it('throws StellarAssetRequired for changeTrust without asset code and issuer', async () => {
await expect(
craftTransaction(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(account, {
...transaction,
type: 'changeTrust',
assetCode: 'USDC',
assetIssuer: 'GISSUER',
})
expect(buildChangeTrustOperation).toHaveBeenCalledWith('USDC', 'GISSUER')
expect(txBuilder.addOperation).toHaveBeenCalledWith('change-trust-op')
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(account, {
...transaction,
memoType: 'MEMO_TEXT',
memoValue: 'hello',
})
expect(buildPaymentOperation).toHaveBeenCalledWith({
destination: 'GRECIPIENT',
amount: 10000000n,
assetCode: undefined,
assetIssuer: undefined,
})
expect(txBuilder.addOperation).toHaveBeenCalledWith('payment-op')
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(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(account, transaction)
expect(buildCreateAccountOperation).toHaveBeenCalledWith('GRECIPIENT', 10000000n)
expect(txBuilder.addOperation).toHaveBeenCalledWith('create-account-op')
})
})