@ledgerhq/coin-tezos
Version:
1,511 lines • 54 kB
text/typescript
// SPDX-FileCopyrightText: © 2026 LEDGER SAS
// SPDX-License-Identifier: Apache-2.0
import type { TezosCoinConfig, TezosContext } from '../config'
import type {
APIDelegationType,
APIOriginationType,
APIRevealType,
APIStakingType,
APITokenTransfer,
APITransactionType,
} from '../network/types'
import type { OtherBlockOperation } from '@ledgerhq/coin-module-framework/api/types'
import { getBlock } from './getBlock'
const context: TezosContext = {
config: async () => ({}) as unknown as TezosCoinConfig,
logger: () => undefined,
}
// ---------------------------------------------------------------------------
// Network mock
// Using the same lazy-reference pattern as the XRP reference tests so that the
// `jest.mock` factory (which is hoisted) can reference variables declared below.
// ---------------------------------------------------------------------------
const mockGetBlockByLevel = jest.fn()
const mockGetOperationsOrigination = jest.fn()
const mockFetchBlockTransactions = jest.fn()
const mockFetchBlockTokenTransfers = jest.fn()
const mockFetchBlockDelegations = jest.fn()
const mockFetchBlockStaking = jest.fn()
const mockFetchBlockOriginations = jest.fn()
const mockFetchBlockReveals = jest.fn()
jest.mock('../network', () => ({
createTzktApi: () => ({
getBlockByLevel: (...args: unknown[]) => mockGetBlockByLevel(...args),
getOperationsOrigination: (...args: unknown[]) => mockGetOperationsOrigination(...args),
}),
fetchBlockTransactions: (...args: unknown[]) => mockFetchBlockTransactions(...args),
fetchBlockTokenTransfers: (...args: unknown[]) => mockFetchBlockTokenTransfers(...args),
fetchBlockDelegations: (...args: unknown[]) => mockFetchBlockDelegations(...args),
fetchBlockStaking: (...args: unknown[]) => mockFetchBlockStaking(...args),
fetchBlockOriginations: (...args: unknown[]) => mockFetchBlockOriginations(...args),
fetchBlockReveals: (...args: unknown[]) => mockFetchBlockReveals(...args),
}))
// ---------------------------------------------------------------------------
// Test-data factories
// ---------------------------------------------------------------------------
function makeBlock(level = 5_000_000, hash = 'BLockHash123', timestamp = '2024-01-01T00:00:00Z') {
return { level, hash, timestamp } as any
}
function makeTx(overrides: Partial<APITransactionType> = {}): APITransactionType {
return {
id: 1,
hash: 'opHash1',
type: 'transaction',
amount: 0,
sender: { address: 'tz1Sender' },
target: { address: 'tz1Target' },
initiator: null,
counter: 1,
level: 5_000_000,
block: 'BLockHash123',
timestamp: '2024-01-01T00:00:00Z',
bakerFee: 0,
storageFee: 0,
allocationFee: 0,
status: 'applied',
...overrides,
} as APITransactionType
}
function makeTokenTransfer(overrides: Partial<APITokenTransfer> = {}): APITokenTransfer {
return {
id: 100,
level: 5_000_000,
timestamp: '2024-01-01T00:00:00Z',
token: {
id: 1,
contract: { address: 'KT1FATezosContract' },
tokenId: '0',
standard: 'fa2',
metadata: { name: 'TezToken', symbol: 'TZT' },
},
from: { address: 'tz1Sender' },
to: { address: 'tz1Target' },
amount: '500',
transactionId: 1,
...overrides,
}
}
function makeStaking(overrides: Partial<APIStakingType> = {}): APIStakingType {
return {
id: 300,
hash: 'opStaking1',
type: 'staking',
action: 'stake',
amount: 500_000_000,
requestedAmount: 500_000_000,
sender: { address: 'tz1Staker' },
staker: { address: 'tz1Staker' },
baker: { address: 'tz1Baker', alias: 'TF Test Baker' },
counter: 1,
level: 5_000_000,
block: 'BLockHash123',
timestamp: '2024-01-01T00:00:00Z',
bakerFee: 800,
storageFee: 0,
allocationFee: 0,
gasLimit: 3630,
storageLimit: 0,
status: 'applied',
...overrides,
} as APIStakingType
}
function makeDelegation(overrides: Partial<APIDelegationType> = {}): APIDelegationType {
return {
id: 200,
hash: 'opDelegation1',
type: 'delegation',
amount: 0,
sender: { address: 'tz1Delegator' },
counter: 1,
level: 5_000_000,
block: 'BLockHash123',
timestamp: '2024-01-01T00:00:00Z',
bakerFee: 1000,
storageFee: 0,
allocationFee: 0,
status: 'applied',
prevDelegate: null,
newDelegate: { address: 'tz1Baker' },
...overrides,
} as APIDelegationType
}
function makeOrigination(overrides: Partial<APIOriginationType> = {}): APIOriginationType {
return {
id: 400,
hash: 'opOrigination1',
type: 'origination',
sender: { address: 'tz1Deployer' },
counter: 1,
level: 5_000_000,
block: 'BLockHash123',
timestamp: '2024-01-01T00:00:00Z',
bakerFee: 5000,
storageFee: 1000000,
allocationFee: 64250,
status: 'applied',
contractBalance: 0,
originatedContract: { address: 'KT1NewContract' },
...overrides,
} as APIOriginationType
}
function makeReveal(overrides: Partial<APIRevealType> = {}): APIRevealType {
return {
id: 500,
hash: 'opReveal1',
type: 'reveal',
sender: { address: 'tz1Revealer' },
counter: 1,
level: 5_000_000,
block: 'BLockHash123',
timestamp: '2024-01-01T00:00:00Z',
bakerFee: 1420,
storageFee: 0,
allocationFee: 0,
status: 'applied',
gasLimit: 10600,
storageLimit: 0,
...overrides,
} as APIRevealType
}
// ---------------------------------------------------------------------------
// Setup
// ---------------------------------------------------------------------------
beforeEach(() => {
jest.clearAllMocks()
mockFetchBlockTransactions.mockResolvedValue([])
mockFetchBlockTokenTransfers.mockResolvedValue([])
mockFetchBlockDelegations.mockResolvedValue([])
mockFetchBlockStaking.mockResolvedValue([])
mockFetchBlockOriginations.mockResolvedValue([])
mockFetchBlockReveals.mockResolvedValue([])
mockGetOperationsOrigination.mockResolvedValue([])
})
// ---------------------------------------------------------------------------
// 1. Block info mapping and guard
// ---------------------------------------------------------------------------
describe('block info and guard', () => {
it('maps level, hash, timestamp, and parent into Block.info', async () => {
// Given
const level = 7_000_000
mockGetBlockByLevel.mockResolvedValueOnce(makeBlock(level, 'BLHash', '2025-06-15T12:00:00Z'))
mockGetBlockByLevel.mockResolvedValueOnce(
makeBlock(level - 1, 'BLParentHash', '2025-06-15T11:59:52Z')
)
// When
const result = await getBlock(context, level)
// Then
expect(mockGetBlockByLevel).toHaveBeenCalledWith(level)
expect(mockGetBlockByLevel).toHaveBeenCalledWith(level - 1)
expect(result.info).toEqual({
height: level,
hash: 'BLHash',
time: new Date('2025-06-15T12:00:00Z'),
parent: { height: level - 1, hash: 'BLParentHash' },
})
})
it('throws for height = 0 without any network calls', async () => {
// When / Then
await expect(getBlock(context, 0)).rejects.toThrow(
'getBlock: height must be a positive integer, got 0'
)
expect(mockGetBlockByLevel).not.toHaveBeenCalled()
expect(mockFetchBlockTransactions).not.toHaveBeenCalled()
expect(mockFetchBlockTokenTransfers).not.toHaveBeenCalled()
})
it('throws for negative height without any network calls', async () => {
// When / Then
await expect(getBlock(context, -3)).rejects.toThrow(
'getBlock: height must be a positive integer, got -3'
)
expect(mockGetBlockByLevel).not.toHaveBeenCalled()
})
it.each([NaN, 1.5, Infinity, -Infinity])(
'throws for non-safe-integer height (%s) without any network calls',
async (height) => {
await expect(getBlock(context, height)).rejects.toThrow(
'getBlock: height must be a positive integer'
)
expect(mockGetBlockByLevel).not.toHaveBeenCalled()
}
)
it('returns an empty transactions array when the block has no operations', async () => {
// Given
mockGetBlockByLevel.mockResolvedValue(makeBlock())
// When
const result = await getBlock(context, 5_000_000)
// Then
expect(result.transactions).toEqual([])
})
it('silently skips a transaction with no hash (covers !tx.hash branch and tx.id && tx.hash false branch)', async () => {
// Given – one tx without a hash and one valid tx
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTransactions.mockResolvedValue([
makeTx({ hash: undefined as unknown as string, id: 5, amount: 500_000 }),
makeTx({ id: 2, hash: 'validHash', amount: 1_000_000 }),
])
// When
const result = await getBlock(context, 5_000_000)
// Then – only the tx with a valid hash is included
expect(result.transactions).toHaveLength(1)
expect(result.transactions[0].hash).toBe('validHash')
})
it('excludes a no-id transaction from the token-transfer lookup (covers tx.id falsy branch)', async () => {
// Given – tx without id cannot be registered in txIdToHash
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTransactions.mockResolvedValue([
makeTx({ id: undefined as unknown as number, hash: 'noIdHash', amount: 2_000_000 }),
])
// When
const result = await getBlock(context, 5_000_000)
// Then – the tx still appears (it has a hash), but no txIdToHash entry was created
expect(result.transactions).toHaveLength(1)
expect(result.transactions[0].hash).toBe('noIdHash')
})
})
// ---------------------------------------------------------------------------
// 2. Native XTZ transfers
// ---------------------------------------------------------------------------
describe('native XTZ operations', () => {
it('produces outgoing and incoming operations for a non-zero XTZ transfer', async () => {
// Given
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTransactions.mockResolvedValue([
makeTx({ amount: 1_000_000, sender: { address: 'tz1Alice' }, target: { address: 'tz1Bob' } }),
])
// When
const result = await getBlock(context, 5_000_000)
const tx = result.transactions[0]
// Then
expect(tx.operations).toHaveLength(2)
expect(tx.operations[0]).toEqual({
type: 'transfer',
address: 'tz1Alice',
peer: 'tz1Bob',
asset: { type: 'native', name: 'XTZ' },
amount: -1_000_000n,
})
expect(tx.operations[1]).toEqual({
type: 'transfer',
address: 'tz1Bob',
peer: 'tz1Alice',
asset: { type: 'native', name: 'XTZ' },
amount: 1_000_000n,
})
})
it('produces no operations for a zero-amount, zero-fee transaction', async () => {
// Given
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTransactions.mockResolvedValue([makeTx({ amount: 0, bakerFee: 0 })])
// When
const result = await getBlock(context, 5_000_000)
// Then — no balance impact, no operations
expect(result.transactions[0].operations).toEqual([])
})
it('produces operations for a zero-amount transaction that carries fees', async () => {
// Given
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTransactions.mockResolvedValue([makeTx({ amount: 0, bakerFee: 1500 })])
// When
const result = await getBlock(context, 5_000_000)
// Then — fees are a balance change, so operations must be emitted for fee attribution
expect(result.transactions[0].fees).toBe(1500n)
expect(result.transactions[0].operations).toEqual([
{
type: 'transfer',
address: 'tz1Sender',
peer: 'tz1Target',
asset: { type: 'native', name: 'XTZ' },
amount: 0n,
},
{
type: 'transfer',
address: 'tz1Target',
peer: 'tz1Sender',
asset: { type: 'native', name: 'XTZ' },
amount: 0n,
},
])
})
it('produces only an incoming operation when sender is null, with no peer field', async () => {
// Given
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTransactions.mockResolvedValue([
makeTx({ amount: 500_000, sender: null, target: { address: 'tz1Receiver' } }),
])
// When
const result = await getBlock(context, 5_000_000)
// Then — peer must be absent (not set to undefined) when the counterparty is unknown
expect(result.transactions[0].operations).toHaveLength(1)
expect(result.transactions[0].operations[0]).toEqual({
type: 'transfer',
address: 'tz1Receiver',
asset: { type: 'native', name: 'XTZ' },
amount: 500_000n,
})
expect(result.transactions[0].operations[0]).not.toHaveProperty('peer')
})
it('produces only an outgoing operation when target is null, with no peer field', async () => {
// Given
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTransactions.mockResolvedValue([
makeTx({ amount: 300_000, sender: { address: 'tz1Sender' }, target: null }),
])
// When
const result = await getBlock(context, 5_000_000)
// Then — peer must be absent (not set to undefined) when the counterparty is unknown
expect(result.transactions[0].operations).toHaveLength(1)
expect(result.transactions[0].operations[0]).toEqual({
type: 'transfer',
address: 'tz1Sender',
asset: { type: 'native', name: 'XTZ' },
amount: -300_000n,
})
expect(result.transactions[0].operations[0]).not.toHaveProperty('peer')
})
it('skips a transaction that has no hash', async () => {
// Given
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTransactions.mockResolvedValue([
makeTx({ hash: undefined, amount: 1_000 }),
makeTx({ id: 2, hash: 'opHash2', amount: 2_000 }),
])
// When
const result = await getBlock(context, 5_000_000)
// Then — only the tx with a hash produces a BlockTransaction
expect(result.transactions).toHaveLength(1)
expect(result.transactions[0].hash).toBe('opHash2')
})
})
// ---------------------------------------------------------------------------
// 3. Fees and feesPayer
// ---------------------------------------------------------------------------
describe('fee computation and fee payer', () => {
it('sums bakerFee, storageFee, and allocationFee as the total fees', async () => {
// Given
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTransactions.mockResolvedValue([
makeTx({ bakerFee: 400, storageFee: 100, allocationFee: 50 }),
])
// When
const result = await getBlock(context, 5_000_000)
// Then
expect(result.transactions[0].fees).toBe(550n)
})
it('uses sender as feesPayer for a top-level (non-contract) operation', async () => {
// Given
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTransactions.mockResolvedValue([
makeTx({ bakerFee: 400, initiator: null, sender: { address: 'tz1FeePayer' } }),
])
// When
const result = await getBlock(context, 5_000_000)
// Then
expect(result.transactions[0].feesPayer).toBe('tz1FeePayer')
})
it('uses initiator as feesPayer when the operation is contract-initiated', async () => {
// Given – top-level op has bakerFee > 0 and an initiator (the real fee payer)
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTransactions.mockResolvedValue([
makeTx({
bakerFee: 600,
initiator: { address: 'tz1RealUser' },
sender: { address: 'KT1Contract' },
}),
])
// When
const result = await getBlock(context, 5_000_000)
// Then
expect(result.transactions[0].feesPayer).toBe('tz1RealUser')
})
it('omits feesPayer when both sender and initiator are null', async () => {
// Given
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTransactions.mockResolvedValue([makeTx({ sender: null, initiator: null })])
// When
const result = await getBlock(context, 5_000_000)
// Then
expect(result.transactions[0].feesPayer).toBeUndefined()
})
it('treats null/undefined fee fields as zero (covers ?? 0 fallback branches)', async () => {
// Given – a transaction where none of the fee fields are populated (possible for
// implicit/internal ops that the protocol injects without explicit fees).
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTransactions.mockResolvedValue([
makeTx({
bakerFee: undefined as unknown as number,
storageFee: undefined as unknown as number,
allocationFee: undefined as unknown as number,
amount: 1_000_000,
}),
])
// When
const result = await getBlock(context, 5_000_000)
// Then – all three ?? 0 branches are exercised; total fees must be 0n
expect(result.transactions[0].fees).toBe(0n)
})
})
// ---------------------------------------------------------------------------
// 4. Failed transactions
// ---------------------------------------------------------------------------
describe('failed transactions', () => {
it("marks a transaction as failed when status is 'failed'", async () => {
// Given
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTransactions.mockResolvedValue([makeTx({ status: 'failed', amount: 1_000_000 })])
// When
const result = await getBlock(context, 5_000_000)
// Then
expect(result.transactions[0].failed).toBe(true)
expect(result.transactions[0].operations).toEqual([])
})
it("marks a transaction as failed when status is 'backtracked'", async () => {
// Given
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTransactions.mockResolvedValue([
makeTx({ status: 'backtracked', amount: 1_000_000 }),
])
// When
const result = await getBlock(context, 5_000_000)
// Then
expect(result.transactions[0].failed).toBe(true)
expect(result.transactions[0].operations).toEqual([])
})
it('marks the whole batch as failed when any op in the group is not applied', async () => {
// Given – two ops share the same hash; the second one backtracked
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTransactions.mockResolvedValue([
makeTx({ id: 1, hash: 'opBatch', amount: 1_000, status: 'applied' }),
makeTx({ id: 2, hash: 'opBatch', amount: 2_000, status: 'backtracked' }),
])
// When
const result = await getBlock(context, 5_000_000)
// Then
expect(result.transactions).toHaveLength(1)
expect(result.transactions[0].failed).toBe(true)
expect(result.transactions[0].operations).toEqual([])
})
it('treats operations with no status field as succeeded', async () => {
// Given – status is undefined (some TzKT responses omit it)
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTransactions.mockResolvedValue([makeTx({ status: undefined, amount: 100_000 })])
// When
const result = await getBlock(context, 5_000_000)
// Then
expect(result.transactions[0].failed).toBe(false)
expect(result.transactions[0].operations).toHaveLength(2)
})
})
// ---------------------------------------------------------------------------
// 5. Batch and internal operations
// ---------------------------------------------------------------------------
describe('batch and internal operations', () => {
it('merges multiple ops sharing the same hash into a single BlockTransaction', async () => {
// Given – two operations in a batch
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTransactions.mockResolvedValue([
makeTx({ id: 1, hash: 'opBatch', amount: 1_000_000, bakerFee: 400, storageFee: 0 }),
makeTx({
id: 2,
hash: 'opBatch',
amount: 2_000_000,
bakerFee: 0,
storageFee: 100,
sender: { address: 'tz1Second' },
target: { address: 'tz1ThirdParty' },
}),
])
// When
const result = await getBlock(context, 5_000_000)
// Then – one BlockTransaction with both ops' ops and combined fees
expect(result.transactions).toHaveLength(1)
const tx = result.transactions[0]
expect(tx.hash).toBe('opBatch')
expect(tx.fees).toBe(500n) // 400 + 0 + 0 + 0 + 100 + 0
expect(tx.operations).toHaveLength(4) // sender+target for each of the 2 ops
})
it('uses the bakerFee-bearing op to determine feesPayer in a batch', async () => {
// Given – first op has no bakerFee; second op has bakerFee > 0
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTransactions.mockResolvedValue([
makeTx({ id: 1, hash: 'opBatch', bakerFee: 0, sender: { address: 'KT1Contract' } }),
makeTx({ id: 2, hash: 'opBatch', bakerFee: 500, sender: { address: 'tz1ActualPayer' } }),
])
// When
const result = await getBlock(context, 5_000_000)
// Then
expect(result.transactions[0].feesPayer).toBe('tz1ActualPayer')
})
it('two distinct hashes produce two BlockTransactions', async () => {
// Given
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTransactions.mockResolvedValue([
makeTx({ id: 1, hash: 'opHash1' }),
makeTx({ id: 2, hash: 'opHash2' }),
])
// When
const result = await getBlock(context, 5_000_000)
// Then
expect(result.transactions).toHaveLength(2)
expect(result.transactions.map((t) => t.hash)).toEqual(
expect.arrayContaining(['opHash1', 'opHash2'])
)
})
})
// ---------------------------------------------------------------------------
// 6. FA token transfers
// ---------------------------------------------------------------------------
describe('FA token transfers', () => {
it('appends token ops to the matching native BlockTransaction', async () => {
// Given – native tx id=1, token transfer transactionId=1
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTransactions.mockResolvedValue([
makeTx({ id: 1, hash: 'opHash1', amount: 100_000, bakerFee: 400 }),
])
mockFetchBlockTokenTransfers.mockResolvedValue([
makeTokenTransfer({ transactionId: 1, amount: '500' }),
])
// When
const result = await getBlock(context, 5_000_000)
// Then – one BlockTransaction with 2 native ops + 2 token ops
expect(result.transactions).toHaveLength(1)
const tx = result.transactions[0]
expect(tx.operations).toHaveLength(4)
const tokenOps = tx.operations.filter(
(op) => op.type === 'transfer' && 'asset' in op && (op as any).asset.type === 'fa2'
)
expect(tokenOps).toHaveLength(2)
})
it('does NOT append token ops to a failed BlockTransaction', async () => {
// Given – parent tx failed; token transfer would be meaningless
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTransactions.mockResolvedValue([
makeTx({ id: 1, hash: 'opFailed', amount: 100_000, status: 'failed' }),
])
mockFetchBlockTokenTransfers.mockResolvedValue([
makeTokenTransfer({ transactionId: 1, amount: '500' }),
])
// When
const result = await getBlock(context, 5_000_000)
// Then – failed tx has no operations at all
expect(result.transactions).toHaveLength(1)
expect(result.transactions[0].failed).toBe(true)
expect(result.transactions[0].operations).toEqual([])
})
it('creates a standalone BlockTransaction for a token transfer with no parent tx', async () => {
// Given – token transfer with transactionId=99 but no matching native tx
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTokenTransfers.mockResolvedValue([
makeTokenTransfer({ id: 200, transactionId: 99, amount: '1000' }),
])
// When
const result = await getBlock(context, 5_000_000)
// Then – one standalone BlockTransaction with 2 token ops
expect(result.transactions).toHaveLength(1)
const tx = result.transactions[0]
expect(tx.hash).toBe('txid-99') // synthetic but unique, derived from transactionId
expect(tx.failed).toBe(false)
expect(tx.fees).toBe(0n)
expect(tx.operations).toHaveLength(2)
})
it('groups multiple token transfers sharing the same transactionId into one standalone entry', async () => {
// Given – two token transfers both orphaned but from the same parent id=42
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTokenTransfers.mockResolvedValue([
makeTokenTransfer({ id: 1, transactionId: 42, amount: '100' }),
makeTokenTransfer({
id: 2,
transactionId: 42,
amount: '200',
from: { address: 'tz1AnotherSender' },
to: { address: 'tz1AnotherReceiver' },
}),
])
// When
const result = await getBlock(context, 5_000_000)
// Then – merged into one standalone BlockTransaction with 4 ops (2 per transfer)
expect(result.transactions).toHaveLength(1)
expect(result.transactions[0].operations).toHaveLength(4)
})
it('creates a separate standalone entry for each token transfer without a transactionId', async () => {
// Given – two protocol-level transfers with no transactionId
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTokenTransfers.mockResolvedValue([
makeTokenTransfer({ id: 1, transactionId: undefined, amount: '100' }),
makeTokenTransfer({ id: 2, transactionId: undefined, amount: '200' }),
])
// When
const result = await getBlock(context, 5_000_000)
// Then – each gets its own BlockTransaction
expect(result.transactions).toHaveLength(2)
})
it('emits only an incoming operation for a minting event (no `from`), with no peer field', async () => {
// Given
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTokenTransfers.mockResolvedValue([
makeTokenTransfer({
transactionId: undefined,
from: null,
to: { address: 'tz1Receiver' },
amount: '750',
}),
])
// When
const result = await getBlock(context, 5_000_000)
// Then — peer must be absent when mint has no sender
expect(result.transactions[0].operations).toHaveLength(1)
expect(result.transactions[0].operations[0]).not.toHaveProperty('peer')
expect(result.transactions[0].operations[0]).toMatchObject({
type: 'transfer',
address: 'tz1Receiver',
amount: 750n,
})
})
it('emits only an outgoing operation for a burning event (no `to`), with no peer field', async () => {
// Given
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTokenTransfers.mockResolvedValue([
makeTokenTransfer({
transactionId: undefined,
from: { address: 'tz1Burner' },
to: null,
amount: '300',
}),
])
// When
const result = await getBlock(context, 5_000_000)
// Then — peer must be absent when burn has no receiver
expect(result.transactions[0].operations).toHaveLength(1)
expect(result.transactions[0].operations[0]).not.toHaveProperty('peer')
expect(result.transactions[0].operations[0]).toMatchObject({
type: 'transfer',
address: 'tz1Burner',
amount: -300n,
})
})
it('produces no operations for a zero-amount FA token transfer', async () => {
// Given
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTokenTransfers.mockResolvedValue([
makeTokenTransfer({ transactionId: undefined, amount: '0' }),
])
// When
const result = await getBlock(context, 5_000_000)
// Then – zero-amount transfer is silently discarded
expect(result.transactions).toHaveLength(0)
})
it("encodes assetReference as 'contract:tokenId' to uniquely identify FA2 tokens", async () => {
// Given
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTokenTransfers.mockResolvedValue([
makeTokenTransfer({
transactionId: undefined,
token: {
id: 5,
contract: { address: 'KT1SpecificContract' },
tokenId: '7',
standard: 'fa2',
metadata: { name: 'MyFA2Token', symbol: 'MFT' },
},
amount: '100',
}),
])
// When
const result = await getBlock(context, 5_000_000)
// Then — tokenId "7" must be part of the reference so different FA2 tokens under
// the same contract are not conflated.
const op = result.transactions[0].operations[0] as any
expect(op.asset).toMatchObject({
type: 'fa2',
assetReference: 'KT1SpecificContract:7',
name: 'MyFA2Token',
})
})
it('falls back to symbol as token name when metadata name is absent', async () => {
// Given
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTokenTransfers.mockResolvedValue([
makeTokenTransfer({
transactionId: undefined,
token: {
id: 6,
contract: { address: 'KT1NoName' },
tokenId: '0',
standard: 'fa1.2',
metadata: { symbol: 'NNT' }, // no name field
},
amount: '50',
}),
])
// When
const result = await getBlock(context, 5_000_000)
// Then
const op = result.transactions[0].operations[0] as any
expect(op.asset.name).toBe('NNT')
})
})
// ---------------------------------------------------------------------------
// 7. Delegation operations
// ---------------------------------------------------------------------------
describe('delegation operations', () => {
it('creates a BlockTransaction for a DELEGATE operation with correct details', async () => {
// Given
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockDelegations.mockResolvedValue([
makeDelegation({
sender: { address: 'tz1Delegator' },
newDelegate: { address: 'tz1Baker' },
prevDelegate: null,
counter: 42,
gasLimit: 1000,
storageLimit: 257,
}),
])
// When
const result = await getBlock(context, 5_000_000)
// Then
expect(result.transactions).toHaveLength(1)
const tx = result.transactions[0]
expect(tx.hash).toBe('opDelegation1')
expect(tx.failed).toBe(false)
expect(tx.feesPayer).toBe('tz1Delegator')
expect(tx.operations).toHaveLength(1)
expect(tx.operations[0]).toEqual({
type: 'other',
ledgerOpType: 'DELEGATE',
operationType: 'DELEGATE',
stakedAmount: 0,
counter: 42,
gasLimit: 1000,
storageLimit: 257,
})
})
it('creates a BlockTransaction for an UNDELEGATE operation with correct details', async () => {
// Given
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockDelegations.mockResolvedValue([
makeDelegation({
sender: { address: 'tz1Delegator' },
newDelegate: null,
prevDelegate: { address: 'tz1PrevBaker' },
counter: 7,
gasLimit: 500,
storageLimit: 100,
}),
])
// When
const result = await getBlock(context, 5_000_000)
// Then
expect(result.transactions).toHaveLength(1)
const tx = result.transactions[0]
expect(tx.operations).toHaveLength(1)
expect(tx.operations[0]).toEqual({
type: 'other',
ledgerOpType: 'UNDELEGATE',
operationType: 'UNDELEGATE',
stakedAmount: 0,
counter: 7,
gasLimit: 500,
storageLimit: 100,
})
})
it('computes delegation fees from bakerFee, storageFee, and allocationFee', async () => {
// Given
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockDelegations.mockResolvedValue([
makeDelegation({ bakerFee: 500, storageFee: 100, allocationFee: 50 }),
])
// When
const result = await getBlock(context, 5_000_000)
// Then
expect(result.transactions[0].fees).toBe(650n)
})
it("marks a delegation as failed when status is not 'applied'", async () => {
// Given
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockDelegations.mockResolvedValue([makeDelegation({ status: 'failed' })])
// When
const result = await getBlock(context, 5_000_000)
// Then — failed but operations still present (listOperations path always includes them)
expect(result.transactions[0].failed).toBe(true)
expect(result.transactions[0].operations).toHaveLength(1)
})
it('skips delegations without a hash', async () => {
// Given
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockDelegations.mockResolvedValue([
makeDelegation({ hash: undefined as unknown as string }),
makeDelegation({ id: 201, hash: 'opDelegation2' }),
])
// When
const result = await getBlock(context, 5_000_000)
// Then
expect(result.transactions).toHaveLength(1)
expect(result.transactions[0].hash).toBe('opDelegation2')
})
it('creates delegation with empty operations when sender is missing', async () => {
// Given
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockDelegations.mockResolvedValue([makeDelegation({ sender: null })])
// When
const result = await getBlock(context, 5_000_000)
// Then
expect(result.transactions).toHaveLength(1)
expect(result.transactions[0].operations).toEqual([])
})
it('merges delegation into existing transaction when they share the same hash', async () => {
// Given - transaction and delegation batched together (same hash)
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTransactions.mockResolvedValue([
makeTx({ hash: 'opSharedHash', amount: 1_000_000, bakerFee: 500 }),
])
mockFetchBlockDelegations.mockResolvedValue([
makeDelegation({ hash: 'opSharedHash', bakerFee: 300 }),
])
// When
const result = await getBlock(context, 5_000_000)
// Then - single BlockTransaction with both transfer and delegation operations, combined fees
expect(result.transactions).toHaveLength(1)
const tx = result.transactions[0]
expect(tx.operations.some((op) => op.type === 'transfer')).toBe(true)
expect(tx.operations.some((op) => op.type === 'other')).toBe(true)
expect(tx.fees).toBe(800n) // 500 + 300
})
it('marks merged transaction as failed and clears operations if delegation failed', async () => {
// Given - transaction succeeded but delegation in same batch failed
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTransactions.mockResolvedValue([
makeTx({ hash: 'opSharedHash', amount: 1_000_000, status: 'applied', bakerFee: 500 }),
])
mockFetchBlockDelegations.mockResolvedValue([
makeDelegation({ hash: 'opSharedHash', status: 'failed', bakerFee: 300 }),
])
// When
const result = await getBlock(context, 5_000_000)
// Then - the whole batch is marked as failed, operations cleared, fees accumulated
expect(result.transactions).toMatchObject([{ failed: true, operations: [], fees: 800n }])
})
it('does not add delegation operations to failed transaction', async () => {
// Given - transaction failed, delegation would be backtracked anyway
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTransactions.mockResolvedValue([
makeTx({ hash: 'opSharedHash', amount: 1_000_000, status: 'failed' }),
])
mockFetchBlockDelegations.mockResolvedValue([
makeDelegation({ hash: 'opSharedHash', status: 'applied' }),
])
// When
const result = await getBlock(context, 5_000_000)
// Then - no operations (tx was failed), but fees still accumulated
expect(result.transactions).toMatchObject([{ failed: true, operations: [], fees: 1000n }])
})
it('includes both transactions and delegations in the same block', async () => {
// Given
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTransactions.mockResolvedValue([makeTx({ hash: 'opTx1', amount: 1_000_000 })])
mockFetchBlockDelegations.mockResolvedValue([makeDelegation({ hash: 'opDelegation1' })])
// When
const result = await getBlock(context, 5_000_000)
// Then
expect(result.transactions).toHaveLength(2)
const hashes = result.transactions.map((t) => t.hash)
expect(hashes).toContain('opTx1')
expect(hashes).toContain('opDelegation1')
})
it('treats delegations with no status field as succeeded', async () => {
// Given
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockDelegations.mockResolvedValue([makeDelegation({ status: undefined })])
// When
const result = await getBlock(context, 5_000_000)
// Then
expect(result.transactions[0].failed).toBe(false)
expect(result.transactions[0].operations).toHaveLength(1)
})
it('omits feesPayer when sender is missing', async () => {
// Given
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockDelegations.mockResolvedValue([makeDelegation({ sender: null })])
// When
const result = await getBlock(context, 5_000_000)
// Then
expect(result.transactions[0].feesPayer).toBeUndefined()
})
it('omits delegate field when neither newDelegate nor prevDelegate is present', async () => {
// Given - edge case: both delegates are null
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockDelegations.mockResolvedValue([
makeDelegation({ newDelegate: null, prevDelegate: null }),
])
// When
const result = await getBlock(context, 5_000_000)
// Then
const op = result.transactions[0].operations[0] as Record<string, unknown>
expect(op).not.toHaveProperty('delegate')
expect(op.ledgerOpType).toBe('UNDELEGATE')
})
})
// ---------------------------------------------------------------------------
// 7b. Staking operations (Paris adaptive issuance)
// ---------------------------------------------------------------------------
describe('staking operations', () => {
it.each([
['stake', 'STAKE', 500_000_000],
['unstake', 'UNSTAKE', 250_000_000],
['finalize', 'FINALIZE_UNSTAKE', 0],
] as const)(
'creates a BlockTransaction for action=%s with ledgerOpType=%s',
async (action, expectedOpType, expectedStakedAmount) => {
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockStaking.mockResolvedValue([
makeStaking({ action, amount: Number(expectedStakedAmount) }),
])
const result = await getBlock(context, 5_000_000)
expect(result.transactions).toHaveLength(1)
const tx = result.transactions[0]
expect(tx.hash).toBe('opStaking1')
expect(tx.failed).toBe(false)
expect(tx.feesPayer).toBe('tz1Staker')
expect(tx.fees).toBe(800n)
expect(tx.operations).toHaveLength(1)
expect(tx.operations[0]).toEqual({
type: 'other',
ledgerOpType: expectedOpType,
operationType: expectedOpType,
stakedAmount: expectedStakedAmount,
counter: 1,
gasLimit: 3630,
storageLimit: 0,
})
}
)
it('marks a staking op as failed but still includes operations with requestedAmount fallback', async () => {
// TzKT omits `amount` on failed ops; only `requestedAmount` is present
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockStaking.mockResolvedValue([
makeStaking({ status: 'failed', amount: undefined, requestedAmount: 750_000_000 }),
])
const result = await getBlock(context, 5_000_000)
expect(result.transactions[0].failed).toBe(true)
expect(result.transactions[0].operations).toHaveLength(1)
const op = result.transactions[0].operations[0] as Record<string, unknown>
expect(op.stakedAmount).toBe(750_000_000)
})
it('skips a staking op without sender (no operations emitted)', async () => {
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockStaking.mockResolvedValue([makeStaking({ sender: null })])
const result = await getBlock(context, 5_000_000)
expect(result.transactions).toHaveLength(1)
expect(result.transactions[0].operations).toEqual([])
})
it('skips a staking op with no hash', async () => {
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockStaking.mockResolvedValue([
makeStaking({ hash: undefined as unknown as string }),
makeStaking({ id: 301, hash: 'opStaking2' }),
])
const result = await getBlock(context, 5_000_000)
expect(result.transactions).toHaveLength(1)
expect(result.transactions[0].hash).toBe('opStaking2')
})
it('merges a staking op into an existing transaction sharing the same hash', async () => {
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTransactions.mockResolvedValue([
makeTx({ hash: 'opShared', amount: 1_000_000, bakerFee: 500 }),
])
mockFetchBlockStaking.mockResolvedValue([
makeStaking({ hash: 'opShared', action: 'stake', amount: 100, bakerFee: 300 }),
])
const result = await getBlock(context, 5_000_000)
expect(result.transactions).toHaveLength(1)
const tx = result.transactions[0]
expect(tx.operations.some((op) => op.type === 'transfer')).toBe(true)
expect(tx.operations.some((op) => op.type === 'other')).toBe(true)
expect(tx.fees).toBe(800n)
})
it('omits delegate field when baker is missing', async () => {
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockStaking.mockResolvedValue([makeStaking({ baker: null })])
const result = await getBlock(context, 5_000_000)
const op = result.transactions[0].operations[0] as Record<string, unknown>
expect(op).not.toHaveProperty('delegate')
expect(op.ledgerOpType).toBe('STAKE')
})
})
// ---------------------------------------------------------------------------
// 8. Origination operations
// ---------------------------------------------------------------------------
describe('origination operations', () => {
it('produces a BlockTransaction with ledgerOpType ORIGINATION and zero amount for zero-balance origination', async () => {
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockOriginations.mockResolvedValue([
makeOrigination({ contractBalance: 0, counter: 42, gasLimit: 3494, storageLimit: 5852 }),
])
const result = await getBlock(context, 5_000_000)
expect(result.transactions).toHaveLength(1)
const tx = result.transactions[0]
expect(tx.hash).toBe('opOrigination1')
expect(tx.fees).toBe(1_069_250n) // 5000 + 1000000 + 64250
expect(tx.feesPayer).toBe('tz1Deployer')
expect(tx.failed).toBe(false)
expect(tx.operations).toHaveLength(1)
expect(tx.operations[0]).toMatchObject({
type: 'other',
address: 'tz1Deployer',
ledgerOpType: 'ORIGINATION',
counter: 42,
gasLimit: 3494,
storageLimit: 5852,
})
expect(tx.operations[0]).not.toHaveProperty('originatedContract')
})
it('produces an origination with contractBalance > 0 (no amount field)', async () => {
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockOriginations.mockResolvedValue([makeOrigination({ contractBalance: 500_000 })])
const result = await getBlock(context, 5_000_000)
expect(result.transactions).toHaveLength(1)
const tx = result.transactions[0]
expect(tx.operations).toHaveLength(1)
const op = tx.operations[0] as OtherBlockOperation
expect(op.type).toBe('other')
expect(op.address).toBe('tz1Deployer')
expect((op as Record<string, unknown>).ledgerOpType).toBe('ORIGINATION')
expect(op).not.toHaveProperty('amount')
})
it('treats negative contractBalance same as zero (no amount field)', async () => {
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockOriginations.mockResolvedValue([makeOrigination({ contractBalance: -500 })])
const result = await getBlock(context, 5_000_000)
expect(result.transactions[0].operations[0]).not.toHaveProperty('amount')
})
it('marks a failed origination but still includes operations', async () => {
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockOriginations.mockResolvedValue([
makeOrigination({ status: 'failed', contractBalance: 500_000 }),
])
const result = await getBlock(context, 5_000_000)
expect(result.transactions).toHaveLength(1)
const tx = result.transactions[0]
expect(tx.failed).toBe(true)
expect(tx.operations).toHaveLength(1)
expect(tx.fees).toBe(1_069_250n)
})
it('skips an origination with no hash', async () => {
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockOriginations.mockResolvedValue([makeOrigination({ hash: undefined })])
const result = await getBlock(context, 5_000_000)
expect(result.transactions).toHaveLength(0)
})
it('returns empty operations when sender is null', async () => {
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockOriginations.mockResolvedValue([
makeOrigination({ sender: null, contractBalance: 100_000 }),
])
const result = await getBlock(context, 5_000_000)
expect(result.transactions).toHaveLength(1)
const tx = result.transactions[0]
expect(tx.operations).toHaveLength(0)
expect(tx.feesPayer).toBeUndefined()
})
it('attaches token transfers to origination parent via originationId', async () => {
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockOriginations.mockResolvedValue([
makeOrigination({ id: 400, hash: 'opOrig1', contractBalance: 0 }),
])
mockFetchBlockTokenTransfers.mockResolvedValue([
makeTokenTransfer({ transactionId: undefined, originationId: 400, amount: '1000' }),
])
const result = await getBlock(context, 5_000_000)
// origination tx should contain both the native transfer and the token ops
const origTx = result.transactions.find((t) => t.hash === 'opOrig1')!
const tokenOps = origTx.operations.filter(
(op) => op.type === 'transfer' && 'asset' in op && (op as any).asset.type === 'fa2'
)
expect(tokenOps).toHaveLength(2) // from + to
})
it('resolves cross-block origination hash for token transfers', async () => {
// Token transfer at this block has originationId=999 pointing to an origination
// from a different block. getBlock should batch-fetch the origination to resolve the hash.
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTokenTransfers.mockResolvedValue([
makeTokenTransfer({ id: 50, transactionId: undefined, originationId: 999, amount: '500' }),
])
mockGetOperationsOrigination.mockResolvedValue([
{ id: 999, hash: 'opCrossBlockOrig', block: 'BLsomeOtherBlock' },
])
const result = await getBlock(context, 5_000_000)
// The token transfer should use the resolved origination hash, not "token-50"
expect(mockGetOperationsOrigination).toHaveBeenCalledWith(0, undefined, { 'id.in': '999' })
const tx = result.transactions.find((t) => t.hash === 'opCrossBlockOrig')!
expect(tx.operations).toHaveLength(2) // from + to token ops
})
})
// ---------------------------------------------------------------------------
// 8b. Reveal operations
// ---------------------------------------------------------------------------
describe('reveal operations', () => {
it('creates a BlockTransaction for a reveal with ledgerOpType REVEAL', async () => {
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockReveals.mockResolvedValue([
makeReveal({ counter: 10, gasLimit: 10600, storageLimit: 0, bakerFee: 1420 }),
])
const result = await getBlock(context, 5_000_000)
expect(result.transactions).toHaveLength(1)
const tx = result.transactions[0]
expect(tx.hash).toBe('opReveal1')
expect(tx.failed).toBe(false)
expect(tx.feesPayer).toBe('tz1Revealer')
expect(tx.fees).toBe(1420n)
expect(tx.operations).toHaveLength(1)
expect(tx.operations[0]).toEqual({
type: 'other',
address: 'tz1Revealer',
ledgerOpType: 'REVEAL',
counter: 10,
gasLimit: 10600,
storageLimit: 0,
})
})
it('merges reveal fee into sibling transaction sharing the same hash', async () => {
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTransactions.mockResolvedValue([
makeTx({ hash: 'opBatchedHash', amount: 0, bakerFee: 5115, sender: { address: 'tz1User' } }),
])
mockFetchBlockReveals.mockResolvedValue([
makeReveal({ hash: 'opBatchedHash', bakerFee: 1420, sender: { address: 'tz1User' } }),
])
const result = await getBlock(context, 5_000_000)
expect(result.transactions).toHaveLength(1)
const tx = result.transactions[0]
expect(tx.fees).toBe(6535n) // 5115 + 1420
expect(tx.operations.some((op) => op.type === 'other')).toBe(true)
const revealOp = tx.operations.find((op) => op.type === 'other') as Record<string, unknown>
expect(revealOp).toMatchObject({ ledgerOpType: 'REVEAL' })
})
it('marks a reveal as failed but still includes operations', async () => {
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockReveals.mockResolvedValue([makeReveal({ status: 'failed' })])
const result = await getBlock(context, 5_000_000)
expect(result.transactions[0].failed).toBe(true)
expect(result.transactions[0].operations).toHaveLength(1)
})
it('skips reveals without a hash', async () => {
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockReveals.mockResolvedValue([
makeReveal({ hash: undefined as unknown as string }),
makeReveal({ id: 501, hash: 'opReveal2' }),
])
const result = await getBlock(context, 5_000_000)
expect(result.transactions).toHaveLength(1)
expect(result.transactions[0].hash).toBe('opReveal2')
})
it('does not add reveal operations to an already-failed sibling transaction', async () => {
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTransactions.mockResolvedValue([
makeTx({ hash: 'opBatchedHash', amount: 1_000_000, status: 'failed', bakerFee: 500 }),
])
mockFetchBlockReveals.mockResolvedValue([makeReveal({ hash: 'opBatchedHash', bakerFee: 1420 })])
const result = await getBlock(context, 5_000_000)
expect(result.transactions).toHaveLength(1)
const tx = result.transactions[0]
expect(tx.failed).toBe(true)
expect(tx.operations).toEqual([])
expect(tx.fees).toBe(1920n) // 500 + 1420 — fees still accumulate
})
it('creates reveal with empty operations when sender is missing', async () => {
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockReveals.mockResolvedValue([makeReveal({ sender: null })])
const result = await getBlock(context, 5_000_000)
expect(result.transactions).toHaveLength(1)
expect(result.transactions[0].operations).toEqual([])
})
})
// ---------------------------------------------------------------------------
// 9. Network call contract
// ---------------------------------------------------------------------------
describe('network calls', () => {
it('issues all eight network calls with the correct heights', async () => {
// Given
const height = 6_000_000
mockGetBlockByLevel.mockResolvedValue(makeBlock(height))
// When
await getBlock(context, height)
// Then – getBlockByLevel is called once for the block and once for the parent
expect(mockGetBlockByLevel).toHaveBeenCalledTimes(2)
expect(mockGetBlockByLevel).toHaveBeenCalledWith(height)
expect(mockGetBlockByLevel).toHaveBeenCalledWith(height - 1)
expect(mockFetchBlockTransactions).toHaveBeenCalledTimes(1)
expect(mockFetchBlockTransactions).toHaveBeenCalledWith(expect.anything(), height)
expect(mockFetchBlockTokenTransfers).toHaveBeenCalledTimes(1)
expect(mockFetchBlockTokenTransfers).toHaveBeenCalledWith(expect.anything(), height)
expect(mockFetchBlockDelegations).toHaveBeenCalledTimes(1)
expect(mockFetchBlockDelegations).toHaveBeenCalledWith(expect.anything(), height)
expect(mockFetchBlockStaking).toHaveBeenCalledTimes(1)
expect(mockFetchBlockStaking).toHaveBeenCalledWith(expect.anything(), height)
expect(mockFetchBlockOriginations).toHaveBeenCalledTimes(1)
expect(mockFetchBlockOriginations).toHaveBeenCalledWith(expect.anything(), height)
expect(mockFetchBlockReveals).toHaveBeenCalledTimes(1)
expect(mockFetchBlockReveals).toHaveBeenCalledWith(expect.anything(), height)
})
it('propagates errors from getBlockByLevel', async () => {
// Given
mockGetBlockByLevel.mockRejectedValue(new Error('block fetch failed'))
// When / Then
await expect(getBlock(context, 1_000_000)).rejects.toThrow('block fetch failed')
})
it('propagates errors from fetchBlockTransactions', async () => {
// Given
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTransactions.mockRejectedValue(new Error('tx fetch failed'))
// When / Then
await expect(getBlock(context, 1_000_000)).rejects.toThrow('tx fetch failed')
})
it('propagates errors from fetchBlockTokenTransfers', async () => {
// Given
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockTokenTransfers.mockRejectedValue(new Error('token fetch failed'))
// When / Then
await expect(getBlock(context, 1_000_000)).rejects.toThrow('token fetch failed')
})
it('propagates errors from fetchBlockDelegations', async () => {
// Given
mockGetBlockByLevel.mockResolvedValue(makeBlock())
mockFetchBlockDelegations.mockRejectedValue(new Error('delegation fetch failed'))
// When / Then
await expect(getBlock(context, 1_000_000)).rejects.toThrow('delegation fetch failed')
})
})