UNPKG

@ledgerhq/coin-stellar

Version:
701 lines (636 loc) 23.5 kB
// SPDX-FileCopyrightText: © 2026 LEDGER SAS // SPDX-License-Identifier: Apache-2.0 import type { RawOperation } from '../types' import type { Horizon } from '@stellar/stellar-sdk' import BigNumber from 'bignumber.js' import * as network from '../network' import { parseAPIValue } from './common' import { getBlock } from './getBlock' jest.mock('../network', () => ({ ...jest.requireActual<typeof import('../network')>('../network'), fetchLedgerRecord: jest.fn(), fetchAllLedgerOperations: jest.fn(), })) const fetchLedgerRecordMock = network.fetchLedgerRecord as jest.MockedFunction< typeof network.fetchLedgerRecord > const fetchAllLedgerOperationsMock = network.fetchAllLedgerOperations as jest.MockedFunction< typeof network.fetchAllLedgerOperations > function txRecord( overrides: Partial<Horizon.ServerApi.TransactionRecord> = {} ): Horizon.ServerApi.TransactionRecord { return { fee_charged: '100', source_account: 'GSOURCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', fee_account: undefined, ledger_attr: 42, memo_type: 'none', ...overrides, } as Horizon.ServerApi.TransactionRecord } function rawOp( base: Record<string, unknown> & { type: string transaction_hash: string transactionRecord?: Horizon.ServerApi.TransactionRecord } ): RawOperation { const tr = base.transactionRecord ?? txRecord() const { transactionRecord: _t, ...rest } = base return { id: '1', paging_token: 'pt', source_account: 'GSOURCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', transaction_successful: true, created_at: '2020-01-01T00:00:00Z', ...rest, transaction: () => Promise.resolve(tr), } as RawOperation } describe('getBlock', () => { beforeEach(() => { jest.clearAllMocks() fetchLedgerRecordMock.mockImplementation( async (seq: number) => ({ sequence: seq, hash: `LEDGER-HASH-${seq}`, closed_at: '2018-09-15T15:40:05Z', ...(seq > 1 && { prev_hash: `LEDGER-HASH-${seq - 1}` }), }) as Awaited<ReturnType<typeof network.fetchLedgerRecord>> ) }) describe('height validation', () => { it('rejects height 0', async () => { await expect(getBlock(0)).rejects.toThrow( 'getBlock: height must be a positive integer, got 0' ) }) it('rejects negative height', async () => { await expect(getBlock(-1)).rejects.toThrow( 'getBlock: height must be a positive integer, got -1' ) }) it('rejects non-integer height', async () => { await expect(getBlock(2.5)).rejects.toThrow( 'getBlock: height must be a positive integer, got 2.5' ) }) it('rejects NaN', async () => { await expect(getBlock(Number.NaN)).rejects.toThrow( 'getBlock: height must be a positive integer, got NaN' ) }) it('rejects Infinity', async () => { await expect(getBlock(Number.POSITIVE_INFINITY)).rejects.toThrow( 'getBlock: height must be a positive integer, got Infinity' ) }) }) it('maps a native payment with two transfer operations', async () => { fetchAllLedgerOperationsMock.mockResolvedValue([ rawOp({ type: 'payment', transaction_hash: 'txpay1', from: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', to: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', amount: '1.0000000', asset_type: 'native', transactionRecord: txRecord({ fee_charged: '200' }), }), ]) const block = await getBlock(20000000) expect(block.info.height).toBe(20000000) expect(block.info.hash).toBe('LEDGER-HASH-20000000') expect(block.info.parent).toEqual({ height: 19999999, hash: 'LEDGER-HASH-19999999', }) expect(block.transactions).toHaveLength(1) const tx0 = block.transactions[0] expect(tx0.hash).toBe('txpay1') expect(tx0.failed).toBe(false) expect(tx0.fees).toBe(200n) expect(tx0.feesPayer).toBe('GSOURCEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA') expect(tx0.operations).toEqual( expect.arrayContaining([ expect.objectContaining({ type: 'transfer', address: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', amount: -10000000n, }), expect.objectContaining({ type: 'transfer', address: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', amount: 10000000n, }), ]) ) }) it('uses fee_account as feesPayer when present (fee bump)', async () => { fetchAllLedgerOperationsMock.mockResolvedValue([ rawOp({ type: 'payment', transaction_hash: 'txfeebump', from: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', to: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', amount: '2.0000000', asset_type: 'native', transactionRecord: txRecord({ fee_charged: '500', source_account: 'GINNERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', fee_account: 'GFEEPAYERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', }), }), ]) const block = await getBlock(10) expect(block.transactions[0].feesPayer).toBe('GFEEPAYERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA') }) it('marks failed transactions with empty operations but keeps fees', async () => { fetchAllLedgerOperationsMock.mockResolvedValue([ rawOp({ type: 'payment', transaction_hash: 'txfail', from: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', to: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', amount: '1.0000000', asset_type: 'native', transaction_successful: false, transactionRecord: txRecord({ fee_charged: '400' }), }), ]) const block = await getBlock(10) expect(block.transactions[0]).toMatchObject({ hash: 'txfail', failed: true, fees: 400n, operations: [], }) }) it('groups multiple allowlisted operations under the same transaction hash', async () => { const sharedTx = txRecord({ fee_charged: '300' }) fetchAllLedgerOperationsMock.mockResolvedValue([ rawOp({ id: '1', type: 'payment', transaction_hash: 'txmulti', from: 'GA', to: 'GB', amount: '1.0000000', asset_type: 'native', transactionRecord: sharedTx, }), rawOp({ id: '2', type: 'payment', transaction_hash: 'txmulti', from: 'GB', to: 'GC', amount: '0.5000000', asset_type: 'native', transactionRecord: sharedTx, }), ]) const block = await getBlock(10) expect(block.transactions).toHaveLength(1) expect(block.transactions[0].operations.length).toBeGreaterThanOrEqual(2) }) it('maps token payments using assetReference and assetOwner', async () => { fetchAllLedgerOperationsMock.mockResolvedValue([ rawOp({ type: 'payment', transaction_hash: 'txtok', from: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', to: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', amount: '10.0000000', asset_type: 'credit_alphanum4', asset_code: 'USD', asset_issuer: 'GISSUERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', transactionRecord: txRecord(), }), ]) const block = await getBlock(10) const transfers = block.transactions[0].operations.filter((o) => o.type === 'transfer') expect(transfers[0]).toMatchObject({ asset: { type: 'token', assetReference: 'USD', assetOwner: 'GISSUERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', }, }) }) it('maps create_account to two native transfers', async () => { fetchAllLedgerOperationsMock.mockResolvedValue([ rawOp({ type: 'create_account', transaction_hash: 'txcreate', funder: 'GFUNDERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', account: 'GNEWACCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', starting_balance: '2.0000000', transactionRecord: txRecord({ fee_charged: '100' }), }), ]) const block = await getBlock(10) expect(block.transactions[0].operations).toEqual( expect.arrayContaining([ expect.objectContaining({ type: 'transfer', address: 'GFUNDERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', amount: -20000000n, }), expect.objectContaining({ type: 'transfer', address: 'GNEWACCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', amount: 20000000n, }), ]) ) }) it('maps change_trust to other operations', async () => { fetchAllLedgerOperationsMock.mockResolvedValue([ rawOp({ type: 'change_trust', transaction_hash: 'txtrust', trustor: 'GTRUSTORAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', asset_code: 'ABC', asset_issuer: 'GISSUERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', limit: '1000.0000000', transactionRecord: txRecord({ source_account_sequence: '12345' }), }), ]) const block = await getBlock(10) expect(block.transactions[0].operations[0]).toMatchObject({ type: 'other', ledgerOpType: 'OPT_IN', assetAmount: '100', memo: { type: 'NO_MEMO' }, sequence: '12345', }) }) it('decodes MEMO_HASH from base64 to hex in change_trust details', async () => { const base64Hash = Buffer.from('abcdef1234567890', 'hex').toString('base64') fetchAllLedgerOperationsMock.mockResolvedValue([ rawOp({ type: 'change_trust', transaction_hash: 'txhash', asset_code: 'USD', asset_issuer: 'GISSUERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', limit: '500.0000000', transactionRecord: txRecord({ memo_type: 'hash', memo: base64Hash }), }), ]) const block = await getBlock(10) expect(block.transactions[0].operations[0]).toMatchObject({ memo: { type: 'MEMO_HASH', value: 'abcdef1234567890' }, }) }) it('decodes MEMO_RETURN from base64 to hex in change_trust details', async () => { const base64Hash = Buffer.from('deadbeef', 'hex').toString('base64') fetchAllLedgerOperationsMock.mockResolvedValue([ rawOp({ type: 'change_trust', transaction_hash: 'txreturn', asset_code: 'EUR', asset_issuer: 'GISSUERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', limit: '100.0000000', transactionRecord: txRecord({ memo_type: 'return', memo: base64Hash }), }), ]) const block = await getBlock(10) expect(block.transactions[0].operations[0]).toMatchObject({ memo: { type: 'MEMO_RETURN', value: 'deadbeef' }, }) }) it('keeps successful transactions with only unsupported operation types for fee tracking', async () => { fetchAllLedgerOperationsMock.mockResolvedValue([ rawOp({ type: 'manage_sell_offer', transaction_hash: 'txoffer', transactionRecord: txRecord(), }), ]) const block = await getBlock(10) expect(block.transactions).toHaveLength(1) expect(block.transactions[0].operations).toHaveLength(0) expect(block.transactions[0].fees).toBe(100n) }) it('maps path_payment_strict_send with distinct source and destination assets', async () => { fetchAllLedgerOperationsMock.mockResolvedValue([ rawOp({ type: 'path_payment_strict_send', transaction_hash: 'txpathsend', from: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', to: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', amount: '0.0033586', asset_type: 'credit_alphanum12', asset_code: 'Shitcoinz', asset_issuer: 'GBVYKIM4QGW7NK3Q5S76GWMZMLAWYB6FB43JQ4J7ZOXHAA3LEZZGYHSD', source_amount: '0.0130225', source_asset_type: 'native', transactionRecord: txRecord(), }), ]) const block = await getBlock(10) const ops = block.transactions[0].operations expect(ops).toHaveLength(2) expect(ops[0]).toMatchObject({ type: 'transfer', asset: { type: 'native' } }) expect(typeof ops[0].amount).toBe('bigint') expect(ops[1]).toMatchObject({ type: 'transfer', asset: { type: 'token', assetReference: 'Shitcoinz' }, }) expect(typeof ops[1].amount).toBe('bigint') }) it('path_payment_strict_receive debits source_amount when both send_max and source_amount are present', async () => { const sourceAmountStr = '0.0100000' const sendMaxStr = '5.0000000' const expectedDebit = BigInt( parseAPIValue(sourceAmountStr).integerValue(BigNumber.ROUND_FLOOR).toFixed(0) ) fetchAllLedgerOperationsMock.mockResolvedValue([ rawOp({ type: 'path_payment_strict_receive', transaction_hash: 'txpathrecv', from: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', to: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', amount: '0.0033586', asset_type: 'credit_alphanum12', asset_code: 'Shitcoinz', asset_issuer: 'GBVYKIM4QGW7NK3Q5S76GWMZMLAWYB6FB43JQ4J7ZOXHAA3LEZZGYHSD', send_max: sendMaxStr, source_amount: sourceAmountStr, source_asset_type: 'native', transactionRecord: txRecord(), }), ]) const block = await getBlock(10) const ops = block.transactions[0].operations expect(ops).toHaveLength(2) expect(ops[0]).toMatchObject({ type: 'transfer', address: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', asset: { type: 'native' }, amount: -expectedDebit, }) const wrongDebit = BigInt( parseAPIValue(sendMaxStr).integerValue(BigNumber.ROUND_FLOOR).toFixed(0) ) expect(ops[0].amount).not.toBe(-wrongDebit) }) it('omits parent in block info for ledger height 1', async () => { fetchLedgerRecordMock.mockImplementation( async (seq: number) => ({ sequence: seq, hash: `LEDGER-HASH-${seq}`, closed_at: '2018-09-15T15:40:05Z', }) as Awaited<ReturnType<typeof network.fetchLedgerRecord>> ) fetchAllLedgerOperationsMock.mockResolvedValue([]) const block = await getBlock(1) expect(block.info.height).toBe(1) expect(block.info.parent).toBeUndefined() }) it('omits feesPayer when fee_account and source_account are absent', async () => { fetchAllLedgerOperationsMock.mockResolvedValue([ rawOp({ type: 'payment', transaction_hash: 'txnopayer', from: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', to: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', amount: '1.0000000', asset_type: 'native', transactionRecord: txRecord({ fee_charged: '100', source_account: '', }), }), ]) const block = await getBlock(10) expect(block.transactions[0]).not.toHaveProperty('feesPayer') }) it('maps create_account with zero starting balance to empty operations but keeps tx for fees', async () => { fetchAllLedgerOperationsMock.mockResolvedValue([ rawOp({ type: 'create_account', transaction_hash: 'txcreatezero', funder: 'GFUNDERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', account: 'GNEWAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', starting_balance: '0.0000000', transactionRecord: txRecord(), }), ]) const block = await getBlock(10) expect(block.transactions).toHaveLength(1) expect(block.transactions[0].operations).toHaveLength(0) expect(block.transactions[0].fees).toBe(100n) }) it('maps payment using to_muxed when to is absent', async () => { fetchAllLedgerOperationsMock.mockResolvedValue([ rawOp({ type: 'payment', transaction_hash: 'txmuxed', from: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', to: undefined, to_muxed: 'GTO_MUXEDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', amount: '0.5000000', asset_type: 'native', transactionRecord: txRecord(), }), ]) const block = await getBlock(10) const transfers = block.transactions[0].operations.filter((o) => o.type === 'transfer') expect( transfers.some( (o) => 'peer' in o && o.peer === 'GTO_MUXEDAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' ) ).toBe(true) }) it('maps change_trust with zero limit as OPT_OUT', async () => { fetchAllLedgerOperationsMock.mockResolvedValue([ rawOp({ type: 'change_trust', transaction_hash: 'txoptout', trustor: 'GTRUSTORAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', asset_code: 'ABC', asset_issuer: 'GISSUERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', limit: '0.0000000', transactionRecord: txRecord(), }), ]) const block = await getBlock(10) expect(block.transactions[0].operations[0]).toMatchObject({ type: 'other', ledgerOpType: 'OPT_OUT', }) }) it('path_payment_strict_send with zero amounts keeps tx for fees', async () => { fetchAllLedgerOperationsMock.mockResolvedValue([ rawOp({ type: 'path_payment_strict_send', transaction_hash: 'txpathzero', from: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', to: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', amount: '0.0000000', asset_type: 'native', source_amount: '0.0000000', source_asset_type: 'native', transactionRecord: txRecord(), }), ]) const block = await getBlock(10) expect(block.transactions).toHaveLength(1) expect(block.transactions[0].operations).toHaveLength(0) expect(block.transactions[0].fees).toBe(100n) }) it('path_payment_strict_receive uses send_max when source_amount is absent', async () => { const sendMaxStr = '0.0200000' const expected = BigInt( parseAPIValue(sendMaxStr).integerValue(BigNumber.ROUND_FLOOR).toFixed(0) ) fetchAllLedgerOperationsMock.mockResolvedValue([ rawOp({ type: 'path_payment_strict_receive', transaction_hash: 'txrecvmax', from: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', to: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', amount: '0.0010000', asset_type: 'native', send_max: sendMaxStr, source_asset_type: 'native', transactionRecord: txRecord(), }), ]) const block = await getBlock(10) const sender = block.transactions[0].operations.find( (o) => o.type === 'transfer' && o.address === 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' ) expect(sender?.amount).toBe(-expected) }) it('path_payment_strict_receive with no source_amount nor send_max only maps destination leg', async () => { fetchAllLedgerOperationsMock.mockResolvedValue([ rawOp({ type: 'path_payment_strict_receive', transaction_hash: 'txrecvnodebit', from: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', to: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', amount: '0.5000000', asset_type: 'native', source_asset_type: 'native', transactionRecord: txRecord(), }), ]) const block = await getBlock(10) expect(block.transactions[0].operations).toHaveLength(1) expect(block.transactions[0].operations[0]).toMatchObject({ type: 'transfer', address: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', asset: { type: 'native' }, amount: 5000000n, }) }) it('path_payment_strict_send with zero source_amount maps only destination leg', async () => { fetchAllLedgerOperationsMock.mockResolvedValue([ rawOp({ type: 'path_payment_strict_send', transaction_hash: 'txpathsenddestonly', from: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', to: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', amount: '0.2500000', asset_type: 'native', source_amount: '0.0000000', source_asset_type: 'native', transactionRecord: txRecord(), }), ]) const block = await getBlock(10) expect(block.transactions[0].operations).toHaveLength(1) expect(block.transactions[0].operations[0]).toMatchObject({ type: 'transfer', address: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', amount: 2500000n, }) }) it('path_payment_strict_send with zero amount maps only source leg', async () => { fetchAllLedgerOperationsMock.mockResolvedValue([ rawOp({ type: 'path_payment_strict_send', transaction_hash: 'txpathsendsourceonly', from: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', to: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', amount: '0.0000000', asset_type: 'native', source_amount: '1.0000000', source_asset_type: 'native', transactionRecord: txRecord(), }), ]) const block = await getBlock(10) expect(block.transactions[0].operations).toHaveLength(1) expect(block.transactions[0].operations[0]).toMatchObject({ type: 'transfer', address: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', amount: -10000000n, }) }) it('maps payment with only to address as single incoming transfer', async () => { fetchAllLedgerOperationsMock.mockResolvedValue([ rawOp({ type: 'payment', transaction_hash: 'txtoonly', to: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', amount: '3.0000000', asset_type: 'native', transactionRecord: txRecord(), }), ]) const block = await getBlock(10) expect(block.transactions[0].operations).toEqual([ expect.objectContaining({ type: 'transfer', address: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', amount: 30000000n, }), ]) }) it('maps payment with only from address as single outgoing transfer', async () => { fetchAllLedgerOperationsMock.mockResolvedValue([ rawOp({ type: 'payment', transaction_hash: 'txfromonly', from: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', amount: '0.2500000', asset_type: 'native', transactionRecord: txRecord(), }), ]) const block = await getBlock(10) expect(block.transactions[0].operations).toEqual([ expect.objectContaining({ type: 'transfer', address: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', amount: -2500000n, }), ]) }) it('treats payment with missing asset_type as native when mapping asset', async () => { fetchAllLedgerOperationsMock.mockResolvedValue([ rawOp({ type: 'payment', transaction_hash: 'txnoassettype', from: 'GFROMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', to: 'GTOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', amount: '1.0000000', asset_type: undefined, asset_code: 'USD', asset_issuer: 'GISSUERAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', transactionRecord: txRecord(), }), ]) const block = await getBlock(10) const transfers = block.transactions[0].operations.filter((o) => o.type === 'transfer') expect(transfers.every((t) => t.type === 'transfer' && t.asset.type === 'native')).toBe(true) }) })