tbc-tx-parser 0.1.7 → 0.1.9

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 (52) hide show
  1. package/README.md +65 -8
  2. package/dist/decode-tx-raw.d.ts.map +1 -1
  3. package/dist/decode-tx-raw.js +28 -5
  4. package/dist/decode-tx-raw.js.map +1 -1
  5. package/dist/index.d.ts +2 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +1 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/protocols/ft/index.d.ts +12 -0
  10. package/dist/protocols/ft/index.d.ts.map +1 -1
  11. package/dist/protocols/ft/index.js +142 -18
  12. package/dist/protocols/ft/index.js.map +1 -1
  13. package/dist/protocols/nft/index.d.ts.map +1 -1
  14. package/dist/protocols/nft/index.js +20 -23
  15. package/dist/protocols/nft/index.js.map +1 -1
  16. package/dist/protocols/order-book/settlement.d.ts.map +1 -1
  17. package/dist/protocols/order-book/settlement.js +4 -1
  18. package/dist/protocols/order-book/settlement.js.map +1 -1
  19. package/dist/protocols/pool-nft/constants.d.ts +2 -0
  20. package/dist/protocols/pool-nft/constants.d.ts.map +1 -1
  21. package/dist/protocols/pool-nft/constants.js +3 -0
  22. package/dist/protocols/pool-nft/constants.js.map +1 -1
  23. package/dist/protocols/pool-nft/fees.d.ts.map +1 -1
  24. package/dist/protocols/pool-nft/fees.js +4 -1
  25. package/dist/protocols/pool-nft/fees.js.map +1 -1
  26. package/dist/protocols/pool-nft/movements.d.ts.map +1 -1
  27. package/dist/protocols/pool-nft/movements.js +4 -1
  28. package/dist/protocols/pool-nft/movements.js.map +1 -1
  29. package/dist/render-transaction-description.d.ts +8 -0
  30. package/dist/render-transaction-description.d.ts.map +1 -0
  31. package/dist/render-transaction-description.js +32 -0
  32. package/dist/render-transaction-description.js.map +1 -0
  33. package/dist/result/classification-description.d.ts +11 -0
  34. package/dist/result/classification-description.d.ts.map +1 -0
  35. package/dist/result/classification-description.js +570 -0
  36. package/dist/result/classification-description.js.map +1 -0
  37. package/dist/result/classification.d.ts +20 -0
  38. package/dist/result/classification.d.ts.map +1 -0
  39. package/dist/result/classification.js +495 -0
  40. package/dist/result/classification.js.map +1 -0
  41. package/dist/result/index.d.ts +2 -1
  42. package/dist/result/index.d.ts.map +1 -1
  43. package/dist/result/index.js +22 -0
  44. package/dist/result/index.js.map +1 -1
  45. package/dist/script-parser/origin.d.ts.map +1 -1
  46. package/dist/script-parser/origin.js +16 -0
  47. package/dist/script-parser/origin.js.map +1 -1
  48. package/dist/types/result.d.ts +44 -0
  49. package/dist/types/result.d.ts.map +1 -1
  50. package/dist/types.d.ts +1 -1
  51. package/dist/types.d.ts.map +1 -1
  52. package/package.json +2 -2
@@ -0,0 +1,8 @@
1
+ import type { DecodeDisplayOptions, DecodeTxRawResult } from './types.js';
2
+ /**
3
+ * Render a transaction description from an existing public decode result.
4
+ * This is a pure display operation: it does not parse txraw, fetch prevouts,
5
+ * mutate the result, or reconsider protocol classification.
6
+ */
7
+ export declare function renderTransactionDescription(result: DecodeTxRawResult, options?: DecodeDisplayOptions): string;
8
+ //# sourceMappingURL=render-transaction-description.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-transaction-description.d.ts","sourceRoot":"","sources":["../src/render-transaction-description.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,oBAAoB,EACpB,iBAAiB,EAGlB,MAAM,YAAY,CAAC;AAkBpB;;;;GAIG;AACH,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,iBAAiB,EACzB,OAAO,GAAE,oBAAyB,UAmBnC"}
@@ -0,0 +1,32 @@
1
+ import { buildStandardClassificationDescription } from './result/classification-description.js';
2
+ function validateDisplayOptions(options) {
3
+ if (options?.tbcUnit !== undefined &&
4
+ options.tbcUnit !== 'satoshi' &&
5
+ options.tbcUnit !== 'tbc') {
6
+ throw new Error('Invalid display.tbcUnit; expected "satoshi" or "tbc"');
7
+ }
8
+ }
9
+ function classificationType(classification) {
10
+ if (classification.protocol === 'unknown')
11
+ return 'unknown';
12
+ return `${classification.protocol}.${classification.action}`;
13
+ }
14
+ /**
15
+ * Render a transaction description from an existing public decode result.
16
+ * This is a pure display operation: it does not parse txraw, fetch prevouts,
17
+ * mutate the result, or reconsider protocol classification.
18
+ */
19
+ export function renderTransactionDescription(result, options = {}) {
20
+ validateDisplayOptions(options);
21
+ if (result.classification.conformance !== 'standard') {
22
+ return result.classification.description;
23
+ }
24
+ return buildStandardClassificationDescription(classificationType(result.classification), result.classification.description, {
25
+ inputs: result.chain.inputs,
26
+ outputs: result.chain.outputs,
27
+ assetFlows: result.assetFlows,
28
+ feeSatoshis: result.chain.fee?.satoshis,
29
+ tbcUnit: options.tbcUnit,
30
+ });
31
+ }
32
+ //# sourceMappingURL=render-transaction-description.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render-transaction-description.js","sourceRoot":"","sources":["../src/render-transaction-description.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,sCAAsC,EAAE,MAAM,wCAAwC,CAAC;AAEhG,SAAS,sBAAsB,CAAC,OAAyC;IACvE,IACE,OAAO,EAAE,OAAO,KAAK,SAAS;QAC9B,OAAO,CAAC,OAAO,KAAK,SAAS;QAC7B,OAAO,CAAC,OAAO,KAAK,KAAK,EACzB,CAAC;QACD,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC;IAC1E,CAAC;AACH,CAAC;AAED,SAAS,kBAAkB,CAAC,cAAyC;IACnE,IAAI,cAAc,CAAC,QAAQ,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IAC5D,OAAO,GAAG,cAAc,CAAC,QAAQ,IAAI,cAAc,CAAC,MAAM,EAAqB,CAAC;AAClF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,4BAA4B,CAC1C,MAAyB,EACzB,UAAgC,EAAE;IAElC,sBAAsB,CAAC,OAAO,CAAC,CAAC;IAEhC,IAAI,MAAM,CAAC,cAAc,CAAC,WAAW,KAAK,UAAU,EAAE,CAAC;QACrD,OAAO,MAAM,CAAC,cAAc,CAAC,WAAW,CAAC;IAC3C,CAAC;IAED,OAAO,sCAAsC,CAC3C,kBAAkB,CAAC,MAAM,CAAC,cAAc,CAAC,EACzC,MAAM,CAAC,cAAc,CAAC,WAAW,EACjC;QACE,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,MAAM;QAC3B,OAAO,EAAE,MAAM,CAAC,KAAK,CAAC,OAAO;QAC7B,UAAU,EAAE,MAAM,CAAC,UAAU;QAC7B,WAAW,EAAE,MAAM,CAAC,KAAK,CAAC,GAAG,EAAE,QAAQ;QACvC,OAAO,EAAE,OAAO,CAAC,OAAO;KACzB,CACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { AssetFlow, DecodedInput, DecodedOutput, ProtocolDetails, PublicDecodedInput, PublicDecodedOutput, TbcDisplayUnit, TransactionType } from '../types.js';
2
+ export interface ClassificationDescriptionFacts {
3
+ assetFlows?: AssetFlow[];
4
+ inputs: Array<DecodedInput | PublicDecodedInput>;
5
+ outputs: Array<DecodedOutput | PublicDecodedOutput>;
6
+ feeSatoshis?: number;
7
+ tbcUnit?: TbcDisplayUnit;
8
+ protocolDetails?: ProtocolDetails;
9
+ }
10
+ export declare function buildStandardClassificationDescription(type: TransactionType, fallback: string, facts: ClassificationDescriptionFacts): string;
11
+ //# sourceMappingURL=classification-description.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"classification-description.d.ts","sourceRoot":"","sources":["../../src/result/classification-description.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,SAAS,EACT,YAAY,EACZ,aAAa,EACb,eAAe,EACf,kBAAkB,EAClB,mBAAmB,EACnB,cAAc,EACd,eAAe,EAChB,MAAM,aAAa,CAAC;AAKrB,MAAM,WAAW,8BAA8B;IAC7C,UAAU,CAAC,EAAE,SAAS,EAAE,CAAC;IACzB,MAAM,EAAE,KAAK,CAAC,YAAY,GAAG,kBAAkB,CAAC,CAAC;IACjD,OAAO,EAAE,KAAK,CAAC,aAAa,GAAG,mBAAmB,CAAC,CAAC;IACpD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB,eAAe,CAAC,EAAE,eAAe,CAAC;CACnC;AAyoBD,wBAAgB,sCAAsC,CACpD,IAAI,EAAE,eAAe,EACrB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,8BAA8B,UAqBtC"}
@@ -0,0 +1,570 @@
1
+ import { SATOSHIS_PER_TBC } from '../constants/chain.js';
2
+ import { getMultiSigAddress, getSignatureAndPublicKeyCount } from '../utils/multi-sig.js';
3
+ import { formatTokenRawAmount } from '../utils/token-amount.js';
4
+ function commaInteger(value) {
5
+ const [integer = '0', decimal] = value.split('.');
6
+ const grouped = integer.replace(/\B(?=(\d{3})+(?!\d))/gu, ',');
7
+ return decimal ? `${grouped}.${decimal}` : grouped;
8
+ }
9
+ function trimDecimal(value) {
10
+ return value.includes('.') ? value.replace(/0+$/u, '').replace(/\.$/u, '') : value;
11
+ }
12
+ function formatNativeAmount(satoshis, unit = 'satoshi', fixedTbcDecimals = false) {
13
+ if (unit === 'satoshi') {
14
+ return `${commaInteger(satoshis.toString())} ${satoshis === 1n ? 'satoshi' : 'satoshis'}`;
15
+ }
16
+ const negative = satoshis < 0n;
17
+ const absolute = negative ? -satoshis : satoshis;
18
+ const scale = BigInt(SATOSHIS_PER_TBC);
19
+ const integer = absolute / scale;
20
+ const fraction = (absolute % scale).toString().padStart(6, '0');
21
+ const displayedFraction = fixedTbcDecimals ? fraction : fraction.replace(/0+$/u, '');
22
+ const unsigned = displayedFraction ? `${integer}.${displayedFraction}` : integer.toString();
23
+ return `${commaInteger(`${negative ? '-' : ''}${unsigned}`)} TBC`;
24
+ }
25
+ function cleanLabel(value, fallback) {
26
+ const normalized = value?.replace(/\s+/gu, ' ').trim();
27
+ if (!normalized)
28
+ return fallback;
29
+ return normalized.length > 40 ? `${normalized.slice(0, 37)}...` : normalized;
30
+ }
31
+ function amountKey(flow) {
32
+ return [
33
+ flow.asset.type,
34
+ 'contractId' in flow.asset ? flow.asset.contractId : undefined,
35
+ 'collectionId' in flow.asset ? flow.asset.collectionId : undefined,
36
+ 'symbol' in flow.asset ? flow.asset.symbol : undefined,
37
+ 'decimal' in flow.asset ? flow.asset.decimal : undefined,
38
+ ].join('|');
39
+ }
40
+ function summarizeAmounts(flows, tbcUnit = 'satoshi') {
41
+ const groups = new Map();
42
+ flows.forEach((flow) => {
43
+ if (!('amount' in flow) || !flow.amount)
44
+ return;
45
+ const key = amountKey(flow);
46
+ groups.set(key, [...(groups.get(key) ?? []), flow]);
47
+ });
48
+ return [...groups.values()].map((group) => {
49
+ const first = group[0];
50
+ if (first.asset.type === 'tbc') {
51
+ const satoshis = group.reduce((total, flow) => total + BigInt('amount' in flow && flow.amount && 'satoshis' in flow.amount ? flow.amount.satoshis : 0), 0n);
52
+ return {
53
+ assetType: 'tbc',
54
+ symbol: 'TBC',
55
+ text: formatNativeAmount(satoshis, tbcUnit),
56
+ };
57
+ }
58
+ const raw = group.reduce((total, flow) => total + BigInt('amount' in flow && flow.amount && 'raw' in flow.amount ? flow.amount.raw : 0), 0n);
59
+ const decimal = 'decimal' in first.asset
60
+ ? first.asset.decimal ?? ('amount' in first && first.amount && 'decimal' in first.amount ? first.amount.decimal : 0)
61
+ : 0;
62
+ const display = trimDecimal(formatTokenRawAmount(raw, decimal ?? 0));
63
+ const symbol = cleanLabel('symbol' in first.asset ? first.asset.symbol : undefined, 'tokens');
64
+ return {
65
+ assetType: first.asset.type,
66
+ symbol,
67
+ text: `${commaInteger(display)} ${symbol}${first.asset.type === 'ftlp' ? ' liquidity tokens' : ''}`,
68
+ };
69
+ });
70
+ }
71
+ function amountText(flows, fallback, tbcUnit = 'satoshi') {
72
+ const summaries = summarizeAmounts(flows, tbcUnit);
73
+ if (!summaries.length)
74
+ return fallback;
75
+ if (summaries.length === 1)
76
+ return summaries[0].text;
77
+ if (summaries.length === 2)
78
+ return `${summaries[0].text} and ${summaries[1].text}`;
79
+ return `${summaries.slice(0, -1).map((item) => item.text).join(', ')}, and ${summaries.at(-1).text}`;
80
+ }
81
+ function isMultisigAddress(address, facts) {
82
+ return facts.outputs.some((output) => output.address === address &&
83
+ (output.script.type === 'MULTISIG' || ('scriptKind' in output && output.scriptKind === 'multisig'))) || facts.inputs.some((input) => input.prevout?.address === address && input.prevout.script.type === 'MULTISIG');
84
+ }
85
+ function addressIdentity(address, facts) {
86
+ return {
87
+ key: `address:${address}`,
88
+ kind: isMultisigAddress(address, facts) ? 'multisig' : 'address',
89
+ value: address,
90
+ };
91
+ }
92
+ function endpointIdentities(endpoint, facts) {
93
+ if (!endpoint)
94
+ return [];
95
+ if (endpoint.poolId)
96
+ return [{ key: `pool:${endpoint.poolId}`, kind: 'pool', value: endpoint.poolId }];
97
+ if (endpoint.address)
98
+ return [addressIdentity(endpoint.address, facts)];
99
+ if (endpoint.addresses?.length) {
100
+ return [...new Set(endpoint.addresses)].map((address) => addressIdentity(address, facts));
101
+ }
102
+ if (endpoint.hash)
103
+ return [{ key: `hash:${endpoint.hash}`, kind: 'hash', value: endpoint.hash }];
104
+ if (endpoint.txid !== undefined || endpoint.vout !== undefined) {
105
+ return [{ key: `outpoint:${endpoint.txid ?? ''}:${endpoint.vout ?? ''}`, kind: 'outpoint' }];
106
+ }
107
+ if (endpoint.outpoints?.length) {
108
+ return endpoint.outpoints.map((outpoint) => ({
109
+ key: `outpoint:${outpoint.txid}:${outpoint.vout}`,
110
+ kind: 'outpoint',
111
+ }));
112
+ }
113
+ if (endpoint.script?.hex || endpoint.script?.asm) {
114
+ return [{ key: `script:${endpoint.script.hex ?? endpoint.script.asm}`, kind: 'script' }];
115
+ }
116
+ return [];
117
+ }
118
+ function summarizeDestinations(flows, facts, direction = 'to') {
119
+ const identities = new Map();
120
+ let complete = flows.length > 0;
121
+ for (const flow of flows) {
122
+ const endpoint = direction === 'to' ? flow.to : flow.from;
123
+ const values = endpointIdentities(endpoint, facts);
124
+ if (!values.length)
125
+ complete = false;
126
+ values.forEach((value) => identities.set(value.key, value));
127
+ }
128
+ return { complete, identities: [...identities.values()] };
129
+ }
130
+ function destinationGroupPhrase(kind, count, other) {
131
+ if (kind === 'address') {
132
+ if (count === 1)
133
+ return other ? 'another address' : 'an address';
134
+ return `${count} ${other ? 'other ' : ''}addresses`;
135
+ }
136
+ if (kind === 'multisig') {
137
+ if (count === 1)
138
+ return 'a multisignature wallet';
139
+ return `${count} multisignature wallets`;
140
+ }
141
+ if (kind === 'pool') {
142
+ if (count === 1)
143
+ return 'a liquidity pool';
144
+ return `${count} liquidity pools`;
145
+ }
146
+ if (count === 1)
147
+ return other ? 'another destination' : 'a destination';
148
+ return `${count} ${other ? 'other ' : ''}destinations`;
149
+ }
150
+ function singleDestinationPhrase(identity) {
151
+ if (identity.kind === 'address' && identity.value)
152
+ return `address ${identity.value}`;
153
+ if (identity.kind === 'multisig' && identity.value)
154
+ return `multisignature wallet ${identity.value}`;
155
+ if (identity.kind === 'pool' && identity.value)
156
+ return `liquidity pool ${identity.value}`;
157
+ if (identity.kind === 'hash' && identity.value)
158
+ return `destination hash ${identity.value}`;
159
+ if (identity.kind === 'script')
160
+ return 'an unrecognized script destination';
161
+ return 'a destination';
162
+ }
163
+ function joinPhrases(values) {
164
+ if (values.length <= 1)
165
+ return values[0] ?? '';
166
+ if (values.length === 2)
167
+ return `${values[0]} and ${values[1]}`;
168
+ return `${values.slice(0, -1).join(', ')}, and ${values.at(-1)}`;
169
+ }
170
+ function destinationPhrase(summary, other) {
171
+ if (!summary.complete || !summary.identities.length) {
172
+ return other ? 'other destinations' : 'destinations';
173
+ }
174
+ if (summary.identities.length === 1) {
175
+ return singleDestinationPhrase(summary.identities[0]);
176
+ }
177
+ const groups = new Map();
178
+ summary.identities.forEach((identity) => groups.set(identity.kind, (groups.get(identity.kind) ?? 0) + 1));
179
+ if (groups.has('hash') || groups.has('script') || groups.has('outpoint')) {
180
+ return destinationGroupPhrase('outpoint', summary.identities.length, other);
181
+ }
182
+ const phrase = joinPhrases([...groups.entries()].map(([kind, count]) => destinationGroupPhrase(kind, count, other)));
183
+ return groups.size > 1 ? `${summary.identities.length} destinations: ${phrase}` : phrase;
184
+ }
185
+ function sendClause(flows, facts, fallback, origin) {
186
+ const destinations = summarizeDestinations(flows, facts);
187
+ const destination = destinationPhrase(destinations, true);
188
+ const originClause = origin ? ` ${origin}` : '';
189
+ const amount = amountText(flows, fallback, facts.tbcUnit);
190
+ if (destinations.complete && destinations.identities.length > 1) {
191
+ return `Sends a total of ${amount}${originClause} to ${destination}`;
192
+ }
193
+ return `Sends ${amount}${originClause} to ${destination}`;
194
+ }
195
+ function actionToDestinationClause(action, flows, facts, fallback, other = false) {
196
+ const destinations = summarizeDestinations(flows, facts);
197
+ const amount = amountText(flows, fallback, facts.tbcUnit);
198
+ const total = destinations.complete && destinations.identities.length > 1 ? 'a total of ' : '';
199
+ return `${action} ${total}${amount} to ${destinationPhrase(destinations, other)}`;
200
+ }
201
+ function internalTransferClauses(flows, facts) {
202
+ const sources = summarizeDestinations(flows, facts, 'from');
203
+ const targets = summarizeDestinations(flows, facts);
204
+ const scope = sources.complete && sources.identities.length ? sources : targets;
205
+ const kinds = new Set(scope.identities.map((identity) => identity.kind));
206
+ const count = scope.identities.length;
207
+ if (scope.complete && count === 1) {
208
+ const identity = scope.identities[0];
209
+ if (identity.kind === 'address' && identity.value) {
210
+ return [`Internal transfer at address ${identity.value}`, 'no funds were sent to another address'];
211
+ }
212
+ if (identity.kind === 'multisig' && identity.value) {
213
+ return [`Internal transfer in multisignature wallet ${identity.value}`, 'no funds were sent elsewhere'];
214
+ }
215
+ if (identity.kind === 'pool' && identity.value) {
216
+ return [`Internal transfer in liquidity pool ${identity.value}`, 'no funds were sent elsewhere'];
217
+ }
218
+ if (identity.kind === 'hash' && identity.value) {
219
+ return [`Internal transfer at destination hash ${identity.value}`, 'no funds were sent elsewhere'];
220
+ }
221
+ return ['Internal transfer at the original destination', 'no funds were sent elsewhere'];
222
+ }
223
+ if (scope.complete && count > 1 && kinds.size === 1 && kinds.has('address')) {
224
+ return ['Transfer within the original addresses', 'no funds were sent outside those addresses'];
225
+ }
226
+ if (scope.complete && count > 1 && kinds.size === 1 && kinds.has('multisig')) {
227
+ return ['Transfer within the original multisignature wallets', 'no funds were sent outside those wallets'];
228
+ }
229
+ return ['Transfer within the original destinations', 'no funds were sent outside those destinations'];
230
+ }
231
+ function feeClause(facts) {
232
+ if (facts.feeSatoshis === undefined)
233
+ return undefined;
234
+ return `network fee: ${formatNativeAmount(BigInt(facts.feeSatoshis), facts.tbcUnit, true)}`;
235
+ }
236
+ function sentence(clauses) {
237
+ const values = clauses.filter((value) => Boolean(value));
238
+ if (!values.length)
239
+ return '';
240
+ return `${values.join('; ')}.`;
241
+ }
242
+ function flowsFor(flows, purpose, assetTypes) {
243
+ return flows.filter((flow) => flow.purpose === purpose && (!assetTypes || assetTypes.includes(flow.asset.type)));
244
+ }
245
+ function scriptAsmContains(asm, value) {
246
+ if (asm === undefined)
247
+ return false;
248
+ const searchable = typeof asm === 'string' ? asm : `${asm.prefix} ${asm.suffix}`;
249
+ return typeof value === 'string' ? searchable.includes(value) : value.test(searchable);
250
+ }
251
+ function poolFlows(flows, direction, assetTypes) {
252
+ return flows.filter((flow) => {
253
+ const endpoint = direction === 'to' ? flow.to : flow.from;
254
+ return Boolean(endpoint?.poolId) && (!assetTypes || assetTypes.includes(flow.asset.type));
255
+ });
256
+ }
257
+ function ftCodeInputCount(inputs) {
258
+ return inputs.reduce((count, input) => {
259
+ if (!input.prevout?.associatedTape?.script.data?.ftTape)
260
+ return count;
261
+ return count + ('compression' in input ? input.compression?.count ?? 1 : 1);
262
+ }, 0);
263
+ }
264
+ function materialSecondaryTbcFlows(facts) {
265
+ return flowsFor(facts.assetFlows ?? [], 'transfer', ['tbc']).filter((flow) => {
266
+ const address = flow.to?.address;
267
+ const satoshis = 'amount' in flow && flow.amount && 'satoshis' in flow.amount
268
+ ? flow.amount.satoshis
269
+ : undefined;
270
+ if (!address || satoshis === undefined)
271
+ return false;
272
+ return facts.outputs.some((output) => output.address === address &&
273
+ output.satoshis === satoshis &&
274
+ !scriptAsmContains(output.script.asm, 'OP_RETURN'));
275
+ });
276
+ }
277
+ function secondaryTbcClause(facts) {
278
+ const transfers = materialSecondaryTbcFlows(facts);
279
+ if (!transfers.length)
280
+ return undefined;
281
+ const description = sendClause(transfers, facts, 'native TBC');
282
+ return `also ${description.charAt(0).toLowerCase()}${description.slice(1)}`;
283
+ }
284
+ function describeNativeTransfer(facts) {
285
+ const flows = facts.assetFlows ?? [];
286
+ const transfers = flowsFor(flows, 'transfer', ['tbc']);
287
+ const self = flowsFor(flows, 'self', ['tbc']);
288
+ if (!transfers.length && self.length) {
289
+ return sentence([
290
+ ...internalTransferClauses(self, facts),
291
+ feeClause(facts),
292
+ ]);
293
+ }
294
+ return sentence([
295
+ sendClause(transfers, facts, 'native TBC'),
296
+ feeClause(facts),
297
+ ]);
298
+ }
299
+ function describeFungible(type, facts, assetType) {
300
+ const flows = facts.assetFlows ?? [];
301
+ const tokenFlows = flows.filter((flow) => flow.asset.type === assetType);
302
+ const mint = flowsFor(tokenFlows, 'mint');
303
+ const transfers = flowsFor(tokenFlows, 'transfer');
304
+ const self = flowsFor(tokenFlows, 'self');
305
+ const fallback = `${assetType === 'stablecoin' ? 'StableCoin' : 'FT'} tokens`;
306
+ if (type.endsWith('.mint')) {
307
+ return sentence([
308
+ actionToDestinationClause('Mints', mint, facts, fallback),
309
+ secondaryTbcClause(facts),
310
+ feeClause(facts),
311
+ ]);
312
+ }
313
+ if (type.endsWith('.merge')) {
314
+ const inputCount = ftCodeInputCount(facts.inputs);
315
+ return sentence([
316
+ `Combines ${inputCount || 'multiple'} token balances into a single balance of ${amountText(self, fallback, facts.tbcUnit)}`,
317
+ secondaryTbcClause(facts),
318
+ feeClause(facts),
319
+ ]);
320
+ }
321
+ return sentence([
322
+ sendClause(transfers, facts, fallback),
323
+ secondaryTbcClause(facts),
324
+ feeClause(facts),
325
+ ]);
326
+ }
327
+ function nftLabel(flow) {
328
+ if (!flow || (flow.asset.type !== 'nft' && flow.asset.type !== 'nftCollection'))
329
+ return 'NFT';
330
+ return `${cleanLabel(flow.asset.name ?? flow.asset.symbol, 'unnamed')} NFT`;
331
+ }
332
+ function nftAssetName(flow) {
333
+ if (!flow || (flow.asset.type !== 'nft' && flow.asset.type !== 'nftCollection'))
334
+ return undefined;
335
+ return flow.asset.name;
336
+ }
337
+ function describeNft(type, facts) {
338
+ const flows = facts.assetFlows ?? [];
339
+ const nftFlows = flows.filter((flow) => flow.asset.type === 'nft' || flow.asset.type === 'nftCollection');
340
+ const primary = nftFlows[0];
341
+ if (type === 'nft.collectionMint') {
342
+ const movement = facts.protocolDetails?.nft?.actions.find((action) => action.action === 'mintCollection');
343
+ const collectionTape = facts.outputs
344
+ .map((output) => output.script.data?.collectionTape)
345
+ .find((tape) => tape !== undefined);
346
+ const slots = movement?.action === 'mintCollection' ? movement.supply : collectionTape?.supply;
347
+ const collectionName = movement?.action === 'mintCollection'
348
+ ? movement.collectionName
349
+ : collectionTape?.collectionName ?? nftAssetName(primary);
350
+ return sentence([
351
+ `Creates the ${cleanLabel(collectionName, 'unnamed')} NFT collection${slots === undefined ? '' : ` with ${commaInteger(String(slots))} mint slots`}`,
352
+ secondaryTbcClause(facts),
353
+ feeClause(facts),
354
+ ]);
355
+ }
356
+ if (type === 'nft.mint') {
357
+ return sentence([
358
+ `Mints the ${nftLabel(primary)} to ${destinationPhrase(summarizeDestinations(nftFlows, facts), false)}`,
359
+ secondaryTbcClause(facts),
360
+ feeClause(facts),
361
+ ]);
362
+ }
363
+ return sentence([
364
+ `Transfers the ${nftLabel(primary)} to ${destinationPhrase(summarizeDestinations(primary ? [primary] : [], facts), true)}`,
365
+ secondaryTbcClause(facts),
366
+ feeClause(facts),
367
+ ]);
368
+ }
369
+ function parseMultiSigTape(value) {
370
+ if (!value || value.length % 2 !== 0 || !/^[0-9a-f]+$/iu.test(value))
371
+ return undefined;
372
+ try {
373
+ const parsed = JSON.parse(Buffer.from(value, 'hex').toString('utf8'));
374
+ if (!parsed || typeof parsed !== 'object')
375
+ return undefined;
376
+ const candidate = parsed;
377
+ if (typeof candidate.address !== 'string' || !Array.isArray(candidate.pubkeys))
378
+ return undefined;
379
+ if (!candidate.pubkeys.every((pubkey) => typeof pubkey === 'string' && /^(?:02|03)[0-9a-f]{64}$/iu.test(pubkey))) {
380
+ return undefined;
381
+ }
382
+ return { address: candidate.address, pubkeys: candidate.pubkeys };
383
+ }
384
+ catch {
385
+ return undefined;
386
+ }
387
+ }
388
+ function multisigCreationClause(transfers, facts) {
389
+ const outputs = facts.outputs.filter((output) => output.script.type === 'MULTISIG' && typeof output.address === 'string');
390
+ const tapeOutputs = facts.outputs.filter((output) => output.script.data?.codeType === 'MTape');
391
+ if (outputs.length !== 1 || tapeOutputs.length !== 1)
392
+ return undefined;
393
+ const output = outputs[0];
394
+ const tape = parseMultiSigTape(tapeOutputs[0].script.data?.publicKeyHash);
395
+ if (!tape || tape.address !== output.address)
396
+ return undefined;
397
+ try {
398
+ const { signatureCount, publicKeyCount } = getSignatureAndPublicKeyCount(tape.address);
399
+ if (publicKeyCount !== tape.pubkeys.length)
400
+ return undefined;
401
+ if (getMultiSigAddress(tape.pubkeys, signatureCount, publicKeyCount) !== tape.address)
402
+ return undefined;
403
+ const participantMarkerCount = facts.outputs.filter((candidate) => candidate.script.type === 'P2PKH' &&
404
+ scriptAsmContains(candidate.script.asm, /(?:^|\s)OP_RETURN\s+6d756c7469736967(?:\s|$)/iu)).length;
405
+ if (participantMarkerCount !== publicKeyCount)
406
+ return undefined;
407
+ const funding = transfers.filter((flow) => flow.to?.address === tape.address || flow.to?.addresses?.includes(tape.address));
408
+ if (!funding.length)
409
+ return undefined;
410
+ return `Creates and funds a ${signatureCount}-of-${publicKeyCount} multisignature wallet ${tape.address} with ${amountText(funding, 'native TBC', facts.tbcUnit)}`;
411
+ }
412
+ catch {
413
+ return undefined;
414
+ }
415
+ }
416
+ function describeMultisig(facts) {
417
+ const flows = facts.assetFlows ?? [];
418
+ const transfers = flowsFor(flows, 'transfer', ['tbc']);
419
+ const spendsMultisig = facts.inputs.some((input) => input.prevout?.script.type === 'MULTISIG');
420
+ const creation = multisigCreationClause(transfers, facts);
421
+ const source = summarizeDestinations(transfers, facts, 'from');
422
+ return sentence([
423
+ creation ?? sendClause(transfers, facts, 'native TBC', spendsMultisig ? `from ${destinationPhrase(source, false)}` : undefined),
424
+ feeClause(facts),
425
+ ]);
426
+ }
427
+ function describePiggyBank(type, facts) {
428
+ const flows = facts.assetFlows ?? [];
429
+ const movement = facts.protocolDetails?.piggyBank?.actions[0];
430
+ if (type === 'piggyBank.freeze') {
431
+ const locked = flowsFor(flows, 'lock', ['tbc']);
432
+ const publicPiggyBank = facts.outputs
433
+ .map((output) => output.script.data?.piggyBank)
434
+ .find((piggyBank) => piggyBank !== undefined);
435
+ const lockTime = movement?.lockTime ?? publicPiggyBank?.lockTime;
436
+ return sentence([
437
+ `Locks ${amountText(locked, 'native TBC', facts.tbcUnit)} in PiggyBank${lockTime === undefined ? '' : ` until block ${commaInteger(String(lockTime))}`}`,
438
+ feeClause(facts),
439
+ ]);
440
+ }
441
+ const released = flowsFor(flows, 'release', ['tbc']);
442
+ return sentence([
443
+ `Withdraws ${amountText(released, 'native TBC', facts.tbcUnit)} from PiggyBank after maturity`,
444
+ feeClause(facts),
445
+ ]);
446
+ }
447
+ function exchangeAmountText(flows, facts) {
448
+ const summaries = summarizeAmounts(flows, facts.tbcUnit);
449
+ if (summaries.length === 2)
450
+ return `${summaries[0].text} for ${summaries[1].text}`;
451
+ return amountText(flows, 'the traded assets', facts.tbcUnit);
452
+ }
453
+ function describeOrderBook(type, facts) {
454
+ const flows = facts.assetFlows ?? [];
455
+ const locked = flowsFor(flows, 'lock');
456
+ const released = flowsFor(flows, 'release');
457
+ const transferred = flowsFor(flows, 'transfer');
458
+ if (type === 'orderBook.placeBuy' || type === 'orderBook.placeSell') {
459
+ return sentence([
460
+ `Places a ${type === 'orderBook.placeBuy' ? 'buy' : 'sell'} order using ${amountText(locked, 'the required asset', facts.tbcUnit)}`,
461
+ feeClause(facts),
462
+ ]);
463
+ }
464
+ if (type === 'orderBook.cancelBuy' || type === 'orderBook.cancelSell') {
465
+ return sentence([
466
+ `Cancels a ${type === 'orderBook.cancelBuy' ? 'buy' : 'sell'} order and releases ${amountText(released, 'the locked asset', facts.tbcUnit)}`,
467
+ feeClause(facts),
468
+ ]);
469
+ }
470
+ if (type === 'orderBook.match') {
471
+ return sentence([
472
+ `Matches a buy and sell order, exchanging ${exchangeAmountText(transferred, facts)}`,
473
+ feeClause(facts),
474
+ ]);
475
+ }
476
+ return sentence([
477
+ `Updates an order while keeping ${amountText(locked, 'its asset', facts.tbcUnit)} locked`,
478
+ feeClause(facts),
479
+ ]);
480
+ }
481
+ function describePoolNft(type, facts) {
482
+ const flows = facts.assetFlows ?? [];
483
+ const intoPool = poolFlows(flows, 'to');
484
+ const fromPool = poolFlows(flows, 'from');
485
+ const lpMinted = flowsFor(flows, 'mint', ['ftlp']);
486
+ const lpSpent = intoPool.filter((flow) => flow.asset.type === 'ftlp');
487
+ const selfTokens = flowsFor(flows, 'self', ['ft', 'ftlp']);
488
+ const burned = flowsFor(flows, 'burn', ['ftlp']);
489
+ const sentToBurnAddress = flowsFor(flows, 'transfer', ['ftlp']);
490
+ const createdPoolFlow = flows.find((flow) => flow.asset.type === 'nft' && flow.asset.subtype === 'poolNft');
491
+ const createdPoolId = createdPoolFlow?.asset.type === 'nft' && createdPoolFlow.asset.subtype === 'poolNft'
492
+ ? createdPoolFlow.asset.contractId
493
+ : undefined;
494
+ const poolTarget = intoPool.length
495
+ ? destinationPhrase(summarizeDestinations(intoPool, facts), false)
496
+ : fromPool.length
497
+ ? destinationPhrase(summarizeDestinations(fromPool, facts, 'from'), false)
498
+ : 'a liquidity pool';
499
+ switch (type) {
500
+ case 'poolNft.init':
501
+ return sentence([`Starts ${poolTarget} with ${amountText(intoPool, 'its starting assets', facts.tbcUnit)}`, feeClause(facts)]);
502
+ case 'poolNft.increaseLiquidity':
503
+ return sentence([
504
+ `Adds ${amountText(intoPool.filter((flow) => flow.asset.type !== 'ftlp'), 'assets', facts.tbcUnit)} to ${poolTarget}`,
505
+ lpMinted.length ? `receives ${amountText(lpMinted, 'liquidity tokens', facts.tbcUnit)}` : undefined,
506
+ feeClause(facts),
507
+ ]);
508
+ case 'poolNft.consumeLiquidity':
509
+ return sentence([
510
+ `Removes liquidity from ${poolTarget} by redeeming ${amountText(lpSpent, 'liquidity tokens', facts.tbcUnit)}`,
511
+ `receives ${amountText(fromPool.filter((flow) => flow.asset.type !== 'ftlp'), 'the underlying assets', facts.tbcUnit)}`,
512
+ feeClause(facts),
513
+ ]);
514
+ case 'poolNft.swapTbcToFt':
515
+ return sentence([
516
+ `Swaps ${amountText(intoPool.filter((flow) => flow.asset.type === 'tbc'), 'native TBC', facts.tbcUnit)} for ${amountText(fromPool.filter((flow) => flow.asset.type === 'ft'), 'FT tokens', facts.tbcUnit)} in ${poolTarget}`,
517
+ feeClause(facts),
518
+ ]);
519
+ case 'poolNft.swapFtToTbc':
520
+ return sentence([
521
+ `Swaps ${amountText(intoPool.filter((flow) => flow.asset.type === 'ft'), 'FT tokens', facts.tbcUnit)} for ${amountText(fromPool.filter((flow) => flow.asset.type === 'tbc'), 'native TBC', facts.tbcUnit)} in ${poolTarget}`,
522
+ feeClause(facts),
523
+ ]);
524
+ case 'poolNft.mergeFtInPool':
525
+ return sentence([`Combines token balances held by ${poolTarget} into ${amountText(selfTokens, 'FT tokens', facts.tbcUnit)}`, feeClause(facts)]);
526
+ case 'poolNft.mergeFtLp':
527
+ return sentence([`Combines liquidity-token balances into ${amountText(selfTokens, 'liquidity tokens', facts.tbcUnit)}`, feeClause(facts)]);
528
+ case 'poolNft.burnFtLp':
529
+ return sentence([
530
+ `Burns ${amountText(burned.length ? burned : sentToBurnAddress.length ? sentToBurnAddress : lpSpent, 'liquidity tokens', facts.tbcUnit)}`,
531
+ feeClause(facts),
532
+ ]);
533
+ default:
534
+ return sentence([
535
+ createdPoolId ? `Creates liquidity pool ${createdPoolId}` : 'Creates a liquidity pool',
536
+ feeClause(facts),
537
+ ]);
538
+ }
539
+ }
540
+ export function buildStandardClassificationDescription(type, fallback, facts) {
541
+ if (!facts.assetFlows?.length && !type.startsWith('poolNft.'))
542
+ return fallback;
543
+ if (type === 'tbc.transfer')
544
+ return describeNativeTransfer(facts);
545
+ if (type === 'tbc.coinbase') {
546
+ const minted = flowsFor(facts.assetFlows ?? [], 'mint', ['tbc']);
547
+ return sentence([
548
+ `Creates ${amountText(minted, 'native TBC', facts.tbcUnit)} as block rewards for ${destinationPhrase(summarizeDestinations(minted, facts), false)}`,
549
+ ]);
550
+ }
551
+ if (type === 'tbc.multisig')
552
+ return describeMultisig(facts);
553
+ if (type.startsWith('ft.'))
554
+ return describeFungible(type, facts, 'ft');
555
+ if (type.startsWith('stablecoin.') && type !== 'stablecoin.bootstrap')
556
+ return describeFungible(type, facts, 'stablecoin');
557
+ if (type === 'stablecoin.bootstrap') {
558
+ return sentence(['Creates the certificate used to issue a StableCoin', feeClause(facts)]);
559
+ }
560
+ if (type.startsWith('nft.'))
561
+ return describeNft(type, facts);
562
+ if (type.startsWith('piggyBank.'))
563
+ return describePiggyBank(type, facts);
564
+ if (type.startsWith('orderBook.'))
565
+ return describeOrderBook(type, facts);
566
+ if (type.startsWith('poolNft.'))
567
+ return describePoolNft(type, facts);
568
+ return fallback;
569
+ }
570
+ //# sourceMappingURL=classification-description.js.map