@bsv/sdk 1.7.7 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/kvstore/GlobalKVStore.js +420 -0
  3. package/dist/cjs/src/kvstore/GlobalKVStore.js.map +1 -0
  4. package/dist/cjs/src/kvstore/LocalKVStore.js +6 -6
  5. package/dist/cjs/src/kvstore/LocalKVStore.js.map +1 -1
  6. package/dist/cjs/src/kvstore/kvStoreInterpreter.js +74 -0
  7. package/dist/cjs/src/kvstore/kvStoreInterpreter.js.map +1 -0
  8. package/dist/cjs/src/kvstore/types.js +11 -0
  9. package/dist/cjs/src/kvstore/types.js.map +1 -0
  10. package/dist/cjs/src/overlay-tools/Historian.js +153 -0
  11. package/dist/cjs/src/overlay-tools/Historian.js.map +1 -0
  12. package/dist/cjs/src/script/templates/PushDrop.js +2 -2
  13. package/dist/cjs/src/script/templates/PushDrop.js.map +1 -1
  14. package/dist/cjs/src/transaction/Transaction.js +4 -4
  15. package/dist/cjs/src/transaction/Transaction.js.map +1 -1
  16. package/dist/cjs/src/transaction/fee-models/LivePolicy.js +90 -0
  17. package/dist/cjs/src/transaction/fee-models/LivePolicy.js.map +1 -0
  18. package/dist/cjs/src/transaction/fee-models/index.js +3 -1
  19. package/dist/cjs/src/transaction/fee-models/index.js.map +1 -1
  20. package/dist/cjs/src/wallet/WalletClient.js +43 -52
  21. package/dist/cjs/src/wallet/WalletClient.js.map +1 -1
  22. package/dist/cjs/src/wallet/substrates/WalletWireProcessor.js +19 -0
  23. package/dist/cjs/src/wallet/substrates/WalletWireProcessor.js.map +1 -1
  24. package/dist/cjs/src/wallet/substrates/WalletWireTransceiver.js +18 -1
  25. package/dist/cjs/src/wallet/substrates/WalletWireTransceiver.js.map +1 -1
  26. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  27. package/dist/esm/src/kvstore/GlobalKVStore.js +416 -0
  28. package/dist/esm/src/kvstore/GlobalKVStore.js.map +1 -0
  29. package/dist/esm/src/kvstore/LocalKVStore.js +6 -6
  30. package/dist/esm/src/kvstore/LocalKVStore.js.map +1 -1
  31. package/dist/esm/src/kvstore/kvStoreInterpreter.js +47 -0
  32. package/dist/esm/src/kvstore/kvStoreInterpreter.js.map +1 -0
  33. package/dist/esm/src/kvstore/types.js +8 -0
  34. package/dist/esm/src/kvstore/types.js.map +1 -0
  35. package/dist/esm/src/overlay-tools/Historian.js +155 -0
  36. package/dist/esm/src/overlay-tools/Historian.js.map +1 -0
  37. package/dist/esm/src/script/templates/PushDrop.js +2 -2
  38. package/dist/esm/src/script/templates/PushDrop.js.map +1 -1
  39. package/dist/esm/src/transaction/Transaction.js +4 -4
  40. package/dist/esm/src/transaction/Transaction.js.map +1 -1
  41. package/dist/esm/src/transaction/fee-models/LivePolicy.js +85 -0
  42. package/dist/esm/src/transaction/fee-models/LivePolicy.js.map +1 -0
  43. package/dist/esm/src/transaction/fee-models/index.js +1 -0
  44. package/dist/esm/src/transaction/fee-models/index.js.map +1 -1
  45. package/dist/esm/src/wallet/WalletClient.js +43 -52
  46. package/dist/esm/src/wallet/WalletClient.js.map +1 -1
  47. package/dist/esm/src/wallet/substrates/WalletWireProcessor.js +19 -0
  48. package/dist/esm/src/wallet/substrates/WalletWireProcessor.js.map +1 -1
  49. package/dist/esm/src/wallet/substrates/WalletWireTransceiver.js +18 -1
  50. package/dist/esm/src/wallet/substrates/WalletWireTransceiver.js.map +1 -1
  51. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  52. package/dist/types/src/kvstore/GlobalKVStore.d.ts +129 -0
  53. package/dist/types/src/kvstore/GlobalKVStore.d.ts.map +1 -0
  54. package/dist/types/src/kvstore/kvStoreInterpreter.d.ts +22 -0
  55. package/dist/types/src/kvstore/kvStoreInterpreter.d.ts.map +1 -0
  56. package/dist/types/src/kvstore/types.d.ts +106 -0
  57. package/dist/types/src/kvstore/types.d.ts.map +1 -0
  58. package/dist/types/src/overlay-tools/Historian.d.ts +92 -0
  59. package/dist/types/src/overlay-tools/Historian.d.ts.map +1 -0
  60. package/dist/types/src/script/templates/PushDrop.d.ts +6 -5
  61. package/dist/types/src/script/templates/PushDrop.d.ts.map +1 -1
  62. package/dist/types/src/transaction/Transaction.d.ts +2 -2
  63. package/dist/types/src/transaction/Transaction.d.ts.map +1 -1
  64. package/dist/types/src/transaction/fee-models/LivePolicy.d.ts +41 -0
  65. package/dist/types/src/transaction/fee-models/LivePolicy.d.ts.map +1 -0
  66. package/dist/types/src/transaction/fee-models/index.d.ts +1 -0
  67. package/dist/types/src/transaction/fee-models/index.d.ts.map +1 -1
  68. package/dist/types/src/wallet/WalletClient.d.ts +1 -1
  69. package/dist/types/src/wallet/WalletClient.d.ts.map +1 -1
  70. package/dist/types/src/wallet/substrates/WalletWireProcessor.d.ts.map +1 -1
  71. package/dist/types/src/wallet/substrates/WalletWireTransceiver.d.ts.map +1 -1
  72. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  73. package/dist/umd/bundle.js +3 -3
  74. package/dist/umd/bundle.js.map +1 -1
  75. package/docs/reference/script.md +7 -19
  76. package/docs/reference/transaction.md +75 -6
  77. package/docs/reference/wallet.md +1 -1
  78. package/package.json +1 -1
  79. package/src/kvstore/GlobalKVStore.ts +478 -0
  80. package/src/kvstore/LocalKVStore.ts +7 -7
  81. package/src/kvstore/__tests/GlobalKVStore.test.ts +965 -0
  82. package/src/kvstore/__tests/LocalKVStore.test.ts +72 -0
  83. package/src/kvstore/kvStoreInterpreter.ts +49 -0
  84. package/src/kvstore/types.ts +114 -0
  85. package/src/overlay-tools/Historian.ts +195 -0
  86. package/src/overlay-tools/__tests/Historian.test.ts +690 -0
  87. package/src/script/templates/PushDrop.ts +6 -5
  88. package/src/transaction/Transaction.ts +4 -4
  89. package/src/transaction/fee-models/LivePolicy.ts +97 -0
  90. package/src/transaction/fee-models/__tests/LivePolicy.test.ts +148 -0
  91. package/src/transaction/fee-models/index.ts +1 -0
  92. package/src/wallet/WalletClient.ts +50 -51
  93. package/src/wallet/substrates/WalletWireProcessor.ts +21 -0
  94. package/src/wallet/substrates/WalletWireTransceiver.ts +22 -10
@@ -481,6 +481,51 @@ describe('localKVStore', () => {
481
481
  })
482
482
  expect(mockWallet.relinquishOutput).not.toHaveBeenCalled()
483
483
  })
484
+
485
+ it('should preserve original error message when createAction fails', async () => {
486
+ const originalErrorMessage = 'Network connection timeout while creating transaction'
487
+ const originalError = new Error(originalErrorMessage)
488
+
489
+ // Mock the lookupValue to return a value that differs from what we're setting
490
+ // to ensure set() will attempt to create a transaction
491
+ const mockedLor: ListOutputsResult = {
492
+ totalOutputs: 0,
493
+ outputs: [],
494
+ BEEF: undefined
495
+ }
496
+
497
+ const lookupValueReal = kvStore['lookupValue']
498
+ kvStore['lookupValue'] = jest.fn().mockResolvedValue({
499
+ value: 'different_value', // Different from testValue to trigger createAction
500
+ outpoint: undefined,
501
+ lor: mockedLor
502
+ })
503
+
504
+ // Mock wallet.createAction to fail with a specific error
505
+ mockWallet.createAction.mockRejectedValue(originalError)
506
+
507
+ // Mock other required methods
508
+ const valueArray = Array.from(testRawValueBuffer)
509
+ const encryptedArray = Array.from(testEncryptedValue)
510
+ MockedUtils.toArray.mockReturnValue(valueArray)
511
+ mockWallet.encrypt.mockResolvedValue({ ciphertext: encryptedArray } as WalletEncryptResult)
512
+
513
+ try {
514
+ await kvStore.set(testKey, testValue)
515
+ fail('Expected set() to throw an error but it succeeded')
516
+ } catch (error) {
517
+ // Verify that the thrown error contains both the contextual message and the original error
518
+ expect(error).toBeInstanceOf(Error)
519
+ const errorMessage = (error as Error).message
520
+ expect(errorMessage).toContain('outputs with tag')
521
+ expect(errorMessage).toContain('cannot be unlocked')
522
+ expect(errorMessage).toContain('Original error:')
523
+ expect(errorMessage).toContain(originalErrorMessage)
524
+ } finally {
525
+ // Restore the original method
526
+ kvStore['lookupValue'] = lookupValueReal
527
+ }
528
+ })
484
529
  })
485
530
 
486
531
  // --- Remove Method Tests ---
@@ -610,5 +655,32 @@ describe('localKVStore', () => {
610
655
  expect(mockWallet.signAction).toHaveBeenCalled() // Called but failed
611
656
 
612
657
  })
658
+
659
+ it('should preserve original error message when wallet operations fail during removal', async () => {
660
+ const originalErrorMessage = 'Insufficient funds to cover transaction fees'
661
+ const originalError = new Error(originalErrorMessage)
662
+
663
+ const existingOutpoint = 'failTxId.0'
664
+ const existingOutput = { outpoint: existingOutpoint, txid: 'failTxId', vout: 0, lockingScript: 's1' }
665
+ const mockBEEF = Buffer.from('mockBEEFFail')
666
+
667
+ // Mock wallet to have outputs but createAction fails
668
+ mockWallet.listOutputs.mockResolvedValue({ outputs: [existingOutput], totalOutputs: 1, BEEF: mockBEEF } as any)
669
+ mockWallet.createAction.mockRejectedValue(originalError)
670
+
671
+ try {
672
+ await kvStore.remove(testKey)
673
+ fail('Expected remove() to throw an error but it succeeded')
674
+ } catch (error) {
675
+ // Verify that the thrown error contains both the contextual message and the original error
676
+ expect(error).toBeInstanceOf(Error)
677
+ const errorMessage = (error as Error).message
678
+ expect(errorMessage).toContain('1 outputs with tag')
679
+ expect(errorMessage).toContain(testKey)
680
+ expect(errorMessage).toContain('cannot be unlocked')
681
+ expect(errorMessage).toContain('Original error:')
682
+ expect(errorMessage).toContain(originalErrorMessage)
683
+ }
684
+ })
613
685
  })
614
686
  })
@@ -0,0 +1,49 @@
1
+ import { PushDrop } from '../script/index.js'
2
+ import Transaction from '../transaction/Transaction.js'
3
+ import * as Utils from '../primitives/utils.js'
4
+ import { kvProtocol } from './types.js'
5
+ import { InterpreterFunction } from '../overlay-tools/Historian.js'
6
+ import { WalletProtocol } from '../wallet/Wallet.interfaces.js'
7
+
8
+ export interface KVContext { key: string, protocolID: WalletProtocol }
9
+
10
+ /**
11
+ * KVStore interpreter used by Historian.
12
+ *
13
+ * Validates the KVStore PushDrop tokens: [protocolID, key, value, controller, signature].
14
+ * Filters outputs by the provided key in the interpreter context.
15
+ * Produces the plaintext value for matching outputs; returns undefined otherwise.
16
+ *
17
+ * @param transaction - The transaction to inspect.
18
+ * @param outputIndex - The index of the output within transaction.outputs.
19
+ * @param ctx - { key: string, protocolID: WalletProtocol } — per-call context specifying which key to match.
20
+ *
21
+ * @returns string | undefined — the decoded KV value if the output is a valid KVStore token for the
22
+ * given key; otherwise undefined.
23
+ */
24
+ export const kvStoreInterpreter: InterpreterFunction<string, KVContext> = async (transaction: Transaction, outputIndex: number, ctx?: KVContext): Promise<string | undefined> => {
25
+ try {
26
+ const output = transaction.outputs[outputIndex]
27
+ if (output == null || output.lockingScript == null) return undefined
28
+ if (ctx == null || ctx.key == null) return undefined
29
+
30
+ // Decode the KVStore token
31
+ const decoded = PushDrop.decode(output.lockingScript)
32
+
33
+ // Validate KVStore token format (must have 5 fields: [protocolID, key, value, controller, signature])
34
+ if (decoded.fields.length !== Object.keys(kvProtocol).length) return undefined
35
+
36
+ // Only return values for the given key and protocolID
37
+ const key = Utils.toUTF8(decoded.fields[kvProtocol.key])
38
+ const protocolID = Utils.toUTF8(decoded.fields[kvProtocol.protocolID])
39
+ if (key !== ctx.key || protocolID !== JSON.stringify(ctx.protocolID)) return undefined
40
+ try {
41
+ return Utils.toUTF8(decoded.fields[kvProtocol.value])
42
+ } catch {
43
+ return undefined
44
+ }
45
+ } catch {
46
+ // Skip non-KVStore outputs or malformed tokens
47
+ return undefined
48
+ }
49
+ }
@@ -0,0 +1,114 @@
1
+ import { Beef } from '../transaction/Beef.js'
2
+ import { PubKeyHex, WalletProtocol } from '../wallet/Wallet.interfaces.js'
3
+ import { WalletInterface } from '../wallet/index.js'
4
+
5
+ /**
6
+ * Configuration interface for GlobalKVStore operations.
7
+ * Defines all options for connecting to overlay services and managing KVStore behavior.
8
+ */
9
+ export interface KVStoreConfig {
10
+ /** The overlay service host URL */
11
+ overlayHost?: string
12
+ /** Protocol ID for the KVStore protocol */
13
+ protocolID?: WalletProtocol
14
+ /** Service name for overlay submission */
15
+ serviceName?: string
16
+ /** Amount of satoshis for each token */
17
+ tokenAmount?: number
18
+ /** Topics for overlay submission */
19
+ topics?: string[]
20
+ /** Originator */
21
+ originator?: string
22
+ /** Wallet interface for operations */
23
+ wallet?: WalletInterface
24
+ /** Network preset for overlay services */
25
+ networkPreset?: 'mainnet' | 'testnet' | 'local'
26
+ /** Whether to accept delayed broadcast */
27
+ acceptDelayedBroadcast?: boolean
28
+ /** Description for token set */
29
+ tokenSetDescription?: string
30
+ /** Description for token update */
31
+ tokenUpdateDescription?: string
32
+ /** Description for token removal */
33
+ tokenRemovalDescription?: string
34
+ }
35
+
36
+ /**
37
+ * Query parameters for KVStore lookups from overlay services.
38
+ * Used when searching for existing key-value pairs in the network.
39
+ */
40
+ export interface KVStoreQuery {
41
+ key?: string
42
+ controller?: PubKeyHex
43
+ protocolID?: WalletProtocol
44
+ limit?: number
45
+ skip?: number
46
+ sortOrder?: 'asc' | 'desc'
47
+ }
48
+
49
+ /**
50
+ * Options for configuring KVStore get operations (local processing)
51
+ */
52
+ export interface KVStoreGetOptions {
53
+ /** Whether to build and include history for each entry */
54
+ history?: boolean
55
+ /** Whether to include token transaction data in results */
56
+ includeToken?: boolean
57
+ /** Service name for overlay retrieval */
58
+ serviceName?: string
59
+ }
60
+
61
+ export interface KVStoreSetOptions {
62
+ protocolID?: WalletProtocol
63
+ tokenSetDescription?: string
64
+ tokenUpdateDescription?: string
65
+ tokenAmount?: number
66
+ }
67
+
68
+ export interface KVStoreRemoveOptions {
69
+ protocolID?: WalletProtocol
70
+ tokenRemovalDescription?: string
71
+ }
72
+
73
+ /**
74
+ * KVStore entry returned from queries
75
+ */
76
+ export interface KVStoreEntry {
77
+ key: string
78
+ value: string
79
+ controller: PubKeyHex
80
+ protocolID: WalletProtocol
81
+ token?: KVStoreToken
82
+ history?: string[]
83
+ }
84
+
85
+ /**
86
+ * Result structure for KVStore lookups from overlay services.
87
+ * Contains the transaction output information for a found key-value pair.
88
+ */
89
+ export interface KVStoreLookupResult {
90
+ txid: string
91
+ outputIndex: number
92
+ outputScript: string
93
+ satoshis: number
94
+ history?: (output: any, currentDepth: number) => Promise<boolean>
95
+ }
96
+
97
+ /**
98
+ * Token structure containing a KVStore token from overlay services.
99
+ * Wraps the transaction data and metadata for a key-value pair.
100
+ */
101
+ export interface KVStoreToken {
102
+ txid: string
103
+ outputIndex: number
104
+ satoshis: number
105
+ beef: Beef
106
+ }
107
+
108
+ export const kvProtocol = {
109
+ protocolID: 0,
110
+ key: 1,
111
+ value: 2,
112
+ controller: 3,
113
+ signature: 4
114
+ }
@@ -0,0 +1,195 @@
1
+ import Transaction from '../transaction/Transaction.js'
2
+
3
+ /**
4
+ * Interpreter function signature used by Historian.
5
+ *
6
+ * Generics:
7
+ * - T: The decoded/typed value produced for a matching output. Returning `undefined`
8
+ * means “this output does not contribute to history.”
9
+ * - C: The per-call context passed through Historian to the interpreter. This carries
10
+ * any metadata needed to interpret outputs (e.g., `{ key: string }` for KVStore).
11
+ *
12
+ * Params:
13
+ * - tx: The transaction containing the output to interpret.
14
+ * - outputIndex: Index of the output within `tx.outputs`.
15
+ * - ctx: Optional context object of type C, supplied at [Historian.buildHistory(startTx, context)]
16
+ *
17
+ * Returns:
18
+ * - `T | undefined` (or a Promise thereof). `undefined` indicates a non-applicable or
19
+ * un-decodable output for the given context.
20
+ */
21
+ export type InterpreterFunction<T, C = unknown> =
22
+ (tx: Transaction, outputIndex: number, ctx?: C) => Promise<T | undefined> | T | undefined
23
+
24
+ /**
25
+ * Historian builds a chronological history (oldest → newest) of typed values by traversing
26
+ * a transaction's input ancestry and interpreting each output with a provided interpreter.
27
+ *
28
+ * Core ideas:
29
+ * - You provide an interpreter `(tx, outputIndex) => T | undefined` that decodes one output
30
+ * into your domain value (e.g., kvstore entries). If it returns `undefined`, that output
31
+ * contributes nothing to history.
32
+ * - Traversal follows `Transaction.inputs[].sourceTransaction` recursively, so callers must
33
+ * supply transactions whose inputs have `sourceTransaction` populated (e.g., via overlay
34
+ * history reconstruction).
35
+ * - The traversal visits each transaction once (cycle-safe) and collects interpreted values
36
+ * in reverse-chronological order, then returns them as chronological (oldest-first).
37
+ * - Optional caching support: provide a `historyCache` Map to cache complete history results
38
+ * and avoid re-traversing identical transaction chains with the same context.
39
+ *
40
+ * Usage:
41
+ * - Construct with an interpreter (and optional cache)
42
+ * - Call historian.buildHistory(tx, context) to get an array of values representing the history of a token over time.
43
+ *
44
+ * Example:
45
+ * const cache = new Map() // Optional: for caching repeated queries
46
+ * const historian = new Historian(interpreter, { historyCache: cache })
47
+ * const history = await historian.buildHistory(tipTransaction, context)
48
+ * // history: T[] (e.g., prior values for a protected kvstore key)
49
+ *
50
+ * Caching:
51
+ * - Cache keys are generated from `interpreterVersion|txid|contextKey`
52
+ * - Cached results are immutable snapshots to prevent external mutation
53
+ * - Bump `interpreterVersion` when interpreter semantics change to invalidate old cache entries
54
+ */
55
+ export class Historian<T, C = unknown> {
56
+ private readonly interpreter: InterpreterFunction<T, C>
57
+ private readonly debug: boolean
58
+
59
+ // --- minimal cache support ---
60
+ private readonly historyCache?: Map<string, readonly T[]>
61
+ private readonly interpreterVersion: string
62
+ private readonly ctxKeyFn: (ctx?: C) => string
63
+
64
+ /**
65
+ * Create a new Historian instance
66
+ *
67
+ * @param interpreter - Function to interpret transaction outputs into typed values
68
+ * @param options - Configuration options
69
+ * @param options.debug - Enable debug logging (default: false)
70
+ * @param options.historyCache - Optional external cache for complete history results
71
+ * @param options.interpreterVersion - Version identifier for cache invalidation (default: 'v1')
72
+ * @param options.ctxKeyFn - Custom function to serialize context for cache keys (default: JSON.stringify)
73
+ */
74
+ constructor (
75
+ interpreter: InterpreterFunction<T, C>,
76
+ options?: {
77
+ debug?: boolean
78
+ /** Optional cache for entire history results keyed by (version|txid|ctxKey) */
79
+ historyCache?: Map<string, readonly T[]>
80
+ /** Bump this if interpreter semantics change to invalidate cache */
81
+ interpreterVersion?: string
82
+ /** Deterministic, non-secret key for context. Default is JSON.stringify(ctx) */
83
+ ctxKeyFn?: (ctx?: C) => string
84
+ }
85
+ ) {
86
+ this.interpreter = interpreter
87
+ this.debug = options?.debug ?? false
88
+
89
+ // Configure caching (all optional)
90
+ this.historyCache = options?.historyCache
91
+ this.interpreterVersion = options?.interpreterVersion ?? 'v1'
92
+ this.ctxKeyFn = options?.ctxKeyFn ?? ((ctx?: C) => {
93
+ try { return JSON.stringify(ctx ?? null) } catch { return '' }
94
+ })
95
+ }
96
+
97
+ private historyKey (startTransaction: Transaction, context?: C): string {
98
+ const txid = startTransaction.id('hex')
99
+ const ctxKey = this.ctxKeyFn(context)
100
+ return `${this.interpreterVersion}|${txid}|${ctxKey}`
101
+ }
102
+
103
+ /**
104
+ * Build history by traversing input chain from a starting transaction
105
+ * Returns values in chronological order (oldest first)
106
+ *
107
+ * If caching is enabled, will first check for cached results matching the
108
+ * startTransaction and context. On cache miss, performs full traversal and
109
+ * stores the result for future queries.
110
+ *
111
+ * @param startTransaction - The transaction to start traversal from
112
+ * @param context - The context to pass to the interpreter
113
+ * @returns Array of interpreted values in chronological order
114
+ */
115
+ async buildHistory (startTransaction: Transaction, context?: C): Promise<T[]> {
116
+ // --- minimal cache fast path ---
117
+ if (this.historyCache != null) {
118
+ const cacheKey = this.historyKey(startTransaction, context)
119
+ if (this.historyCache.has(cacheKey)) {
120
+ const cached = this.historyCache.get(cacheKey)
121
+ if (cached != null) {
122
+ if (this.debug) console.log('[Historian] History cache hit:', cacheKey)
123
+ // Return a shallow copy to avoid external mutation of the cached array
124
+ return cached.slice()
125
+ }
126
+ }
127
+ }
128
+
129
+ const history: T[] = []
130
+ const visited = new Set<string>()
131
+
132
+ // Recursively traverse input transactions to build history
133
+ const traverseHistory = async (transaction: Transaction): Promise<void> => {
134
+ const txid = transaction.id('hex')
135
+
136
+ // Prevent infinite loops
137
+ if (visited.has(txid)) {
138
+ if (this.debug) {
139
+ console.log(`[Historian] Skipping already visited transaction: ${txid}`)
140
+ }
141
+ return
142
+ }
143
+ visited.add(txid)
144
+
145
+ if (this.debug) {
146
+ console.log(`[Historian] Processing transaction: ${txid}`)
147
+ }
148
+
149
+ // Check all outputs in this transaction for interpretable values
150
+ for (let outputIndex = 0; outputIndex < transaction.outputs.length; outputIndex++) {
151
+ try {
152
+ // Try to interpret this output
153
+ const interpretedValue = await Promise.resolve(this.interpreter(transaction, outputIndex, context))
154
+
155
+ if (interpretedValue !== undefined) {
156
+ history.push(interpretedValue)
157
+ if (this.debug) {
158
+ console.log('[Historian] Added value to history:', interpretedValue)
159
+ }
160
+ }
161
+ } catch (error) {
162
+ if (this.debug) {
163
+ console.log(`[Historian] Failed to interpret output ${outputIndex}:`, error)
164
+ }
165
+ // Skip outputs that can't be interpreted
166
+ }
167
+ }
168
+
169
+ // Recursively traverse input transactions
170
+ for (const input of transaction.inputs) {
171
+ if (input.sourceTransaction != null) {
172
+ await traverseHistory(input.sourceTransaction)
173
+ } else if (this.debug) {
174
+ console.log('[Historian] Input missing sourceTransaction, skipping')
175
+ }
176
+ }
177
+ }
178
+
179
+ // Start traversal from the provided transaction
180
+ await traverseHistory(startTransaction)
181
+
182
+ // History is built in reverse chronological order during traversal,
183
+ // so we reverse it to return oldest-first
184
+ const chronological = history.reverse()
185
+
186
+ if (this.historyCache != null) {
187
+ const cacheKey = this.historyKey(startTransaction, context)
188
+ // Store an immutable snapshot to avoid accidental external mutation
189
+ this.historyCache.set(cacheKey, Object.freeze(chronological.slice()))
190
+ if (this.debug) console.log('[Historian] History cached:', cacheKey)
191
+ }
192
+
193
+ return chronological
194
+ }
195
+ }