@bsv/sdk
Version:
BSV Blockchain Software Development Kit
691 lines (554 loc) • 28.9 kB
text/typescript
/** eslint-env jest */
import { Historian, InterpreterFunction } from '../Historian.js'
import Transaction from '../../transaction/Transaction.js'
import { TransactionInput, TransactionOutput } from '../../transaction/index.js'
// --- Module mocks ------------------------------------------------------------
jest.mock('../../transaction/Transaction.js')
// --- Test constants ----------------------------------------------------------
const TEST_TXID_1 = '1111111111111111111111111111111111111111111111111111111111111111'
const TEST_TXID_2 = '2222222222222222222222222222222222222222222222222222222222222222'
const TEST_TXID_3 = '3333333333333333333333333333333333333333333333333333333333333333'
// --- Test types --------------------------------------------------------------
interface TestValue {
data: string
outputIndex?: number
}
interface TestContext {
filter?: string
}
// --- Helpers ----------------------------------------------------------------
type MTx = jest.Mocked<InstanceType<typeof Transaction>>
function makeMockTx(txid: string, outputs: any[] = [], inputs: any[] = []): MTx {
return {
id: jest.fn().mockReturnValue(txid),
outputs,
inputs,
} as any
}
function makeMockOutput(scriptHex?: string): TransactionOutput {
const hex = scriptHex || '76a914' // Default to P2PKH prefix if no script provided
const scriptArray = hex.match(/.{2}/g)?.map(byte => parseInt(byte, 16)) || [0x76, 0xa9, 0x14]
return {
satoshis: 1,
lockingScript: {
toHex: jest.fn().mockReturnValue(hex),
toArray: jest.fn().mockReturnValue(scriptArray),
},
} as any
}
function makeMockInput(sourceTransaction?: MTx): TransactionInput {
return {
sourceTransaction,
sourceTXID: sourceTransaction?.id('hex'),
sourceOutputIndex: 0,
} as any
}
// Simple interpreter that extracts data from P2PKH scripts (starts with 76a914)
const simpleInterpreter: InterpreterFunction<TestValue, TestContext> = (
tx: Transaction,
outputIndex: number,
ctx?: TestContext
): TestValue | undefined => {
const output = tx.outputs[outputIndex]
const scriptHex = output.lockingScript.toHex()
// Only interpret P2PKH scripts (starts with 76a914)
if (!scriptHex.startsWith('76a914')) {
return undefined
}
// Apply context filter if provided (e.g., only certain script prefixes)
if (ctx?.filter && !scriptHex.startsWith(ctx.filter)) {
return undefined
}
return {
data: `value_from_${scriptHex.slice(0, 8)}`, // Extract identifier from script
outputIndex
}
}
// Interpreter that throws errors for specific script patterns
const throwingInterpreter: InterpreterFunction<TestValue> = (
tx: Transaction,
outputIndex: number
): TestValue | undefined => {
const output = tx.outputs[outputIndex]
const scriptHex = output.lockingScript.toHex()
// Throw error for scripts containing 'deadbeef' pattern
if (scriptHex.includes('deadbeef')) {
throw new Error('Interpreter error')
}
// Only interpret P2PKH scripts
if (scriptHex.startsWith('76a914')) {
return { data: `value_from_${scriptHex.slice(0, 8)}`, outputIndex }
}
return undefined
}
// Async interpreter for testing Promise handling
const asyncInterpreter: InterpreterFunction<TestValue> = async (
tx: Transaction,
outputIndex: number
): Promise<TestValue | undefined> => {
await new Promise(resolve => setTimeout(resolve, 1)) // Simulate async work
const output = tx.outputs[outputIndex]
const scriptHex = output.lockingScript.toHex()
if (scriptHex.startsWith('76a914')) {
return { data: `async_value_from_${scriptHex.slice(0, 8)}`, outputIndex }
}
return undefined
}
// Async interpreter that rejects for testing Promise rejection
const asyncRejectingInterpreter: InterpreterFunction<TestValue> = async (
tx: Transaction,
outputIndex: number
): Promise<TestValue | undefined> => {
await new Promise(resolve => setTimeout(resolve, 1))
const output = tx.outputs[outputIndex]
const scriptHex = output.lockingScript.toHex()
// Reject for scripts containing 'badf00d' pattern
if (scriptHex.includes('badf00d')) {
throw new Error('Async interpreter error')
}
if (scriptHex.startsWith('76a914')) {
return { data: `async_value_from_${scriptHex.slice(0, 8)}`, outputIndex }
}
return undefined
}
// Interpreter that returns falsy but valid values
const falsyValueInterpreter: InterpreterFunction<TestValue> = (
tx: Transaction,
outputIndex: number
): TestValue | undefined => {
const output = tx.outputs[outputIndex]
const scriptHex = output.lockingScript.toHex()
// Return empty string for scripts ending with '00'
if (scriptHex.endsWith('00')) {
return { data: '' } // Falsy but valid
}
// Return '0' for scripts ending with '30' (ASCII '0')
if (scriptHex.endsWith('30')) {
return { data: '0' } // Falsy but valid
}
return undefined
}
// --- Test suite --------------------------------------------------------------
describe('Historian', () => {
let historian: Historian<TestValue, TestContext>
let mockConsoleLog: jest.SpyInstance
beforeEach(() => {
jest.clearAllMocks()
mockConsoleLog = jest.spyOn(console, 'log').mockImplementation()
historian = new Historian(simpleInterpreter)
})
afterEach(() => {
mockConsoleLog.mockRestore()
})
// --------------------------------------------------------------------------
describe('Constructor', () => {
it('creates with interpreter function', () => {
const testHistorian = new Historian(simpleInterpreter)
expect(testHistorian).toBeInstanceOf(Historian)
})
it('accepts debug option', () => {
const debugHistorian = new Historian(simpleInterpreter, { debug: true })
expect(debugHistorian).toBeInstanceOf(Historian)
})
})
// --------------------------------------------------------------------------
describe('buildHistory', () => {
describe('happy paths', () => {
it('returns empty array for transaction with no interpretable outputs', async () => {
const tx = makeMockTx(TEST_TXID_1, [
makeMockOutput('6a'), // OP_RETURN script (not P2PKH)
makeMockOutput('a914') // P2SH script (not P2PKH)
])
const history = await historian.buildHistory(tx)
expect(history).toEqual([])
})
it('extracts single value from transaction with one interpretable output', async () => {
const tx = makeMockTx(TEST_TXID_1, [
makeMockOutput('76a914abcdef1234567890abcdef1234567890abcdef88ac'), // P2PKH script
makeMockOutput('6a') // OP_RETURN (not interpretable)
])
const history = await historian.buildHistory(tx)
expect(history).toHaveLength(1)
expect(history[0]).toMatchObject({ data: 'value_from_76a914ab' })
})
it('extracts multiple values from transaction with multiple interpretable outputs', async () => {
const tx = makeMockTx(TEST_TXID_1, [
makeMockOutput('76a914abcdef1234567890abcdef1234567890abcdef88ac'), // P2PKH script 1
makeMockOutput('76a914123456789012345678901234567890123456788ac'), // P2PKH script 2
makeMockOutput('6a') // OP_RETURN (not interpretable)
])
const history = await historian.buildHistory(tx)
expect(history).toHaveLength(2)
// The order is reversed due to history.reverse() at the end
expect(history[0]).toMatchObject({ data: 'value_from_76a91412' })
expect(history[1]).toMatchObject({ data: 'value_from_76a914ab' })
})
it('traverses input chain and returns history in chronological order', async () => {
// Create a chain: tx1 <- tx2 <- tx3 (tx3 is newest)
const tx1 = makeMockTx(TEST_TXID_1, [makeMockOutput('76a914111111111111111111111111111111111111111188ac')])
const tx2 = makeMockTx(TEST_TXID_2, [makeMockOutput('76a914222222222222222222222222222222222222222288ac')], [
makeMockInput(tx1)
])
const tx3 = makeMockTx(TEST_TXID_3, [makeMockOutput('76a914333333333333333333333333333333333333333388ac')], [
makeMockInput(tx2)
])
const history = await historian.buildHistory(tx3)
expect(history).toHaveLength(3)
expect(history[0]).toMatchObject({ data: 'value_from_76a91411' }) // Chronological order
expect(history[1]).toMatchObject({ data: 'value_from_76a91422' })
expect(history[2]).toMatchObject({ data: 'value_from_76a91433' })
})
it('handles multiple inputs per transaction', async () => {
const tx1 = makeMockTx(TEST_TXID_1, [makeMockOutput('76a914aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa88ac')])
const tx2 = makeMockTx(TEST_TXID_2, [makeMockOutput('76a914bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb88ac')])
const tx3 = makeMockTx(TEST_TXID_3, [makeMockOutput('76a914cccccccccccccccccccccccccccccccccccccccc88ac')], [
makeMockInput(tx1),
makeMockInput(tx2)
])
const history = await historian.buildHistory(tx3)
expect(history).toHaveLength(3)
// The order depends on traversal: tx3 outputs first, then tx1, then tx2, then reversed
expect(history.map(h => h.data)).toEqual(['value_from_76a914bb', 'value_from_76a914aa', 'value_from_76a914cc'])
})
it('passes context to interpreter function', async () => {
const contextHistorian = new Historian(simpleInterpreter)
const tx = makeMockTx(TEST_TXID_1, [
makeMockOutput('76a914filtered123456789012345678901234567888ac'), // Matches filter
makeMockOutput('76a914other1234567890123456789012345678988ac') // Doesn't match filter
])
const history = await contextHistorian.buildHistory(tx, { filter: '76a914filtered' })
expect(history).toHaveLength(1)
expect(history[0]).toMatchObject({ data: 'value_from_76a914fi' })
})
it('works with async interpreter functions', async () => {
const asyncHistorian = new Historian(asyncInterpreter)
const tx = makeMockTx(TEST_TXID_1, [
makeMockOutput('76a914async12345678901234567890123456789088ac')
])
const history = await asyncHistorian.buildHistory(tx)
expect(history).toHaveLength(1)
expect(history[0]).toMatchObject({ data: 'async_value_from_76a914as' })
})
it('includes falsy but valid values from interpreter', async () => {
const falsyHistorian = new Historian(falsyValueInterpreter)
const tx = makeMockTx(TEST_TXID_1, [
makeMockOutput('76a914123456789012345678901234567890123400'), // Returns { data: '' }
makeMockOutput('76a914123456789012345678901234567890123430'), // Returns { data: '0' }
makeMockOutput('76a914123456789012345678901234567890123456') // Returns undefined
])
const history = await falsyHistorian.buildHistory(tx)
expect(history).toHaveLength(2)
expect(history[0]).toMatchObject({ data: '0' }) // Falsy but valid
expect(history[1]).toMatchObject({ data: '' }) // Falsy but valid
})
})
describe('sad paths', () => {
it('handles interpreter errors gracefully', async () => {
const errorHistorian = new Historian(throwingInterpreter)
const tx = makeMockTx(TEST_TXID_1, [
makeMockOutput('76a914abcdef1234567890abcdef1234567890abcdef88ac'), // Good P2PKH
makeMockOutput('76a914deadbeef1234567890123456789012345688ac'), // Contains 'deadbeef' - will throw
makeMockOutput('76a914fedcba0987654321fedcba0987654321fedcba88ac') // Good P2PKH
])
const history = await errorHistorian.buildHistory(tx)
expect(history).toHaveLength(2)
// The order is reversed due to history.reverse() at the end
expect(history[0]).toMatchObject({ data: 'value_from_76a914fe' })
expect(history[1]).toMatchObject({ data: 'value_from_76a914ab' })
})
it('handles async interpreter rejection gracefully', async () => {
const rejectingHistorian = new Historian(asyncRejectingInterpreter)
const tx = makeMockTx(TEST_TXID_1, [
makeMockOutput('76a914abcdef1234567890abcdef1234567890abcdef88ac'), // Good P2PKH
makeMockOutput('76a914badf00d1234567890123456789012345688ac'), // Contains 'badf00d' - will reject
makeMockOutput('76a914fedcba0987654321fedcba0987654321fedcba88ac') // Good P2PKH
])
const history = await rejectingHistorian.buildHistory(tx)
expect(history).toHaveLength(2)
expect(history[0]).toMatchObject({ data: 'async_value_from_76a914fe' })
expect(history[1]).toMatchObject({ data: 'async_value_from_76a914ab' })
})
it('handles missing sourceTransaction in inputs', async () => {
const tx1 = makeMockTx(TEST_TXID_1, [makeMockOutput('76a914abcdef1234567890abcdef1234567890abcdef88ac')], [
makeMockInput(), // No sourceTransaction
makeMockInput() // No sourceTransaction
])
const history = await historian.buildHistory(tx1)
expect(history).toHaveLength(1)
expect(history[0]).toMatchObject({ data: 'value_from_76a914ab' })
})
it('prevents infinite loops in circular transaction chains', async () => {
const tx1 = makeMockTx(TEST_TXID_1, [makeMockOutput('76a914111111111111111111111111111111111111111188ac')])
const tx2 = makeMockTx(TEST_TXID_2, [makeMockOutput('76a914222222222222222222222222222222222222222288ac')], [
makeMockInput(tx1)
])
// Create circular reference: tx1 -> tx2 -> tx1
tx1.inputs = [makeMockInput(tx2)]
const history = await historian.buildHistory(tx2)
expect(history).toHaveLength(2)
expect(history[0]).toMatchObject({ data: 'value_from_76a91411' })
expect(history[1]).toMatchObject({ data: 'value_from_76a91422' })
})
})
describe('debug mode', () => {
it('logs debug information when debug=true', async () => {
const debugHistorian = new Historian(simpleInterpreter, { debug: true })
const tx = makeMockTx(TEST_TXID_1, [makeMockOutput('76a914abcdef1234567890abcdef1234567890abcdef88ac')])
await debugHistorian.buildHistory(tx)
expect(mockConsoleLog).toHaveBeenCalledWith(
expect.stringContaining('[Historian] Processing transaction:')
)
expect(mockConsoleLog).toHaveBeenCalledWith(
'[Historian] Added value to history:',
expect.objectContaining({ data: 'value_from_76a914ab' })
)
})
it('logs cycle detection in debug mode', async () => {
const debugHistorian = new Historian(simpleInterpreter, { debug: true })
const tx1 = makeMockTx(TEST_TXID_1, [makeMockOutput('76a914111111111111111111111111111111111111111188ac')])
const tx2 = makeMockTx(TEST_TXID_2, [makeMockOutput('76a914222222222222222222222222222222222222222288ac')], [
makeMockInput(tx1)
])
// Create circular reference
tx1.inputs = [makeMockInput(tx2)]
await debugHistorian.buildHistory(tx2)
expect(mockConsoleLog).toHaveBeenCalledWith(
expect.stringContaining('[Historian] Skipping already visited transaction:')
)
})
it('logs missing sourceTransaction in debug mode', async () => {
const debugHistorian = new Historian(simpleInterpreter, { debug: true })
const tx = makeMockTx(TEST_TXID_1, [makeMockOutput('76a914abcdef1234567890abcdef1234567890abcdef88ac')], [
makeMockInput() // No sourceTransaction
])
await debugHistorian.buildHistory(tx)
// Precise assertion on exact log message
expect(mockConsoleLog).toHaveBeenCalledWith(
'[Historian] Input missing sourceTransaction, skipping'
)
})
it('logs interpreter errors in debug mode', async () => {
const debugHistorian = new Historian(throwingInterpreter, { debug: true })
const tx = makeMockTx(TEST_TXID_1, [
makeMockOutput('76a914deadbeef1234567890123456789012345688ac') // Contains 'deadbeef' - will throw
])
await debugHistorian.buildHistory(tx)
expect(mockConsoleLog).toHaveBeenCalledWith(
expect.stringContaining('[Historian] Failed to interpret output'),
expect.any(Error)
)
})
it('does not log when debug=false', async () => {
const quietHistorian = new Historian(simpleInterpreter, { debug: false })
const tx = makeMockTx(TEST_TXID_1, [makeMockOutput('76a914abcdef1234567890abcdef1234567890abcdef88ac')])
await quietHistorian.buildHistory(tx)
expect(mockConsoleLog).not.toHaveBeenCalled()
})
})
describe('caching', () => {
it('accepts historyCache option in constructor', () => {
const cache = new Map<string, readonly TestValue[]>()
const cachedHistorian = new Historian(simpleInterpreter, { historyCache: cache })
expect(cachedHistorian).toBeInstanceOf(Historian)
})
it('uses cache when provided and returns cached results', async () => {
const cache = new Map<string, readonly TestValue[]>()
const cachedHistorian = new Historian(simpleInterpreter, {
historyCache: cache,
debug: true // Enable debug to verify cache hit logs
})
const tx = makeMockTx(TEST_TXID_1, [makeMockOutput('76a914cached1234567890123456789012345678888ac')])
// First call - should populate cache
const history1 = await cachedHistorian.buildHistory(tx)
expect(cache.size).toBe(1)
expect(history1).toHaveLength(1)
expect(history1[0]).toMatchObject({ data: 'value_from_76a914ca' })
// Verify cache was populated (debug log should contain "cached")
expect(mockConsoleLog).toHaveBeenCalledWith(
expect.stringContaining('[Historian] History cached:'),
expect.any(String)
)
// Second call - should use cache (returns shallow copy, not same reference)
const history2 = await cachedHistorian.buildHistory(tx)
expect(history1).toStrictEqual(history2) // Same content from cache
expect(history1).not.toBe(history2) // Different references (shallow copy)
expect(cache.size).toBe(1) // No new cache entries
// Verify cache hit (debug log should contain "cache hit")
expect(mockConsoleLog).toHaveBeenCalledWith(
expect.stringContaining('[Historian] History cache hit:'),
expect.any(String)
)
})
it('generates different cache keys for different transactions', async () => {
const cache = new Map<string, readonly TestValue[]>()
const cachedHistorian = new Historian(simpleInterpreter, { historyCache: cache })
const tx1 = makeMockTx(TEST_TXID_1, [makeMockOutput('76a914tx1data123456789012345678901234567888ac')])
const tx2 = makeMockTx(TEST_TXID_2, [makeMockOutput('76a914tx2data123456789012345678901234567888ac')])
await cachedHistorian.buildHistory(tx1)
await cachedHistorian.buildHistory(tx2)
expect(cache.size).toBe(2) // Different transactions = different cache keys
// Verify both are cached independently
const history1 = await cachedHistorian.buildHistory(tx1)
const history2 = await cachedHistorian.buildHistory(tx2)
expect(history1[0].data).toBe('value_from_76a914tx')
expect(history2[0].data).toBe('value_from_76a914tx')
expect(cache.size).toBe(2) // Still only 2 entries
})
it('generates different cache keys for different contexts', async () => {
const cache = new Map<string, readonly TestValue[]>()
const cachedHistorian = new Historian(simpleInterpreter, { historyCache: cache })
const tx = makeMockTx(TEST_TXID_1, [
makeMockOutput('76a914filtered123456789012345678901234567888ac'), // Matches filter
makeMockOutput('76a914other1234567890123456789012345678988ac') // Doesn't match filter
])
// Same transaction, different contexts
await cachedHistorian.buildHistory(tx, { filter: '76a914filtered' })
await cachedHistorian.buildHistory(tx, { filter: '76a914other' })
await cachedHistorian.buildHistory(tx) // No context
expect(cache.size).toBe(3) // Different contexts = different cache keys
})
it('invalidates cache when interpreterVersion changes', async () => {
const cache = new Map<string, readonly TestValue[]>()
const historian1 = new Historian(simpleInterpreter, {
historyCache: cache,
interpreterVersion: 'v1'
})
const historian2 = new Historian(simpleInterpreter, {
historyCache: cache,
interpreterVersion: 'v2'
})
const tx = makeMockTx(TEST_TXID_1, [makeMockOutput('76a914version123456789012345678901234567888ac')])
await historian1.buildHistory(tx)
await historian2.buildHistory(tx) // Different version = new cache entry
expect(cache.size).toBe(2) // Two entries for different versions
// Verify both versions work independently
const history1 = await historian1.buildHistory(tx)
const history2 = await historian2.buildHistory(tx)
expect(history1).toBeDefined()
expect(history2).toBeDefined()
expect(cache.size).toBe(2) // Still only 2 entries
})
it('returns immutable cached results that cannot be mutated externally', async () => {
const cache = new Map<string, readonly TestValue[]>()
const cachedHistorian = new Historian(simpleInterpreter, { historyCache: cache })
const tx = makeMockTx(TEST_TXID_1, [makeMockOutput('76a914immutable123456789012345678901234567888ac')])
const history1 = await cachedHistorian.buildHistory(tx)
const history2 = await cachedHistorian.buildHistory(tx)
// Should be different references but same content (shallow copies from cache)
expect(history1).toStrictEqual(history2)
expect(history1).not.toBe(history2)
// Original cached value should be frozen, but returned copies are mutable
// Mutating returned copy should not affect the cache or future calls
; (history1 as any).push({ data: 'malicious', outputIndex: 999 })
const history3 = await cachedHistorian.buildHistory(tx)
expect(history3).toStrictEqual(history2) // Still original content from cache
expect(history3).toHaveLength(1) // Original length preserved
expect(history3).not.toStrictEqual(history1) // Different from mutated copy
})
it('works correctly with transaction chains when caching is enabled', async () => {
const cache = new Map<string, readonly TestValue[]>()
const cachedHistorian = new Historian(simpleInterpreter, { historyCache: cache })
// Create a simple chain: tx1 <- tx2
const tx1 = makeMockTx(TEST_TXID_1, [makeMockOutput('76a914chain11234567890123456789012345678888ac')])
const tx2 = makeMockTx(TEST_TXID_2, [makeMockOutput('76a914chain21234567890123456789012345678888ac')], [
makeMockInput(tx1)
])
// First call - should cache the results
const history1 = await cachedHistorian.buildHistory(tx2)
expect(history1).toHaveLength(2)
expect(cache.size).toBeGreaterThan(0)
// Second call - should use cache (same content, different reference)
const history2 = await cachedHistorian.buildHistory(tx2)
expect(history1).toStrictEqual(history2) // Same content from cache
expect(history1).not.toBe(history2) // Different references (shallow copy)
// Individual transaction should also be cached
const tx1History = await cachedHistorian.buildHistory(tx1)
expect(tx1History).toHaveLength(1)
expect(tx1History[0]).toMatchObject({ data: 'value_from_76a914ch' })
})
})
})
// --------------------------------------------------------------------------
describe('Integration scenarios', () => {
it('handles complex transaction chain with mixed interpretable outputs', async () => {
// Build a realistic scenario with multiple transactions and branches
const genesis = makeMockTx(TEST_TXID_1, [
makeMockOutput('76a914genesis123456789012345678901234567888ac'), // P2PKH
makeMockOutput('6a') // OP_RETURN (non-interpretable)
])
const branch1 = makeMockTx(TEST_TXID_2, [
makeMockOutput('76a914branch1123456789012345678901234567888ac') // P2PKH
], [makeMockInput(genesis)])
const branch2 = makeMockTx(TEST_TXID_3, [
makeMockOutput('76a914branch2123456789012345678901234567888ac'), // P2PKH
makeMockOutput('76a914extrabr123456789012345678901234567888ac') // P2PKH
], [makeMockInput(genesis)])
const merge = makeMockTx('4444444444444444444444444444444444444444444444444444444444444444', [
makeMockOutput('76a914finalme123456789012345678901234567888ac') // P2PKH
], [
makeMockInput(branch1),
makeMockInput(branch2)
])
const history = await historian.buildHistory(merge)
expect(history).toHaveLength(5)
// Verify all expected values are present by checking for the extracted prefixes
const dataValues = history.map(h => h.data)
expect(dataValues).toContain('value_from_76a914ge') // genesis
expect(dataValues).toContain('value_from_76a914br') // branch1 or branch2
expect(dataValues).toContain('value_from_76a914ex') // extrabr
expect(dataValues).toContain('value_from_76a914fi') // finalme
// All should be P2PKH interpretations
expect(dataValues.every(data => data.startsWith('value_from_76a914'))).toBe(true)
})
it('handles deep transaction chains efficiently', async () => {
// Create a chain of 10 transactions
let currentTx = makeMockTx('0000000000000000000000000000000000000000000000000000000000000000', [
makeMockOutput('76a914000000000000000000000000000000000000000088ac') // P2PKH for value_0
])
for (let i = 1; i < 10; i++) { // Keep it reasonable for test performance
const scriptHex = `76a914${i.toString(16).padStart(2, '0')}000000000000000000000000000000000000000088ac`
const newTx = makeMockTx(
i.toString().padStart(64, '0'),
[makeMockOutput(scriptHex)], // P2PKH scripts
[makeMockInput(currentTx)]
)
currentTx = newTx
}
const history = await historian.buildHistory(currentTx)
expect(history).toHaveLength(10)
expect(history[0]).toMatchObject({ data: 'value_from_76a91400' }) // Oldest first (genesis)
expect(history[9]).toMatchObject({ data: 'value_from_76a91409' }) // Newest last (tx 9)
// Verify the chain contains genesis and final transaction
expect(history.map(h => h.data)).toContain('value_from_76a91400') // genesis
expect(history.map(h => h.data)).toContain('value_from_76a91409') // tx 9
})
it('handles moderately large chains without stack overflow (sanity check)', async () => {
// Test stack safety with 50 transactions - not a benchmark, just sanity
const startTime = Date.now()
let currentTx = makeMockTx('0000000000000000000000000000000000000000000000000000000000000000', [
makeMockOutput('76a914genesis123456789012345678901234567888ac') // P2PKH for genesis
])
// Build chain of 50 transactions
for (let i = 1; i < 50; i++) {
// Create unique P2PKH scripts for each transaction
const scriptHex = `76a914${i.toString(16).padEnd(4, '0')}00000000000000000000000000000000000088ac`
const newTx = makeMockTx(
i.toString().padStart(64, '0'),
[makeMockOutput(scriptHex)],
[makeMockInput(currentTx)]
)
currentTx = newTx
}
const history = await historian.buildHistory(currentTx)
const duration = Date.now() - startTime
// Sanity checks: should complete quickly without errors
expect(history).toHaveLength(50)
expect(history[0]).toMatchObject({ data: 'value_from_76a914ge' }) // genesis - Oldest first
// Verify we have genesis and some chain transactions
expect(history.map(h => h.data)).toContain('value_from_76a914ge') // genesis
expect(history.map(h => h.data)).toContain('value_from_76a91410') // tx 1
expect(history.map(h => h.data)).toContain('value_from_76a91431') // tx 49 (31 in hex)
expect(duration).toBeLessThan(1000) // Should complete in under 1 second
})
})
})