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,804 @@
1
+ import { calculatePartialSha256 } from '../../adapters/partial-sha256.js';
2
+ import { COIN_CODE_LENGTH, COIN_PARTIAL_HASH_OFFSET, FT_A_CODE_MARKER_HEX, FT_LP_CODE_MARKER_HEX, FT_V1_CODE_LENGTH, FT_V1_PARTIAL_HASH_OFFSET, FT_V2_CODE_LENGTH, FT_V2_PARTIAL_HASH_OFFSET, } from './constants.js';
3
+ import { allPoolNftAmountsAreZero, collectPoolNftFacts, formatPoolNftFtAmount, hasPoolStateCarrier, inputPoolNftTape, isPoolNftBurnReceiver, } from './facts.js';
4
+ import { actionRequiresExistingPoolState, actionRequiresFtAReserveDecrease, actionRequiresFtAReserveIncrease, actionRequiresFtContractMatch, actionRequiresLiquidityAmountMatch, actionRequiresLpReserveDecrease, actionRequiresLpReserveIncrease, actionRequiresLpTokenIdentityMatch, actionRequiresTbcReserveDecrease, actionRequiresTbcReserveIncrease, createPoolNftActionMatch, downgradePoolNftMatch, poolNftTransactionType, } from './actions.js';
5
+ import { parsePoolNftRawAmount, poolNftAbsoluteRawAmount, poolNftFtADelta, poolNftLpDelta, poolNftRawDeltaString, poolNftTbcDelta, } from './amounts.js';
6
+ import { findPoolNftReceiver, findPoolNftReceiverHash, firstBurnFtMovement, poolNftFtAOutputMovementForAction, poolNftExpectedFeeCollector, poolNftFeeOutputs, poolNftFtAMovementByRaw, poolNftFtAMovementForAction, poolNftFtAMovementForEvidence, poolNftFtAPartialHash, poolNftTapeIdentity, poolNftLpMovementByRaw, poolNftLpMovementForAction, poolNftLpPartialHash, poolNftLpTokenId, poolNftLpTokenIdSource, poolNftMovementHasFtACodeMarker, poolNftSender, poolNftSenderFromInputs, poolNftStateOutputAddress, poolNftStateOutputHash, poolNftTbcPayoutOutput, } from './selectors.js';
7
+ import { hasTbcOutPayoutOrFeeMismatch, hasTbcPayoutOrFeeRelationMismatch, poolNftServiceFeeQuote, poolNftVisibleTbcOutSatoshis, withSwapServiceFeeFormulaConfidence, } from './fees.js';
8
+ function withPoolStateConfidence(match, facts) {
9
+ if (!actionRequiresExistingPoolState(match.action) || facts.poolInputs.length) {
10
+ return match;
11
+ }
12
+ return downgradePoolNftMatch(match, 'no previous PoolNFT state input is available');
13
+ }
14
+ function hasKnownFtContractMismatch(action, facts) {
15
+ const ftContractTxid = facts.tape?.ftContractTxid;
16
+ const ftAMovement = poolNftFtAMovementForEvidence(action, facts);
17
+ if (!ftContractTxid || !ftAMovement?.contractId) {
18
+ return false;
19
+ }
20
+ return ftAMovement.contractId !== ftContractTxid;
21
+ }
22
+ function withFtContractConfidence(match, facts) {
23
+ if (!actionRequiresFtContractMatch(match.action) || !hasKnownFtContractMismatch(match.action, facts)) {
24
+ return match;
25
+ }
26
+ return downgradePoolNftMatch(match, 'no related FT movement matches PoolNFT tape ftContractTxid');
27
+ }
28
+ function hasLiquidityAmountMismatch(action, facts) {
29
+ if (action === 'increaseLiquidity' && poolNftFtADelta(facts) !== undefined && poolNftLpDelta(facts) !== undefined) {
30
+ return false;
31
+ }
32
+ const rawAmounts = new Set(facts.relatedFtMovements.map((movement) => movement.rawAmount));
33
+ const ftAAmountRaw = facts.tape?.ftAAmountRaw;
34
+ const ftLpAmountRaw = facts.tape?.ftLpAmountRaw;
35
+ const hasFtAAmount = ftAAmountRaw !== undefined && rawAmounts.has(ftAAmountRaw);
36
+ const hasFtLpAmount = ftLpAmountRaw !== undefined && rawAmounts.has(ftLpAmountRaw);
37
+ return !hasFtAAmount || !hasFtLpAmount;
38
+ }
39
+ function withLiquidityAmountConfidence(match, facts) {
40
+ if (!actionRequiresLiquidityAmountMatch(match.action) || !hasLiquidityAmountMismatch(match.action, facts)) {
41
+ return match;
42
+ }
43
+ return downgradePoolNftMatch(match, 'liquidity FT movements do not match PoolNFT tape FT A and LP amounts');
44
+ }
45
+ function hasLpTokenIdentityMismatch(action, facts) {
46
+ const ftContractTxid = facts.tape?.ftContractTxid;
47
+ if (!ftContractTxid) {
48
+ return false;
49
+ }
50
+ const lpMovement = action === 'consumeLiquidity'
51
+ ? firstBurnFtMovement(facts)
52
+ : poolNftLpMovementForAction(action, facts);
53
+ const expectedLpRawAmount = action === 'increaseLiquidity'
54
+ ? poolNftRawDeltaString(poolNftLpDelta(facts))
55
+ : facts.tape?.ftLpAmountRaw;
56
+ const matchingRawMovements = facts.relatedFtMovements.filter((movement) => movement.rawAmount === expectedLpRawAmount);
57
+ return lpMovement?.contractId === ftContractTxid ||
58
+ (!lpMovement && matchingRawMovements.some((movement) => movement.contractId === ftContractTxid));
59
+ }
60
+ function withLpTokenIdentityConfidence(match, facts) {
61
+ if (!actionRequiresLpTokenIdentityMatch(match.action) || !hasLpTokenIdentityMismatch(match.action, facts)) {
62
+ return match;
63
+ }
64
+ return downgradePoolNftMatch(match, 'PoolNFT LP movement uses the FT A contractId');
65
+ }
66
+ function hasPoolIdentityMismatch(facts) {
67
+ const currentPoolId = poolNftTapeIdentity(facts.tape);
68
+ const poolInputTapes = facts.poolInputs
69
+ .map((input) => input.tape)
70
+ .filter((tape) => Boolean(tape));
71
+ if (!currentPoolId || !poolInputTapes.length) {
72
+ return false;
73
+ }
74
+ return poolInputTapes.some((tape) => poolNftTapeIdentity(tape) !== currentPoolId);
75
+ }
76
+ function hasCombinedPartialHashMismatch(facts) {
77
+ const combinedPartialHash = facts.tape?.combinedPartialHash;
78
+ const ftLpPartialHash = facts.tape?.ftLpPartialHash;
79
+ const ftAPartialHash = facts.tape?.ftAPartialHash;
80
+ if (!combinedPartialHash || !ftLpPartialHash || !ftAPartialHash) {
81
+ return false;
82
+ }
83
+ const expectedCombinedPartialHash = `${ftLpPartialHash}${ftAPartialHash}`;
84
+ if (combinedPartialHash.length !== expectedCombinedPartialHash.length) {
85
+ return false;
86
+ }
87
+ return combinedPartialHash !== expectedCombinedPartialHash;
88
+ }
89
+ function hasPoolStateScriptMismatch(facts) {
90
+ const previousScriptHex = facts.poolInputs[0]?.script.hex;
91
+ const currentScriptHex = facts.poolOutput?.script.hex;
92
+ if (!previousScriptHex || !currentScriptHex) {
93
+ return false;
94
+ }
95
+ return previousScriptHex !== currentScriptHex;
96
+ }
97
+ function hasMultiplePoolStateInputs(facts) {
98
+ return facts.poolInputs.length > 1;
99
+ }
100
+ function withPoolIdentityConfidence(match, facts) {
101
+ if (!actionRequiresExistingPoolState(match.action) || !hasPoolIdentityMismatch(facts)) {
102
+ return match;
103
+ }
104
+ return downgradePoolNftMatch(match, 'previous PoolNFT state uses a different pool identity');
105
+ }
106
+ function withTbcReserveDeltaConfidence(match, facts) {
107
+ const delta = poolNftTbcDelta(facts);
108
+ if (delta === undefined) {
109
+ return match;
110
+ }
111
+ if (actionRequiresTbcReserveIncrease(match.action) && delta <= 0) {
112
+ return downgradePoolNftMatch(match, 'PoolNFT TBC reserve did not increase for this action');
113
+ }
114
+ if (actionRequiresTbcReserveDecrease(match.action) && delta >= 0) {
115
+ return downgradePoolNftMatch(match, 'PoolNFT TBC reserve did not decrease for this action');
116
+ }
117
+ return match;
118
+ }
119
+ function withFtAReserveDeltaConfidence(match, facts) {
120
+ const delta = poolNftFtADelta(facts);
121
+ if (delta === undefined) {
122
+ return match;
123
+ }
124
+ if (actionRequiresFtAReserveIncrease(match.action) && delta <= 0n) {
125
+ return downgradePoolNftMatch(match, 'PoolNFT FT A reserve did not increase for this action');
126
+ }
127
+ if (actionRequiresFtAReserveDecrease(match.action) && delta >= 0n) {
128
+ return downgradePoolNftMatch(match, 'PoolNFT FT A reserve did not decrease for this action');
129
+ }
130
+ return match;
131
+ }
132
+ function withLpReserveDeltaConfidence(match, facts) {
133
+ const delta = poolNftLpDelta(facts);
134
+ if (delta === undefined) {
135
+ return match;
136
+ }
137
+ if (actionRequiresLpReserveIncrease(match.action) && delta <= 0n) {
138
+ return downgradePoolNftMatch(match, 'PoolNFT LP reserve did not increase for this action');
139
+ }
140
+ if (actionRequiresLpReserveDecrease(match.action) && delta >= 0n) {
141
+ return downgradePoolNftMatch(match, 'PoolNFT LP reserve did not decrease for this action');
142
+ }
143
+ return match;
144
+ }
145
+ function withSwapAmountRelationConfidence(match, facts) {
146
+ if (match.confidence === 'heuristic') {
147
+ return match;
148
+ }
149
+ if (match.action === 'swapTbcToFt') {
150
+ const ftADelta = poolNftFtADelta(facts);
151
+ const ftOutAmount = parsePoolNftRawAmount(poolNftFtAOutputMovementForAction(match.action, facts)?.rawAmount);
152
+ if (ftADelta !== undefined && ftOutAmount !== undefined && poolNftAbsoluteRawAmount(ftADelta) !== ftOutAmount) {
153
+ return downgradePoolNftMatch(match, 'PoolNFT FT A swap output does not match reserve delta');
154
+ }
155
+ }
156
+ if (match.action === 'swapFtToTbc') {
157
+ const ftADelta = poolNftFtADelta(facts);
158
+ const ftInAmount = parsePoolNftRawAmount(facts.relatedFtMovements[0]?.rawAmount);
159
+ if (ftADelta !== undefined && ftInAmount !== undefined && poolNftAbsoluteRawAmount(ftADelta) !== ftInAmount) {
160
+ return downgradePoolNftMatch(match, 'PoolNFT FT A swap input does not match reserve delta');
161
+ }
162
+ const tbcDelta = poolNftTbcDelta(facts);
163
+ const tbcOutSatoshis = poolNftVisibleTbcOutSatoshis(match.action, facts);
164
+ const hasFormula = poolNftServiceFeeQuote(BigInt(Math.abs(tbcDelta ?? 0)), facts) !== undefined;
165
+ if (!hasFormula && tbcDelta !== undefined && tbcOutSatoshis !== undefined && Math.abs(tbcDelta) !== tbcOutSatoshis) {
166
+ return downgradePoolNftMatch(match, 'PoolNFT TBC swap payout plus fee does not match reserve delta');
167
+ }
168
+ }
169
+ return match;
170
+ }
171
+ function withLiquidityAmountRelationConfidence(match, facts) {
172
+ if (match.confidence === 'heuristic') {
173
+ return match;
174
+ }
175
+ if (match.action === 'increaseLiquidity') {
176
+ const ftADelta = poolNftFtADelta(facts);
177
+ const ftInMovement = poolNftFtAMovementByRaw(facts, poolNftRawDeltaString(ftADelta));
178
+ if (ftADelta !== undefined && parsePoolNftRawAmount(ftInMovement?.rawAmount) !== poolNftAbsoluteRawAmount(ftADelta)) {
179
+ return downgradePoolNftMatch(match, 'PoolNFT FT A liquidity input does not match reserve delta');
180
+ }
181
+ const lpDelta = poolNftLpDelta(facts);
182
+ const lpOutMovement = poolNftLpMovementByRaw(facts, poolNftRawDeltaString(lpDelta));
183
+ if (lpDelta !== undefined && parsePoolNftRawAmount(lpOutMovement?.rawAmount) !== poolNftAbsoluteRawAmount(lpDelta)) {
184
+ return downgradePoolNftMatch(match, 'PoolNFT LP liquidity output does not match reserve delta');
185
+ }
186
+ }
187
+ if (match.action === 'consumeLiquidity') {
188
+ const ftADelta = poolNftFtADelta(facts);
189
+ const ftOutMovement = poolNftFtAOutputMovementForAction(match.action, facts);
190
+ if (ftADelta !== undefined && parsePoolNftRawAmount(ftOutMovement?.rawAmount) !== poolNftAbsoluteRawAmount(ftADelta)) {
191
+ return downgradePoolNftMatch(match, 'PoolNFT FT A liquidity output does not match reserve delta');
192
+ }
193
+ const lpDelta = poolNftLpDelta(facts);
194
+ const lpInMovement = firstBurnFtMovement(facts);
195
+ if (lpDelta !== undefined && parsePoolNftRawAmount(lpInMovement?.rawAmount) !== poolNftAbsoluteRawAmount(lpDelta)) {
196
+ return downgradePoolNftMatch(match, 'PoolNFT LP liquidity burn does not match reserve delta');
197
+ }
198
+ const tbcDelta = poolNftTbcDelta(facts);
199
+ if (tbcDelta !== undefined && hasTbcOutPayoutOrFeeMismatch('consumeLiquidity', facts)) {
200
+ return downgradePoolNftMatch(match, 'PoolNFT TBC liquidity payout plus fee does not match reserve delta');
201
+ }
202
+ }
203
+ return match;
204
+ }
205
+ function isPoolNftPartialHashMismatch(verification) {
206
+ return verification === 'computedPartialHashMismatch' ||
207
+ verification === 'matchedWrongPartialHashLiteral' ||
208
+ verification === 'codeTypeMismatch';
209
+ }
210
+ function actionRequiresFtAAndLpPartialHashIdentity(action) {
211
+ return action === 'init' || action === 'increaseLiquidity' || action === 'consumeLiquidity';
212
+ }
213
+ function hasBothPartialHashIdentitiesMismatched(action, facts) {
214
+ if (!actionRequiresFtAAndLpPartialHashIdentity(action)) {
215
+ return false;
216
+ }
217
+ return isPoolNftPartialHashMismatch(poolNftFtAPartialHashVerification(action, facts)) &&
218
+ isPoolNftPartialHashMismatch(poolNftLpPartialHashVerification(action, facts));
219
+ }
220
+ function hasLpPartialHashIdentityMismatched(action, facts) {
221
+ return actionRequiresLpTokenIdentityMatch(action) &&
222
+ isPoolNftPartialHashMismatch(poolNftLpPartialHashVerification(action, facts));
223
+ }
224
+ function poolNftFtMovementsExpectedToLockToPool(action, facts) {
225
+ if (action === 'init' || action === 'increaseLiquidity') {
226
+ return [poolNftFtAMovementForAction(action, facts)];
227
+ }
228
+ if (action === 'swapFtToTbc' || action === 'mergeFtInPool') {
229
+ return [facts.relatedFtMovements[0]];
230
+ }
231
+ return [];
232
+ }
233
+ function poolNftFtMovementsExpectedToLockAwayFromPool(action, facts) {
234
+ if (action === 'init' || action === 'increaseLiquidity') {
235
+ return [poolNftLpMovementForAction(action, facts)];
236
+ }
237
+ if (action === 'consumeLiquidity' || action === 'swapTbcToFt') {
238
+ return [poolNftFtAOutputMovementForAction(action, facts)];
239
+ }
240
+ return [];
241
+ }
242
+ function hasPoolDirectedFtReceiverHashMismatch(action, facts) {
243
+ const poolStateHash = poolNftStateOutputHash(facts);
244
+ if (!poolStateHash) {
245
+ return false;
246
+ }
247
+ return poolNftFtMovementsExpectedToLockToPool(action, facts).some((movement) => Boolean(movement?.receiverHash && movement.receiverHash !== poolStateHash));
248
+ }
249
+ function hasUserDirectedFtReceiverHashLocksToPool(action, facts) {
250
+ const poolStateHash = poolNftStateOutputHash(facts);
251
+ if (!poolStateHash) {
252
+ return false;
253
+ }
254
+ return poolNftFtMovementsExpectedToLockAwayFromPool(action, facts).some((movement) => Boolean(movement?.receiverHash && movement.receiverHash === poolStateHash));
255
+ }
256
+ function hasTbcReserveDirectionReversed(action, facts) {
257
+ const delta = poolNftTbcDelta(facts);
258
+ if (delta === undefined) {
259
+ return false;
260
+ }
261
+ return (actionRequiresTbcReserveIncrease(action) && delta < 0) ||
262
+ (actionRequiresTbcReserveDecrease(action) && delta > 0);
263
+ }
264
+ function hasFtAReserveDirectionReversed(action, facts) {
265
+ const delta = poolNftFtADelta(facts);
266
+ if (delta === undefined) {
267
+ return false;
268
+ }
269
+ return (actionRequiresFtAReserveIncrease(action) && delta < 0n) ||
270
+ (actionRequiresFtAReserveDecrease(action) && delta > 0n);
271
+ }
272
+ function hasLpReserveDirectionReversed(action, facts) {
273
+ const delta = poolNftLpDelta(facts);
274
+ if (delta === undefined) {
275
+ return false;
276
+ }
277
+ return (actionRequiresLpReserveIncrease(action) && delta < 0n) ||
278
+ (actionRequiresLpReserveDecrease(action) && delta > 0n);
279
+ }
280
+ function hasReserveDirectionReversed(action, facts) {
281
+ return hasTbcReserveDirectionReversed(action, facts) ||
282
+ hasFtAReserveDirectionReversed(action, facts) ||
283
+ hasLpReserveDirectionReversed(action, facts);
284
+ }
285
+ function poolNftRawAmountDoesNotMatchDelta(delta, movement) {
286
+ const amount = parsePoolNftRawAmount(movement?.rawAmount);
287
+ return delta !== undefined && delta !== 0n && amount !== undefined && amount !== poolNftAbsoluteRawAmount(delta);
288
+ }
289
+ function poolNftIncreaseFtAInputMovement(facts) {
290
+ const ftContractTxid = facts.tape?.ftContractTxid;
291
+ return facts.relatedFtMovements.find((movement) => !movement.receiver && (!ftContractTxid || !movement.contractId || movement.contractId === ftContractTxid));
292
+ }
293
+ function poolNftIncreaseLpOutputMovement(facts) {
294
+ const ftContractTxid = facts.tape?.ftContractTxid;
295
+ const receiverMovements = facts.relatedFtMovements.filter((movement) => Boolean(movement.receiver && !isPoolNftBurnReceiver(movement.receiver)));
296
+ return receiverMovements.find((movement) => movement.contractId && movement.contractId !== ftContractTxid) ??
297
+ receiverMovements.find((movement) => movement.contractId !== ftContractTxid);
298
+ }
299
+ function hasSwapRawAmountRelationMismatch(action, facts) {
300
+ const ftADelta = poolNftFtADelta(facts);
301
+ if (action === 'swapTbcToFt') {
302
+ return poolNftRawAmountDoesNotMatchDelta(ftADelta, poolNftFtAOutputMovementForAction(action, facts));
303
+ }
304
+ if (action === 'swapFtToTbc') {
305
+ return poolNftRawAmountDoesNotMatchDelta(ftADelta, facts.relatedFtMovements[0]);
306
+ }
307
+ return false;
308
+ }
309
+ function hasMergeFtInPoolRawAmountRelationMismatch(action, facts) {
310
+ if (action !== 'mergeFtInPool') {
311
+ return false;
312
+ }
313
+ const currentFtAAmount = parsePoolNftRawAmount(facts.tape?.ftAAmountRaw);
314
+ const movementAmount = parsePoolNftRawAmount(facts.relatedFtMovements[0]?.rawAmount);
315
+ return currentFtAAmount !== undefined && movementAmount !== undefined && movementAmount !== currentFtAAmount;
316
+ }
317
+ function hasLiquidityRawAmountRelationMismatch(action, facts) {
318
+ if (action === 'increaseLiquidity') {
319
+ return poolNftRawAmountDoesNotMatchDelta(poolNftFtADelta(facts), poolNftIncreaseFtAInputMovement(facts)) ||
320
+ poolNftRawAmountDoesNotMatchDelta(poolNftLpDelta(facts), poolNftIncreaseLpOutputMovement(facts));
321
+ }
322
+ if (action === 'consumeLiquidity') {
323
+ return poolNftRawAmountDoesNotMatchDelta(poolNftFtADelta(facts), poolNftFtAOutputMovementForAction(action, facts)) ||
324
+ poolNftRawAmountDoesNotMatchDelta(poolNftLpDelta(facts), firstBurnFtMovement(facts));
325
+ }
326
+ return false;
327
+ }
328
+ function hasRawAmountRelationMismatch(action, facts) {
329
+ return hasSwapRawAmountRelationMismatch(action, facts) ||
330
+ hasMergeFtInPoolRawAmountRelationMismatch(action, facts) ||
331
+ hasLiquidityRawAmountRelationMismatch(action, facts);
332
+ }
333
+ function isPoolNftFtAMovementByIdentity(facts, movement) {
334
+ return movement.contractId === facts.tape?.ftContractTxid ||
335
+ poolNftMovementHasFtACodeMarker(facts, movement);
336
+ }
337
+ function isPoolNftUserDirectedMovement(facts, movement) {
338
+ const poolStateAddress = poolNftStateOutputAddress(facts);
339
+ const poolStateHash = poolNftStateOutputHash(facts);
340
+ return !isPoolNftBurnReceiver(movement.receiver) &&
341
+ Boolean(movement.receiver || movement.receiverHash) &&
342
+ (!movement.receiver || movement.receiver !== poolStateAddress) &&
343
+ (!movement.receiverHash || movement.receiverHash !== poolStateHash);
344
+ }
345
+ function hasUnexplainedUserFtAOutput(action, facts) {
346
+ if (action !== 'consumeLiquidity' && action !== 'swapTbcToFt') {
347
+ return false;
348
+ }
349
+ const ftADelta = poolNftFtADelta(facts);
350
+ const selectedOutput = poolNftFtAOutputMovementForAction(action, facts);
351
+ const selectedAmount = parsePoolNftRawAmount(selectedOutput?.rawAmount);
352
+ if (ftADelta === undefined ||
353
+ selectedAmount === undefined ||
354
+ selectedAmount !== poolNftAbsoluteRawAmount(ftADelta)) {
355
+ return false;
356
+ }
357
+ return facts.relatedFtMovements.some((movement) => movement !== selectedOutput &&
358
+ isPoolNftUserDirectedMovement(facts, movement) &&
359
+ isPoolNftFtAMovementByIdentity(facts, movement));
360
+ }
361
+ function hasUnexpectedFtMovementCount(action, facts) {
362
+ if (action === 'consumeLiquidity') {
363
+ return facts.burnMovements.length > 1;
364
+ }
365
+ if (action === 'init') {
366
+ return facts.relatedFtMovements.length > 3;
367
+ }
368
+ if (action === 'increaseLiquidity') {
369
+ return facts.relatedFtMovements.length > 3;
370
+ }
371
+ return false;
372
+ }
373
+ function withPartialHashIdentityConfidence(match, facts) {
374
+ if (match.confidence === 'heuristic') {
375
+ return match;
376
+ }
377
+ const ftAVerification = poolNftFtAPartialHashVerification(match.action, facts);
378
+ const lpVerification = poolNftLpPartialHashVerification(match.action, facts);
379
+ if (isPoolNftPartialHashMismatch(ftAVerification) || isPoolNftPartialHashMismatch(lpVerification)) {
380
+ return downgradePoolNftMatch(match, 'PoolNFT partial hash identity does not match related FT Code output');
381
+ }
382
+ return match;
383
+ }
384
+ function shouldRejectPoolNftActionMatch(match, facts) {
385
+ return (actionRequiresExistingPoolState(match.action) && hasMultiplePoolStateInputs(facts)) ||
386
+ (actionRequiresExistingPoolState(match.action) && hasPoolIdentityMismatch(facts)) ||
387
+ (actionRequiresExistingPoolState(match.action) && hasPoolStateScriptMismatch(facts)) ||
388
+ (actionRequiresFtContractMatch(match.action) && hasKnownFtContractMismatch(match.action, facts)) ||
389
+ hasReserveDirectionReversed(match.action, facts) ||
390
+ hasRawAmountRelationMismatch(match.action, facts) ||
391
+ hasUnexplainedUserFtAOutput(match.action, facts) ||
392
+ hasTbcPayoutOrFeeRelationMismatch(match.action, facts) ||
393
+ hasUnexpectedFtMovementCount(match.action, facts) ||
394
+ hasCombinedPartialHashMismatch(facts) ||
395
+ hasPoolDirectedFtReceiverHashMismatch(match.action, facts) ||
396
+ hasUserDirectedFtReceiverHashLocksToPool(match.action, facts) ||
397
+ hasLpPartialHashIdentityMismatched(match.action, facts) ||
398
+ hasBothPartialHashIdentitiesMismatched(match.action, facts);
399
+ }
400
+ function applyPoolNftConfidenceChecks(match, facts) {
401
+ return [
402
+ withPoolStateConfidence,
403
+ withPoolIdentityConfidence,
404
+ withFtContractConfidence,
405
+ withLiquidityAmountConfidence,
406
+ withLpTokenIdentityConfidence,
407
+ withFtAReserveDeltaConfidence,
408
+ withLpReserveDeltaConfidence,
409
+ withTbcReserveDeltaConfidence,
410
+ withSwapAmountRelationConfidence,
411
+ withSwapServiceFeeFormulaConfidence,
412
+ withLiquidityAmountRelationConfidence,
413
+ withPartialHashIdentityConfidence,
414
+ ].reduce((currentMatch, confidenceCheck) => confidenceCheck(currentMatch, facts), match);
415
+ }
416
+ function matchCreate(facts) {
417
+ if (!allPoolNftAmountsAreZero(facts.tape) || facts.relatedFtMovements.length) {
418
+ return undefined;
419
+ }
420
+ return createPoolNftActionMatch('create', [
421
+ 'PoolNFT tape amounts are all zero',
422
+ 'no related FT movement after tape',
423
+ ]);
424
+ }
425
+ function matchConsumeLiquidity(facts) {
426
+ if (!facts.burnMovements.length) {
427
+ return undefined;
428
+ }
429
+ return createPoolNftActionMatch('consumeLiquidity', [
430
+ 'related FT movement is sent to a PoolNFT burn address',
431
+ ]);
432
+ }
433
+ function matchSwapFtToTbc(facts) {
434
+ const firstRelatedFt = facts.relatedFtMovements[0];
435
+ const tbcPayoutBeforeFirstFt = facts.tbcOutputsAfterTape.some((output) => output.index < (firstRelatedFt?.outputIndex ?? Number.POSITIVE_INFINITY));
436
+ if (!tbcPayoutBeforeFirstFt || !firstRelatedFt || firstRelatedFt.receiver) {
437
+ return undefined;
438
+ }
439
+ return createPoolNftActionMatch('swapFtToTbc', [
440
+ 'TBC payout appears after PoolNFT tape and before first related FT movement',
441
+ 'first related FT movement has no external receiver',
442
+ ]);
443
+ }
444
+ function matchSwapTbcToFt(facts) {
445
+ const firstRelatedFt = facts.relatedFtMovements[0];
446
+ if (facts.relatedFtMovements.length !== 2 || !firstRelatedFt?.receiver) {
447
+ return undefined;
448
+ }
449
+ return createPoolNftActionMatch('swapTbcToFt', [
450
+ 'two related FT movements appear after PoolNFT tape',
451
+ 'first related FT movement has an external receiver',
452
+ ]);
453
+ }
454
+ function matchInit(facts) {
455
+ if (facts.relatedFtMovements.length < 3) {
456
+ return undefined;
457
+ }
458
+ const lpMovement = facts.relatedFtMovements[1];
459
+ if (lpMovement?.rawAmount !== facts.tape?.ftLpAmountRaw) {
460
+ return undefined;
461
+ }
462
+ return createPoolNftActionMatch('init', [
463
+ 'at least three related FT movements appear after PoolNFT tape',
464
+ 'LP movement amount matches PoolNFT tape LP amount',
465
+ ]);
466
+ }
467
+ function matchIncreaseLiquidity(facts) {
468
+ const tbcDelta = poolNftTbcDelta(facts);
469
+ const ftADelta = poolNftFtADelta(facts);
470
+ const lpDelta = poolNftLpDelta(facts);
471
+ const ftAMovement = poolNftFtAMovementByRaw(facts, poolNftRawDeltaString(ftADelta));
472
+ const lpMovement = poolNftLpMovementByRaw(facts, poolNftRawDeltaString(lpDelta));
473
+ const hasLpMovementCandidate = facts.relatedFtMovements.some((movement) => Boolean(movement.contractId && movement.contractId !== facts.tape?.ftContractTxid));
474
+ if (tbcDelta !== undefined &&
475
+ tbcDelta > 0 &&
476
+ ftADelta !== undefined &&
477
+ ftADelta > 0n &&
478
+ lpDelta !== undefined &&
479
+ lpDelta > 0n &&
480
+ ftAMovement) {
481
+ return createPoolNftActionMatch('increaseLiquidity', [
482
+ 'pool TBC, FT A, and LP reserves all increase',
483
+ lpMovement
484
+ ? 'related FT A and LP movements match reserve deltas'
485
+ : hasLpMovementCandidate
486
+ ? 'related FT A movement matches reserve delta'
487
+ : 'LP output movement is inferred from PoolNFT reserve delta',
488
+ ], lpMovement || hasLpMovementCandidate ? 'exact' : 'heuristic');
489
+ }
490
+ if (facts.relatedFtMovements.length < 3) {
491
+ return undefined;
492
+ }
493
+ return createPoolNftActionMatch('increaseLiquidity', [
494
+ 'at least three related FT movements appear after PoolNFT tape',
495
+ 'LP movement amount does not match PoolNFT tape LP amount',
496
+ ]);
497
+ }
498
+ function matchMergeFtInPool(facts) {
499
+ const firstRelatedFt = facts.relatedFtMovements[0];
500
+ if (facts.relatedFtMovements.length !== 1 || firstRelatedFt?.receiver) {
501
+ return undefined;
502
+ }
503
+ return createPoolNftActionMatch('mergeFtInPool', [
504
+ 'single related FT movement appears after PoolNFT tape',
505
+ 'related FT movement has no external receiver',
506
+ ]);
507
+ }
508
+ function matchUnknownPoolNftAction() {
509
+ return createPoolNftActionMatch('unknown', [
510
+ 'no PoolNFT action matcher accepted the collected facts',
511
+ ]);
512
+ }
513
+ function inferPoolNftAction(facts) {
514
+ // Preserve the existing heuristic priority; matcher extraction allows rules and counterexamples to tighten incrementally.
515
+ const matchers = [
516
+ matchCreate,
517
+ matchConsumeLiquidity,
518
+ matchSwapFtToTbc,
519
+ matchSwapTbcToFt,
520
+ matchInit,
521
+ matchIncreaseLiquidity,
522
+ matchMergeFtInPool,
523
+ ];
524
+ for (const matcher of matchers) {
525
+ const match = matcher(facts);
526
+ if (match) {
527
+ return applyPoolNftConfidenceChecks(match, facts);
528
+ }
529
+ }
530
+ return matchUnknownPoolNftAction();
531
+ }
532
+ function poolNftLpTokenIdEvidence(action, facts) {
533
+ const source = poolNftLpTokenIdSource(action, facts);
534
+ if (source === 'movementContractId') {
535
+ const scriptEvidence = poolNftLpPartialHashVerification(action, facts);
536
+ return scriptEvidence === 'notChecked' ? 'movementContractId' : scriptEvidence;
537
+ }
538
+ if (source === 'poolNftTape') {
539
+ return poolNftLpPartialHashVerification(action, facts);
540
+ }
541
+ return undefined;
542
+ }
543
+ function poolNftPartialHashVerification(partialHash, movement, facts, expectedCodeMarkerHex, wrongPartialHashes = []) {
544
+ const output = facts.outputs.find((candidate) => candidate.index === movement?.outputIndex);
545
+ const scriptHex = output?.script.hex?.toLowerCase();
546
+ if (!partialHash || !scriptHex) {
547
+ return 'notChecked';
548
+ }
549
+ if (!scriptHex.endsWith(expectedCodeMarkerHex)) {
550
+ return 'codeTypeMismatch';
551
+ }
552
+ const computedPartialHash = poolNftComputedPartialHash(scriptHex);
553
+ if (computedPartialHash && partialHash.length === 64) {
554
+ return computedPartialHash === partialHash ? 'matchedComputedPartialHash' : 'computedPartialHashMismatch';
555
+ }
556
+ if (scriptHex.includes(partialHash)) {
557
+ return 'matchedScriptLiteral';
558
+ }
559
+ if (wrongPartialHashes.some((wrongPartialHash) => wrongPartialHash && scriptHex.includes(wrongPartialHash))) {
560
+ return 'matchedWrongPartialHashLiteral';
561
+ }
562
+ return 'notStaticallyVisible';
563
+ }
564
+ function poolNftPartialHashOffset(scriptByteLength) {
565
+ if (scriptByteLength === FT_V1_CODE_LENGTH) {
566
+ return FT_V1_PARTIAL_HASH_OFFSET;
567
+ }
568
+ if (scriptByteLength === FT_V2_CODE_LENGTH) {
569
+ return FT_V2_PARTIAL_HASH_OFFSET;
570
+ }
571
+ if (scriptByteLength === COIN_CODE_LENGTH) {
572
+ return COIN_PARTIAL_HASH_OFFSET;
573
+ }
574
+ return undefined;
575
+ }
576
+ function poolNftComputedPartialHash(scriptHex) {
577
+ if (scriptHex.length % 2 !== 0) {
578
+ return undefined;
579
+ }
580
+ const script = Buffer.from(scriptHex, 'hex');
581
+ const offset = poolNftPartialHashOffset(script.length);
582
+ if (offset === undefined || script.length < offset) {
583
+ return undefined;
584
+ }
585
+ return calculatePartialSha256(script.subarray(0, offset));
586
+ }
587
+ function poolNftFtAPartialHashVerification(action, facts) {
588
+ return poolNftPartialHashVerification(poolNftFtAPartialHash(facts.tape), poolNftFtAMovementForEvidence(action, facts), facts, FT_A_CODE_MARKER_HEX, [poolNftLpPartialHash(facts.tape)]);
589
+ }
590
+ function poolNftLpPartialHashVerification(action, facts) {
591
+ const lpMovement = action === 'consumeLiquidity'
592
+ ? firstBurnFtMovement(facts)
593
+ : poolNftLpMovementForAction(action, facts);
594
+ return poolNftPartialHashVerification(poolNftLpPartialHash(facts.tape), lpMovement, facts, FT_LP_CODE_MARKER_HEX, [poolNftFtAPartialHash(facts.tape)]);
595
+ }
596
+ function poolNftTbcAmountFields(action, facts) {
597
+ const delta = poolNftTbcDelta(facts);
598
+ const positiveDelta = delta !== undefined && delta > 0 ? delta : undefined;
599
+ const negativeDelta = delta !== undefined && delta < 0 ? Math.abs(delta) : undefined;
600
+ const payoutOutput = poolNftTbcPayoutOutput(action, facts);
601
+ if (action === 'init') {
602
+ return {
603
+ tbcInSatoshis: facts.tape?.tbcSatoshis,
604
+ stateTbcSatoshis: facts.tape?.tbcSatoshis,
605
+ };
606
+ }
607
+ if (action === 'increaseLiquidity' || action === 'swapTbcToFt') {
608
+ return {
609
+ tbcInSatoshis: positiveDelta,
610
+ stateTbcSatoshis: facts.tape?.tbcSatoshis,
611
+ };
612
+ }
613
+ if (action === 'consumeLiquidity') {
614
+ return {
615
+ tbcOutSatoshis: payoutOutput?.satoshis ?? negativeDelta,
616
+ tbcOutOutputIndex: payoutOutput?.index,
617
+ stateTbcSatoshis: facts.tape?.tbcSatoshis,
618
+ };
619
+ }
620
+ if (action === 'swapFtToTbc') {
621
+ return {
622
+ tbcOutSatoshis: payoutOutput?.satoshis ?? negativeDelta,
623
+ tbcOutOutputIndex: payoutOutput?.index,
624
+ stateTbcSatoshis: facts.tape?.tbcSatoshis,
625
+ };
626
+ }
627
+ return {
628
+ stateTbcSatoshis: facts.tape?.tbcSatoshis,
629
+ };
630
+ }
631
+ function poolNftFtAmountFields(action, facts) {
632
+ const ftAMovement = poolNftFtAMovementForAction(action, facts);
633
+ const ftLpMovement = poolNftLpMovementForAction(action, facts);
634
+ const firstFtMovement = facts.relatedFtMovements[0];
635
+ const firstBurnMovement = firstBurnFtMovement(facts);
636
+ const ftAOutputMovement = poolNftFtAOutputMovementForAction(action, facts);
637
+ if (action === 'init' || action === 'increaseLiquidity') {
638
+ const ftInRawAmount = action === 'increaseLiquidity'
639
+ ? poolNftRawDeltaString(poolNftFtADelta(facts))
640
+ : facts.tape?.ftAAmountRaw;
641
+ const ftLpOutRawAmount = action === 'increaseLiquidity'
642
+ ? poolNftRawDeltaString(poolNftLpDelta(facts))
643
+ : facts.tape?.ftLpAmountRaw;
644
+ return {
645
+ ftInRawAmount: ftAMovement?.rawAmount ?? ftInRawAmount,
646
+ ftInOutputIndex: ftAMovement?.outputIndex,
647
+ ftLpOutRawAmount: ftLpMovement?.rawAmount ?? ftLpOutRawAmount,
648
+ ftLpOutOutputIndex: ftLpMovement?.outputIndex,
649
+ };
650
+ }
651
+ if (action === 'consumeLiquidity') {
652
+ return {
653
+ ftOutRawAmount: ftAOutputMovement?.rawAmount,
654
+ ftOutOutputIndex: ftAOutputMovement?.outputIndex,
655
+ ftLpInRawAmount: firstBurnMovement?.rawAmount,
656
+ ftLpInOutputIndex: firstBurnMovement?.outputIndex,
657
+ };
658
+ }
659
+ if (action === 'swapTbcToFt') {
660
+ return {
661
+ ftOutRawAmount: ftAOutputMovement?.rawAmount,
662
+ ftOutOutputIndex: ftAOutputMovement?.outputIndex,
663
+ };
664
+ }
665
+ if (action === 'swapFtToTbc' || action === 'mergeFtInPool') {
666
+ return {
667
+ ftInRawAmount: firstFtMovement?.rawAmount,
668
+ ftInOutputIndex: firstFtMovement?.outputIndex,
669
+ };
670
+ }
671
+ return {};
672
+ }
673
+ function poolNftMovementMetadata(action, facts, actionMatch) {
674
+ const sender = poolNftSender(facts);
675
+ const feeOutputs = poolNftFeeOutputs(action, facts);
676
+ const firstFeeOutput = feeOutputs[0];
677
+ const feeSatoshis = feeOutputs.reduce((total, output) => total + output.satoshis, 0);
678
+ const expectedFeeCollector = poolNftExpectedFeeCollector(facts);
679
+ const poolStateAddress = poolNftStateOutputAddress(facts);
680
+ const poolStateHash = poolNftStateOutputHash(facts);
681
+ return {
682
+ reasons: actionMatch.reasons,
683
+ poolId: facts.poolInputs[0]?.contractId,
684
+ poolStateInputIndex: facts.poolInputs[0]?.inputIndex,
685
+ poolStateOutputIndex: facts.poolOutput?.index,
686
+ poolStateAddress,
687
+ poolStateHash: poolStateAddress ? undefined : poolStateHash,
688
+ poolOutputIndex: facts.poolOutput?.index,
689
+ sender: sender.sender,
690
+ senderHash: sender.senderHash,
691
+ receiverHash: findPoolNftReceiverHash(action, facts),
692
+ ftTokenId: facts.tape?.ftContractTxid,
693
+ ftAPartialHash: poolNftFtAPartialHash(facts.tape),
694
+ ftAPartialHashSource: facts.tape?.ftAPartialHash ? 'poolNftTape' : undefined,
695
+ ftAPartialHashVerification: poolNftFtAPartialHashVerification(action, facts),
696
+ lpTokenId: poolNftLpTokenId(action, facts),
697
+ lpTokenIdSource: poolNftLpTokenIdSource(action, facts),
698
+ lpTokenIdEvidence: poolNftLpTokenIdEvidence(action, facts),
699
+ lpPartialHash: poolNftLpPartialHash(facts.tape),
700
+ lpPartialHashSource: facts.tape?.ftLpPartialHash ? 'poolNftTape' : undefined,
701
+ lpPartialHashVerification: poolNftLpPartialHashVerification(action, facts),
702
+ expectedFeeCollector,
703
+ ...(feeOutputs.length ? {
704
+ feeSatoshis,
705
+ feeCollector: firstFeeOutput?.address,
706
+ feeOutputIndex: firstFeeOutput?.outputIndex,
707
+ feeOutputs,
708
+ } : {}),
709
+ ...poolNftFtAmountFields(action, facts),
710
+ ...poolNftTbcAmountFields(action, facts),
711
+ ...(actionMatch.confidence === 'heuristic' ? { note: actionMatch.reasons.at(-1) } : {}),
712
+ };
713
+ }
714
+ function matchBurnFtLp(inputs, ftMovements) {
715
+ if (ftMovements.length !== 1 ||
716
+ inputs.length <= 1 ||
717
+ inputs.some((input) => Boolean(inputPoolNftTape(input))) ||
718
+ !isPoolNftBurnReceiver(ftMovements[0]?.receiver)) {
719
+ return undefined;
720
+ }
721
+ return createPoolNftActionMatch('burnFtLp', [
722
+ 'single FT movement is sent to a PoolNFT burn address',
723
+ 'transaction has multiple inputs and no PoolNFT tape movement',
724
+ ]);
725
+ }
726
+ export function collectPoolNftMovements(inputs, outputs, ftMovements, currentTxid) {
727
+ const poolNftTapeOutputs = outputs.filter((output) => output.script.data?.poolNft);
728
+ if (poolNftTapeOutputs.length > 1) {
729
+ return [];
730
+ }
731
+ // Collect primary movements from PoolNFT tape outputs, then detect standalone LP burns when no tape exists.
732
+ const tapeMovements = poolNftTapeOutputs
733
+ .map((output) => {
734
+ const facts = collectPoolNftFacts(inputs, outputs, ftMovements, output);
735
+ const tape = facts.tape;
736
+ if (!tape || !hasPoolStateCarrier(facts)) {
737
+ return undefined;
738
+ }
739
+ const firstFtMovement = facts.relatedFtMovements[0];
740
+ const actionMatch = inferPoolNftAction(facts);
741
+ if (shouldRejectPoolNftActionMatch(actionMatch, facts)) {
742
+ return undefined;
743
+ }
744
+ const action = actionMatch.action;
745
+ const ftDecimal = firstFtMovement?.decimal;
746
+ return {
747
+ action,
748
+ transactionType: poolNftTransactionType(action),
749
+ version: tape.version,
750
+ ...poolNftMovementMetadata(action, facts, actionMatch),
751
+ ...(action === 'create' && currentTxid ? { contractId: currentTxid, poolId: currentTxid } : {}),
752
+ tapeOutputIndex: output.index,
753
+ receiver: findPoolNftReceiver(action, facts),
754
+ ftName: firstFtMovement?.name,
755
+ ftSymbol: firstFtMovement?.symbol,
756
+ ftDecimal,
757
+ ftContractTxid: tape.ftContractTxid,
758
+ ftLpAmountRaw: tape.ftLpAmountRaw,
759
+ ftLpDisplayAmount: tape.ftLpDisplayAmount,
760
+ ftAAmountRaw: tape.ftAAmountRaw,
761
+ ftADisplayAmount: formatPoolNftFtAmount(tape.ftAAmountRaw, ftDecimal),
762
+ tbcSatoshis: tape.tbcSatoshis,
763
+ serviceFeeRate: tape.serviceFeeRate,
764
+ lpPlan: tape.lpPlan,
765
+ withLock: tape.withLock,
766
+ withLockTime: tape.withLockTime,
767
+ confidence: actionMatch.confidence,
768
+ };
769
+ })
770
+ .filter((movement) => Boolean(movement));
771
+ if (tapeMovements.length) {
772
+ return tapeMovements;
773
+ }
774
+ // An LP burn may contain only an FT movement and burn address with no new PoolNFT tape.
775
+ const burnMatch = matchBurnFtLp(inputs, ftMovements);
776
+ if (burnMatch) {
777
+ const movement = ftMovements[0];
778
+ const sender = poolNftSenderFromInputs(inputs);
779
+ return [
780
+ {
781
+ action: burnMatch.action,
782
+ transactionType: poolNftTransactionType(burnMatch.action),
783
+ sender: sender.sender,
784
+ senderHash: sender.senderHash,
785
+ receiver: movement.receiver,
786
+ ftName: movement.name,
787
+ ftSymbol: movement.symbol,
788
+ ftDecimal: movement.decimal,
789
+ lpTokenId: movement.contractId,
790
+ ftLpAmountRaw: movement.rawAmount,
791
+ ftLpDisplayAmount: movement.displayAmount,
792
+ ftLpInRawAmount: movement.rawAmount,
793
+ ftLpInOutputIndex: movement.outputIndex,
794
+ reasons: burnMatch.reasons,
795
+ confidence: burnMatch.confidence,
796
+ },
797
+ ];
798
+ }
799
+ return [];
800
+ }
801
+ export function inferPoolNftTransactionType(movements) {
802
+ return movements[0]?.transactionType;
803
+ }
804
+ //# sourceMappingURL=movements.js.map