bitbadgesjs-sdk 0.31.2 → 0.31.3

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 (87) hide show
  1. package/dist/cjs/api-indexer/BitBadgesApi.d.ts +4 -2
  2. package/dist/cjs/api-indexer/BitBadgesApi.d.ts.map +1 -1
  3. package/dist/cjs/api-indexer/BitBadgesApi.js +5 -3
  4. package/dist/cjs/api-indexer/BitBadgesApi.js.map +1 -1
  5. package/dist/cjs/api-indexer/BitBadgesCollection.d.ts +3 -2
  6. package/dist/cjs/api-indexer/BitBadgesCollection.d.ts.map +1 -1
  7. package/dist/cjs/api-indexer/BitBadgesCollection.js +7 -0
  8. package/dist/cjs/api-indexer/BitBadgesCollection.js.map +1 -1
  9. package/dist/cjs/core/balances.d.ts +1 -0
  10. package/dist/cjs/core/balances.d.ts.map +1 -1
  11. package/dist/cjs/core/balances.js +6 -1
  12. package/dist/cjs/core/balances.js.map +1 -1
  13. package/dist/cjs/core/index.d.ts +2 -0
  14. package/dist/cjs/core/index.d.ts.map +1 -1
  15. package/dist/cjs/core/index.js +2 -0
  16. package/dist/cjs/core/index.js.map +1 -1
  17. package/dist/cjs/core/interpret.d.ts +9 -0
  18. package/dist/cjs/core/interpret.d.ts.map +1 -0
  19. package/dist/cjs/core/interpret.js +1321 -0
  20. package/dist/cjs/core/interpret.js.map +1 -0
  21. package/dist/cjs/core/interpret.spec.d.ts +2 -0
  22. package/dist/cjs/core/interpret.spec.d.ts.map +1 -0
  23. package/dist/cjs/core/interpret.spec.js +439 -0
  24. package/dist/cjs/core/interpret.spec.js.map +1 -0
  25. package/dist/cjs/core/simulation.d.ts +49 -0
  26. package/dist/cjs/core/simulation.d.ts.map +1 -0
  27. package/dist/cjs/core/simulation.js +198 -0
  28. package/dist/cjs/core/simulation.js.map +1 -0
  29. package/dist/cjs/core/simulation.spec.d.ts +2 -0
  30. package/dist/cjs/core/simulation.spec.d.ts.map +1 -0
  31. package/dist/cjs/core/simulation.spec.js +397 -0
  32. package/dist/cjs/core/simulation.spec.js.map +1 -0
  33. package/dist/cjs/signing/BitBadgesSigningClient.d.ts +7 -1
  34. package/dist/cjs/signing/BitBadgesSigningClient.d.ts.map +1 -1
  35. package/dist/cjs/signing/BitBadgesSigningClient.js +58 -1
  36. package/dist/cjs/signing/BitBadgesSigningClient.js.map +1 -1
  37. package/dist/cjs/signing/index.d.ts +1 -1
  38. package/dist/cjs/signing/index.d.ts.map +1 -1
  39. package/dist/cjs/signing/index.js.map +1 -1
  40. package/dist/cjs/signing/types.d.ts +10 -0
  41. package/dist/cjs/signing/types.d.ts.map +1 -1
  42. package/dist/cjs/signing/types.js.map +1 -1
  43. package/dist/cjs/tsconfig.build.tsbuildinfo +1 -1
  44. package/dist/esm/api-indexer/BitBadgesApi.d.ts +4 -2
  45. package/dist/esm/api-indexer/BitBadgesApi.d.ts.map +1 -1
  46. package/dist/esm/api-indexer/BitBadgesApi.js +6 -4
  47. package/dist/esm/api-indexer/BitBadgesApi.js.map +1 -1
  48. package/dist/esm/api-indexer/BitBadgesCollection.d.ts +3 -2
  49. package/dist/esm/api-indexer/BitBadgesCollection.d.ts.map +1 -1
  50. package/dist/esm/api-indexer/BitBadgesCollection.js +7 -0
  51. package/dist/esm/api-indexer/BitBadgesCollection.js.map +1 -1
  52. package/dist/esm/core/balances.d.ts +1 -0
  53. package/dist/esm/core/balances.d.ts.map +1 -1
  54. package/dist/esm/core/balances.js +4 -0
  55. package/dist/esm/core/balances.js.map +1 -1
  56. package/dist/esm/core/index.d.ts +2 -0
  57. package/dist/esm/core/index.d.ts.map +1 -1
  58. package/dist/esm/core/index.js +2 -0
  59. package/dist/esm/core/index.js.map +1 -1
  60. package/dist/esm/core/interpret.d.ts +9 -0
  61. package/dist/esm/core/interpret.d.ts.map +1 -0
  62. package/dist/esm/core/interpret.js +1314 -0
  63. package/dist/esm/core/interpret.js.map +1 -0
  64. package/dist/esm/core/interpret.spec.d.ts +2 -0
  65. package/dist/esm/core/interpret.spec.d.ts.map +1 -0
  66. package/dist/esm/core/interpret.spec.js +437 -0
  67. package/dist/esm/core/interpret.spec.js.map +1 -0
  68. package/dist/esm/core/simulation.d.ts +49 -0
  69. package/dist/esm/core/simulation.d.ts.map +1 -0
  70. package/dist/esm/core/simulation.js +194 -0
  71. package/dist/esm/core/simulation.js.map +1 -0
  72. package/dist/esm/core/simulation.spec.d.ts +2 -0
  73. package/dist/esm/core/simulation.spec.d.ts.map +1 -0
  74. package/dist/esm/core/simulation.spec.js +395 -0
  75. package/dist/esm/core/simulation.spec.js.map +1 -0
  76. package/dist/esm/signing/BitBadgesSigningClient.d.ts +7 -1
  77. package/dist/esm/signing/BitBadgesSigningClient.d.ts.map +1 -1
  78. package/dist/esm/signing/BitBadgesSigningClient.js +58 -1
  79. package/dist/esm/signing/BitBadgesSigningClient.js.map +1 -1
  80. package/dist/esm/signing/index.d.ts +1 -1
  81. package/dist/esm/signing/index.d.ts.map +1 -1
  82. package/dist/esm/signing/index.js.map +1 -1
  83. package/dist/esm/signing/types.d.ts +10 -0
  84. package/dist/esm/signing/types.d.ts.map +1 -1
  85. package/dist/esm/signing/types.js.map +1 -1
  86. package/dist/esm/tsconfig-esm.build.tsbuildinfo +1 -1
  87. package/package.json +1 -1
@@ -0,0 +1,1321 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.timestampToDate = timestampToDate;
4
+ exports.durationToHuman = durationToHuman;
5
+ exports.denomToHuman = denomToHuman;
6
+ exports.amountToHuman = amountToHuman;
7
+ exports.interpretCollection = interpretCollection;
8
+ const math_js_1 = require("../common/math.js");
9
+ const constants_js_1 = require("../common/constants.js");
10
+ const BitBadgesCollection_js_1 = require("../api-indexer/BitBadgesCollection.js");
11
+ const approval_utils_js_1 = require("./approval-utils.js");
12
+ const MAX_UINT64 = math_js_1.GO_MAX_UINT_64;
13
+ function timestampToDate(ms) {
14
+ if (ms <= 0n)
15
+ return 'the beginning of time';
16
+ if (ms >= MAX_UINT64)
17
+ return 'the end of time';
18
+ const d = new Date(Number(ms));
19
+ if (isNaN(d.getTime()))
20
+ return `timestamp ${ms}`;
21
+ const months = [
22
+ 'January', 'February', 'March', 'April', 'May', 'June',
23
+ 'July', 'August', 'September', 'October', 'November', 'December'
24
+ ];
25
+ const month = months[d.getUTCMonth()];
26
+ const day = d.getUTCDate();
27
+ const year = d.getUTCFullYear();
28
+ const hours = d.getUTCHours();
29
+ const minutes = d.getUTCMinutes();
30
+ if (hours === 0 && minutes === 0) {
31
+ return `${month} ${day}, ${year}`;
32
+ }
33
+ return `${month} ${day}, ${year} at ${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')} UTC`;
34
+ }
35
+ function durationToHuman(ms) {
36
+ if (ms <= 0n)
37
+ return 'instant';
38
+ if (ms >= MAX_UINT64)
39
+ return 'forever';
40
+ const seconds = Number(ms) / 1000;
41
+ const minutes = seconds / 60;
42
+ const hours = minutes / 60;
43
+ const days = hours / 24;
44
+ const years = days / 365.25;
45
+ if (years >= 1 && years === Math.floor(years))
46
+ return `${Math.floor(years)} year${Math.floor(years) > 1 ? 's' : ''}`;
47
+ if (days >= 7 && days === Math.floor(days)) {
48
+ if (days === 7)
49
+ return '7 days (weekly)';
50
+ if (days === 30 || days === 31)
51
+ return `${Math.floor(days)} days (monthly)`;
52
+ return `${Math.floor(days)} days`;
53
+ }
54
+ if (hours >= 1 && hours === Math.floor(hours))
55
+ return `${Math.floor(hours)} hour${Math.floor(hours) > 1 ? 's' : ''}`;
56
+ if (minutes >= 1 && minutes === Math.floor(minutes))
57
+ return `${Math.floor(minutes)} minute${Math.floor(minutes) > 1 ? 's' : ''}`;
58
+ return `${seconds} second${seconds !== 1 ? 's' : ''}`;
59
+ }
60
+ function denomToHuman(denom) {
61
+ const entry = constants_js_1.CoinsRegistry[denom];
62
+ if (entry)
63
+ return entry.symbol;
64
+ if (denom.toLowerCase().includes('usdc'))
65
+ return 'USDC';
66
+ if (denom.toLowerCase().includes('atom'))
67
+ return 'ATOM';
68
+ if (denom.toLowerCase().includes('osmo'))
69
+ return 'OSMO';
70
+ return denom;
71
+ }
72
+ function amountToHuman(amount, denom) {
73
+ const symbol = denomToHuman(denom);
74
+ const entry = constants_js_1.CoinsRegistry[denom];
75
+ if (entry) {
76
+ const decimals = parseInt(entry.decimals, 10);
77
+ if (decimals > 0) {
78
+ const divisor = 10n ** BigInt(decimals);
79
+ const whole = amount / divisor;
80
+ const remainder = amount % divisor;
81
+ if (remainder === 0n) {
82
+ return `${whole.toLocaleString('en-US')} ${symbol}`;
83
+ }
84
+ const fracStr = remainder.toString().padStart(decimals, '0').replace(/0+$/, '');
85
+ return `${whole}.${fracStr} ${symbol}`;
86
+ }
87
+ }
88
+ return `${amount.toLocaleString('en-US')} ${symbol}`;
89
+ }
90
+ function isFullRange(ranges) {
91
+ if (!ranges || ranges.length === 0)
92
+ return false;
93
+ return ranges.some((r) => BigInt(r.start.valueOf()) === 1n && BigInt(r.end.valueOf()) === MAX_UINT64);
94
+ }
95
+ function isForbidden(entries) {
96
+ if (!entries || entries.length === 0)
97
+ return false;
98
+ return entries.some((e) => isFullRange(e.permanentlyForbiddenTimes));
99
+ }
100
+ function isPermitted(entries) {
101
+ if (!entries || entries.length === 0)
102
+ return false;
103
+ return entries.some((e) => isFullRange(e.permanentlyPermittedTimes));
104
+ }
105
+ function permState(entries) {
106
+ if (isForbidden(entries))
107
+ return 'locked';
108
+ if (isPermitted(entries))
109
+ return 'open';
110
+ return 'undecided';
111
+ }
112
+ function rangeStr(ranges) {
113
+ if (!ranges || ranges.length === 0)
114
+ return 'none';
115
+ return ranges
116
+ .map((r) => {
117
+ const s = BigInt(r.start.valueOf());
118
+ const e = BigInt(r.end.valueOf());
119
+ if (s === 1n && e === MAX_UINT64)
120
+ return 'all';
121
+ if (s === e)
122
+ return `#${s}`;
123
+ return `#${s}-#${e}`;
124
+ })
125
+ .join(', ');
126
+ }
127
+ function timeRangeStr(ranges) {
128
+ if (!ranges || ranges.length === 0)
129
+ return 'none';
130
+ return ranges
131
+ .map((r) => {
132
+ const s = BigInt(r.start.valueOf());
133
+ const e = BigInt(r.end.valueOf());
134
+ if (s === 1n && e === MAX_UINT64)
135
+ return 'all time';
136
+ return `${timestampToDate(s)} through ${timestampToDate(e)}`;
137
+ })
138
+ .join(', ');
139
+ }
140
+ function countTokenIds(ranges) {
141
+ if (!ranges || ranges.length === 0)
142
+ return '0';
143
+ let total = 0n;
144
+ for (const r of ranges) {
145
+ const s = BigInt(r.start.valueOf());
146
+ const e = BigInt(r.end.valueOf());
147
+ total += e - s + 1n;
148
+ }
149
+ if (total >= MAX_UINT64)
150
+ return 'unlimited';
151
+ if (total > 1000000n)
152
+ return `${total.toLocaleString('en-US')} (very large range)`;
153
+ return total.toLocaleString('en-US');
154
+ }
155
+ function listIdHuman(listId) {
156
+ if (listId === 'All')
157
+ return 'anyone';
158
+ if (listId === 'Mint')
159
+ return 'the Mint address (new token creation)';
160
+ if (listId === '!Mint')
161
+ return 'any existing holder (not the Mint)';
162
+ if (listId === 'Total')
163
+ return 'Total (aggregate tracker)';
164
+ if (listId.startsWith('!Mint:'))
165
+ return `any holder except ${listId.slice(6)}`;
166
+ if (listId.startsWith('!'))
167
+ return `anyone except ${listId.slice(1)}`;
168
+ if (listId.includes(':'))
169
+ return listId.split(':').join(' and ');
170
+ if (listId.startsWith('bb1'))
171
+ return `address ${listId.slice(0, 12)}...`;
172
+ return listId;
173
+ }
174
+ function detectType(collection) {
175
+ const s = collection.standards ?? [];
176
+ if (s.some((x) => x.toLowerCase().includes('subscription')))
177
+ return 'Subscription Token';
178
+ if (s.some((x) => x.toLowerCase().includes('ai agent stablecoin')))
179
+ return 'AI Agent Stablecoin';
180
+ if (s.some((x) => x.toLowerCase().includes('smart token')) || collection.invariants?.cosmosCoinBackedPath)
181
+ return 'Smart Token (IBC-backed)';
182
+ if (s.includes('NFTs'))
183
+ return 'NFT Collection';
184
+ if (s.includes('Fungible Tokens'))
185
+ return 'Fungible Token';
186
+ return 'Token Collection';
187
+ }
188
+ function bigVal(v) {
189
+ if (v == null)
190
+ return 0n;
191
+ return BigInt(v.valueOf());
192
+ }
193
+ const PLUGIN_DISPLAY_NAMES = {
194
+ numUses: 'Usage Limit (limits how many times each user can claim)',
195
+ codes: 'Claim Code (user must enter a secret code)',
196
+ password: 'Password (user must enter the correct password)',
197
+ whitelist: 'Whitelist (user must be on an approved address list)',
198
+ discord: 'Discord Verification (user must authenticate via Discord)',
199
+ twitter: 'Twitter/X Verification (user must authenticate via Twitter/X)',
200
+ github: 'GitHub Verification (user must authenticate via GitHub)',
201
+ google: 'Google Verification (user must authenticate via Google)',
202
+ email: 'Email Verification (user must verify their email address)',
203
+ geolocation: 'Geolocation Check (user must be in an allowed region)',
204
+ initiatedBy: 'Initiator Check (verifies the claiming address)',
205
+ transferTimes: 'Transfer Time Window (claim is only available during specific times)',
206
+ requiresProofOfAddress: 'Address Ownership Proof (user must prove they control the address)'
207
+ };
208
+ function pluginDisplayName(pluginId) {
209
+ return PLUGIN_DISPLAY_NAMES[pluginId] || pluginId;
210
+ }
211
+ function buildOverview(col) {
212
+ const type = detectType(col);
213
+ const tokenCount = countTokenIds(col.validTokenIds);
214
+ const maxSupply = col.invariants?.maxSupplyPerId != null ? bigVal(col.invariants.maxSupplyPerId) : 0n;
215
+ const metadata = col.getCollectionMetadata();
216
+ let md = '## Collection Overview\n\n';
217
+ const name = metadata?.name || 'Unnamed Collection';
218
+ const desc = metadata?.description || '';
219
+ md += `**"${name}"** (creator-provided name) is a ${type} on BitBadges`;
220
+ if (col.collectionId) {
221
+ md += ` (Collection ID: ${col.collectionId})`;
222
+ }
223
+ else {
224
+ md += ' (not yet deployed)';
225
+ }
226
+ md += '. ';
227
+ if (desc)
228
+ md += `\n\n> *Creator-provided description:* ${desc}\n\n`;
229
+ const rangeDisplay = rangeStr(col.validTokenIds);
230
+ if (rangeDisplay === 'all') {
231
+ md += `The collection supports an unlimited range of token IDs. `;
232
+ }
233
+ else if (rangeDisplay === 'none') {
234
+ md += `No token IDs have been defined yet. This means no tokens can currently be minted or transferred until valid token IDs are added by the manager. `;
235
+ }
236
+ else {
237
+ md += `It contains ${tokenCount} unique token ID${tokenCount === '1' ? '' : 's'} (${rangeDisplay}). `;
238
+ }
239
+ if (maxSupply === 0n) {
240
+ md += 'There is no hard cap on supply per token ID, meaning tokens can be minted without an on-chain maximum.';
241
+ }
242
+ else {
243
+ md += `Each token ID has a maximum supply of ${maxSupply.toLocaleString('en-US')} tokens, enforced at the protocol level.`;
244
+ }
245
+ md += '\n\n';
246
+ if (type === 'NFT Collection') {
247
+ md += `As an NFT collection, each token ID represents a distinct, individually identifiable asset. `;
248
+ if (maxSupply === 1n) {
249
+ md += 'With a maximum supply of 1 per ID, each token is truly unique and non-fungible.';
250
+ }
251
+ else if (maxSupply === 0n) {
252
+ md += 'There is no supply cap, so multiple copies of each token ID can exist.';
253
+ }
254
+ else {
255
+ md += `Up to ${maxSupply.toLocaleString('en-US')} copies of each token ID can exist.`;
256
+ }
257
+ }
258
+ else if (type.includes('Smart Token')) {
259
+ const backing = col.invariants?.cosmosCoinBackedPath;
260
+ const denom = backing?.conversion?.sideA?.denom ? denomToHuman(backing.conversion.sideA.denom) : 'an IBC asset';
261
+ md += `This is a smart token backed 1:1 by ${denom}. Users can deposit the backing asset to receive collection tokens, and redeem collection tokens to withdraw the backing asset.`;
262
+ }
263
+ else if (type === 'Fungible Token') {
264
+ md += `As a fungible token, all tokens of the same ID are interchangeable, similar to an ERC-20 token. `;
265
+ if (maxSupply === 0n) {
266
+ md += 'There is no supply cap.';
267
+ }
268
+ else {
269
+ md += `The maximum total supply is ${maxSupply.toLocaleString('en-US')} tokens.`;
270
+ }
271
+ }
272
+ else if (type === 'Subscription Token') {
273
+ md += 'This is a subscription token. Holding it grants access to a service or resource for a defined time period. It may require periodic renewal or payment.';
274
+ }
275
+ else if (type === 'AI Agent Stablecoin') {
276
+ md += 'This is an AI agent-managed stablecoin vault. An AI agent manages the vault and controls minting and burning operations.';
277
+ }
278
+ else {
279
+ md += 'This is a token collection on BitBadges.';
280
+ }
281
+ if (col.standards && col.standards.length > 0) {
282
+ md += `\n\nThe collection declares the following standards: **${col.standards.join(', ')}**. `;
283
+ md += 'Standards tell wallets, marketplaces, and other tools how to interpret and display these tokens. ';
284
+ const standardExplanations = [];
285
+ for (const std of col.standards) {
286
+ const lower = std.toLowerCase();
287
+ if (std === 'NFTs')
288
+ standardExplanations.push('"NFTs" -- each token ID is a distinct asset with its own metadata, similar to ERC-721 on Ethereum');
289
+ else if (std === 'Fungible Tokens')
290
+ standardExplanations.push('"Fungible Tokens" -- all tokens of the same ID are interchangeable, similar to ERC-20 on Ethereum');
291
+ else if (lower.includes('non-transferable') || lower.includes('soulbound'))
292
+ standardExplanations.push(`"${std}" -- tokens cannot be transferred once received; they are permanently bound to the holder`);
293
+ else if (lower.includes('subscription'))
294
+ standardExplanations.push(`"${std}" -- tokens represent time-limited access that may require periodic renewal`);
295
+ else if (lower.includes('tradable') || lower.includes('marketplace') || std === 'NFTMarketplace')
296
+ standardExplanations.push(`"${std}" -- the collection is designed for secondary market trading`);
297
+ else if (lower.includes('smart token'))
298
+ standardExplanations.push(`"${std}" -- the token is backed by an external asset and supports deposit/withdrawal`);
299
+ else if (lower.includes('ai agent'))
300
+ standardExplanations.push(`"${std}" -- the token is managed by an AI agent that controls minting, burning, or other operations`);
301
+ }
302
+ if (standardExplanations.length > 0) {
303
+ md += 'Specifically: ' + standardExplanations.join('; ') + '.';
304
+ }
305
+ }
306
+ if (col.isArchived) {
307
+ md += '\n\n> **This collection is ARCHIVED.** It is no longer active and no new operations should be expected.';
308
+ }
309
+ if (metadata?.image) {
310
+ md += `\n\nCollection image: ${metadata.image}`;
311
+ }
312
+ const tokenMeta = col.getTokenMetadata();
313
+ if (tokenMeta && tokenMeta.length > 0) {
314
+ const hasPerToken = tokenMeta.some((tm) => tm.uri || tm.customData);
315
+ if (hasPerToken) {
316
+ md += '\n\nEach token ID has its own individual metadata (name, image, description). This means different token IDs may represent different items or assets.';
317
+ }
318
+ }
319
+ else if (type === 'Fungible Token') {
320
+ md += '\n\nAll tokens share the same collection-level metadata, as is typical for fungible tokens.';
321
+ }
322
+ md += '\n\n';
323
+ return md;
324
+ }
325
+ function buildBackingAndCrossChain(col) {
326
+ const hasAlias = col.aliasPaths && col.aliasPaths.length > 0;
327
+ const hasWrapper = col.cosmosCoinWrapperPaths && col.cosmosCoinWrapperPaths.length > 0;
328
+ const hasBacking = !!col.invariants?.cosmosCoinBackedPath;
329
+ let md = '## Token Backing & Cross-Chain\n\n';
330
+ if (!hasAlias && !hasWrapper && !hasBacking) {
331
+ md += 'This is a native BitBadges collection with no cross-chain backing. Tokens are created and managed entirely within the BitBadges chain and are not pegged to any external asset or IBC denomination.\n\n';
332
+ return md;
333
+ }
334
+ if (hasBacking) {
335
+ const backing = col.invariants.cosmosCoinBackedPath;
336
+ const rawDenom = backing.conversion?.sideA?.denom || 'unknown';
337
+ const symbol = denomToHuman(rawDenom);
338
+ const address = backing.address || 'unknown';
339
+ const sideAAmount = backing.conversion?.sideA?.amount != null ? bigVal(backing.conversion.sideA.amount) : 1n;
340
+ const sideBBalances = backing.conversion?.sideB;
341
+ const sideBAmount = sideBBalances && sideBBalances.length > 0 ? bigVal(sideBBalances[0].amount) : 1n;
342
+ let rateDesc;
343
+ if (sideAAmount === sideBAmount) {
344
+ rateDesc = '1:1';
345
+ }
346
+ else if (sideBAmount > 0n) {
347
+ rateDesc = `${sideAAmount.toLocaleString('en-US')} ${symbol} per ${sideBAmount.toLocaleString('en-US')} collection token${sideBAmount > 1n ? 's' : ''}`;
348
+ }
349
+ else {
350
+ rateDesc = '1:1';
351
+ }
352
+ md += `This collection is **IBC-backed**, meaning each token is redeemable for the underlying asset at a **${rateDesc}** conversion rate. `;
353
+ md += `The backing asset is **${symbol}** (denomination: \`${rawDenom}\`). `;
354
+ md += `All backing funds are held at the backing address (\`${address}\`). `;
355
+ md += 'Users can deposit the backing asset to mint new collection tokens, and burn collection tokens to withdraw the backing asset. ';
356
+ md += 'This mechanism ensures that the total supply of collection tokens never exceeds the reserves held at the backing address.\n\n';
357
+ }
358
+ if (hasAlias) {
359
+ md += '### Alias Paths\n\n';
360
+ md += 'Alias paths provide alternative denominations for display and trading purposes:\n\n';
361
+ for (const alias of col.aliasPaths) {
362
+ const aliasName = alias.metadata?.metadata?.name || alias.symbol || alias.denom || 'unnamed';
363
+ md += `- **${aliasName}**: denomination \`${alias.denom || 'N/A'}\`, symbol \`${alias.symbol || 'N/A'}\`. `;
364
+ md += 'This alias allows the token to appear under a different symbol in wallets and DEX interfaces.\n';
365
+ }
366
+ md += '\n';
367
+ }
368
+ if (hasWrapper) {
369
+ md += '### Cosmos Coin Wrapper Paths\n\n';
370
+ md += 'Wrapper paths allow this collection token to be wrapped as a native Cosmos SDK coin, enabling integration with standard Cosmos modules (staking, governance, IBC transfers):\n\n';
371
+ for (const wrapper of col.cosmosCoinWrapperPaths) {
372
+ const wrapperName = wrapper.metadata?.metadata?.name || wrapper.symbol || wrapper.denom || 'unnamed';
373
+ md += `- **${wrapperName}**: denomination \`${wrapper.denom || 'N/A'}\`, symbol \`${wrapper.symbol || 'N/A'}\`\n`;
374
+ }
375
+ md += '\n';
376
+ }
377
+ return md;
378
+ }
379
+ function buildApprovalParagraph(approval, isForMint) {
380
+ const criteria = approval.approvalCriteria;
381
+ let md = '';
382
+ const label = isForMint ? 'Mint Approval' : 'Transfer Approval';
383
+ md += `### ${label}: "${approval.approvalId}"\n\n`;
384
+ const approvalDetails = approval.details;
385
+ if (approvalDetails?.name) {
386
+ md += `**"${approvalDetails.name}"** (creator-provided approval name)`;
387
+ if (approvalDetails.description) {
388
+ md += `\n\n> *Creator-provided approval description:* ${approvalDetails.description}\n\n`;
389
+ }
390
+ else {
391
+ md += '\n\n';
392
+ }
393
+ }
394
+ else if (approvalDetails?.description) {
395
+ md += `> *Creator-provided approval description:* ${approvalDetails.description}\n\n`;
396
+ }
397
+ if (approvalDetails?.image) {
398
+ md += `Approval image: ${approvalDetails.image}\n\n`;
399
+ }
400
+ if (isForMint) {
401
+ md += `This approval governs the creation of new tokens. `;
402
+ md += `It allows ${listIdHuman(approval.initiatedByListId)} to initiate minting, `;
403
+ md += `with tokens being delivered to ${listIdHuman(approval.toListId)}. `;
404
+ }
405
+ else {
406
+ md += `This approval governs token transfers. `;
407
+ md += `It permits tokens to be sent from ${listIdHuman(approval.fromListId)} `;
408
+ md += `to ${listIdHuman(approval.toListId)}, `;
409
+ md += `initiated by ${listIdHuman(approval.initiatedByListId)}. `;
410
+ if (approval.initiatedByListId === 'All' && !criteria?.overridesFromOutgoingApprovals) {
411
+ md += 'Note that while the collection-level rule does not restrict who initiates, the sender\'s personal outgoing approval must still pass. By default, only the sender themselves can initiate their own outgoing transfer. ';
412
+ }
413
+ }
414
+ const tokenRange = rangeStr(approval.tokenIds);
415
+ const transferTiming = timeRangeStr(approval.transferTimes);
416
+ const ownershipTiming = timeRangeStr(approval.ownershipTimes);
417
+ if (tokenRange === 'all' && transferTiming === 'all time' && ownershipTiming === 'all time') {
418
+ md += 'This applies to all token IDs, at all times, for all ownership periods.';
419
+ }
420
+ else {
421
+ md += `This applies to token IDs ${tokenRange}`;
422
+ if (transferTiming !== 'all time')
423
+ md += `, available during ${transferTiming}`;
424
+ if (ownershipTiming !== 'all time')
425
+ md += `, for ownership times ${ownershipTiming}`;
426
+ md += '.';
427
+ }
428
+ md += '\n\n';
429
+ if (!criteria)
430
+ return md;
431
+ if (criteria.coinTransfers && criteria.coinTransfers.length > 0) {
432
+ for (const ct of criteria.coinTransfers) {
433
+ const coins = ct.coins || [];
434
+ if (coins.length > 0) {
435
+ const costParts = coins.map((c) => {
436
+ const amt = bigVal(c.amount);
437
+ const denom = c.denom || 'unknown';
438
+ return amountToHuman(amt, denom);
439
+ });
440
+ md += `**Cost**: ${costParts.join(' + ')} per transfer. `;
441
+ if (ct.overrideFromWithApproverAddress) {
442
+ md += 'The payment is deducted from the approver address (the address that set up this approval). ';
443
+ }
444
+ else {
445
+ md += 'The payment is deducted from the transfer initiator. ';
446
+ }
447
+ if (ct.overrideToWithInitiator) {
448
+ md += 'Payment is sent to the transfer initiator. ';
449
+ }
450
+ else if (ct.to) {
451
+ md += `Payment is sent to address \`${ct.to}\`. `;
452
+ }
453
+ }
454
+ }
455
+ md += '\n\n';
456
+ }
457
+ else {
458
+ md += '**Cost**: Free (no payment required).\n\n';
459
+ }
460
+ const amounts = criteria.approvalAmounts;
461
+ const maxNum = criteria.maxNumTransfers;
462
+ const limitParts = [];
463
+ if (amounts) {
464
+ const oa = bigVal(amounts.overallApprovalAmount);
465
+ if (oa > 0n && oa < MAX_UINT64)
466
+ limitParts.push(`a total cap of ${oa.toLocaleString('en-US')} tokens across all users`);
467
+ const pa = bigVal(amounts.perInitiatedByAddressApprovalAmount);
468
+ if (pa > 0n && pa < MAX_UINT64)
469
+ limitParts.push(`a per-user limit of ${pa.toLocaleString('en-US')} tokens`);
470
+ const fa = bigVal(amounts.perFromAddressApprovalAmount);
471
+ if (fa > 0n && fa < MAX_UINT64)
472
+ limitParts.push(`a per-sender limit of ${fa.toLocaleString('en-US')} tokens`);
473
+ const ta = bigVal(amounts.perToAddressApprovalAmount);
474
+ if (ta > 0n && ta < MAX_UINT64)
475
+ limitParts.push(`a per-recipient limit of ${ta.toLocaleString('en-US')} tokens`);
476
+ }
477
+ if (maxNum) {
478
+ const on = bigVal(maxNum.overallMaxNumTransfers);
479
+ if (on > 0n && on < MAX_UINT64)
480
+ limitParts.push(`a total of ${on.toLocaleString('en-US')} transfer transactions overall`);
481
+ const pn = bigVal(maxNum.perInitiatedByAddressMaxNumTransfers);
482
+ if (pn > 0n && pn < MAX_UINT64)
483
+ limitParts.push(`${pn.toLocaleString('en-US')} transfer transaction${pn > 1n ? 's' : ''} per user`);
484
+ const fn = bigVal(maxNum.perFromAddressMaxNumTransfers);
485
+ if (fn > 0n && fn < MAX_UINT64)
486
+ limitParts.push(`${fn.toLocaleString('en-US')} transfer transaction${fn > 1n ? 's' : ''} per sender`);
487
+ const tn = bigVal(maxNum.perToAddressMaxNumTransfers);
488
+ if (tn > 0n && tn < MAX_UINT64)
489
+ limitParts.push(`${tn.toLocaleString('en-US')} transfer transaction${tn > 1n ? 's' : ''} per recipient`);
490
+ }
491
+ if (limitParts.length > 0) {
492
+ md += `**Limits**: This approval enforces ${limitParts.join(', ')}.`;
493
+ const amountsReset = amounts?.resetTimeIntervals;
494
+ const maxNumReset = maxNum?.resetTimeIntervals;
495
+ const resetInterval = amountsReset || maxNumReset;
496
+ if (resetInterval) {
497
+ const intervalLen = bigVal(resetInterval.intervalLength);
498
+ if (intervalLen > 0n) {
499
+ md += ` These limits reset every ${durationToHuman(intervalLen)}.`;
500
+ }
501
+ }
502
+ md += '\n\n';
503
+ }
504
+ if (criteria.mustOwnTokens && criteria.mustOwnTokens.length > 0) {
505
+ md += '**Ownership Requirements**: ';
506
+ const reqs = criteria.mustOwnTokens.map((mot) => {
507
+ const minAmount = mot.amountRange ? bigVal(mot.amountRange.start) : 1n;
508
+ const maxAmount = mot.amountRange ? bigVal(mot.amountRange.end) : 0n;
509
+ let desc = '';
510
+ if (maxAmount > 0n && maxAmount < MAX_UINT64 && maxAmount !== minAmount) {
511
+ desc += `between ${minAmount.toLocaleString('en-US')} and ${maxAmount.toLocaleString('en-US')} tokens`;
512
+ }
513
+ else {
514
+ desc += `at least ${minAmount.toLocaleString('en-US')} token${minAmount > 1n ? 's' : ''}`;
515
+ }
516
+ desc += ` from collection ${mot.collectionId}`;
517
+ if (mot.tokenIds && mot.tokenIds.length > 0) {
518
+ const tokenRange = rangeStr(mot.tokenIds);
519
+ if (tokenRange !== 'all')
520
+ desc += ` (token IDs: ${tokenRange})`;
521
+ }
522
+ if (mot.ownershipTimes && mot.ownershipTimes.length > 0) {
523
+ const ownershipRange = timeRangeStr(mot.ownershipTimes);
524
+ if (ownershipRange !== 'all time')
525
+ desc += ` during ${ownershipRange}`;
526
+ }
527
+ if (mot.overrideWithCurrentTime) {
528
+ desc += ' (ownership checked at the current block time)';
529
+ }
530
+ if (mot.mustSatisfyForAllAssets) {
531
+ desc += ' (must hold ALL specified tokens)';
532
+ }
533
+ return desc;
534
+ });
535
+ md += `To use this approval, the user must already hold ${reqs.join(' and ')}. This acts as a token gate, restricting access to existing holders of specific collections.\n\n`;
536
+ }
537
+ if (criteria.predeterminedBalances) {
538
+ const pb = criteria.predeterminedBalances;
539
+ md += '**Distribution Method**: Tokens are distributed using sequential allocation. ';
540
+ const ocm = pb.orderCalculationMethod;
541
+ if (ocm) {
542
+ if (ocm.useOverallNumTransfers) {
543
+ md += 'The order number is determined by the overall number of transfers across all users (the Nth person to claim gets the Nth allocation). ';
544
+ }
545
+ else if (ocm.usePerToAddressNumTransfers) {
546
+ md += 'The order number is determined per recipient address (each recipient\'s own claim count determines their allocation). ';
547
+ }
548
+ else if (ocm.usePerFromAddressNumTransfers) {
549
+ md += 'The order number is determined per sender address. ';
550
+ }
551
+ else if (ocm.usePerInitiatedByAddressNumTransfers) {
552
+ md += 'The order number is determined per initiator address. ';
553
+ }
554
+ else if (ocm.useMerkleChallengeLeafIndex) {
555
+ md += 'The order number is determined by the Merkle proof leaf index, reserving specific allocations for specific whitelist entries or codes. ';
556
+ }
557
+ }
558
+ const inc = pb.incrementedBalances;
559
+ if (inc) {
560
+ const incTokenIds = bigVal(inc.incrementTokenIdsBy);
561
+ const incOwnership = bigVal(inc.incrementOwnershipTimesBy);
562
+ const duration = bigVal(inc.durationFromTimestamp);
563
+ if (incTokenIds > 0n) {
564
+ md += `Each successive claim receives the next token ID in sequence, incrementing by ${incTokenIds.toLocaleString('en-US')}. `;
565
+ }
566
+ if (incOwnership > 0n) {
567
+ md += `Ownership times increment by ${durationToHuman(incOwnership)} per claim. `;
568
+ }
569
+ if (duration > 0n) {
570
+ md += `Each claim grants ownership for a duration of ${durationToHuman(duration)} starting from the claim timestamp. `;
571
+ }
572
+ if (inc.allowOverrideTimestamp) {
573
+ md += 'The claimant can override the start timestamp. ';
574
+ }
575
+ }
576
+ if (pb.manualBalances && pb.manualBalances.length > 0) {
577
+ md += `There are ${pb.manualBalances.length} manually defined balance allocation(s). `;
578
+ }
579
+ md += '\n\n';
580
+ }
581
+ const restrictions = [];
582
+ if (criteria.requireToEqualsInitiatedBy) {
583
+ restrictions.push('the recipient must be the same address that initiates the transfer (self-claim only)');
584
+ }
585
+ if (criteria.requireFromEqualsInitiatedBy) {
586
+ restrictions.push('the sender must be the same address that initiates the transfer');
587
+ }
588
+ if (criteria.requireToDoesNotEqualInitiatedBy) {
589
+ restrictions.push('the recipient cannot be the same address that initiates the transfer');
590
+ }
591
+ if (criteria.requireFromDoesNotEqualInitiatedBy) {
592
+ restrictions.push('the sender cannot be the same address that initiates the transfer');
593
+ }
594
+ if (restrictions.length > 0) {
595
+ md += `**Restrictions**: ${restrictions.join('; ')}.\n\n`;
596
+ }
597
+ if (criteria.overridesFromOutgoingApprovals || criteria.overridesToIncomingApprovals) {
598
+ md += '**Forceful Behavior**: ';
599
+ if (criteria.overridesFromOutgoingApprovals) {
600
+ md += 'This approval **overrides the sender\'s personal outgoing approval**. This means the sender\'s personal outgoing approval tier is skipped — the sender does NOT need to consent to this specific transfer. Normally, the sender must approve their own outgoing transfers, but this override bypasses that check. Tokens can be moved from a holder without their explicit per-transfer consent. ';
601
+ }
602
+ if (criteria.overridesToIncomingApprovals) {
603
+ md += 'This approval **overrides the recipient\'s personal incoming approval**. This means the recipient\'s incoming approval tier is skipped — the recipient does NOT need to consent to receiving these tokens. Normally, the recipient can control which incoming transfers they accept, but this override bypasses that check. Tokens can be deposited into any address without the recipient opting in. ';
604
+ }
605
+ md += '\n\n';
606
+ }
607
+ if (criteria.allowBackedMinting) {
608
+ md += '**IBC Backing**: This approval is used for IBC-backed minting or withdrawal operations. Tokens are created or destroyed in exchange for the underlying IBC asset.\n\n';
609
+ }
610
+ if (criteria.allowSpecialWrapping) {
611
+ md += '**Special Wrapping**: This approval is used for Cosmos coin wrapping/unwrapping operations. Tokens can be wrapped into or unwrapped from a native Cosmos SDK coin denomination.\n\n';
612
+ }
613
+ if (criteria.mustPrioritize) {
614
+ md += '**Priority**: This approval must be explicitly prioritized to be used. It will not be automatically selected during transfer matching.\n\n';
615
+ }
616
+ if (criteria.merkleChallenges && criteria.merkleChallenges.length > 0) {
617
+ const linkedClaims = criteria.merkleChallenges
618
+ .map((mc) => mc.challengeInfoDetails?.claim)
619
+ .filter((c) => !!c);
620
+ md += `**Verification Challenges**: ${criteria.merkleChallenges.length} Merkle proof challenge${criteria.merkleChallenges.length > 1 ? 's' : ''} must be satisfied. `;
621
+ if (linkedClaims.length === 0) {
622
+ md += 'This typically means the user must provide a valid proof of inclusion in a whitelist or enter a valid claim code.\n\n';
623
+ }
624
+ else {
625
+ md += '\n\n';
626
+ for (const claim of linkedClaims) {
627
+ md += `#### Linked Claim: "${claim.claimId}"\n\n`;
628
+ const claimName = claim.metadata?.name;
629
+ const claimDesc = claim.metadata?.description;
630
+ if (claimName)
631
+ md += `**"${claimName}"** (creator-provided claim name)\n\n`;
632
+ if (claimDesc)
633
+ md += `> *Creator-provided claim description:* ${claimDesc}\n\n`;
634
+ if (claim.categories && claim.categories.length > 0) {
635
+ md += `**Categories**: ${claim.categories.join(', ')}\n\n`;
636
+ }
637
+ if (claim.plugins && claim.plugins.length > 0) {
638
+ md += 'To complete this claim, the user must satisfy the following verification steps:\n\n';
639
+ for (const plugin of claim.plugins) {
640
+ const pluginName = pluginDisplayName(plugin.pluginId || 'unknown plugin');
641
+ md += `- **${pluginName}**`;
642
+ if (plugin.publicParams) {
643
+ const publicKeys = Object.keys(plugin.publicParams).filter((k) => k !== 'seedCode' && k !== 'preimages' && !k.startsWith('_'));
644
+ if (publicKeys.length > 0) {
645
+ const paramStrs = publicKeys.map((k) => `${k}: ${JSON.stringify(plugin.publicParams[k])}`);
646
+ md += ` (${paramStrs.join(', ')})`;
647
+ }
648
+ }
649
+ md += '\n';
650
+ }
651
+ md += '\n';
652
+ }
653
+ if (claim.rewards && claim.rewards.length > 0) {
654
+ md += `**Rewards**: ${claim.rewards.length} reward${claim.rewards.length > 1 ? 's' : ''} upon successful completion.\n\n`;
655
+ }
656
+ }
657
+ }
658
+ }
659
+ if (criteria.evmQueryChallenges && criteria.evmQueryChallenges.length > 0) {
660
+ md += `**On-Chain Verification**: ${criteria.evmQueryChallenges.length} EVM smart contract query check${criteria.evmQueryChallenges.length > 1 ? 's' : ''} must pass. The on-chain contract is queried to verify eligibility before the transfer can proceed.\n\n`;
661
+ }
662
+ if (criteria.dynamicStoreChallenges && criteria.dynamicStoreChallenges.length > 0) {
663
+ md += `**Dynamic Store Checks**: ${criteria.dynamicStoreChallenges.length} dynamic store verification${criteria.dynamicStoreChallenges.length > 1 ? 's' : ''} must pass. Dynamic stores are on-chain key-value databases that can be updated externally (for example, by an oracle or off-chain service). This approval checks values in the dynamic store to determine eligibility, enabling conditions that can change over time without modifying the approval itself.\n\n`;
664
+ }
665
+ if (criteria.ethSignatureChallenges && criteria.ethSignatureChallenges.length > 0) {
666
+ md += `**Signature Verification**: ${criteria.ethSignatureChallenges.length} Ethereum signature challenge${criteria.ethSignatureChallenges.length > 1 ? 's' : ''} must be satisfied. The user must cryptographically sign a specific message with their Ethereum wallet to prove ownership of an address or authorization.\n\n`;
667
+ }
668
+ if (criteria.votingChallenges && criteria.votingChallenges.length > 0) {
669
+ md += `**Voting Requirements**: ${criteria.votingChallenges.length} voting challenge${criteria.votingChallenges.length > 1 ? 's' : ''} must be satisfied. A governance vote must reach the required threshold before this approval can be used, enabling community-controlled transfers or minting.\n\n`;
670
+ }
671
+ if (criteria.senderChecks) {
672
+ const sc = criteria.senderChecks;
673
+ const parts = [];
674
+ if (sc.mustBeEvmContract)
675
+ parts.push('the sender must be an EVM contract');
676
+ if (sc.mustNotBeEvmContract)
677
+ parts.push('the sender must NOT be an EVM contract (must be an externally-owned account)');
678
+ if (sc.mustBeLiquidityPool)
679
+ parts.push('the sender must be a liquidity pool');
680
+ if (sc.mustNotBeLiquidityPool)
681
+ parts.push('the sender must NOT be a liquidity pool');
682
+ if (parts.length > 0) {
683
+ md += `**Sender Checks**: ${parts.join('; ')}.\n\n`;
684
+ }
685
+ else {
686
+ md += '**Sender Checks**: Additional sender address verification is configured.\n\n';
687
+ }
688
+ }
689
+ if (criteria.recipientChecks) {
690
+ const rc = criteria.recipientChecks;
691
+ const parts = [];
692
+ if (rc.mustBeEvmContract)
693
+ parts.push('the recipient must be an EVM contract');
694
+ if (rc.mustNotBeEvmContract)
695
+ parts.push('the recipient must NOT be an EVM contract (must be an externally-owned account)');
696
+ if (rc.mustBeLiquidityPool)
697
+ parts.push('the recipient must be a liquidity pool');
698
+ if (rc.mustNotBeLiquidityPool)
699
+ parts.push('the recipient must NOT be a liquidity pool');
700
+ if (parts.length > 0) {
701
+ md += `**Recipient Checks**: ${parts.join('; ')}.\n\n`;
702
+ }
703
+ else {
704
+ md += '**Recipient Checks**: Additional recipient address verification is configured.\n\n';
705
+ }
706
+ }
707
+ if (criteria.initiatorChecks) {
708
+ const ic = criteria.initiatorChecks;
709
+ const parts = [];
710
+ if (ic.mustBeEvmContract)
711
+ parts.push('the initiator must be an EVM contract');
712
+ if (ic.mustNotBeEvmContract)
713
+ parts.push('the initiator must NOT be an EVM contract (must be an externally-owned account)');
714
+ if (ic.mustBeLiquidityPool)
715
+ parts.push('the initiator must be a liquidity pool');
716
+ if (ic.mustNotBeLiquidityPool)
717
+ parts.push('the initiator must NOT be a liquidity pool');
718
+ if (parts.length > 0) {
719
+ md += `**Initiator Checks**: ${parts.join('; ')}.\n\n`;
720
+ }
721
+ else {
722
+ md += '**Initiator Checks**: Additional initiator address verification is configured.\n\n';
723
+ }
724
+ }
725
+ if (criteria.altTimeChecks) {
726
+ const atc = criteria.altTimeChecks;
727
+ const timeParts = [];
728
+ if (atc.offlineHours && atc.offlineHours.length > 0) {
729
+ const hourRanges = atc.offlineHours.map((r) => `${bigVal(r.start)}:00-${bigVal(r.end)}:00 UTC`).join(', ');
730
+ timeParts.push(`denied during hours ${hourRanges}`);
731
+ }
732
+ if (atc.offlineDays && atc.offlineDays.length > 0) {
733
+ const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
734
+ const dayRanges = atc.offlineDays.map((r) => {
735
+ const s = Number(bigVal(r.start));
736
+ const e = Number(bigVal(r.end));
737
+ if (s === e)
738
+ return dayNames[s] || `day ${s}`;
739
+ return `${dayNames[s] || `day ${s}`} through ${dayNames[e] || `day ${e}`}`;
740
+ }).join(', ');
741
+ timeParts.push(`denied on ${dayRanges}`);
742
+ }
743
+ if (timeParts.length > 0) {
744
+ md += `**Time Restrictions**: Transfers are ${timeParts.join(' and ')}.\n\n`;
745
+ }
746
+ else {
747
+ md += '**Time Restrictions**: Transfers may be restricted during certain hours or days based on alternative time-check rules.\n\n';
748
+ }
749
+ }
750
+ if (criteria.userRoyalties) {
751
+ const ur = criteria.userRoyalties;
752
+ const percentage = bigVal(ur.percentage);
753
+ const payoutAddress = ur.payoutAddress || 'unspecified';
754
+ if (percentage > 0n) {
755
+ const basisPoints = Number(percentage);
756
+ const pct = (basisPoints / 100).toFixed(2);
757
+ md += `**Royalties**: A royalty of **${pct}%** (${basisPoints} basis points) of the transfer value is automatically redistributed to \`${payoutAddress}\` on each transfer.\n\n`;
758
+ }
759
+ else {
760
+ md += '**Royalties**: User royalties are configured. A portion of the transfer value is automatically distributed to the designated royalty recipients.\n\n';
761
+ }
762
+ }
763
+ if (criteria.autoDeletionOptions)
764
+ md += '**Auto-Deletion**: This approval may be automatically removed after its conditions are fully consumed.\n\n';
765
+ if (!isForMint && approval.fromListId === 'All' && approval.initiatedByListId === 'All' && !criteria.requireToEqualsInitiatedBy && !criteria.requireFromEqualsInitiatedBy) {
766
+ if (!criteria.overridesFromOutgoingApprovals) {
767
+ }
768
+ else {
769
+ md += '> **Warning**: This approval allows anyone to move tokens from any holder, combined with an override on outgoing approvals. This means forceful transfers or seizure of tokens is possible under this rule.\n\n';
770
+ }
771
+ }
772
+ return md;
773
+ }
774
+ function buildHowTokensAreCreated(col) {
775
+ let md = '## How Tokens Are Created\n\n';
776
+ const mintApprovals = (0, approval_utils_js_1.getMintApprovals)(col.collectionApprovals);
777
+ const backingApprovals = col.collectionApprovals.filter((a) => a.approvalCriteria?.allowBackedMinting && a.fromList.checkAddress('Mint'));
778
+ if (mintApprovals.length === 0 && backingApprovals.length === 0) {
779
+ md += 'There are no active minting approvals configured for this collection. New tokens cannot be created through the standard approval system. ';
780
+ md += 'The only way to obtain tokens is through transfers from existing holders (if the collection supports transfers).\n\n';
781
+ return md;
782
+ }
783
+ for (const approval of mintApprovals) {
784
+ md += buildApprovalParagraph(approval, true);
785
+ }
786
+ for (const approval of backingApprovals) {
787
+ if (mintApprovals.some((m) => m.approvalId === approval.approvalId))
788
+ continue;
789
+ const backing = col.invariants?.cosmosCoinBackedPath;
790
+ const rawDenom = backing?.conversion?.sideA?.denom || 'the IBC asset';
791
+ const symbol = denomToHuman(rawDenom);
792
+ md += `### IBC Deposit: "${approval.approvalId}"\n\n`;
793
+ md += `This approval enables IBC-backed minting. Users send **${symbol}** to the backing address and receive collection tokens in return at a 1:1 conversion rate. `;
794
+ md += `Deposits are available to ${listIdHuman(approval.toListId)}. `;
795
+ md += 'The backing mechanism ensures that every collection token in circulation is fully reserved by the underlying asset.\n\n';
796
+ }
797
+ if (mintApprovals.length > 0) {
798
+ const hasClaims = mintApprovals.some((a) => a.approvalCriteria?.merkleChallenges && a.approvalCriteria.merkleChallenges.length > 0);
799
+ const hasCost = mintApprovals.some((a) => a.approvalCriteria?.coinTransfers && a.approvalCriteria.coinTransfers.length > 0);
800
+ const hasSelfClaim = mintApprovals.some((a) => a.approvalCriteria?.requireToEqualsInitiatedBy);
801
+ md += '### How to Mint\n\n';
802
+ if (hasClaims) {
803
+ md += 'To mint tokens from this collection, you would typically go through a claim flow: visit the collection page, complete the required verification steps (listed above for each approval), and submit the claim. ';
804
+ }
805
+ else if (hasSelfClaim) {
806
+ md += 'To mint tokens, you submit a mint transaction where you are both the initiator and the recipient. ';
807
+ }
808
+ else {
809
+ md += 'To mint tokens, submit a transfer transaction from the Mint address to the desired recipient. ';
810
+ }
811
+ if (hasCost) {
812
+ md += 'A payment is required (see the cost listed for each approval above). ';
813
+ }
814
+ md += 'Make sure you meet any ownership requirements and that the mint has not exceeded its limits.\n\n';
815
+ }
816
+ return md;
817
+ }
818
+ function buildTransferRules(col) {
819
+ let md = '## Transfer & Approval Rules\n\n';
820
+ md += 'When a transfer is submitted, the chain checks each collection-level approval in order. The first approval whose criteria match the transfer (correct sender, recipient, token IDs, timing, etc.) is used. If an approval with `mustPrioritize` is set, it must be explicitly referenced. If no approval matches, the transfer is rejected.\n\n';
821
+ const nonMintApprovals = (0, approval_utils_js_1.getNonMintApprovals)(col.collectionApprovals);
822
+ const transferApprovals = nonMintApprovals.filter((a) => !a.approvalCriteria?.allowBackedMinting);
823
+ const unbackingApprovals = col.collectionApprovals.filter((a) => a.approvalCriteria?.allowBackedMinting && !a.fromList.checkAddress('Mint'));
824
+ if (transferApprovals.length === 0 && unbackingApprovals.length === 0) {
825
+ md += 'This collection is **non-transferable (soulbound)**. Once tokens are minted to a holder, they cannot be sent to another address. ';
826
+ md += 'This is a permanent characteristic of the tokens as long as the transfer rules remain unchanged. ';
827
+ md += 'Soulbound tokens are commonly used for credentials, membership badges, and reputation scores that should not be tradeable.\n\n';
828
+ return md;
829
+ }
830
+ for (const approval of transferApprovals) {
831
+ md += buildApprovalParagraph(approval, false);
832
+ }
833
+ for (const approval of unbackingApprovals) {
834
+ const backing = col.invariants?.cosmosCoinBackedPath;
835
+ const rawDenom = backing?.conversion?.sideA?.denom || 'the IBC asset';
836
+ const symbol = denomToHuman(rawDenom);
837
+ md += `### IBC Withdrawal: "${approval.approvalId}"\n\n`;
838
+ md += `This approval enables IBC-backed withdrawal (burning). Holders send collection tokens to the backing address and receive **${symbol}** back at a 1:1 conversion rate. `;
839
+ md += `Withdrawals are available to ${listIdHuman(approval.fromListId)}. `;
840
+ const amounts = approval.approvalCriteria?.approvalAmounts;
841
+ if (amounts) {
842
+ const oa = bigVal(amounts.overallApprovalAmount);
843
+ if (oa > 0n && oa < MAX_UINT64)
844
+ md += `There is a total withdrawal limit of ${oa.toLocaleString('en-US')} tokens. `;
845
+ const fa = bigVal(amounts.perFromAddressApprovalAmount);
846
+ if (fa > 0n && fa < MAX_UINT64)
847
+ md += `Each address can withdraw up to ${fa.toLocaleString('en-US')} tokens. `;
848
+ }
849
+ md += '\n\n';
850
+ }
851
+ md += 'Any transfer that does not match one of the approvals listed above will be rejected. This is a default-deny system — only explicitly approved operations can proceed.\n\n';
852
+ if (col.invariants?.noForcefulPostMintTransfers) {
853
+ md += '> **Safety Guarantee**: The on-chain invariant `noForcefulPostMintTransfers` is active. No one can forcefully move tokens from holders after minting. This is enforced at the protocol level and cannot be overridden.\n\n';
854
+ }
855
+ return md;
856
+ }
857
+ function buildPermissions(col) {
858
+ let md = '## Permissions -- What Can Change Later\n\n';
859
+ const manager = col.manager || 'not set';
860
+ md += `The **manager** is a single address that has administrative control over the collection. The manager is the ONLY address that can execute changes allowed by unfrozen permissions. If a permission is locked (forbidden), even the manager cannot make that change. If no manager is set, no one can make administrative changes.\n\n`;
861
+ if (!col.manager) {
862
+ md += '> **No Manager Set**: This collection has no manager address. No administrative changes can be made to the collection, regardless of permission settings. All unlocked permissions below are effectively inert until a manager is set (if the "Manager Transfer" permission allows it).\n\n';
863
+ }
864
+ md += `Current manager: \`${manager}\`. Each permission below determines whether the manager can modify a specific aspect of the collection in the future.\n\n`;
865
+ const permDescriptions = {
866
+ canDeleteCollection: {
867
+ label: 'Delete Collection',
868
+ lockedDesc: 'The collection cannot be deleted. This is permanent and provides assurance that the collection will continue to exist.',
869
+ openDesc: 'The manager can permanently destroy the entire collection and all tokens at any time.',
870
+ undecidedDesc: 'The manager can currently delete the collection. This permission could be locked in the future.'
871
+ },
872
+ canArchiveCollection: {
873
+ label: 'Archive Collection',
874
+ lockedDesc: 'The collection cannot be archived or unarchived. Its active status is permanent.',
875
+ openDesc: 'The manager can mark the collection as archived (inactive) at any time.',
876
+ undecidedDesc: 'The manager can currently archive the collection. This permission could be locked in the future.'
877
+ },
878
+ canUpdateStandards: {
879
+ label: 'Standards',
880
+ lockedDesc: 'The declared standards for this collection are permanently locked and cannot be changed.',
881
+ openDesc: 'The manager can change the declared standards at any time, which could alter how wallets and interfaces interpret the tokens.',
882
+ undecidedDesc: 'The manager can currently update standards. This permission could be locked in the future.'
883
+ },
884
+ canUpdateCustomData: {
885
+ label: 'Custom Data',
886
+ lockedDesc: 'The custom data field is permanently locked and cannot be modified.',
887
+ openDesc: 'The manager can change the custom JSON data stored on the collection at any time.',
888
+ undecidedDesc: 'The manager can currently update custom data. This permission could be locked in the future.'
889
+ },
890
+ canUpdateManager: {
891
+ label: 'Manager Transfer',
892
+ lockedDesc: 'The manager address is permanently locked. Management cannot be transferred to another address.',
893
+ openDesc: 'The manager can transfer management control to a different address at any time.',
894
+ undecidedDesc: 'The manager can currently transfer management. This permission could be locked in the future.'
895
+ },
896
+ canUpdateCollectionMetadata: {
897
+ label: 'Collection Metadata',
898
+ lockedDesc: 'The collection name, description, and image are permanently locked and cannot be changed.',
899
+ openDesc: 'The manager can change the collection name, description, and image at any time.',
900
+ undecidedDesc: 'The manager can currently update collection metadata. This permission could be locked in the future.'
901
+ },
902
+ canUpdateValidTokenIds: {
903
+ label: 'Token ID Creation',
904
+ lockedDesc: 'No new token IDs can be created. The set of valid token IDs is permanent, protecting against supply dilution.',
905
+ openDesc: 'The manager can add new token IDs at any time, which means new supply can be minted.',
906
+ undecidedDesc: 'The manager can currently create new token IDs. This permission could be locked in the future.'
907
+ },
908
+ canUpdateTokenMetadata: {
909
+ label: 'Token Metadata',
910
+ lockedDesc: 'Individual token names, images, and descriptions are permanently locked and cannot be changed.',
911
+ openDesc: 'The manager can change individual token metadata (names, images, descriptions) at any time.',
912
+ undecidedDesc: 'The manager can currently update token metadata. This permission could be locked in the future.'
913
+ },
914
+ canUpdateCollectionApprovals: {
915
+ label: 'Transfer Rules',
916
+ lockedDesc: 'The transfer and minting rules described in this report are permanently locked. They can never be changed by anyone, including the collection manager. This provides the strongest possible guarantee that the rules will remain as described.',
917
+ openDesc: 'The manager can modify transfer and minting rules at any time. This means the transferability, fees, limits, and minting conditions described in this report could be changed in the future without holder consent. This is the most powerful permission.',
918
+ undecidedDesc: 'The manager can currently modify transfer and minting rules. This permission could be locked in the future, but until then, the rules described in this report may change.'
919
+ },
920
+ canUpdateAutoApproveSelfInitiatedIncomingTransfers: {
921
+ label: 'Auto-Approve Incoming (Self-Initiated)',
922
+ lockedDesc: 'The default auto-approve setting for self-initiated incoming transfers is permanently locked.',
923
+ openDesc: 'The manager can change the default auto-approve setting for self-initiated incoming transfers.',
924
+ undecidedDesc: 'The manager can currently update this setting. This permission could be locked in the future.'
925
+ },
926
+ canUpdateAutoApproveSelfInitiatedOutgoingTransfers: {
927
+ label: 'Auto-Approve Outgoing (Self-Initiated)',
928
+ lockedDesc: 'The default auto-approve setting for self-initiated outgoing transfers is permanently locked.',
929
+ openDesc: 'The manager can change the default auto-approve setting for self-initiated outgoing transfers.',
930
+ undecidedDesc: 'The manager can currently update this setting. This permission could be locked in the future.'
931
+ },
932
+ canUpdateAutoApproveAllIncomingTransfers: {
933
+ label: 'Auto-Approve All Incoming',
934
+ lockedDesc: 'The default auto-approve setting for all incoming transfers is permanently locked.',
935
+ openDesc: 'The manager can change the default auto-approve setting for all incoming transfers.',
936
+ undecidedDesc: 'The manager can currently update this setting. This permission could be locked in the future.'
937
+ },
938
+ canAddMoreAliasPaths: {
939
+ label: 'Alias Paths',
940
+ lockedDesc: 'No new alias paths (trading pairs) can be added. The current set is permanent.',
941
+ openDesc: 'The manager can add new alias paths (trading pairs) for liquidity pools.',
942
+ undecidedDesc: 'The manager can currently add alias paths. This permission could be locked in the future.'
943
+ },
944
+ canAddMoreCosmosCoinWrapperPaths: {
945
+ label: 'IBC Wrapper Paths',
946
+ lockedDesc: 'No new IBC asset backing configurations can be added. The current set is permanent.',
947
+ openDesc: 'The manager can add new IBC asset backing configurations.',
948
+ undecidedDesc: 'The manager can currently add IBC wrapper paths. This permission could be locked in the future.'
949
+ }
950
+ };
951
+ let lockedCount = 0;
952
+ let openCount = 0;
953
+ let undecidedCount = 0;
954
+ const perms = col.collectionPermissions;
955
+ for (const [key, info] of Object.entries(permDescriptions)) {
956
+ const permArray = perms[key];
957
+ const state = permState(permArray);
958
+ if (state === 'locked')
959
+ lockedCount++;
960
+ else if (state === 'open')
961
+ openCount++;
962
+ else
963
+ undecidedCount++;
964
+ let desc;
965
+ if (state === 'locked')
966
+ desc = info.lockedDesc;
967
+ else if (state === 'open')
968
+ desc = info.openDesc;
969
+ else
970
+ desc = info.undecidedDesc;
971
+ const stateLabel = state === 'locked' ? 'Permanently Locked' : state === 'open' ? 'Explicitly Allowed' : 'Unlocked (Undecided)';
972
+ md += `**${info.label}**: *${stateLabel}*. ${desc}\n\n`;
973
+ }
974
+ const total = lockedCount + openCount + undecidedCount;
975
+ md += '### Trust Summary\n\n';
976
+ md += `This collection has **${lockedCount} of ${total}** permissions permanently locked. `;
977
+ if (lockedCount === total) {
978
+ md += 'The collection is **fully immutable**. The manager cannot change anything about this collection. This provides the maximum level of trust and predictability for token holders.';
979
+ }
980
+ else if (lockedCount >= total * 0.7) {
981
+ md += `Only ${total - lockedCount} aspect${total - lockedCount > 1 ? 's' : ''} can be changed, providing **strong guarantees** that the rules will remain as described.`;
982
+ }
983
+ else if (lockedCount >= total * 0.4) {
984
+ md += 'The collection is **partially locked**. The manager retains significant control over some aspects. Holders should carefully review which permissions remain open.';
985
+ }
986
+ else {
987
+ md += 'The collection is **highly mutable**. The manager can change most aspects of the collection. Only trust this collection if you trust the manager.';
988
+ }
989
+ md += '\n\n';
990
+ const approvalState = permState(perms.canUpdateCollectionApprovals);
991
+ if (approvalState !== 'locked') {
992
+ md += `> **Important**: Transfer rules are ${approvalState === 'open' ? 'permanently changeable' : 'currently changeable'}. `;
993
+ const hasMint = col.collectionApprovals.some((a) => a.fromList.checkAddress('Mint'));
994
+ if (hasMint) {
995
+ md += 'Since the collection has mint approvals, the manager could potentially change minting rules (add unlimited minting, change prices, etc.).\n\n';
996
+ }
997
+ else {
998
+ md += 'The manager could add new transfer rules or modify existing ones.\n\n';
999
+ }
1000
+ }
1001
+ const validTokenState = permState(perms.canUpdateValidTokenIds);
1002
+ if (validTokenState !== 'locked') {
1003
+ md += `> **Important**: Token ID creation is ${validTokenState === 'open' ? 'permanently allowed' : 'currently allowed'}. The manager can create new token IDs, which means new supply can be minted, potentially diluting existing holders.`;
1004
+ if (approvalState === 'locked') {
1005
+ const hasMint = col.collectionApprovals.some((a) => a.fromList.checkAddress('Mint'));
1006
+ if (hasMint) {
1007
+ md += ' Note: even though the transfer rules are permanently locked, existing mint approvals may cover a broad range of token IDs (such as "all"), so newly created token IDs could be minted through the existing rules without any rule changes.';
1008
+ }
1009
+ }
1010
+ md += '\n\n';
1011
+ }
1012
+ return md;
1013
+ }
1014
+ function buildInvariants(col) {
1015
+ const inv = col.invariants;
1016
+ let md = '## Invariants -- On-Chain Guarantees\n\n';
1017
+ if (!inv) {
1018
+ md += 'No invariants are set for this collection. Without invariants, the on-chain protocol does not enforce any additional constraints beyond the standard approval system.\n\n';
1019
+ return md;
1020
+ }
1021
+ md += 'Invariants are permanent, on-chain rules that cannot be changed once set. They provide the strongest level of guarantee available on BitBadges, as they are enforced by the protocol itself regardless of any permission or approval changes.\n\n';
1022
+ const maxSupply = inv.maxSupplyPerId != null ? bigVal(inv.maxSupplyPerId) : 0n;
1023
+ if (maxSupply > 0n) {
1024
+ md += `**Maximum Supply**: Each token ID has a hard cap of **${maxSupply.toLocaleString('en-US')} tokens**. This is enforced at the protocol level and cannot be overridden by anyone, even if other permissions change. This guarantees scarcity and protects holders against unlimited inflation.\n\n`;
1025
+ }
1026
+ else {
1027
+ md += '**Maximum Supply**: No maximum supply limit is set. Tokens can be minted without an on-chain cap, subject only to the approval rules.\n\n';
1028
+ }
1029
+ if (inv.noForcefulPostMintTransfers) {
1030
+ md += '**No Forceful Post-Mint Transfers**: Once tokens are minted to a holder, they cannot be forcefully seized or moved by anyone other than the holder. This is a critical safety guarantee that protects holders from rug pulls and unauthorized token seizure. Even if transfer approval rules are changed in the future, this invariant ensures that no approval can override a holder\'s custody of their tokens.\n\n';
1031
+ }
1032
+ else {
1033
+ md += '**No Forceful Post-Mint Transfers**: Not enforced. Depending on the collection\'s approval rules, it may be possible for a third party to move tokens from a holder\'s account without their consent. Review the transfer rules carefully.\n\n';
1034
+ }
1035
+ if (inv.noCustomOwnershipTimes) {
1036
+ md += '**No Custom Ownership Times**: All ownership is treated as full-range (always active). This simplifies the token model by preventing time-windowed ownership, making balances straightforward to understand and display.\n\n';
1037
+ }
1038
+ else {
1039
+ md += '**Custom Ownership Times**: Allowed. Tokens can have time-windowed ownership periods, where a holder owns a token only during specified time ranges. This enables time-based access passes and subscriptions.\n\n';
1040
+ }
1041
+ if (inv.disablePoolCreation) {
1042
+ md += '**Pool Creation**: Disabled. No liquidity pools can be created for this collection\'s tokens. This prevents automated market-making and ensures tokens can only be transferred through the standard approval system.\n\n';
1043
+ }
1044
+ else {
1045
+ md += '**Pool Creation**: Allowed. Liquidity pools can be created for this collection\'s tokens, enabling automated market-making and decentralized trading.\n\n';
1046
+ }
1047
+ if (inv.cosmosCoinBackedPath) {
1048
+ const rawDenom = inv.cosmosCoinBackedPath.conversion?.sideA?.denom || 'unknown';
1049
+ const symbol = denomToHuman(rawDenom);
1050
+ const address = inv.cosmosCoinBackedPath.address || 'unknown';
1051
+ md += `**IBC Backing**: This collection is permanently backed 1:1 by **${symbol}** (\`${rawDenom}\`). The backing address is \`${address}\`. This invariant guarantees that the backing relationship cannot be altered or removed.\n\n`;
1052
+ }
1053
+ if (inv.evmQueryChallenges && inv.evmQueryChallenges.length > 0) {
1054
+ md += `**EVM Query Invariants**: ${inv.evmQueryChallenges.length} on-chain contract verification${inv.evmQueryChallenges.length > 1 ? 's are' : ' is'} checked after every transfer. These invariants call external smart contracts to enforce custom rules at the protocol level.\n\n`;
1055
+ }
1056
+ return md;
1057
+ }
1058
+ function buildDefaultBalances(col) {
1059
+ let md = '## Default Balances & Auto-Approve Settings\n\n';
1060
+ const defaults = col.defaultBalances;
1061
+ if (!defaults) {
1062
+ md += 'No default balance configuration is set for this collection.\n\n';
1063
+ return md;
1064
+ }
1065
+ if (defaults.balances && defaults.balances.length > 0) {
1066
+ md += 'Every new address that interacts with this collection starts with the following default balances:\n\n';
1067
+ for (const bal of defaults.balances) {
1068
+ const amount = bigVal(bal.amount);
1069
+ md += `- **${amount.toLocaleString('en-US')} tokens** for token IDs ${rangeStr(bal.tokenIds)}`;
1070
+ const ownershipRange = rangeStr(bal.ownershipTimes);
1071
+ if (ownershipRange !== 'all')
1072
+ md += ` (ownership times: ${timeRangeStr(bal.ownershipTimes)})`;
1073
+ md += '\n';
1074
+ }
1075
+ md += '\n';
1076
+ }
1077
+ else {
1078
+ md += 'Users start with zero balance. Tokens must be obtained through minting, claiming, or receiving a transfer.\n\n';
1079
+ }
1080
+ md += '### Auto-Approve Settings\n\n';
1081
+ md += 'These settings determine how transfers are handled at the user level by default:\n\n';
1082
+ if (defaults.autoApproveSelfInitiatedOutgoingTransfers) {
1083
+ md += '- **Auto-approve self-initiated outgoing transfers**: Yes. Users can send their own tokens without needing to set up additional approvals. This is the standard, expected behavior.\n';
1084
+ }
1085
+ else {
1086
+ md += '- **Auto-approve self-initiated outgoing transfers**: No. Users must explicitly approve even their own outgoing transfers. This is unusual and may complicate the user experience.\n';
1087
+ }
1088
+ if (defaults.autoApproveSelfInitiatedIncomingTransfers) {
1089
+ md += '- **Auto-approve self-initiated incoming transfers**: Yes. When a user initiates a transfer to themselves, it is automatically approved on the receiving side.\n';
1090
+ }
1091
+ else {
1092
+ md += '- **Auto-approve self-initiated incoming transfers**: No. Even self-initiated incoming transfers require explicit approval.\n';
1093
+ }
1094
+ if (defaults.autoApproveAllIncomingTransfers) {
1095
+ md += '- **Auto-approve all incoming transfers**: Yes. Any token sent to any address is automatically accepted. No one can block incoming tokens. **Warning**: This means holders cannot prevent unwanted or spam tokens from appearing in their balance. If collection-level approvals allow open transfers, anyone can send tokens to any address.\n';
1096
+ }
1097
+ else {
1098
+ md += '- **Auto-approve all incoming transfers**: No. Recipients can control which incoming transfers they accept. This prevents unwanted token spam and gives holders the ability to refuse unsolicited tokens.\n';
1099
+ }
1100
+ md += '\n';
1101
+ return md;
1102
+ }
1103
+ function collectLinkedClaimIds(col) {
1104
+ const linked = new Set();
1105
+ for (const approval of col.collectionApprovals) {
1106
+ const challenges = approval.approvalCriteria?.merkleChallenges;
1107
+ if (!challenges)
1108
+ continue;
1109
+ for (const mc of challenges) {
1110
+ const claim = mc.challengeInfoDetails?.claim;
1111
+ if (claim?.claimId)
1112
+ linked.add(claim.claimId);
1113
+ }
1114
+ }
1115
+ return linked;
1116
+ }
1117
+ function buildClaims(col, linkedClaimIds) {
1118
+ if (!col.claims || col.claims.length === 0) {
1119
+ return '## Claims\n\nNo off-chain claims are configured for this collection. Tokens can only be obtained through on-chain minting approvals or transfers.\n\n';
1120
+ }
1121
+ const orphanedClaims = col.claims.filter((c) => !linkedClaimIds.has(c.claimId));
1122
+ if (orphanedClaims.length === 0) {
1123
+ return '## Claims\n\nAll claims are described above alongside their corresponding approvals.\n\n';
1124
+ }
1125
+ let md = '## Claims\n\n';
1126
+ if (linkedClaimIds.size > 0) {
1127
+ md += `${linkedClaimIds.size} claim${linkedClaimIds.size > 1 ? 's are' : ' is'} described above alongside ${linkedClaimIds.size > 1 ? 'their' : 'its'} corresponding approval${linkedClaimIds.size > 1 ? 's' : ''}. `;
1128
+ }
1129
+ md += `${orphanedClaims.length} additional claim${orphanedClaims.length > 1 ? 's are' : ' is'} configured below. Claims allow users to obtain tokens through an off-chain verification flow before triggering the on-chain mint.\n\n`;
1130
+ for (const claim of orphanedClaims) {
1131
+ md += `### Claim: "${claim.claimId}"\n\n`;
1132
+ const claimName = claim.metadata?.name;
1133
+ const claimDesc = claim.metadata?.description;
1134
+ if (claimName)
1135
+ md += `**"${claimName}"** (creator-provided claim name)\n\n`;
1136
+ if (claimDesc)
1137
+ md += `> *Creator-provided claim description:* ${claimDesc}\n\n`;
1138
+ const details = [];
1139
+ if (claim.categories && claim.categories.length > 0)
1140
+ details.push(`categories: ${claim.categories.join(', ')}`);
1141
+ if (claim.approach)
1142
+ details.push(`approach: ${claim.approach}`);
1143
+ if (claim.estimatedCost)
1144
+ details.push(`estimated cost: ${claim.estimatedCost}`);
1145
+ if (claim.estimatedTime)
1146
+ details.push(`estimated time: ${claim.estimatedTime}`);
1147
+ if (claim.assignMethod)
1148
+ details.push(`assignment method: ${claim.assignMethod}`);
1149
+ if (claim.manualDistribution)
1150
+ details.push('manual distribution is required');
1151
+ if (details.length > 0) {
1152
+ md += `This claim uses the following configuration: ${details.join(', ')}.\n\n`;
1153
+ }
1154
+ if (claim.plugins && claim.plugins.length > 0) {
1155
+ md += 'To complete this claim, the user must satisfy the following verification steps:\n\n';
1156
+ for (const plugin of claim.plugins) {
1157
+ const pluginName = pluginDisplayName(plugin.pluginId || 'unknown plugin');
1158
+ md += `- **${pluginName}**`;
1159
+ if (plugin.publicParams) {
1160
+ const publicKeys = Object.keys(plugin.publicParams).filter((k) => k !== 'seedCode' && k !== 'preimages' && !k.startsWith('_'));
1161
+ if (publicKeys.length > 0) {
1162
+ const paramStrs = publicKeys.map((k) => `${k}: ${JSON.stringify(plugin.publicParams[k])}`);
1163
+ md += ` (${paramStrs.join(', ')})`;
1164
+ }
1165
+ }
1166
+ md += '\n';
1167
+ }
1168
+ md += '\n';
1169
+ }
1170
+ if (claim.rewards && claim.rewards.length > 0) {
1171
+ md += `Upon successful completion, the user receives ${claim.rewards.length} reward${claim.rewards.length > 1 ? 's' : ''}.\n\n`;
1172
+ }
1173
+ }
1174
+ return md;
1175
+ }
1176
+ function buildKeyReference(col) {
1177
+ let md = '## Key Reference Information\n\n';
1178
+ md += `- **Manager**: \`${col.manager || 'not set'}\`\n`;
1179
+ md += `- **Collection ID**: ${col.collectionId || '(new, not yet deployed)'}\n`;
1180
+ md += `- **Created by**: \`${col.createdBy || 'unknown'}\`\n`;
1181
+ if (col.invariants?.cosmosCoinBackedPath?.address) {
1182
+ md += `- **Backing address**: \`${col.invariants.cosmosCoinBackedPath.address}\`\n`;
1183
+ }
1184
+ if (col.collectionApprovals && col.collectionApprovals.length > 0) {
1185
+ md += '\n**Approval IDs**:\n\n';
1186
+ for (const approval of col.collectionApprovals) {
1187
+ const isMint = approval.fromList.checkAddress('Mint');
1188
+ const isBacked = approval.approvalCriteria?.allowBackedMinting;
1189
+ let type = 'transfer';
1190
+ if (isMint && isBacked)
1191
+ type = 'IBC deposit/mint';
1192
+ else if (isMint)
1193
+ type = 'mint';
1194
+ else if (isBacked)
1195
+ type = 'IBC withdrawal';
1196
+ md += `- \`${approval.approvalId}\` -- ${type} approval for ${listIdHuman(approval.toListId)}\n`;
1197
+ }
1198
+ }
1199
+ const denomsSeen = new Set();
1200
+ for (const approval of col.collectionApprovals) {
1201
+ const coinTransfers = approval.approvalCriteria?.coinTransfers;
1202
+ if (coinTransfers) {
1203
+ for (const ct of coinTransfers) {
1204
+ for (const coin of ct.coins || []) {
1205
+ if (coin.denom)
1206
+ denomsSeen.add(coin.denom);
1207
+ }
1208
+ }
1209
+ }
1210
+ }
1211
+ if (col.invariants?.cosmosCoinBackedPath?.conversion?.sideA?.denom) {
1212
+ denomsSeen.add(col.invariants.cosmosCoinBackedPath.conversion.sideA.denom);
1213
+ }
1214
+ if (denomsSeen.size > 0) {
1215
+ md += '\n**Denominations used**:\n\n';
1216
+ for (const denom of denomsSeen) {
1217
+ const entry = constants_js_1.CoinsRegistry[denom];
1218
+ if (entry) {
1219
+ md += `- \`${denom}\` = **${entry.symbol}** (${entry.decimals} decimals)\n`;
1220
+ }
1221
+ else {
1222
+ md += `- \`${denom}\` = ${denomToHuman(denom)}\n`;
1223
+ }
1224
+ }
1225
+ }
1226
+ md += '\n';
1227
+ return md;
1228
+ }
1229
+ function interpretCollection(collection) {
1230
+ const col = collection instanceof BitBadgesCollection_js_1.BitBadgesCollection ? collection : new BitBadgesCollection_js_1.BitBadgesCollection(collection);
1231
+ const linkedClaimIds = collectLinkedClaimIds(col);
1232
+ let report = '';
1233
+ report += '## How BitBadges Collections Work\n\n';
1234
+ report += 'BitBadges uses a **three-tier approval system** to control all token operations. For any transfer to succeed, it must pass through three levels of approval:\n\n';
1235
+ report += '1. **Collection-level approvals** — rules set by the collection creator that apply to everyone (described in this report)\n';
1236
+ report += '2. **Sender\'s outgoing approvals** — personal rules each holder sets for tokens leaving their account (by default, holders auto-approve their own sends)\n';
1237
+ report += '3. **Recipient\'s incoming approvals** — personal rules each address sets for tokens arriving (by default, all incoming transfers are auto-approved)\n\n';
1238
+ report += 'All three tiers must approve for a transfer to proceed. If any tier rejects, the transfer fails. This is a **default-deny** system — if no approval matches a transfer, it is rejected.\n\n';
1239
+ report += '**Permissions** control what the collection manager can change in the future. **Invariants** are permanent on-chain rules that can never be changed by anyone. Together, they determine how much you can trust that the rules described below will remain as they are.\n\n';
1240
+ report += buildOverview(col);
1241
+ report += buildBackingAndCrossChain(col);
1242
+ report += buildHowTokensAreCreated(col);
1243
+ report += buildTransferRules(col);
1244
+ report += buildPermissions(col);
1245
+ report += buildInvariants(col);
1246
+ report += buildDefaultBalances(col);
1247
+ report += buildClaims(col, linkedClaimIds);
1248
+ report += buildKeyReference(col);
1249
+ report += buildSummary(col);
1250
+ return report;
1251
+ }
1252
+ function buildSummary(col) {
1253
+ let md = '## Summary & Key Takeaways\n\n';
1254
+ const type = detectType(col);
1255
+ const metadata = col.getCollectionMetadata();
1256
+ const name = metadata?.name || 'This collection';
1257
+ md += `**${name}** is a ${type} on BitBadges. `;
1258
+ const nonMintApprovals = (0, approval_utils_js_1.getNonMintApprovals)(col.collectionApprovals);
1259
+ const transferApprovals = nonMintApprovals.filter((a) => !a.approvalCriteria?.allowBackedMinting);
1260
+ if (transferApprovals.length === 0) {
1261
+ md += 'Tokens are non-transferable (soulbound). ';
1262
+ }
1263
+ else {
1264
+ md += 'Tokens are transferable. ';
1265
+ }
1266
+ const mintApprovals = (0, approval_utils_js_1.getMintApprovals)(col.collectionApprovals);
1267
+ if (mintApprovals.length > 0) {
1268
+ md += `There ${mintApprovals.length === 1 ? 'is 1 minting rule' : `are ${mintApprovals.length} minting rules`} configured. `;
1269
+ }
1270
+ else {
1271
+ md += 'No minting is currently possible. ';
1272
+ }
1273
+ const perms = col.collectionPermissions;
1274
+ const permKeys = [
1275
+ 'canDeleteCollection', 'canArchiveCollection', 'canUpdateStandards',
1276
+ 'canUpdateCustomData', 'canUpdateManager', 'canUpdateCollectionMetadata',
1277
+ 'canUpdateValidTokenIds', 'canUpdateTokenMetadata', 'canUpdateCollectionApprovals',
1278
+ 'canUpdateAutoApproveSelfInitiatedIncomingTransfers', 'canUpdateAutoApproveSelfInitiatedOutgoingTransfers',
1279
+ 'canUpdateAutoApproveAllIncomingTransfers', 'canAddMoreAliasPaths', 'canAddMoreCosmosCoinWrapperPaths'
1280
+ ];
1281
+ let lockedCount = 0;
1282
+ for (const key of permKeys) {
1283
+ if (permState(perms[key]) === 'locked')
1284
+ lockedCount++;
1285
+ }
1286
+ const total = permKeys.length;
1287
+ md += '\n\n**Trust Level**: ';
1288
+ if (lockedCount === total) {
1289
+ md += 'Fully immutable -- no aspect of this collection can be changed by anyone.';
1290
+ }
1291
+ else if (lockedCount >= total * 0.7) {
1292
+ md += `High trust -- ${lockedCount} of ${total} permissions are permanently locked.`;
1293
+ }
1294
+ else if (lockedCount >= total * 0.4) {
1295
+ md += `Moderate trust -- ${lockedCount} of ${total} permissions are locked. The manager retains meaningful control.`;
1296
+ }
1297
+ else {
1298
+ md += `Low trust -- only ${lockedCount} of ${total} permissions are locked. The manager can change most aspects of this collection. Only use if you trust the manager.`;
1299
+ }
1300
+ const risks = [];
1301
+ if (permState(perms.canUpdateCollectionApprovals) !== 'locked')
1302
+ risks.push('transfer and minting rules can be changed');
1303
+ if (permState(perms.canUpdateValidTokenIds) !== 'locked')
1304
+ risks.push('new token IDs can be created (supply dilution)');
1305
+ if (permState(perms.canDeleteCollection) !== 'locked')
1306
+ risks.push('the collection can be deleted');
1307
+ if (!col.invariants?.noForcefulPostMintTransfers) {
1308
+ const hasForceOverride = col.collectionApprovals.some((a) => a.approvalCriteria?.overridesFromOutgoingApprovals && !a.fromList.checkAddress('Mint'));
1309
+ if (hasForceOverride)
1310
+ risks.push('forceful token seizure is possible through at least one approval');
1311
+ }
1312
+ if (risks.length > 0) {
1313
+ md += '\n\n**Key Risks**: ' + risks.join('; ') + '.';
1314
+ }
1315
+ else {
1316
+ md += '\n\n**Key Risks**: None identified -- the collection is well-locked with strong holder protections.';
1317
+ }
1318
+ md += '\n\n';
1319
+ return md;
1320
+ }
1321
+ //# sourceMappingURL=interpret.js.map