tbc-tx-parser 0.1.0

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 (183) hide show
  1. package/README.md +214 -0
  2. package/dist/adapters/partial-sha256.d.ts +2 -0
  3. package/dist/adapters/partial-sha256.d.ts.map +1 -0
  4. package/dist/adapters/partial-sha256.js +5 -0
  5. package/dist/adapters/partial-sha256.js.map +1 -0
  6. package/dist/adapters/tbc-lib.d.ts +4 -0
  7. package/dist/adapters/tbc-lib.d.ts.map +1 -0
  8. package/dist/adapters/tbc-lib.js +4 -0
  9. package/dist/adapters/tbc-lib.js.map +1 -0
  10. package/dist/constants/chain.d.ts +4 -0
  11. package/dist/constants/chain.d.ts.map +1 -0
  12. package/dist/constants/chain.js +7 -0
  13. package/dist/constants/chain.js.map +1 -0
  14. package/dist/core/chain.d.ts +10 -0
  15. package/dist/core/chain.d.ts.map +1 -0
  16. package/dist/core/chain.js +41 -0
  17. package/dist/core/chain.js.map +1 -0
  18. package/dist/core/input-utxos.d.ts +3 -0
  19. package/dist/core/input-utxos.d.ts.map +1 -0
  20. package/dist/core/input-utxos.js +69 -0
  21. package/dist/core/input-utxos.js.map +1 -0
  22. package/dist/core/public-chain-compression.d.ts +4 -0
  23. package/dist/core/public-chain-compression.d.ts.map +1 -0
  24. package/dist/core/public-chain-compression.js +87 -0
  25. package/dist/core/public-chain-compression.js.map +1 -0
  26. package/dist/core/public-script.d.ts +4 -0
  27. package/dist/core/public-script.d.ts.map +1 -0
  28. package/dist/core/public-script.js +94 -0
  29. package/dist/core/public-script.js.map +1 -0
  30. package/dist/decode-tx-raw.d.ts +3 -0
  31. package/dist/decode-tx-raw.d.ts.map +1 -0
  32. package/dist/decode-tx-raw.js +350 -0
  33. package/dist/decode-tx-raw.js.map +1 -0
  34. package/dist/index.d.ts +3 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +3 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/protocols/ft/index.d.ts +7 -0
  39. package/dist/protocols/ft/index.d.ts.map +1 -0
  40. package/dist/protocols/ft/index.js +393 -0
  41. package/dist/protocols/ft/index.js.map +1 -0
  42. package/dist/protocols/index.d.ts +5 -0
  43. package/dist/protocols/index.d.ts.map +1 -0
  44. package/dist/protocols/index.js +61 -0
  45. package/dist/protocols/index.js.map +1 -0
  46. package/dist/protocols/nft/index.d.ts +8 -0
  47. package/dist/protocols/nft/index.d.ts.map +1 -0
  48. package/dist/protocols/nft/index.js +335 -0
  49. package/dist/protocols/nft/index.js.map +1 -0
  50. package/dist/protocols/nft/media.js.map +1 -0
  51. package/dist/protocols/order-book/actions.d.ts +7 -0
  52. package/dist/protocols/order-book/actions.d.ts.map +1 -0
  53. package/dist/protocols/order-book/actions.js +102 -0
  54. package/dist/protocols/order-book/actions.js.map +1 -0
  55. package/dist/protocols/order-book/facts.d.ts +7 -0
  56. package/dist/protocols/order-book/facts.d.ts.map +1 -0
  57. package/dist/protocols/order-book/facts.js +81 -0
  58. package/dist/protocols/order-book/facts.js.map +1 -0
  59. package/dist/protocols/order-book/identity.d.ts +3 -0
  60. package/dist/protocols/order-book/identity.d.ts.map +1 -0
  61. package/dist/protocols/order-book/identity.js +46 -0
  62. package/dist/protocols/order-book/identity.js.map +1 -0
  63. package/dist/protocols/order-book/index.d.ts +12 -0
  64. package/dist/protocols/order-book/index.d.ts.map +1 -0
  65. package/dist/protocols/order-book/index.js +34 -0
  66. package/dist/protocols/order-book/index.js.map +1 -0
  67. package/dist/protocols/order-book/settlement.d.ts +10 -0
  68. package/dist/protocols/order-book/settlement.d.ts.map +1 -0
  69. package/dist/protocols/order-book/settlement.js +318 -0
  70. package/dist/protocols/order-book/settlement.js.map +1 -0
  71. package/dist/protocols/order-book/types.d.ts +101 -0
  72. package/dist/protocols/order-book/types.d.ts.map +1 -0
  73. package/dist/protocols/order-book/types.js +2 -0
  74. package/dist/protocols/order-book/types.js.map +1 -0
  75. package/dist/protocols/piggy-bank/index.d.ts +7 -0
  76. package/dist/protocols/piggy-bank/index.d.ts.map +1 -0
  77. package/dist/protocols/piggy-bank/index.js +91 -0
  78. package/dist/protocols/piggy-bank/index.js.map +1 -0
  79. package/dist/protocols/pool-nft/actions.d.ts +15 -0
  80. package/dist/protocols/pool-nft/actions.d.ts.map +1 -0
  81. package/dist/protocols/pool-nft/actions.js +90 -0
  82. package/dist/protocols/pool-nft/actions.js.map +1 -0
  83. package/dist/protocols/pool-nft/amounts.d.ts +10 -0
  84. package/dist/protocols/pool-nft/amounts.d.ts.map +1 -0
  85. package/dist/protocols/pool-nft/amounts.js +45 -0
  86. package/dist/protocols/pool-nft/amounts.js.map +1 -0
  87. package/dist/protocols/pool-nft/constants.d.ts +15 -0
  88. package/dist/protocols/pool-nft/constants.d.ts.map +1 -0
  89. package/dist/protocols/pool-nft/constants.js +27 -0
  90. package/dist/protocols/pool-nft/constants.js.map +1 -0
  91. package/dist/protocols/pool-nft/facts.d.ts +11 -0
  92. package/dist/protocols/pool-nft/facts.d.ts.map +1 -0
  93. package/dist/protocols/pool-nft/facts.js +113 -0
  94. package/dist/protocols/pool-nft/facts.js.map +1 -0
  95. package/dist/protocols/pool-nft/fees.d.ts +14 -0
  96. package/dist/protocols/pool-nft/fees.d.ts.map +1 -0
  97. package/dist/protocols/pool-nft/fees.js +161 -0
  98. package/dist/protocols/pool-nft/fees.js.map +1 -0
  99. package/dist/protocols/pool-nft/index.d.ts +4 -0
  100. package/dist/protocols/pool-nft/index.d.ts.map +1 -0
  101. package/dist/protocols/pool-nft/index.js +19 -0
  102. package/dist/protocols/pool-nft/index.js.map +1 -0
  103. package/dist/protocols/pool-nft/movements.d.ts +5 -0
  104. package/dist/protocols/pool-nft/movements.d.ts.map +1 -0
  105. package/dist/protocols/pool-nft/movements.js +804 -0
  106. package/dist/protocols/pool-nft/movements.js.map +1 -0
  107. package/dist/protocols/pool-nft/selectors.d.ts +40 -0
  108. package/dist/protocols/pool-nft/selectors.d.ts.map +1 -0
  109. package/dist/protocols/pool-nft/selectors.js +173 -0
  110. package/dist/protocols/pool-nft/selectors.js.map +1 -0
  111. package/dist/protocols/pool-nft/types.d.ts +34 -0
  112. package/dist/protocols/pool-nft/types.d.ts.map +1 -0
  113. package/dist/protocols/pool-nft/types.js +2 -0
  114. package/dist/protocols/pool-nft/types.js.map +1 -0
  115. package/dist/protocols/tbc/index.d.ts +3 -0
  116. package/dist/protocols/tbc/index.d.ts.map +1 -0
  117. package/dist/protocols/tbc/index.js +21 -0
  118. package/dist/protocols/tbc/index.js.map +1 -0
  119. package/dist/protocols/types.d.ts +15 -0
  120. package/dist/protocols/types.d.ts.map +1 -0
  121. package/dist/protocols/types.js +2 -0
  122. package/dist/protocols/types.js.map +1 -0
  123. package/dist/result/index.d.ts +7 -0
  124. package/dist/result/index.d.ts.map +1 -0
  125. package/dist/result/index.js +869 -0
  126. package/dist/result/index.js.map +1 -0
  127. package/dist/result/semantic.d.ts +6 -0
  128. package/dist/result/semantic.d.ts.map +1 -0
  129. package/dist/result/semantic.js +950 -0
  130. package/dist/result/semantic.js.map +1 -0
  131. package/dist/script-parser/contracts.d.ts +7 -0
  132. package/dist/script-parser/contracts.d.ts.map +1 -0
  133. package/dist/script-parser/contracts.js +104 -0
  134. package/dist/script-parser/contracts.js.map +1 -0
  135. package/dist/script-parser/op-return.d.ts +8 -0
  136. package/dist/script-parser/op-return.d.ts.map +1 -0
  137. package/dist/script-parser/op-return.js +315 -0
  138. package/dist/script-parser/op-return.js.map +1 -0
  139. package/dist/script-parser/origin.d.ts +6 -0
  140. package/dist/script-parser/origin.d.ts.map +1 -0
  141. package/dist/script-parser/origin.js +68 -0
  142. package/dist/script-parser/origin.js.map +1 -0
  143. package/dist/script-parser.d.ts +3 -0
  144. package/dist/script-parser.d.ts.map +1 -0
  145. package/dist/script-parser.js +183 -0
  146. package/dist/script-parser.js.map +1 -0
  147. package/dist/types/protocol.d.ts +175 -0
  148. package/dist/types/protocol.d.ts.map +1 -0
  149. package/dist/types/protocol.js +2 -0
  150. package/dist/types/protocol.js.map +1 -0
  151. package/dist/types/result.d.ts +167 -0
  152. package/dist/types/result.d.ts.map +1 -0
  153. package/dist/types/result.js +2 -0
  154. package/dist/types/result.js.map +1 -0
  155. package/dist/types/script.d.ts +180 -0
  156. package/dist/types/script.d.ts.map +1 -0
  157. package/dist/types/script.js +9 -0
  158. package/dist/types/script.js.map +1 -0
  159. package/dist/types/semantic.d.ts +94 -0
  160. package/dist/types/semantic.d.ts.map +1 -0
  161. package/dist/types/semantic.js +2 -0
  162. package/dist/types/semantic.js.map +1 -0
  163. package/dist/types.d.ts +6 -0
  164. package/dist/types.d.ts.map +1 -0
  165. package/dist/types.js +4 -0
  166. package/dist/types.js.map +1 -0
  167. package/dist/utils/asm-converter.d.ts +7 -0
  168. package/dist/utils/asm-converter.d.ts.map +1 -0
  169. package/dist/utils/asm-converter.js +105 -0
  170. package/dist/utils/asm-converter.js.map +1 -0
  171. package/dist/utils/binary.d.ts +3 -0
  172. package/dist/utils/binary.d.ts.map +1 -0
  173. package/dist/utils/binary.js +17 -0
  174. package/dist/utils/binary.js.map +1 -0
  175. package/dist/utils/multi-sig.d.ts +9 -0
  176. package/dist/utils/multi-sig.d.ts.map +1 -0
  177. package/dist/utils/multi-sig.js +125 -0
  178. package/dist/utils/multi-sig.js.map +1 -0
  179. package/dist/utils/token-amount.d.ts +3 -0
  180. package/dist/utils/token-amount.d.ts.map +1 -0
  181. package/dist/utils/token-amount.js +37 -0
  182. package/dist/utils/token-amount.js.map +1 -0
  183. package/package.json +47 -0
@@ -0,0 +1,950 @@
1
+ import { buildOrderBookFacts, matchOrderBookAction } from '../protocols/order-book/index.js';
2
+ // Convert verified movements and orders into human-readable semantics without scanning raw script markers.
3
+ const NATIVE_TBC_ASSET = { type: 'native', symbol: 'TBC' };
4
+ function uniqueSemanticParties(parties) {
5
+ // Actors may come from multiple effects or relations; deduplicate by role and owner for stable UI output.
6
+ const seen = new Set();
7
+ const unique = [];
8
+ for (const party of parties) {
9
+ if (!party) {
10
+ continue;
11
+ }
12
+ const key = `${party.role}:${party.address ?? ''}:${party.hash ?? ''}:${party.inputIndex ?? ''}:${party.outputIndex ?? ''}:${party.label ?? ''}`;
13
+ if (seen.has(key)) {
14
+ continue;
15
+ }
16
+ seen.add(key);
17
+ unique.push(party);
18
+ }
19
+ return unique;
20
+ }
21
+ function uniqueNumbers(values) {
22
+ return [...new Set(values.filter((value) => value !== undefined && value >= 0))];
23
+ }
24
+ function firstInputAddress(inputs) {
25
+ return inputs.map((input) => input.prevout?.address).find((address) => Boolean(address));
26
+ }
27
+ function tokenAssetFromMovement(movement) {
28
+ return {
29
+ type: movement.isStableCoin ? 'stablecoin' : 'ft',
30
+ symbol: movement.symbol,
31
+ name: movement.name,
32
+ contractId: movement.contractId,
33
+ decimal: movement.decimal,
34
+ };
35
+ }
36
+ function nftAssetFromMovement(movement) {
37
+ if (movement.action === 'mintCollection') {
38
+ return {
39
+ type: 'nftCollection',
40
+ name: movement.collectionName,
41
+ collectionId: movement.collectionId,
42
+ contractId: movement.contractId,
43
+ };
44
+ }
45
+ return {
46
+ type: 'nft',
47
+ symbol: movement.symbol,
48
+ name: movement.nftName ?? movement.collectionName,
49
+ collectionId: movement.collectionId,
50
+ contractId: movement.contractId,
51
+ };
52
+ }
53
+ function poolNftTokenAsset(movement) {
54
+ return {
55
+ type: 'ft',
56
+ symbol: movement.ftSymbol ?? 'FT',
57
+ name: movement.ftName,
58
+ contractId: movement.ftTokenId ?? movement.ftContractTxid,
59
+ decimal: movement.ftDecimal,
60
+ };
61
+ }
62
+ function poolNftLpAsset(movement) {
63
+ return {
64
+ type: 'ft',
65
+ symbol: movement.ftSymbol ? `${movement.ftSymbol}-LP` : 'LP',
66
+ name: movement.ftName ? `${movement.ftName} LP` : undefined,
67
+ contractId: movement.lpTokenId,
68
+ decimal: movement.ftDecimal,
69
+ };
70
+ }
71
+ function poolNftCreateAsset(movement) {
72
+ if (movement.action !== 'create' || !movement.contractId) {
73
+ return undefined;
74
+ }
75
+ return {
76
+ type: 'nft',
77
+ subtype: 'poolNft',
78
+ contractId: movement.contractId,
79
+ poolNft: {
80
+ version: movement.version ?? 1,
81
+ ...(movement.serviceFeeRate !== undefined ? { serviceFeeRate: movement.serviceFeeRate } : {}),
82
+ ...(movement.lpPlan !== undefined ? { lpPlan: movement.lpPlan } : {}),
83
+ ...(movement.withLock !== undefined ? { withLock: movement.withLock } : {}),
84
+ ...(movement.withLockTime !== undefined ? { withLockTime: movement.withLockTime } : {}),
85
+ },
86
+ };
87
+ }
88
+ function ownerParty(role, owner) {
89
+ // An owner may be an address, hash, or label; never present a hash as an address.
90
+ if (!owner.address && !owner.hash && !owner.label) {
91
+ return undefined;
92
+ }
93
+ return {
94
+ role,
95
+ address: owner.address,
96
+ hash: owner.hash,
97
+ inputIndex: owner.inputIndex,
98
+ outputIndex: owner.outputIndex,
99
+ label: owner.label,
100
+ };
101
+ }
102
+ function poolNftPoolParty(movement) {
103
+ return {
104
+ role: 'pool',
105
+ address: movement.poolStateAddress,
106
+ hash: movement.poolStateAddress ? undefined : movement.poolStateHash,
107
+ label: movement.poolId ?? movement.ftContractTxid ?? 'PoolNFT pool',
108
+ outputIndex: movement.poolStateOutputIndex ?? movement.poolOutputIndex,
109
+ };
110
+ }
111
+ function poolNftReceiverParty(movement) {
112
+ return ownerParty('receiver', {
113
+ address: movement.receiver,
114
+ hash: movement.receiverHash,
115
+ label: movement.receiver || movement.receiverHash ? undefined : 'PoolNFT user',
116
+ });
117
+ }
118
+ function poolNftSenderParty(movement) {
119
+ if (movement.action === 'mergeFtInPool') {
120
+ return poolNftPoolParty(movement);
121
+ }
122
+ return ownerParty('sender', {
123
+ address: movement.sender,
124
+ hash: movement.senderHash,
125
+ });
126
+ }
127
+ function poolNftFeeCollectorParty(feeOutput) {
128
+ return ownerParty('feeCollector', {
129
+ address: feeOutput.address,
130
+ outputIndex: feeOutput.outputIndex,
131
+ label: feeOutput.address ? undefined : 'PoolNFT fee collector',
132
+ });
133
+ }
134
+ function poolNftRawAmount(rawAmount, decimal) {
135
+ if (!rawAmount) {
136
+ return undefined;
137
+ }
138
+ return {
139
+ rawAmount,
140
+ decimal,
141
+ };
142
+ }
143
+ function buildTbcSemantic(transactionType, inputs, outputs) {
144
+ // TBC semantics cover ordinary transfers; the asset-flow layer adds TBC flows carried by protocol transactions.
145
+ if (transactionType !== 'TBC transfer') {
146
+ return undefined;
147
+ }
148
+ const sourceAddress = firstInputAddress(inputs);
149
+ const spendableOutputs = outputs.filter((output) => output.satoshis > 0 && output.scriptKind === 'p2pkh');
150
+ if (!spendableOutputs.length) {
151
+ return undefined;
152
+ }
153
+ const effects = spendableOutputs.map((output) => ({
154
+ kind: 'transfer',
155
+ asset: NATIVE_TBC_ASSET,
156
+ amount: { satoshis: output.satoshis },
157
+ from: sourceAddress ? { role: 'sender', address: sourceAddress } : { role: 'unknown' },
158
+ to: { role: 'receiver', address: output.address, outputIndex: output.index },
159
+ outputIndex: output.index,
160
+ source: 'decoded',
161
+ }));
162
+ return {
163
+ type: transactionType,
164
+ confidence: sourceAddress ? 'heuristic' : 'partial',
165
+ actors: uniqueSemanticParties([
166
+ sourceAddress ? { role: 'sender', address: sourceAddress } : undefined,
167
+ ...spendableOutputs.map((output) => ({ role: 'receiver', address: output.address, outputIndex: output.index })),
168
+ ]),
169
+ effects,
170
+ relations: [
171
+ {
172
+ kind: 'nativeTransfer',
173
+ confidence: sourceAddress ? 'heuristic' : 'partial',
174
+ inputIndexes: inputs.map((input) => input.index),
175
+ outputIndexes: spendableOutputs.map((output) => output.index),
176
+ },
177
+ ],
178
+ ...(sourceAddress ? {} : { notes: ['Input prevouts are required to identify the native TBC sender.'] }),
179
+ };
180
+ }
181
+ function buildCoinbaseSemantic(transactionType, inputs, outputs) {
182
+ if (transactionType !== 'TBC coinbase') {
183
+ return undefined;
184
+ }
185
+ const rewardOutputs = outputs.filter((output) => output.satoshis > 0);
186
+ const effects = rewardOutputs.map((output) => ({
187
+ kind: 'mint',
188
+ asset: NATIVE_TBC_ASSET,
189
+ amount: { satoshis: output.satoshis },
190
+ to: {
191
+ role: 'miner',
192
+ address: output.address,
193
+ hash: output.address ? undefined : output.script.hex,
194
+ outputIndex: output.index,
195
+ },
196
+ outputIndex: output.index,
197
+ source: 'decoded',
198
+ }));
199
+ return {
200
+ type: transactionType,
201
+ confidence: 'exact',
202
+ actors: uniqueSemanticParties(rewardOutputs.map((output) => ({
203
+ role: 'miner',
204
+ address: output.address,
205
+ hash: output.address ? undefined : output.script.hex,
206
+ outputIndex: output.index,
207
+ }))),
208
+ effects,
209
+ relations: [
210
+ {
211
+ kind: 'nativeCoinbase',
212
+ confidence: 'exact',
213
+ inputIndexes: inputs.map((input) => input.index),
214
+ outputIndexes: rewardOutputs.map((output) => output.index),
215
+ },
216
+ ],
217
+ notes: ['Coinbase transaction creates block reward outputs and has no spendable input prevout.'],
218
+ };
219
+ }
220
+ function buildFtSemantic(transactionType, inputs, outputs, movements) {
221
+ // FT semantics reuse movement asset data and infer an input owner only for assets that already existed.
222
+ if (!movements.length) {
223
+ return undefined;
224
+ }
225
+ const sourceAddress = firstInputAddress(inputs);
226
+ const isMintTransaction = transactionType.endsWith('mint');
227
+ const effects = movements.map((movement, movementIndex) => {
228
+ const isMint = movementIndex === 0 && isMintTransaction;
229
+ const from = isMint
230
+ ? undefined
231
+ : sourceAddress
232
+ ? { role: 'sender', address: sourceAddress }
233
+ : { role: 'unknown' };
234
+ return {
235
+ kind: isMint ? 'mint' : 'transfer',
236
+ asset: tokenAssetFromMovement(movement),
237
+ amount: {
238
+ rawAmount: movement.rawAmount,
239
+ displayAmount: movement.displayAmount,
240
+ decimal: movement.decimal,
241
+ },
242
+ ...(from ? { from } : {}),
243
+ to: { role: 'receiver', address: movement.receiver, outputIndex: movement.outputIndex },
244
+ outputIndex: movement.outputIndex,
245
+ tapeOutputIndex: movement.tapeOutputIndex,
246
+ movementIndex,
247
+ source: 'decoded',
248
+ };
249
+ });
250
+ const relationKind = movements.some((movement) => movement.isStableCoin) ? 'stablecoinTransfer' : 'ftTransfer';
251
+ return {
252
+ type: transactionType,
253
+ confidence: sourceAddress ? 'heuristic' : 'partial',
254
+ actors: uniqueSemanticParties([
255
+ !isMintTransaction && sourceAddress ? { role: 'sender', address: sourceAddress } : undefined,
256
+ ...movements.map((movement) => ({ role: 'receiver', address: movement.receiver, outputIndex: movement.outputIndex })),
257
+ ]),
258
+ effects,
259
+ relations: [
260
+ {
261
+ kind: relationKind,
262
+ confidence: sourceAddress ? 'heuristic' : 'partial',
263
+ inputIndexes: inputs.map((input) => input.index),
264
+ outputIndexes: movements.flatMap((movement) => [movement.outputIndex, movement.tapeOutputIndex]),
265
+ movementIndexes: movements.map((_movement, index) => index),
266
+ },
267
+ ],
268
+ ...(sourceAddress || isMintTransaction ? {} : { notes: ['Input prevouts are required to identify the token sender.'] }),
269
+ };
270
+ }
271
+ function buildNftSemantic(transactionType, movements) {
272
+ // NFT semantics are built only from verified movements.
273
+ if (!movements.length) {
274
+ return undefined;
275
+ }
276
+ const effects = movements.map((movement, movementIndex) => {
277
+ if (movement.action === 'mintCollection') {
278
+ const to = ownerParty('receiver', { address: movement.mintSlotAddress });
279
+ return {
280
+ kind: 'mint',
281
+ asset: nftAssetFromMovement(movement),
282
+ amount: {
283
+ rawAmount: '1',
284
+ displayAmount: '1',
285
+ decimal: 0,
286
+ },
287
+ ...(to ? { to } : {}),
288
+ tapeOutputIndex: movement.tapeOutputIndex,
289
+ movementIndex,
290
+ source: 'decoded',
291
+ };
292
+ }
293
+ const from = movement.action === 'transfer' || movement.action === 'burn'
294
+ ? ownerParty('sender', { address: movement.sender })
295
+ : undefined;
296
+ const to = movement.action === 'burn'
297
+ ? undefined
298
+ : ownerParty('receiver', {
299
+ address: movement.receiver,
300
+ outputIndex: movement.holdOutputIndex ?? movement.outputIndex,
301
+ });
302
+ return {
303
+ kind: movement.action,
304
+ asset: nftAssetFromMovement(movement),
305
+ amount: {
306
+ rawAmount: '1',
307
+ displayAmount: '1',
308
+ decimal: 0,
309
+ },
310
+ ...(from ? { from } : {}),
311
+ ...(to ? { to } : {}),
312
+ outputIndex: movement.outputIndex ?? movement.holdOutputIndex,
313
+ tapeOutputIndex: movement.tapeOutputIndex,
314
+ movementIndex,
315
+ source: 'decoded',
316
+ };
317
+ });
318
+ const relationKind = movements[0]?.action === 'mintCollection'
319
+ ? 'nftCollectionMint'
320
+ : movements[0]?.action === 'mint'
321
+ ? 'nftMint'
322
+ : movements[0]?.action === 'burn'
323
+ ? 'nftBurn'
324
+ : 'nftTransfer';
325
+ const hasPartialOwner = movements.some((movement) => (movement.action === 'transfer' && !movement.sender) ||
326
+ ((movement.action === 'mint' || movement.action === 'transfer') && !movement.receiver));
327
+ return {
328
+ type: transactionType,
329
+ confidence: hasPartialOwner ? 'partial' : 'exact',
330
+ actors: uniqueSemanticParties([
331
+ ...movements.map((movement) => movement.action === 'transfer' || movement.action === 'burn'
332
+ ? ownerParty('sender', { address: movement.sender })
333
+ : undefined),
334
+ ...movements.map((movement) => movement.action === 'burn'
335
+ ? undefined
336
+ : ownerParty('receiver', {
337
+ address: movement.receiver,
338
+ outputIndex: movement.holdOutputIndex ?? movement.outputIndex,
339
+ })),
340
+ ...movements.map((movement) => movement.action === 'mintCollection'
341
+ ? ownerParty('creator', { label: movement.collectionName, outputIndex: movement.tapeOutputIndex })
342
+ : undefined),
343
+ ]),
344
+ effects,
345
+ relations: [
346
+ {
347
+ kind: relationKind,
348
+ confidence: hasPartialOwner ? 'partial' : 'exact',
349
+ outputIndexes: movements
350
+ .flatMap((movement) => [movement.outputIndex, movement.holdOutputIndex, movement.tapeOutputIndex])
351
+ .filter((index) => index !== undefined),
352
+ movementIndexes: movements.map((_movement, index) => index),
353
+ },
354
+ ],
355
+ };
356
+ }
357
+ function buildPoolNftSemantic(transactionType, movements) {
358
+ if (!movements.length) {
359
+ return undefined;
360
+ }
361
+ const effects = movements.flatMap((movement, movementIndex) => {
362
+ const pool = poolNftPoolParty(movement);
363
+ const receiver = poolNftReceiverParty(movement);
364
+ const sender = poolNftSenderParty(movement);
365
+ const userIn = sender ?? receiver;
366
+ const userOut = receiver ?? sender;
367
+ const tokenAsset = poolNftTokenAsset(movement);
368
+ const lpAsset = poolNftLpAsset(movement);
369
+ const movementEffects = [];
370
+ const createAsset = poolNftCreateAsset(movement);
371
+ if (createAsset) {
372
+ movementEffects.push({
373
+ kind: 'mint',
374
+ asset: createAsset,
375
+ amount: { rawAmount: '1', displayAmount: '1', decimal: 0 },
376
+ to: pool,
377
+ outputIndex: movement.poolStateOutputIndex ?? movement.poolOutputIndex,
378
+ tapeOutputIndex: movement.tapeOutputIndex,
379
+ movementIndex,
380
+ source: 'decoded',
381
+ });
382
+ }
383
+ if (movement.stateTbcSatoshis !== undefined || movement.tbcSatoshis !== undefined) {
384
+ movementEffects.push({
385
+ kind: 'state',
386
+ asset: NATIVE_TBC_ASSET,
387
+ amount: { satoshis: movement.stateTbcSatoshis ?? movement.tbcSatoshis },
388
+ to: pool,
389
+ outputIndex: movement.poolStateOutputIndex ?? movement.poolOutputIndex,
390
+ tapeOutputIndex: movement.tapeOutputIndex,
391
+ movementIndex,
392
+ source: 'decoded',
393
+ });
394
+ }
395
+ if (movement.tbcInSatoshis !== undefined && movement.tbcInSatoshis > 0) {
396
+ movementEffects.push({
397
+ kind: 'transfer',
398
+ asset: NATIVE_TBC_ASSET,
399
+ amount: { satoshis: movement.tbcInSatoshis },
400
+ ...(userIn ? { from: userIn } : {}),
401
+ to: pool,
402
+ outputIndex: movement.poolStateOutputIndex ?? movement.poolOutputIndex,
403
+ tapeOutputIndex: movement.tapeOutputIndex,
404
+ movementIndex,
405
+ source: 'derived',
406
+ });
407
+ }
408
+ if (movement.tbcOutSatoshis !== undefined && movement.tbcOutSatoshis > 0) {
409
+ movementEffects.push({
410
+ kind: 'transfer',
411
+ asset: NATIVE_TBC_ASSET,
412
+ amount: { satoshis: movement.tbcOutSatoshis },
413
+ from: pool,
414
+ ...(userOut ? { to: userOut } : {}),
415
+ outputIndex: movement.poolStateOutputIndex ?? movement.poolOutputIndex,
416
+ tapeOutputIndex: movement.tapeOutputIndex,
417
+ movementIndex,
418
+ source: 'derived',
419
+ });
420
+ }
421
+ const ftInAmount = poolNftRawAmount(movement.ftInRawAmount, movement.ftDecimal);
422
+ if (ftInAmount) {
423
+ movementEffects.push({
424
+ kind: movement.action === 'mergeFtInPool' ? 'self' : 'transfer',
425
+ asset: tokenAsset,
426
+ amount: ftInAmount,
427
+ ...(userIn ? { from: userIn } : {}),
428
+ to: pool,
429
+ outputIndex: movement.poolStateOutputIndex ?? movement.poolOutputIndex,
430
+ tapeOutputIndex: movement.tapeOutputIndex,
431
+ movementIndex,
432
+ source: 'derived',
433
+ });
434
+ }
435
+ const ftOutAmount = poolNftRawAmount(movement.ftOutRawAmount, movement.ftDecimal);
436
+ if (ftOutAmount) {
437
+ movementEffects.push({
438
+ kind: 'transfer',
439
+ asset: tokenAsset,
440
+ amount: ftOutAmount,
441
+ from: pool,
442
+ ...(userOut ? { to: userOut } : {}),
443
+ outputIndex: movement.poolStateOutputIndex ?? movement.poolOutputIndex,
444
+ tapeOutputIndex: movement.tapeOutputIndex,
445
+ movementIndex,
446
+ source: 'derived',
447
+ });
448
+ }
449
+ const ftLpInAmount = poolNftRawAmount(movement.ftLpInRawAmount, movement.ftDecimal);
450
+ if (ftLpInAmount) {
451
+ movementEffects.push({
452
+ kind: 'burn',
453
+ asset: lpAsset,
454
+ amount: ftLpInAmount,
455
+ ...(userIn ? { from: userIn } : {}),
456
+ to: {
457
+ role: 'burn',
458
+ ...(movement.action === 'burnFtLp' ? { address: movement.receiver } : { label: 'PoolNFT burn' }),
459
+ },
460
+ tapeOutputIndex: movement.tapeOutputIndex,
461
+ movementIndex,
462
+ source: 'decoded',
463
+ });
464
+ }
465
+ const ftLpOutAmount = poolNftRawAmount(movement.ftLpOutRawAmount, movement.ftDecimal);
466
+ if (ftLpOutAmount) {
467
+ movementEffects.push({
468
+ kind: 'mint',
469
+ asset: lpAsset,
470
+ amount: ftLpOutAmount,
471
+ ...(userOut ? { to: userOut } : {}),
472
+ outputIndex: movement.poolStateOutputIndex ?? movement.poolOutputIndex,
473
+ tapeOutputIndex: movement.tapeOutputIndex,
474
+ movementIndex,
475
+ source: 'derived',
476
+ });
477
+ }
478
+ for (const feeOutput of movement.feeOutputs ?? []) {
479
+ const feeCollector = poolNftFeeCollectorParty(feeOutput);
480
+ movementEffects.push({
481
+ kind: 'fee',
482
+ asset: NATIVE_TBC_ASSET,
483
+ amount: { satoshis: feeOutput.satoshis },
484
+ ...(userIn ? { from: userIn } : {}),
485
+ ...(feeCollector ? { to: feeCollector } : {}),
486
+ outputIndex: feeOutput.outputIndex,
487
+ tapeOutputIndex: movement.tapeOutputIndex,
488
+ movementIndex,
489
+ source: 'derived',
490
+ });
491
+ }
492
+ return movementEffects;
493
+ });
494
+ const hasHeuristic = movements.some((movement) => movement.confidence === 'heuristic');
495
+ return {
496
+ type: transactionType,
497
+ confidence: hasHeuristic ? 'heuristic' : 'exact',
498
+ actors: uniqueSemanticParties([
499
+ ...movements.map(poolNftPoolParty),
500
+ ...movements.map(poolNftSenderParty),
501
+ ...movements.map(poolNftReceiverParty),
502
+ ...movements.flatMap((movement) => (movement.feeOutputs ?? []).map(poolNftFeeCollectorParty)),
503
+ ...movements
504
+ .filter((movement) => movement.ftLpInRawAmount)
505
+ .map((movement) => ownerParty('burn', movement.action === 'burnFtLp' ? { address: movement.receiver } : { label: 'PoolNFT burn' })),
506
+ ]),
507
+ effects,
508
+ relations: [
509
+ {
510
+ kind: 'poolNftAction',
511
+ confidence: hasHeuristic ? 'heuristic' : 'exact',
512
+ inputIndexes: movements.map((movement) => movement.poolStateInputIndex).filter((index) => index !== undefined),
513
+ outputIndexes: movements
514
+ .flatMap((movement) => [movement.poolStateOutputIndex, movement.poolOutputIndex, movement.tapeOutputIndex])
515
+ .filter((index) => index !== undefined),
516
+ movementIndexes: movements.map((_movement, index) => index),
517
+ },
518
+ ],
519
+ notes: movements.flatMap((movement) => movement.reasons ?? []),
520
+ };
521
+ }
522
+ function buildOrderBookSemantic(transactionType, orderBookOrders, ftMovements, outputs) {
523
+ // OrderBook semantics consume protocol facts and matcher results; settlement centralizes amount formulas and output matching.
524
+ if (!orderBookOrders.length) {
525
+ return undefined;
526
+ }
527
+ const facts = buildOrderBookFacts(orderBookOrders, ftMovements, outputs);
528
+ const match = matchOrderBookAction(facts);
529
+ const inputOrders = match?.inputOrders ?? facts.inputOrders;
530
+ const outputOrders = match?.outputOrders ?? facts.outputOrders;
531
+ const actors = facts.orders.map((order) => ({
532
+ role: order.side === 'buy' ? 'buyer' : 'seller',
533
+ address: order.holdAddress,
534
+ inputIndex: order.inputIndex,
535
+ outputIndex: order.outputIndex,
536
+ }));
537
+ if (match?.action === 'match' && match.buyOrder && match.sellOrder && match.settlement) {
538
+ const { buyOrder, sellOrder, settlement } = match;
539
+ const { buyerTbcSatoshis, tbcFeeSatoshis, sellerFtRaw, ftFeeRaw, buyerTbcOutput, tbcFeeOutput, sellerFtMovement, ftFeeMovement, tokenTemplate, matchedSatoshis, remainders, } = settlement;
540
+ const tokenAsset = tokenTemplate ? tokenAssetFromMovement(tokenTemplate) : undefined;
541
+ const effects = [];
542
+ const completeRemainders = remainders.filter((remainder) => remainder.isComplete);
543
+ const settlementMovements = [
544
+ sellerFtMovement,
545
+ ftFeeMovement,
546
+ ...completeRemainders.map((remainder) => remainder.ftMovement),
547
+ ].filter((movement) => Boolean(movement));
548
+ if (buyerTbcOutput) {
549
+ effects.push({
550
+ kind: 'transfer',
551
+ asset: NATIVE_TBC_ASSET,
552
+ amount: { satoshis: buyerTbcSatoshis },
553
+ from: { role: 'seller', address: sellOrder.holdAddress, inputIndex: sellOrder.inputIndex },
554
+ to: { role: 'buyer', address: buyOrder.holdAddress, outputIndex: buyerTbcOutput.index },
555
+ inputIndex: sellOrder.inputIndex,
556
+ outputIndex: buyerTbcOutput.index,
557
+ source: 'derived',
558
+ });
559
+ }
560
+ if (tokenAsset && sellerFtMovement) {
561
+ effects.push({
562
+ kind: 'transfer',
563
+ asset: tokenAsset,
564
+ amount: {
565
+ rawAmount: sellerFtRaw,
566
+ displayAmount: sellerFtMovement.displayAmount,
567
+ decimal: sellerFtMovement.decimal,
568
+ },
569
+ from: { role: 'buyer', address: buyOrder.holdAddress, inputIndex: buyOrder.inputIndex },
570
+ to: { role: 'seller', address: sellOrder.holdAddress, outputIndex: sellerFtMovement.outputIndex },
571
+ inputIndex: buyOrder.inputIndex,
572
+ outputIndex: sellerFtMovement.outputIndex,
573
+ tapeOutputIndex: sellerFtMovement.tapeOutputIndex,
574
+ movementIndex: ftMovements.indexOf(sellerFtMovement),
575
+ source: 'derived',
576
+ });
577
+ }
578
+ if (tbcFeeSatoshis > 0 && tbcFeeOutput) {
579
+ effects.push({
580
+ kind: 'fee',
581
+ asset: NATIVE_TBC_ASSET,
582
+ amount: { satoshis: tbcFeeSatoshis },
583
+ from: { role: 'seller', address: sellOrder.holdAddress, inputIndex: sellOrder.inputIndex },
584
+ to: { role: 'feeCollector', address: tbcFeeOutput.address, outputIndex: tbcFeeOutput.index },
585
+ inputIndex: sellOrder.inputIndex,
586
+ outputIndex: tbcFeeOutput.index,
587
+ source: 'derived',
588
+ });
589
+ }
590
+ if (tokenAsset && BigInt(ftFeeRaw) > 0n && ftFeeMovement) {
591
+ effects.push({
592
+ kind: 'fee',
593
+ asset: tokenAsset,
594
+ amount: {
595
+ rawAmount: ftFeeRaw,
596
+ displayAmount: ftFeeMovement.displayAmount,
597
+ decimal: ftFeeMovement.decimal,
598
+ },
599
+ from: { role: 'buyer', address: buyOrder.holdAddress, inputIndex: buyOrder.inputIndex },
600
+ to: { role: 'feeCollector', address: ftFeeMovement.receiver, outputIndex: ftFeeMovement.outputIndex },
601
+ inputIndex: buyOrder.inputIndex,
602
+ outputIndex: ftFeeMovement.outputIndex,
603
+ tapeOutputIndex: ftFeeMovement.tapeOutputIndex,
604
+ movementIndex: ftMovements.indexOf(ftFeeMovement),
605
+ source: 'derived',
606
+ });
607
+ }
608
+ for (const remainder of completeRemainders) {
609
+ if (!remainder.outputOrder || remainder.outputOrder.satoshis === undefined) {
610
+ continue;
611
+ }
612
+ effects.push({
613
+ kind: 'state',
614
+ asset: NATIVE_TBC_ASSET,
615
+ amount: { satoshis: remainder.outputOrder.satoshis },
616
+ to: {
617
+ role: remainder.outputOrder.side === 'buy' ? 'buyer' : 'seller',
618
+ address: remainder.outputOrder.holdAddress,
619
+ outputIndex: remainder.outputOrder.outputIndex,
620
+ },
621
+ inputIndex: remainder.inputOrder.inputIndex,
622
+ outputIndex: remainder.outputOrder.outputIndex,
623
+ movementIndex: remainder.outputOrder.orderIndex,
624
+ source: 'decoded',
625
+ });
626
+ }
627
+ return {
628
+ type: transactionType,
629
+ confidence: match.confidence,
630
+ actors: uniqueSemanticParties([
631
+ ...actors,
632
+ tbcFeeOutput ? { role: 'feeCollector', address: tbcFeeOutput.address, outputIndex: tbcFeeOutput.index } : undefined,
633
+ ftFeeMovement ? { role: 'feeCollector', address: ftFeeMovement.receiver, outputIndex: ftFeeMovement.outputIndex } : undefined,
634
+ ]),
635
+ effects,
636
+ relations: [
637
+ {
638
+ kind: 'orderBookMatch',
639
+ confidence: match.confidence,
640
+ inputIndexes: inputOrders.map((order) => order.inputIndex).filter((index) => index !== undefined),
641
+ outputIndexes: [
642
+ buyerTbcOutput?.index,
643
+ tbcFeeOutput?.index,
644
+ sellerFtMovement?.outputIndex,
645
+ sellerFtMovement?.tapeOutputIndex,
646
+ ftFeeMovement?.outputIndex,
647
+ ftFeeMovement?.tapeOutputIndex,
648
+ ...completeRemainders.map((remainder) => remainder.outputOrder?.outputIndex),
649
+ ...completeRemainders.flatMap((remainder) => [
650
+ remainder.ftMovement?.outputIndex,
651
+ remainder.ftMovement?.tapeOutputIndex,
652
+ ]),
653
+ ].filter((index) => index !== undefined),
654
+ movementIndexes: uniqueNumbers(settlementMovements.map((movement) => ftMovements.indexOf(movement))),
655
+ orderIndexes: uniqueNumbers([
656
+ buyOrder.orderIndex,
657
+ sellOrder.orderIndex,
658
+ ...completeRemainders.map((remainder) => remainder.outputOrder?.orderIndex),
659
+ ]),
660
+ buyOrderIndex: buyOrder.orderIndex,
661
+ sellOrderIndex: sellOrder.orderIndex,
662
+ remainingOrderIndexes: remainders
663
+ .filter((remainder) => remainder.isComplete)
664
+ .map((remainder) => remainder.outputOrder?.orderIndex)
665
+ .filter((index) => index !== undefined),
666
+ priceRaw: sellOrder.unitPriceRaw,
667
+ matchedSatoshis,
668
+ },
669
+ ],
670
+ ...(match.reasons.length ? { notes: match.reasons } : {}),
671
+ };
672
+ }
673
+ if ((match?.action === 'cancelBuy' || match?.action === 'cancelSell') && match.cancelRelease) {
674
+ const { cancelRelease } = match;
675
+ const { order } = cancelRelease;
676
+ const effects = [];
677
+ if (cancelRelease.asset === 'tbc' && cancelRelease.tbcOutput) {
678
+ effects.push({
679
+ kind: 'orderRelease',
680
+ asset: NATIVE_TBC_ASSET,
681
+ amount: { satoshis: cancelRelease.tbcOutput.satoshis },
682
+ to: {
683
+ role: 'orderHolder',
684
+ address: order.holdAddress,
685
+ inputIndex: order.inputIndex,
686
+ outputIndex: cancelRelease.tbcOutput.index,
687
+ },
688
+ inputIndex: order.inputIndex,
689
+ outputIndex: cancelRelease.tbcOutput.index,
690
+ movementIndex: order.orderIndex,
691
+ source: 'derived',
692
+ });
693
+ }
694
+ if (cancelRelease.asset === 'ft' && cancelRelease.ftMovement) {
695
+ effects.push({
696
+ kind: 'orderRelease',
697
+ asset: tokenAssetFromMovement(cancelRelease.ftMovement),
698
+ amount: {
699
+ rawAmount: cancelRelease.ftMovement.rawAmount,
700
+ displayAmount: cancelRelease.ftMovement.displayAmount,
701
+ decimal: cancelRelease.ftMovement.decimal,
702
+ },
703
+ to: {
704
+ role: 'orderHolder',
705
+ address: order.holdAddress,
706
+ outputIndex: cancelRelease.ftMovement.outputIndex,
707
+ },
708
+ inputIndex: order.inputIndex,
709
+ outputIndex: cancelRelease.ftMovement.outputIndex,
710
+ tapeOutputIndex: cancelRelease.ftMovement.tapeOutputIndex,
711
+ movementIndex: ftMovements.indexOf(cancelRelease.ftMovement),
712
+ source: 'derived',
713
+ });
714
+ }
715
+ return {
716
+ type: transactionType,
717
+ confidence: match.confidence,
718
+ actors: uniqueSemanticParties(actors),
719
+ effects,
720
+ relations: [
721
+ {
722
+ kind: 'orderBookCancel',
723
+ confidence: match.confidence,
724
+ inputIndexes: inputOrders.map((inputOrder) => inputOrder.inputIndex).filter((index) => index !== undefined),
725
+ outputIndexes: [
726
+ cancelRelease.tbcOutput?.index,
727
+ cancelRelease.ftMovement?.outputIndex,
728
+ cancelRelease.ftMovement?.tapeOutputIndex,
729
+ ].filter((index) => index !== undefined),
730
+ movementIndexes: cancelRelease.ftMovement ? [ftMovements.indexOf(cancelRelease.ftMovement)] : [],
731
+ orderIndexes: [order.orderIndex],
732
+ },
733
+ ],
734
+ ...(match.reasons.length ? { notes: match.reasons } : {}),
735
+ };
736
+ }
737
+ if (match?.action === 'update' && match.updateContinuity) {
738
+ const { updateContinuity } = match;
739
+ const { inputOrder, outputOrder } = updateContinuity;
740
+ const effects = [];
741
+ const updateStateIsComplete = updateContinuity.isContinuous &&
742
+ outputOrder &&
743
+ outputOrder.satoshis !== undefined &&
744
+ (!match.updateDelta ||
745
+ match.updateDelta.direction === 'none' ||
746
+ match.updateDelta.isComplete);
747
+ if (updateStateIsComplete) {
748
+ effects.push({
749
+ kind: 'state',
750
+ asset: NATIVE_TBC_ASSET,
751
+ amount: { satoshis: outputOrder.satoshis },
752
+ to: {
753
+ role: 'orderHolder',
754
+ address: outputOrder.holdAddress,
755
+ outputIndex: outputOrder.outputIndex,
756
+ },
757
+ inputIndex: inputOrder?.inputIndex,
758
+ outputIndex: outputOrder.outputIndex,
759
+ movementIndex: outputOrder.orderIndex,
760
+ source: 'decoded',
761
+ });
762
+ }
763
+ if (match.updateDelta?.isComplete && match.updateDelta.direction !== 'none') {
764
+ const { updateDelta } = match;
765
+ if (updateDelta.asset === 'ft' && updateDelta.ftMovement) {
766
+ effects.push({
767
+ kind: updateDelta.direction === 'lock' ? 'orderLock' : 'orderRelease',
768
+ asset: tokenAssetFromMovement(updateDelta.ftMovement),
769
+ amount: {
770
+ rawAmount: updateDelta.ftMovement.rawAmount,
771
+ displayAmount: updateDelta.ftMovement.displayAmount,
772
+ decimal: updateDelta.ftMovement.decimal,
773
+ },
774
+ to: {
775
+ role: 'orderHolder',
776
+ address: updateDelta.outputOrder.holdAddress,
777
+ outputIndex: updateDelta.ftMovement.outputIndex,
778
+ },
779
+ inputIndex: updateDelta.inputOrder.inputIndex,
780
+ outputIndex: updateDelta.ftMovement.outputIndex,
781
+ tapeOutputIndex: updateDelta.ftMovement.tapeOutputIndex,
782
+ movementIndex: ftMovements.indexOf(updateDelta.ftMovement),
783
+ source: 'derived',
784
+ });
785
+ }
786
+ if (updateDelta.asset === 'tbc' && updateDelta.tbcOutput) {
787
+ effects.push({
788
+ kind: updateDelta.direction === 'lock' ? 'orderLock' : 'orderRelease',
789
+ asset: NATIVE_TBC_ASSET,
790
+ amount: { satoshis: updateDelta.expectedSatoshis },
791
+ to: {
792
+ role: 'orderHolder',
793
+ address: updateDelta.outputOrder.holdAddress,
794
+ outputIndex: updateDelta.tbcOutput.index,
795
+ },
796
+ inputIndex: updateDelta.inputOrder.inputIndex,
797
+ outputIndex: updateDelta.tbcOutput.index,
798
+ movementIndex: updateDelta.outputOrder.orderIndex,
799
+ source: 'derived',
800
+ });
801
+ }
802
+ }
803
+ return {
804
+ type: transactionType,
805
+ confidence: match.confidence,
806
+ actors: uniqueSemanticParties(actors),
807
+ effects,
808
+ relations: [
809
+ {
810
+ kind: 'orderBookUpdate',
811
+ confidence: match.confidence,
812
+ inputIndexes: inputOrders.map((order) => order.inputIndex).filter((index) => index !== undefined),
813
+ outputIndexes: outputOrders.map((order) => order.outputIndex).filter((index) => index !== undefined),
814
+ orderIndexes: [inputOrder?.orderIndex, outputOrder?.orderIndex].filter((index) => index !== undefined),
815
+ movementIndexes: match.updateDelta?.ftMovement ? [ftMovements.indexOf(match.updateDelta.ftMovement)] : [],
816
+ oldOrderIndex: inputOrder?.orderIndex,
817
+ newOrderIndex: outputOrder?.orderIndex,
818
+ changedFields: updateContinuity.changedFields,
819
+ },
820
+ ],
821
+ ...(match.reasons.length ? { notes: match.reasons } : {}),
822
+ };
823
+ }
824
+ if ((match?.action === 'placeBuy' || match?.action === 'placeSell') && match.placeLock) {
825
+ const { placeLock } = match;
826
+ const { order } = placeLock;
827
+ const effects = [];
828
+ if (placeLock.asset === 'tbc' && placeLock.tbcOutput) {
829
+ effects.push({
830
+ kind: 'orderLock',
831
+ asset: NATIVE_TBC_ASSET,
832
+ amount: { satoshis: placeLock.tbcOutput.satoshis },
833
+ to: {
834
+ role: 'orderHolder',
835
+ address: order.holdAddress,
836
+ outputIndex: placeLock.tbcOutput.index,
837
+ },
838
+ outputIndex: placeLock.tbcOutput.index,
839
+ movementIndex: order.orderIndex,
840
+ source: 'derived',
841
+ });
842
+ }
843
+ if (placeLock.asset === 'ft' && placeLock.ftMovement) {
844
+ effects.push({
845
+ kind: 'orderLock',
846
+ asset: tokenAssetFromMovement(placeLock.ftMovement),
847
+ amount: {
848
+ rawAmount: placeLock.ftMovement.rawAmount,
849
+ displayAmount: placeLock.ftMovement.displayAmount,
850
+ decimal: placeLock.ftMovement.decimal,
851
+ },
852
+ to: {
853
+ role: 'orderHolder',
854
+ address: order.holdAddress,
855
+ outputIndex: placeLock.ftMovement.outputIndex,
856
+ },
857
+ outputIndex: placeLock.ftMovement.outputIndex,
858
+ tapeOutputIndex: placeLock.ftMovement.tapeOutputIndex,
859
+ movementIndex: ftMovements.indexOf(placeLock.ftMovement),
860
+ source: 'derived',
861
+ });
862
+ }
863
+ return {
864
+ type: transactionType,
865
+ confidence: match.confidence,
866
+ actors: uniqueSemanticParties(actors),
867
+ effects,
868
+ relations: [
869
+ {
870
+ kind: 'orderBookOrder',
871
+ confidence: match.confidence,
872
+ outputIndexes: [
873
+ order.outputIndex,
874
+ placeLock.tbcOutput?.index,
875
+ placeLock.ftMovement?.outputIndex,
876
+ placeLock.ftMovement?.tapeOutputIndex,
877
+ ].filter((index) => index !== undefined),
878
+ movementIndexes: placeLock.ftMovement ? [ftMovements.indexOf(placeLock.ftMovement)] : [],
879
+ orderIndexes: [order.orderIndex],
880
+ },
881
+ ],
882
+ ...(match.reasons.length ? { notes: match.reasons } : {}),
883
+ };
884
+ }
885
+ const relationKind = match?.action === 'cancelBuy' || match?.action === 'cancelSell'
886
+ ? 'orderBookCancel'
887
+ : match?.action === 'update'
888
+ ? 'orderBookUpdate'
889
+ : 'orderBookOrder';
890
+ const effects = facts.orders.map((order) => ({
891
+ kind: order.source === 'output' ? 'orderLock' : 'orderRelease',
892
+ asset: NATIVE_TBC_ASSET,
893
+ amount: { satoshis: order.saleVolumeSatoshis },
894
+ to: { role: 'orderHolder', address: order.holdAddress, inputIndex: order.inputIndex, outputIndex: order.outputIndex },
895
+ inputIndex: order.inputIndex,
896
+ outputIndex: order.outputIndex,
897
+ movementIndex: order.orderIndex,
898
+ source: 'decoded',
899
+ }));
900
+ return {
901
+ type: transactionType,
902
+ confidence: match?.confidence ?? 'heuristic',
903
+ actors: uniqueSemanticParties(actors),
904
+ effects,
905
+ relations: [
906
+ {
907
+ kind: relationKind,
908
+ confidence: match?.confidence ?? 'heuristic',
909
+ inputIndexes: inputOrders.map((order) => order.inputIndex).filter((index) => index !== undefined),
910
+ outputIndexes: outputOrders.map((order) => order.outputIndex).filter((index) => index !== undefined),
911
+ orderIndexes: facts.orders.map((order) => order.orderIndex),
912
+ },
913
+ ],
914
+ ...(match?.reasons.length ? { notes: match.reasons } : {}),
915
+ };
916
+ }
917
+ export function buildSemantic(transactionType, inputs, outputs, ftMovements, nftMovements, orderBookOrders, poolNftMovements = []) {
918
+ // Apply semantic adapters by complexity: OrderBook consumes FT movements before NFT, FT, and TBC fallbacks.
919
+ return (buildOrderBookSemantic(transactionType, orderBookOrders, ftMovements, outputs) ??
920
+ buildPoolNftSemantic(transactionType, poolNftMovements) ??
921
+ buildNftSemantic(transactionType, nftMovements) ??
922
+ buildFtSemantic(transactionType, inputs, outputs, ftMovements) ??
923
+ buildCoinbaseSemantic(transactionType, inputs, outputs) ??
924
+ buildTbcSemantic(transactionType, inputs, outputs));
925
+ }
926
+ export function buildFallbackSemantic(transactionType, unsupported) {
927
+ // Return partial semantics when no adapter matches so callers know chain facts and asset flows remain available.
928
+ if (unsupported.length) {
929
+ return {
930
+ type: 'Unsupported contract',
931
+ confidence: 'partial',
932
+ actors: [],
933
+ effects: [],
934
+ relations: [],
935
+ notes: [
936
+ `Inferred transaction type before fallback: ${transactionType}.`,
937
+ 'Contract script detected, but no supported protocol decoder matched it. Chain data is still available under chain.',
938
+ ],
939
+ };
940
+ }
941
+ return {
942
+ type: transactionType,
943
+ confidence: 'partial',
944
+ actors: [],
945
+ effects: [],
946
+ relations: [],
947
+ notes: ['No semantic adapter matched this transaction. Use chain data and assetFlows for protocol-specific UI.'],
948
+ };
949
+ }
950
+ //# sourceMappingURL=semantic.js.map