@ledgerhq/coin-stellar
Version:
Ledger Stellar Coin integration
167 lines (142 loc) • 5.75 kB
text/typescript
// SPDX-FileCopyrightText: © 2026 LEDGER SAS
// SPDX-License-Identifier: Apache-2.0
import { BalanceOptions, TransactionIntent } from '@ledgerhq/coin-module-framework/api/types'
import { InvalidParameterError } from '@ledgerhq/errors'
import { StellarMemo } from '../types'
import { createApi, envelopeFromAnyXDR } from './index'
const mockGetOperations = jest.fn()
jest.mock('../logic/listOperations', () => ({
listOperations: () => mockGetOperations(),
}))
const CUSTOM_FEES = 300n
const estimateFeesMock = jest.fn(() => CUSTOM_FEES)
jest.mock('../logic/estimateFees', () => ({
estimateFees: () => estimateFeesMock(),
}))
const logicCraftTransactionMock = jest.fn((_account: unknown, _transaction: { fee: bigint }) => {
return { xdr: undefined }
})
jest.mock('../logic/craftTransaction', () => ({
craftTransaction: (account: unknown, transaction: { fee: bigint }) =>
logicCraftTransactionMock(account, transaction),
}))
const api = createApi({
explorer: {
url: 'explorer.com',
fetchLimit: 200,
},
useStaticFees: true,
enableNetworkLogs: false,
})
const fromGenesisMinHeight = 0
const fromGenesisOrder = 'asc' as const
describe('operations', () => {
beforeEach(() => {
jest.clearAllMocks()
})
const mockOperation = {
asset: { type: 'native' },
tx: {
hash: 'e035a56c32003e3b0e4c9c5499b0750d71d98233ae6ae94323ff0a458b05a30b',
fees: 0.0291,
block: {
hash: 'hash',
time: new Date('2024-03-20T10:00:00Z'),
height: 10,
},
date: new Date('2024-03-20T10:00:00Z'),
},
type: 'Operation',
value: 200,
senders: ['addr'],
recipients: ['recipient'],
}
it('should return 0 operations for a valid account', async () => {
mockGetOperations.mockResolvedValue({ items: [], next: '' })
// When
const operations = await api.listOperations('addr', {
minHeight: fromGenesisMinHeight,
order: fromGenesisOrder,
})
// Then
expect(operations).toEqual({ items: [], next: undefined })
expect(mockGetOperations).toHaveBeenCalledTimes(1)
})
it('should return 1 operation for a valid account', async () => {
mockGetOperations.mockResolvedValue({ items: [mockOperation], next: '' })
// When
const operations = await api.listOperations('addr', {
minHeight: fromGenesisMinHeight,
order: fromGenesisOrder,
})
// Then
expect(operations).toEqual({ items: [mockOperation], next: undefined })
expect(mockGetOperations).toHaveBeenCalledTimes(1)
})
it('should return cursor for caller-driven pagination', async () => {
mockGetOperations.mockResolvedValueOnce({ items: [mockOperation], next: 'cursor-abc' })
// When
const operations = await api.listOperations('addr', {
minHeight: fromGenesisMinHeight,
order: fromGenesisOrder,
})
expect(operations).toEqual({ items: [mockOperation], next: 'cursor-abc' })
expect(mockGetOperations).toHaveBeenCalledTimes(1)
})
})
describe('Testing craftTransaction function', () => {
beforeEach(() => {
estimateFeesMock.mockClear()
logicCraftTransactionMock.mockClear()
})
it('should use estimated fees when user does not provide them for crafting a transaction', async () => {
await api.craftTransaction({ asset: {} } as TransactionIntent<StellarMemo>)
expect(estimateFeesMock).toHaveBeenCalledTimes(1)
expect(logicCraftTransactionMock).toHaveBeenCalledWith(
expect.any(Object),
expect.objectContaining({ fee: CUSTOM_FEES })
)
})
it.each([[1n], [50n], [99n]])(
'should use custom user fees when user provide them for crafting a transaction',
async (fees: bigint) => {
await api.craftTransaction({ asset: {} } as TransactionIntent<StellarMemo>, { value: fees })
expect(estimateFeesMock).toHaveBeenCalledTimes(0)
expect(logicCraftTransactionMock).toHaveBeenCalledWith(
expect.any(Object),
expect.objectContaining({ fee: fees })
)
}
)
})
describe('Testing transaction loading functions', () => {
it('should deserialize a transaction as expected', async () => {
const transactionPayloadXDR =
'esM5l1ROMXXSZr0CJDmyLNsWUIwBFj8m5csqPhBFqXkAAAACAAAAAEFMhHdla/OhHE2CYrF1VVPnLgBThGuzpNFZyYMh' +
'8L6XAAAAZAAAJ/cAAAkYAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAABAAAABHRlc3QAAAABAAAAAAAAAAAAAAAA/QIumXyU' +
'+Nq3dDZfGCXjgxYI7uvPElz8zGb0gN+vWD8AAAAAAA9CQAAAAAA='
const transactionEnvelopeXDR =
'AAAAAgAAAABBTIR3ZWvzoRxNgmKxdVVT5y4AU4Rrs6TRWcmDIfC+lwAAAGQAACf3AAAJGAAAAAEAAAAAAAAAAAAAAAAA' +
'AAAAAAAAAQAAAAR0ZXN0AAAAAQAAAAAAAAAAAAAAAP0CLpl8lPjat3Q2Xxgl44MWCO7rzxJc/Mxm9IDfr1g/AAAAAAAP' +
'QkAAAAAAAAAAAA=='
const txFromSignaturePayload = envelopeFromAnyXDR(transactionPayloadXDR, 'base64')
const txFromEnvelope = envelopeFromAnyXDR(transactionEnvelopeXDR, 'base64')
expect(txFromEnvelope).toEqual(txFromSignaturePayload)
expect(txFromEnvelope.toXDR('base64')).toEqual(transactionEnvelopeXDR)
expect(txFromSignaturePayload.toXDR('base64')).toEqual(transactionEnvelopeXDR)
})
it('throw expected error when deserializing an invalid transaction', async () => {
expect(() => envelopeFromAnyXDR('lulz', 'base64')).toThrow(
'Failed decoding transaction as an envelope (TypeError: XDR Read Error: attempt to read outside the boundary of' +
' the buffer) or as a signature base: (TypeError: XDR Read Error: attempt to read outside the boundary of the' +
' buffer)'
)
})
})
describe('getBalance', () => {
it('should throw an exception when options is provided', async () => {
await expect(api.getBalance('random address', {} as unknown as BalanceOptions)).rejects.toThrow(
InvalidParameterError
)
})
})