@bananapus/nana-sdk-core 2.15.0 → 2.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/cjs/review/contractWrite.d.ts +9 -1
  2. package/dist/cjs/review/contractWrite.d.ts.map +1 -1
  3. package/dist/cjs/review/contractWrite.js +20 -5
  4. package/dist/cjs/review/contractWrite.js.map +1 -1
  5. package/dist/cjs/review/decode.d.ts +138 -0
  6. package/dist/cjs/review/decode.d.ts.map +1 -0
  7. package/dist/cjs/review/decode.js +1118 -0
  8. package/dist/cjs/review/decode.js.map +1 -0
  9. package/dist/cjs/review/index.d.ts +2 -0
  10. package/dist/cjs/review/index.d.ts.map +1 -1
  11. package/dist/cjs/review/index.js +2 -0
  12. package/dist/cjs/review/index.js.map +1 -1
  13. package/dist/cjs/review/receipt.d.ts +32 -0
  14. package/dist/cjs/review/receipt.d.ts.map +1 -0
  15. package/dist/cjs/review/receipt.js +57 -0
  16. package/dist/cjs/review/receipt.js.map +1 -0
  17. package/dist/cjs/review/simulation.d.ts +47 -0
  18. package/dist/cjs/review/simulation.d.ts.map +1 -0
  19. package/dist/cjs/review/simulation.js +142 -0
  20. package/dist/cjs/review/simulation.js.map +1 -0
  21. package/dist/cjs/v6/feeBuyback.d.ts +27 -0
  22. package/dist/cjs/v6/feeBuyback.d.ts.map +1 -1
  23. package/dist/cjs/v6/feeBuyback.js +66 -0
  24. package/dist/cjs/v6/feeBuyback.js.map +1 -1
  25. package/dist/esm/review/contractWrite.d.ts +9 -1
  26. package/dist/esm/review/contractWrite.d.ts.map +1 -1
  27. package/dist/esm/review/contractWrite.js +19 -5
  28. package/dist/esm/review/contractWrite.js.map +1 -1
  29. package/dist/esm/review/decode.d.ts +138 -0
  30. package/dist/esm/review/decode.d.ts.map +1 -0
  31. package/dist/esm/review/decode.js +1102 -0
  32. package/dist/esm/review/decode.js.map +1 -0
  33. package/dist/esm/review/index.d.ts +2 -0
  34. package/dist/esm/review/index.d.ts.map +1 -1
  35. package/dist/esm/review/index.js +2 -0
  36. package/dist/esm/review/index.js.map +1 -1
  37. package/dist/esm/review/receipt.d.ts +32 -0
  38. package/dist/esm/review/receipt.d.ts.map +1 -0
  39. package/dist/esm/review/receipt.js +51 -0
  40. package/dist/esm/review/receipt.js.map +1 -0
  41. package/dist/esm/review/simulation.d.ts +47 -0
  42. package/dist/esm/review/simulation.d.ts.map +1 -0
  43. package/dist/esm/review/simulation.js +137 -0
  44. package/dist/esm/review/simulation.js.map +1 -0
  45. package/dist/esm/v6/feeBuyback.d.ts +27 -0
  46. package/dist/esm/v6/feeBuyback.d.ts.map +1 -1
  47. package/dist/esm/v6/feeBuyback.js +62 -0
  48. package/dist/esm/v6/feeBuyback.js.map +1 -1
  49. package/package.json +6 -1
@@ -0,0 +1,1118 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.knownAddressName = knownAddressName;
4
+ exports.readableValue = readableValue;
5
+ exports.namedValue = namedValue;
6
+ exports.nativeValue = nativeValue;
7
+ exports.functionFromCall = functionFromCall;
8
+ exports.reviewDescription = reviewDescription;
9
+ exports.describeV4UnlockData = describeV4UnlockData;
10
+ exports.describeUniversalRouterExecute = describeUniversalRouterExecute;
11
+ exports.describeJBHookMetadata = describeJBHookMetadata;
12
+ exports.describeSuckerClaim = describeSuckerClaim;
13
+ exports.describeSafeInnerCall = describeSafeInnerCall;
14
+ exports.describeSafeInitializer = describeSafeInitializer;
15
+ exports.describePermissionsData = describePermissionsData;
16
+ exports.describeSplitGroups = describeSplitGroups;
17
+ const viem_1 = require("viem");
18
+ const constants_js_1 = require("../constants.js");
19
+ const juicebox_js_1 = require("../generated/juicebox.js");
20
+ const permissions_js_1 = require("../v6/permissions.js");
21
+ const sticky_js_1 = require("../v6/sticky.js");
22
+ const uniswapV4Deployments_js_1 = require("../v6/uniswapV4Deployments.js");
23
+ /**
24
+ * Decode `data` as `params`, requiring their canonical encoding byte for
25
+ * byte. Dirty padding, trailing bytes or unusual offsets throw.
26
+ */
27
+ function decodeExact(params, data) {
28
+ const decoded = (0, viem_1.decodeAbiParameters)(params, data);
29
+ if ((0, viem_1.encodeAbiParameters)(params, decoded).toLowerCase() !==
30
+ data.toLowerCase()) {
31
+ throw new Error("Noncanonical ABI encoding.");
32
+ }
33
+ return decoded;
34
+ }
35
+ let addressNames;
36
+ const addressKey = (chainId, address) => `${chainId}:${address.toLowerCase()}`;
37
+ /** Every named deployment by chain and address, built on first use. */
38
+ function namedAddresses() {
39
+ if (addressNames)
40
+ return addressNames;
41
+ const names = new Map();
42
+ // A later table takes precedence for an address two tables share.
43
+ const add = (table, name) => {
44
+ for (const [chainId, address] of Object.entries(table)) {
45
+ names.set(addressKey(chainId, address), name);
46
+ }
47
+ };
48
+ const history = juicebox_js_1.jbContractAddressHistory["6"];
49
+ for (const [contract, table] of Object.entries(juicebox_js_1.jbContractAddress["6"])) {
50
+ add(table, contract in history ? `${contract} (current)` : contract);
51
+ }
52
+ // A retired generation keeps its name, so a project that still selects it
53
+ // reads as "JBRouterTerminal (previous)" rather than as an unknown address.
54
+ for (const [contract, generations] of Object.entries(history)) {
55
+ for (const [generation, table] of Object.entries(generations)) {
56
+ add(table, `${contract} (${generation})`);
57
+ }
58
+ }
59
+ add(constants_js_1.USDC_ADDRESSES, "USDC");
60
+ add(uniswapV4Deployments_js_1.UNISWAP_V4_UNIVERSAL_ROUTER_ADDRESSES, "Uniswap Universal Router");
61
+ return (addressNames = names);
62
+ }
63
+ /**
64
+ * The name of a Juicebox V6 deployment, Permit2, the Uniswap Universal Router
65
+ * or USDC at `value` on `chainId`, or null. A contract with retired
66
+ * generations names the generation: "JBBuybackHook (current)",
67
+ * "JBBuybackHook (previous)".
68
+ */
69
+ function knownAddressName(chainId, value) {
70
+ if (typeof value !== "string" || !/^0x[0-9a-f]{40}$/iu.test(value)) {
71
+ return null;
72
+ }
73
+ if (value.toLowerCase() === uniswapV4Deployments_js_1.UNISWAP_PERMIT2_ADDRESS.toLowerCase()) {
74
+ return "Permit2";
75
+ }
76
+ return namedAddresses().get(addressKey(chainId, value)) ?? null;
77
+ }
78
+ /** "Name | 0x…" for a known address, otherwise the address. */
79
+ function addressLabel(chainId, address) {
80
+ const name = knownAddressName(chainId, address);
81
+ return name ? `${name} | ${address}` : address;
82
+ }
83
+ /** Like {@link addressLabel}, for a Uniswap currency, where address zero is native ETH. */
84
+ function currencyLabel(chainId, currency) {
85
+ return currency.toLowerCase() === viem_1.zeroAddress
86
+ ? `native ETH | ${currency}`
87
+ : addressLabel(chainId, currency);
88
+ }
89
+ // ── Argument values ──────────────────────────────────────────────────────────
90
+ function stringify(value) {
91
+ try {
92
+ return (JSON.stringify(value, (_, item) => (typeof item === "bigint" ? item.toString() : item), 2) ?? String(value));
93
+ }
94
+ catch {
95
+ return String(value);
96
+ }
97
+ }
98
+ /**
99
+ * One ABI value as review text. An address carries its known name
100
+ * ("JBController | 0x…"), and long bytes are shortened; the raw payload keeps
101
+ * the full value.
102
+ */
103
+ function readableValue(type, value, chainId) {
104
+ if (value === null || value === undefined)
105
+ return "—";
106
+ if (typeof value === "bigint")
107
+ return value.toString();
108
+ if (typeof value === "boolean" || typeof value === "number") {
109
+ return String(value);
110
+ }
111
+ if (typeof value === "string") {
112
+ if (type === "address")
113
+ return addressLabel(chainId, value);
114
+ if (type.startsWith("bytes") && value.length > 50) {
115
+ return `${value.slice(0, 22)}…${value.slice(-12)}`;
116
+ }
117
+ return value;
118
+ }
119
+ return stringify(value);
120
+ }
121
+ /** A tuple component's value, from a positional array or a named object. */
122
+ function namedValue(value, name, index) {
123
+ if (Array.isArray(value))
124
+ return value[index];
125
+ if (value && typeof value === "object") {
126
+ const row = value;
127
+ return row[name] ?? row[index];
128
+ }
129
+ return undefined;
130
+ }
131
+ /** A native amount as "0.5 ETH | 500000000000000000 wei". */
132
+ function nativeValue(value = 0n) {
133
+ return `${(0, viem_1.formatEther)(value)} ETH | ${value.toString()} wei`;
134
+ }
135
+ /**
136
+ * The ABI function a reviewed call actually invokes, or null. It is the item
137
+ * named `functionName` whose selector is the calldata's first four bytes, and
138
+ * only when `args` encode to exactly that calldata. The dialog renders `args`,
139
+ * so a name alone, or arguments the bytes don't carry, never match.
140
+ */
141
+ function functionFromCall(call) {
142
+ if (!call.abi || !call.functionName)
143
+ return null;
144
+ const selector = call.data.slice(0, 10).toLowerCase();
145
+ const item = call.abi.find((entry) => entry.type === "function" &&
146
+ entry.name === call.functionName &&
147
+ (0, viem_1.toFunctionSelector)(entry) === selector);
148
+ if (!item)
149
+ return null;
150
+ try {
151
+ const encoded = (0, viem_1.encodeFunctionData)({
152
+ abi: [item],
153
+ functionName: item.name,
154
+ args: call.args ?? [],
155
+ });
156
+ return encoded.toLowerCase() === call.data.toLowerCase() ? item : null;
157
+ }
158
+ catch {
159
+ return null;
160
+ }
161
+ }
162
+ /**
163
+ * The review's lead paragraph. Callers assemble descriptions from optional
164
+ * parts, so a blank one falls back to the standing guidance instead of an
165
+ * empty banner.
166
+ */
167
+ function reviewDescription(request) {
168
+ const description = request.description?.trim();
169
+ if (description)
170
+ return description;
171
+ if (request.kind === "authorization") {
172
+ return "This authorization commits to the exact destination, native value, and calldata below. A Safe or relayer can submit that call onchain after you continue.";
173
+ }
174
+ return `This is the exact destination, native value, and calldata the app will ask your wallet to send. ${request.calls.every((call) => call.gas !== undefined || call.safeTxGas !== undefined)
175
+ ? "Your wallet adds the nonce and network fees."
176
+ : "Your wallet shows the gas limit and network fees before you send."}`;
177
+ }
178
+ const POOL_KEY = {
179
+ type: "tuple",
180
+ components: [
181
+ { type: "address" },
182
+ { type: "address" },
183
+ { type: "uint24" },
184
+ { type: "int24" },
185
+ { type: "address" },
186
+ ],
187
+ };
188
+ /** A position change: `(tokenId, liquidity, amount0, amount1, hookData)`. */
189
+ const MODIFY_LIQUIDITY = [
190
+ { type: "uint256" },
191
+ { type: "uint256" },
192
+ { type: "uint128" },
193
+ { type: "uint128" },
194
+ { type: "bytes" },
195
+ ];
196
+ /** An action list: `(bytes actions, bytes[] params)`, one param per action byte. */
197
+ const ACTION_PLAN = [{ type: "bytes" }, { type: "bytes[]" }];
198
+ /**
199
+ * Decode a Uniswap V4 PositionManager `unlockData` plan into typed steps.
200
+ * It covers the actions the apps build (increase, decrease, mint, burn, take
201
+ * pair, close, sweep) with empty hook data and canonical encodings; anything
202
+ * else is null. Amounts stay in raw token units and addresses stay raw, since
203
+ * the dialog shows the exact payload.
204
+ */
205
+ function describeV4UnlockData(value) {
206
+ if (typeof value !== "string" || !value.startsWith("0x"))
207
+ return null;
208
+ try {
209
+ const [actions, params] = decodeExact(ACTION_PLAN, value);
210
+ const codes = actions.slice(2).match(/.{2}/g) ?? [];
211
+ if (!codes.length || codes.length !== params.length)
212
+ return null;
213
+ const steps = [];
214
+ for (const [index, byte] of codes.entries()) {
215
+ const data = params[index];
216
+ switch (parseInt(byte, 16)) {
217
+ case 0x00: {
218
+ const [tokenId, liquidity, amount0Max, amount1Max, hookData] = decodeExact(MODIFY_LIQUIDITY, data);
219
+ if (hookData !== "0x")
220
+ return null;
221
+ steps.push({
222
+ action: "INCREASE_LIQUIDITY",
223
+ position: `#${tokenId}`,
224
+ liquidity,
225
+ maximumIn: { currency0: amount0Max, currency1: amount1Max },
226
+ });
227
+ break;
228
+ }
229
+ case 0x01: {
230
+ // The liquidity word is a uint256 in the PositionManager's decoder.
231
+ const [tokenId, liquidity, amount0Min, amount1Min, hookData] = decodeExact(MODIFY_LIQUIDITY, data);
232
+ if (hookData !== "0x")
233
+ return null;
234
+ steps.push({
235
+ action: "DECREASE_LIQUIDITY",
236
+ position: `#${tokenId}`,
237
+ liquidity,
238
+ minimumOut: { currency0: amount0Min, currency1: amount1Min },
239
+ });
240
+ break;
241
+ }
242
+ case 0x02: {
243
+ const [key, tickLower, tickUpper, liquidity, amount0Max, amount1Max, owner, hookData,] = decodeExact([
244
+ POOL_KEY,
245
+ { type: "int24" },
246
+ { type: "int24" },
247
+ { type: "uint256" },
248
+ { type: "uint128" },
249
+ { type: "uint128" },
250
+ { type: "address" },
251
+ { type: "bytes" },
252
+ ], data);
253
+ if (hookData !== "0x")
254
+ return null;
255
+ steps.push({
256
+ action: "MINT_POSITION",
257
+ owner,
258
+ pool: {
259
+ currency0: key[0],
260
+ currency1: key[1],
261
+ fee: key[2],
262
+ tickSpacing: key[3],
263
+ hook: key[4],
264
+ },
265
+ ticks: { lower: tickLower, upper: tickUpper },
266
+ liquidity,
267
+ maximumIn: { currency0: amount0Max, currency1: amount1Max },
268
+ });
269
+ break;
270
+ }
271
+ case 0x03: {
272
+ const [tokenId, amount0Min, amount1Min, hookData] = decodeExact([
273
+ { type: "uint256" },
274
+ { type: "uint128" },
275
+ { type: "uint128" },
276
+ { type: "bytes" },
277
+ ], data);
278
+ if (hookData !== "0x")
279
+ return null;
280
+ steps.push({
281
+ action: "BURN_POSITION",
282
+ position: `#${tokenId}`,
283
+ minimumOut: { currency0: amount0Min, currency1: amount1Min },
284
+ });
285
+ break;
286
+ }
287
+ case 0x11: {
288
+ const [currency0, currency1, recipient] = decodeExact([{ type: "address" }, { type: "address" }, { type: "address" }], data);
289
+ steps.push({ action: "TAKE_PAIR", currency0, currency1, recipient });
290
+ break;
291
+ }
292
+ case 0x12: {
293
+ const [currency] = decodeExact([{ type: "address" }], data);
294
+ steps.push({ action: "CLOSE_CURRENCY", currency });
295
+ break;
296
+ }
297
+ case 0x14: {
298
+ const [currency, recipient] = decodeExact([{ type: "address" }, { type: "address" }], data);
299
+ steps.push({ action: "SWEEP", currency, recipient });
300
+ break;
301
+ }
302
+ default:
303
+ return null;
304
+ }
305
+ }
306
+ return steps;
307
+ }
308
+ catch {
309
+ return null;
310
+ }
311
+ }
312
+ // ── Uniswap Universal Router ─────────────────────────────────────────────────
313
+ /** Universal Router sentinels the direct-pay swap builders use. */
314
+ const UR_MSG_SENDER = "0x0000000000000000000000000000000000000001";
315
+ const UR_ADDRESS_THIS = "0x0000000000000000000000000000000000000002";
316
+ const UR_CONTRACT_BALANCE = 0x8000000000000000000000000000000000000000000000000000000000000000n;
317
+ const CONTRACT_BALANCE_TEXT = "the router's entire balance from the previous step";
318
+ function urRecipient(chainId, address) {
319
+ if (address.toLowerCase() === UR_MSG_SENDER)
320
+ return "you (msg.sender)";
321
+ if (address.toLowerCase() === UR_ADDRESS_THIS) {
322
+ return "the router (kept for the next step)";
323
+ }
324
+ return addressLabel(chainId, address);
325
+ }
326
+ /** A V3 swap or wrap amount: the contract-balance sentinel, else the number. */
327
+ function routerAmount(value) {
328
+ return value === UR_CONTRACT_BALANCE
329
+ ? CONTRACT_BALANCE_TEXT
330
+ : value.toString();
331
+ }
332
+ /** A V4 swap or take amount, where 0 takes the open delta the previous step left. */
333
+ function openDeltaAmount(value) {
334
+ return value === 0n
335
+ ? "0 (the open amount from the previous step)"
336
+ : value.toString();
337
+ }
338
+ /** A V4 settle amount, which reads both the contract-balance sentinel and 0. */
339
+ function settleAmount(value) {
340
+ return value === UR_CONTRACT_BALANCE
341
+ ? CONTRACT_BALANCE_TEXT
342
+ : openDeltaAmount(value);
343
+ }
344
+ /** Who pays a router or V4 step. */
345
+ function payer(payerIsUser) {
346
+ return payerIsUser ? "you (via Permit2)" : "the router's balance";
347
+ }
348
+ /**
349
+ * Unix seconds as a UTC time and the raw value, the same in every locale. A
350
+ * value past the last date JavaScript can show (a max uint48, say) keeps its
351
+ * raw value and says so.
352
+ */
353
+ function unixTime(seconds) {
354
+ const value = BigInt(seconds);
355
+ if (value > 8640000000000n)
356
+ return `${value} (after year 275760)`;
357
+ const time = new Date(Number(value) * 1000).toISOString();
358
+ return `${time.replace(".000Z", " UTC").replace("T", " ")} (${value})`;
359
+ }
360
+ /** A packed V3 path: 20-byte token, 3-byte fee, 20-byte token, … */
361
+ function urV3Path(chainId, path) {
362
+ const raw = path.slice(2);
363
+ if (raw.length < 86 || (raw.length - 40) % 46 !== 0)
364
+ return null;
365
+ const parts = [addressLabel(chainId, `0x${raw.slice(0, 40)}`)];
366
+ for (let offset = 40; offset < raw.length; offset += 46) {
367
+ const fee = parseInt(raw.slice(offset, offset + 6), 16);
368
+ parts.push(`-${fee / 10_000}%→`, addressLabel(chainId, `0x${raw.slice(offset + 6, offset + 46)}`));
369
+ }
370
+ return parts.join(" ");
371
+ }
372
+ /** `SWAP_EXACT_IN_SINGLE`: `(poolKey, zeroForOne, amountIn, amountOutMinimum, hookData)`. */
373
+ const V4_EXACT_IN_SINGLE = [
374
+ {
375
+ type: "tuple",
376
+ components: [
377
+ POOL_KEY,
378
+ { type: "bool" },
379
+ { type: "uint128" },
380
+ { type: "uint128" },
381
+ { type: "bytes" },
382
+ ],
383
+ },
384
+ ];
385
+ /** The V4_SWAP command's inner action plan, in the shapes the pay builders emit. */
386
+ function urV4SwapSteps(chainId, input) {
387
+ const [actions, params] = decodeExact(ACTION_PLAN, input);
388
+ const codes = actions.slice(2).match(/.{2}/g) ?? [];
389
+ if (!codes.length || codes.length !== params.length)
390
+ return null;
391
+ const steps = [];
392
+ for (const [index, byte] of codes.entries()) {
393
+ const data = params[index];
394
+ switch (parseInt(byte, 16)) {
395
+ case 0x06: {
396
+ const [[key, zeroForOne, amountIn, minimumOut, hookData]] = decodeExact(V4_EXACT_IN_SINGLE, data);
397
+ if (hookData !== "0x")
398
+ return null;
399
+ steps.push({
400
+ title: "Swap in the project's V4 pool (exact input)",
401
+ rows: [
402
+ ["Sell", currencyLabel(chainId, zeroForOne ? key[0] : key[1])],
403
+ ["Buy", currencyLabel(chainId, zeroForOne ? key[1] : key[0])],
404
+ ["Amount in", openDeltaAmount(amountIn)],
405
+ ["Minimum out", `${minimumOut} — reverts below this`],
406
+ ["Fee", `${key[2]} (${key[2] / 10_000}%) | tick spacing ${key[3]}`],
407
+ ["Hook", addressLabel(chainId, key[4])],
408
+ ],
409
+ });
410
+ break;
411
+ }
412
+ case 0x0b: {
413
+ const [currency, amount, payerIsUser] = decodeExact([{ type: "address" }, { type: "uint256" }, { type: "bool" }], data);
414
+ steps.push({
415
+ title: "Pay the pool",
416
+ rows: [
417
+ ["Currency", currencyLabel(chainId, currency)],
418
+ ["Amount", settleAmount(amount)],
419
+ ["Paid by", payer(payerIsUser)],
420
+ ],
421
+ });
422
+ break;
423
+ }
424
+ case 0x0c: {
425
+ const [currency, maximum] = decodeExact([{ type: "address" }, { type: "uint256" }], data);
426
+ steps.push({
427
+ title: "Pay the pool everything owed",
428
+ rows: [
429
+ ["Currency", currencyLabel(chainId, currency)],
430
+ ["At most", maximum.toString()],
431
+ ],
432
+ });
433
+ break;
434
+ }
435
+ case 0x0e: {
436
+ const [currency, recipient, amount] = decodeExact([{ type: "address" }, { type: "address" }, { type: "uint256" }], data);
437
+ steps.push({
438
+ title: "Take the swap output",
439
+ rows: [
440
+ ["Currency", currencyLabel(chainId, currency)],
441
+ ["Recipient", urRecipient(chainId, recipient)],
442
+ ["Amount", openDeltaAmount(amount)],
443
+ ],
444
+ });
445
+ break;
446
+ }
447
+ default:
448
+ return null;
449
+ }
450
+ }
451
+ return steps;
452
+ }
453
+ /**
454
+ * Decode a Uniswap Universal Router `execute(commands, inputs, deadline)` into
455
+ * readable steps. It covers the command shapes the pay flow builds (Permit2
456
+ * permit, wrap, V3 hop, unwrap, V4 swap) in canonical encodings; anything
457
+ * else is null.
458
+ */
459
+ function describeUniversalRouterExecute(chainId, args) {
460
+ if (!args || args.length < 2)
461
+ return null;
462
+ const [commands, inputs] = args;
463
+ if (typeof commands !== "string" ||
464
+ !commands.startsWith("0x") ||
465
+ !Array.isArray(inputs)) {
466
+ return null;
467
+ }
468
+ try {
469
+ const codes = commands.slice(2).match(/.{2}/g) ?? [];
470
+ if (!codes.length || codes.length !== inputs.length)
471
+ return null;
472
+ const steps = [];
473
+ for (const [index, byte] of codes.entries()) {
474
+ const data = inputs[index];
475
+ switch (parseInt(byte, 16)) {
476
+ case 0x00: {
477
+ const [recipient, amountIn, minimumOut, path, payerIsUser] = decodeExact([
478
+ { type: "address" },
479
+ { type: "uint256" },
480
+ { type: "uint256" },
481
+ { type: "bytes" },
482
+ { type: "bool" },
483
+ ], data);
484
+ const route = urV3Path(chainId, path);
485
+ if (!route)
486
+ return null;
487
+ steps.push({
488
+ title: "Swap through a V3 pool (exact input)",
489
+ rows: [
490
+ ["Route", route],
491
+ ["Amount in", routerAmount(amountIn)],
492
+ [
493
+ "Minimum out",
494
+ minimumOut === 0n
495
+ ? "0 — the final V4 minimum below is the real floor"
496
+ : minimumOut.toString(),
497
+ ],
498
+ ["Paid by", payer(payerIsUser)],
499
+ ["Recipient", urRecipient(chainId, recipient)],
500
+ ],
501
+ });
502
+ break;
503
+ }
504
+ case 0x0a: {
505
+ const [[details, spender, sigDeadline]] = decodeExact([
506
+ {
507
+ type: "tuple",
508
+ components: [
509
+ {
510
+ type: "tuple",
511
+ components: [
512
+ { type: "address" },
513
+ { type: "uint160" },
514
+ { type: "uint48" },
515
+ { type: "uint48" },
516
+ ],
517
+ },
518
+ { type: "address" },
519
+ { type: "uint256" },
520
+ ],
521
+ },
522
+ { type: "bytes" },
523
+ ], data);
524
+ steps.push({
525
+ title: "Apply your signed Permit2 authorization",
526
+ rows: [
527
+ ["Token", addressLabel(chainId, details[0])],
528
+ ["Amount", details[1].toString()],
529
+ ["Spender", addressLabel(chainId, spender)],
530
+ [
531
+ "Expires",
532
+ // Permit2 stores an expiration of 0 as the current block's timestamp.
533
+ details[2] === 0 ? "0 (this block only)" : unixTime(details[2]),
534
+ ],
535
+ ["Signature deadline", unixTime(sigDeadline)],
536
+ ],
537
+ });
538
+ break;
539
+ }
540
+ case 0x0b: {
541
+ const [recipient, amount] = decodeExact([{ type: "address" }, { type: "uint256" }], data);
542
+ steps.push({
543
+ title: "Wrap ETH into WETH",
544
+ rows: [
545
+ ["Amount", routerAmount(amount)],
546
+ ["Recipient", urRecipient(chainId, recipient)],
547
+ ],
548
+ });
549
+ break;
550
+ }
551
+ case 0x0c: {
552
+ const [recipient, minimum] = decodeExact([{ type: "address" }, { type: "uint256" }], data);
553
+ steps.push({
554
+ title: "Unwrap WETH back to ETH",
555
+ rows: [
556
+ [
557
+ "Minimum",
558
+ minimum === 0n ? "0 (no minimum)" : minimum.toString(),
559
+ ],
560
+ ["Recipient", urRecipient(chainId, recipient)],
561
+ ],
562
+ });
563
+ break;
564
+ }
565
+ case 0x10: {
566
+ const inner = urV4SwapSteps(chainId, data);
567
+ if (!inner)
568
+ return null;
569
+ steps.push(...inner);
570
+ break;
571
+ }
572
+ default:
573
+ return null;
574
+ }
575
+ }
576
+ return steps;
577
+ }
578
+ catch {
579
+ return null;
580
+ }
581
+ }
582
+ // ── JB hook metadata (JBMetadataResolver envelope) ───────────────────────────
583
+ const bigintJson = (value) => JSON.stringify(value, (_, item) => typeof item === "bigint" ? item.toString() : item);
584
+ /** Strict decode and byte-exact re-encode, else null. */
585
+ function roundTripDecode(types, payload) {
586
+ try {
587
+ return decodeExact(types, payload);
588
+ }
589
+ catch {
590
+ return null;
591
+ }
592
+ }
593
+ /**
594
+ * Parse the JBMetadataResolver layout exactly as `getDataFor` reads it: a
595
+ * 32-byte reserved word, a word-padded table of `(bytes4 id, uint8 wordOffset)`
596
+ * entries, then word-aligned payload segments. Offsets must strictly increase
597
+ * and the segments must tile the rest of the bytes.
598
+ */
599
+ function parseHookMetadataEnvelope(value) {
600
+ if (typeof value !== "string" || !/^0x([0-9a-fA-F]{2})+$/.test(value)) {
601
+ return null;
602
+ }
603
+ const body = value.slice(2).toLowerCase();
604
+ if (body.length % 64 !== 0)
605
+ return null;
606
+ const totalWords = body.length / 64;
607
+ // A reserved word, a table word, and at least one payload word.
608
+ if (totalWords < 3)
609
+ return null;
610
+ const firstOffset = parseInt(body.slice(64 + 8, 64 + 10), 16);
611
+ const tableWords = firstOffset - 1;
612
+ if (tableWords < 1 || firstOffset >= totalWords)
613
+ return null;
614
+ const tableArea = body.slice(64, 64 + tableWords * 64);
615
+ const entries = [];
616
+ let cursor = 0;
617
+ while (cursor + 10 <= tableArea.length) {
618
+ const chunk = tableArea.slice(cursor, cursor + 10);
619
+ if (/^0+$/.test(chunk))
620
+ break;
621
+ const id = chunk.slice(0, 8);
622
+ const offset = parseInt(chunk.slice(8, 10), 16);
623
+ // A zero id with a nonzero offset is malformed. `getDataFor` returns the
624
+ // first entry for an id, so a repeated id would show dead data as live.
625
+ if (/^0+$/.test(id) || entries.some((entry) => entry.id === `0x${id}`)) {
626
+ return null;
627
+ }
628
+ entries.push({ id: `0x${id}`, offset });
629
+ cursor += 10;
630
+ }
631
+ // The rest of the table must be zero padding.
632
+ if (!/^0*$/.test(tableArea.slice(cursor)))
633
+ return null;
634
+ // The entry count must be what sized the table.
635
+ if (Math.ceil((entries.length * 5) / 32) !== tableWords)
636
+ return null;
637
+ // Offsets must ascend and the payloads must tile the remaining bytes exactly.
638
+ for (let i = 0; i < entries.length; i++) {
639
+ if (entries[i].offset >= totalWords)
640
+ return null;
641
+ if (i > 0 && entries[i].offset <= entries[i - 1].offset)
642
+ return null;
643
+ }
644
+ const segments = entries.map((entry, i) => {
645
+ const start = entry.offset * 64;
646
+ const end = i + 1 < entries.length ? entries[i + 1].offset * 64 : body.length;
647
+ return { id: entry.id, payload: `0x${body.slice(start, end)}` };
648
+ });
649
+ return { reserved: `0x${body.slice(0, 64)}`, entries: segments };
650
+ }
651
+ /** Repeated tier ids as "2× #4", in first-seen order. */
652
+ function tierIdCounts(tierIds) {
653
+ const counts = new Map();
654
+ for (const id of tierIds)
655
+ counts.set(id, (counts.get(id) ?? 0) + 1);
656
+ return [...counts.entries()]
657
+ .map(([id, count]) => (count > 1 ? `${count}× #${id}` : `#${id}`))
658
+ .join(", ");
659
+ }
660
+ /**
661
+ * Decode a `pay`/`addToBalanceOf`/`cashOutTokensOf` `metadata` argument into
662
+ * its hook entries, reading the payload shapes the ecosystem's builders
663
+ * produce (721 mints and redeems, buyback routing). A payload that matches
664
+ * several shapes is reported as ambiguous instead of picking one.
665
+ */
666
+ function describeJBHookMetadata(context, value) {
667
+ const envelope = parseHookMetadataEnvelope(value);
668
+ if (!envelope)
669
+ return null;
670
+ const steps = [];
671
+ if (!/^0x0+$/.test(envelope.reserved)) {
672
+ steps.push({
673
+ title: "Protocol-reserved word (nonzero)",
674
+ rows: [["Value", envelope.reserved]],
675
+ });
676
+ }
677
+ for (const entry of envelope.entries) {
678
+ const payloadWords = (entry.payload.length - 2) / 64;
679
+ const base = [["Hook lookup id", entry.id]];
680
+ // Every known shape that round-trips byte for byte. Exactly one is an
681
+ // interpretation; several (degenerate payloads such as empty arrays) are
682
+ // ambiguous.
683
+ const readings = [];
684
+ if (context === "pay") {
685
+ const mint = roundTripDecode([{ type: "bool" }, { type: "uint16[]" }], entry.payload);
686
+ if (mint) {
687
+ readings.push({
688
+ title: "721 shop mint instructions",
689
+ rows: [
690
+ ...base,
691
+ [
692
+ "Tier IDs to mint",
693
+ mint[1].length ? tierIdCounts(mint[1]) : "none (credits only)",
694
+ ],
695
+ [
696
+ "Allow overspending",
697
+ mint[0]
698
+ ? "yes — excess becomes pay credits"
699
+ : "no — any excess reverts",
700
+ ],
701
+ ],
702
+ });
703
+ }
704
+ if (payloadWords === 3) {
705
+ const buyback = roundTripDecode([{ type: "uint256" }, { type: "uint256" }, { type: "bool" }], entry.payload);
706
+ if (buyback) {
707
+ readings.push({
708
+ title: "Buyback hook swap instructions",
709
+ rows: [
710
+ ...base,
711
+ ["Amount to swap", buyback[0].toString()],
712
+ ["Minimum swap output", `${buyback[1]} — reverts below this`],
713
+ ["Skip splits on swapped tokens", buyback[2] ? "yes" : "no"],
714
+ ],
715
+ });
716
+ }
717
+ }
718
+ }
719
+ else {
720
+ if (payloadWords === 2) {
721
+ const buyback = roundTripDecode([{ type: "uint256" }, { type: "bool" }], entry.payload);
722
+ if (buyback) {
723
+ readings.push({
724
+ title: "Buyback hook cash-out routing",
725
+ rows: [
726
+ ...base,
727
+ ["Minimum swap output", buyback[0].toString()],
728
+ [
729
+ "Force the direct terminal path",
730
+ buyback[1] ? "yes — never route through the pool" : "no",
731
+ ],
732
+ ],
733
+ });
734
+ }
735
+ }
736
+ const redeem = roundTripDecode([{ type: "uint256[]" }], entry.payload);
737
+ if (redeem) {
738
+ readings.push({
739
+ title: "721 shop items to redeem",
740
+ rows: [
741
+ ...base,
742
+ [
743
+ "Token IDs",
744
+ redeem[0].length
745
+ ? redeem[0].map((id) => `#${id}`).join(", ")
746
+ : "none",
747
+ ],
748
+ ],
749
+ });
750
+ }
751
+ }
752
+ if (readings.length === 1) {
753
+ steps.push(readings[0]);
754
+ }
755
+ else if (readings.length > 1) {
756
+ steps.push({
757
+ title: "Payload matches multiple known shapes — verify against the raw bytes",
758
+ rows: [
759
+ ...base,
760
+ ...readings.map((reading, i) => [
761
+ `Reading ${i + 1}`,
762
+ `${reading.title}: ${reading.rows
763
+ .slice(base.length)
764
+ .map(([label, val]) => `${label.toLowerCase()}: ${val}`)
765
+ .join("; ")}`,
766
+ ]),
767
+ ],
768
+ });
769
+ }
770
+ else {
771
+ steps.push({
772
+ title: `Unrecognized hook payload (${payloadWords} word${payloadWords === 1 ? "" : "s"})`,
773
+ rows: [...base, ["Payload", entry.payload]],
774
+ });
775
+ }
776
+ }
777
+ return steps;
778
+ }
779
+ // ── Sucker bridge claim ──────────────────────────────────────────────────────
780
+ /** A bytes32 that is a left-padded address reads as the address. */
781
+ function paddedAddress(value) {
782
+ if (/^0x000000000000000000000000[0-9a-fA-F]{40}$/.test(value)) {
783
+ return `0x${value.slice(26)}`;
784
+ }
785
+ return value;
786
+ }
787
+ /** A sucker `claim` argument: its leaf, with the 32-hash proof summarized. */
788
+ function describeSuckerClaim(chainId, value) {
789
+ const claim = value;
790
+ if (!claim ||
791
+ typeof claim.token !== "string" ||
792
+ !claim.leaf ||
793
+ typeof claim.leaf.beneficiary !== "string" ||
794
+ typeof claim.leaf.index !== "bigint" ||
795
+ typeof claim.leaf.projectTokenCount !== "bigint" ||
796
+ typeof claim.leaf.terminalTokenAmount !== "bigint" ||
797
+ !Array.isArray(claim.proof) ||
798
+ claim.proof.length !== 32 ||
799
+ !claim.proof.every((hash) => typeof hash === "string" && /^0x[0-9a-fA-F]{64}$/.test(hash))) {
800
+ return null;
801
+ }
802
+ const rows = [
803
+ ["Terminal token", addressLabel(chainId, claim.token)],
804
+ ["Leaf index", claim.leaf.index.toString()],
805
+ ["Beneficiary", paddedAddress(claim.leaf.beneficiary)],
806
+ ["Project tokens", claim.leaf.projectTokenCount.toString()],
807
+ ["Terminal token amount", claim.leaf.terminalTokenAmount.toString()],
808
+ ];
809
+ if (typeof claim.leaf.metadata === "string" &&
810
+ !/^0x0+$/.test(claim.leaf.metadata)) {
811
+ rows.push(["Leaf metadata", claim.leaf.metadata]);
812
+ }
813
+ rows.push([
814
+ "Merkle proof",
815
+ "32 hashes — exact bytes in the raw payload below",
816
+ ]);
817
+ return [
818
+ { title: "Claim a bridged balance from the sucker's inbox tree", rows },
819
+ ];
820
+ }
821
+ // ── Safe execTransaction inner call ──────────────────────────────────────────
822
+ let safeInnerAbis;
823
+ /** The ABI a queued Safe call to a Juicebox deployment is read with, by deployment name. */
824
+ function safeInnerAbiOf(contract) {
825
+ safeInnerAbis ??= new Map([
826
+ ["JBController", juicebox_js_1.jbControllerAbi],
827
+ ["JBMultiTerminal", juicebox_js_1.jbMultiTerminalAbi],
828
+ ["JBDirectory", juicebox_js_1.jbDirectoryAbi],
829
+ ["JBTokens", juicebox_js_1.jbTokensAbi],
830
+ ["JBPermissions", juicebox_js_1.jbPermissionsAbi],
831
+ ["JBSplits", juicebox_js_1.jbSplitsAbi],
832
+ ["JBProjects", juicebox_js_1.jbProjectsAbi],
833
+ ["JBBuybackHookRegistry", juicebox_js_1.jbBuybackHookRegistryAbi],
834
+ ["JBBuybackHook", juicebox_js_1.jbBuybackHookAbi],
835
+ ["JBRouterTerminalRegistry", juicebox_js_1.jbRouterTerminalRegistryAbi],
836
+ ["JBRouterTerminalGateway", juicebox_js_1.jbRouterTerminalGatewayAbi],
837
+ ]);
838
+ return safeInnerAbis.get(contract);
839
+ }
840
+ /**
841
+ * The call a Safe `execTransaction` makes: `data` sent to `to`. Several of
842
+ * these contracts share selectors (`pay` on the terminal and the router
843
+ * gateway, `approve` on JBProjects and any ERC-20), so the ABI comes from the
844
+ * target. A known Juicebox deployment, a retired generation included, is read
845
+ * with its contract's ABI, and USDC as ERC-20. An unknown target is read as
846
+ * ERC-20 and titled as unrecognized, except for `approve` and `transferFrom`:
847
+ * ERC-721 shares those selectors with a token ID where ERC-20 has an amount,
848
+ * so they are null. Any other target, a selector its ABI lacks, or a
849
+ * non-canonical encoding is null too.
850
+ */
851
+ function describeSafeInnerCall(chainId, to, data) {
852
+ if (typeof data !== "string" ||
853
+ !/^0x(?:[0-9a-f]{2})+$/iu.test(data) ||
854
+ data.length < 10) {
855
+ return null;
856
+ }
857
+ if (typeof to !== "string" || !/^0x[0-9a-f]{40}$/iu.test(to))
858
+ return null;
859
+ const target = knownAddressName(chainId, to);
860
+ const abi = target === null || target === "USDC"
861
+ ? viem_1.erc20Abi
862
+ : safeInnerAbiOf(target.replace(/ \([^)]*\)$/u, ""));
863
+ if (!abi)
864
+ return null;
865
+ const selector = data.slice(0, 10).toLowerCase();
866
+ if (target === null &&
867
+ (selector === "0x095ea7b3" || selector === "0x23b872dd")) {
868
+ return null;
869
+ }
870
+ const item = abi.find((entry) => entry.type === "function" && (0, viem_1.toFunctionSelector)(entry) === selector);
871
+ if (!item)
872
+ return null;
873
+ let args;
874
+ try {
875
+ args = (0, viem_1.decodeFunctionData)({ abi: [item], data: data }).args ?? [];
876
+ const canonical = (0, viem_1.encodeFunctionData)({
877
+ abi: [item],
878
+ functionName: item.name,
879
+ args,
880
+ });
881
+ if (canonical.toLowerCase() !== data.toLowerCase())
882
+ return null;
883
+ }
884
+ catch {
885
+ return null;
886
+ }
887
+ const rows = args.map((argument, index) => [
888
+ item.inputs[index].name || `argument ${index + 1}`,
889
+ bigintJson(argument),
890
+ ]);
891
+ return [
892
+ {
893
+ title: target === null
894
+ ? `Queued call — ${item.name}(…) on an unrecognized contract, read as ERC-20`
895
+ : `Queued call — ${target}.${item.name}(…)`,
896
+ rows: rows.length ? rows : [["Arguments", "none"]],
897
+ },
898
+ ];
899
+ }
900
+ // ── Safe proxy initializer ───────────────────────────────────────────────────
901
+ const safeSetupAbi = [
902
+ {
903
+ type: "function",
904
+ name: "setup",
905
+ stateMutability: "nonpayable",
906
+ inputs: [
907
+ { name: "_owners", type: "address[]" },
908
+ { name: "_threshold", type: "uint256" },
909
+ { name: "to", type: "address" },
910
+ { name: "data", type: "bytes" },
911
+ { name: "fallbackHandler", type: "address" },
912
+ { name: "paymentToken", type: "address" },
913
+ { name: "payment", type: "uint256" },
914
+ { name: "paymentReceiver", type: "address" },
915
+ ],
916
+ outputs: [],
917
+ },
918
+ ];
919
+ const safeToL2SetupAbi = [
920
+ {
921
+ type: "function",
922
+ name: "setupToL2",
923
+ stateMutability: "nonpayable",
924
+ inputs: [{ name: "l2Singleton", type: "address" }],
925
+ outputs: [],
926
+ },
927
+ ];
928
+ /** The canonical SafeToL2Setup deployment, the same address on every chain. */
929
+ const SAFE_TO_L2_SETUP = "0xbd89a1ce4dde368ffab0ec35506eece0b1ffdc54";
930
+ /** A Safe proxy `initializer`: owners, threshold, setup hook and any payment. */
931
+ function describeSafeInitializer(chainId, value) {
932
+ if (typeof value !== "string" || !value.startsWith("0x"))
933
+ return null;
934
+ const args = (() => {
935
+ try {
936
+ return (0, viem_1.decodeFunctionData)({ abi: safeSetupAbi, data: value }).args;
937
+ }
938
+ catch {
939
+ return null;
940
+ }
941
+ })();
942
+ if (!args)
943
+ return null;
944
+ // A noncanonical encoding could make the summary disagree with the bytes.
945
+ const canonical = (0, viem_1.encodeFunctionData)({
946
+ abi: safeSetupAbi,
947
+ functionName: "setup",
948
+ args,
949
+ });
950
+ if (canonical.toLowerCase() !== value.toLowerCase())
951
+ return null;
952
+ const [owners, threshold, to, data, fallbackHandler, paymentToken, payment, paymentReceiver,] = args;
953
+ const rows = [
954
+ ["Owners", owners.join(", ") || "none"],
955
+ ["Threshold", `${threshold} of ${owners.length}`],
956
+ ["Fallback handler", addressLabel(chainId, fallbackHandler)],
957
+ ];
958
+ if (to.toLowerCase() === viem_1.zeroAddress) {
959
+ // Safe runs the setup delegatecall only when `to` is set.
960
+ rows.push([
961
+ "Setup hook",
962
+ data === "0x"
963
+ ? "none"
964
+ : "none — Safe ignores the setup data when `to` is zero",
965
+ ]);
966
+ }
967
+ else {
968
+ let hook = `DELEGATECALL to ${to} — data in the raw payload below`;
969
+ // Only the canonical deployment is known to run SafeToL2Setup's code; any
970
+ // other target could run anything under the same calldata.
971
+ if (to.toLowerCase() === SAFE_TO_L2_SETUP) {
972
+ try {
973
+ const inner = (0, viem_1.decodeFunctionData)({ abi: safeToL2SetupAbi, data });
974
+ const canonicalInner = (0, viem_1.encodeFunctionData)({
975
+ abi: safeToL2SetupAbi,
976
+ functionName: "setupToL2",
977
+ args: inner.args,
978
+ });
979
+ if (canonicalInner.toLowerCase() === data.toLowerCase()) {
980
+ hook = `SafeToL2Setup.setupToL2(${inner.args[0]}) via ${to}`;
981
+ }
982
+ }
983
+ catch {
984
+ // Keep the generic delegatecall warning.
985
+ }
986
+ }
987
+ rows.push(["Setup hook", hook]);
988
+ }
989
+ if (payment !== 0n || paymentToken.toLowerCase() !== viem_1.zeroAddress) {
990
+ rows.push([
991
+ "Deployment payment",
992
+ `${payment} of ${currencyLabel(chainId, paymentToken)} to ${paymentReceiver} — unusual, verify`,
993
+ ]);
994
+ }
995
+ return [{ title: "Safe setup", rows }];
996
+ }
997
+ // ── Permission grants ────────────────────────────────────────────────────────
998
+ /** A `setPermissionsFor` grant, naming each permission id from the V6 catalog. */
999
+ function describePermissionsData(chainId, value) {
1000
+ const data = value;
1001
+ if (!data ||
1002
+ typeof data.operator !== "string" ||
1003
+ (typeof data.projectId !== "bigint" &&
1004
+ typeof data.projectId !== "number") ||
1005
+ !Array.isArray(data.permissionIds) ||
1006
+ !data.permissionIds.every((id) => typeof id === "number" && Number.isInteger(id))) {
1007
+ return null;
1008
+ }
1009
+ const permissionIds = data.permissionIds;
1010
+ const projectId = BigInt(data.projectId);
1011
+ const names = permissionIds.map((id) => {
1012
+ const name = (0, permissions_js_1.permissionKeyV6)(id);
1013
+ return name ? `${name} (${id})` : `UNKNOWN PERMISSION (${id})`;
1014
+ });
1015
+ const rows = [
1016
+ ["Operator", addressLabel(chainId, data.operator)],
1017
+ [
1018
+ "Scope",
1019
+ projectId === 0n
1020
+ ? "project 0 — EVERY project this account ever owns"
1021
+ : `project #${projectId}`,
1022
+ ],
1023
+ [
1024
+ "Permissions",
1025
+ names.length
1026
+ ? names.join(", ")
1027
+ : "none — revokes everything previously granted",
1028
+ ],
1029
+ ];
1030
+ if (permissionIds.includes(1)) {
1031
+ rows.push([
1032
+ "Warning",
1033
+ "ROOT grants every permission across all Juicebox contracts",
1034
+ ]);
1035
+ }
1036
+ return [{ title: "Set operator permissions", rows }];
1037
+ }
1038
+ // ── Split groups ─────────────────────────────────────────────────────────────
1039
+ function splitPercent(percent) {
1040
+ const share = (percent * 100) / constants_js_1.SPLITS_TOTAL_PERCENT;
1041
+ return `${Number(share.toFixed(4))}%`;
1042
+ }
1043
+ /** Whether `hook` is this chain's StickyDistributor (false where Sticky is not deployed). */
1044
+ function isStickyHook(hook, chainId) {
1045
+ try {
1046
+ return (0, sticky_js_1.isStickySplit)({ hook: hook }, chainId);
1047
+ }
1048
+ catch {
1049
+ return false;
1050
+ }
1051
+ }
1052
+ /**
1053
+ * `setSplitGroupsOf` groups: each split's share, recipient, hook and lock,
1054
+ * with an honest total. A Sticky split names the holder group it pays.
1055
+ */
1056
+ function describeSplitGroups(chainId, value) {
1057
+ if (!Array.isArray(value))
1058
+ return null;
1059
+ const steps = [];
1060
+ for (const group of value) {
1061
+ if (typeof group?.groupId !== "bigint" || !Array.isArray(group.splits)) {
1062
+ return null;
1063
+ }
1064
+ const groupLabel = group.groupId === 1n
1065
+ ? "Reserved tokens"
1066
+ : group.groupId < 1n << 160n
1067
+ ? `Payouts of ${addressLabel(chainId, `0x${group.groupId.toString(16).padStart(40, "0")}`)}`
1068
+ : `Group ${group.groupId}`;
1069
+ const rows = [];
1070
+ let total = 0;
1071
+ for (const [index, split] of group.splits.entries()) {
1072
+ if (typeof split?.percent !== "number" ||
1073
+ typeof split.beneficiary !== "string" ||
1074
+ typeof split.projectId !== "bigint" ||
1075
+ (split.lockedUntil !== undefined &&
1076
+ !Number.isSafeInteger(split.lockedUntil))) {
1077
+ return null;
1078
+ }
1079
+ total += split.percent;
1080
+ // A Sticky split's projectId is its holder group and its beneficiary is the Sticky token.
1081
+ const sticky = typeof split.hook === "string" && isStickyHook(split.hook, chainId);
1082
+ const parts = [
1083
+ sticky
1084
+ ? `${(0, sticky_js_1.describeStickySplit)({ projectId: split.projectId })} → Sticky token ${addressLabel(chainId, split.beneficiary)}`
1085
+ : split.projectId !== 0n
1086
+ ? `project #${split.projectId} (beneficiary ${split.beneficiary})`
1087
+ : addressLabel(chainId, split.beneficiary),
1088
+ ];
1089
+ if (sticky) {
1090
+ parts.push(`via StickyDistributor ${split.hook}`);
1091
+ }
1092
+ else if (typeof split.hook === "string" &&
1093
+ split.hook.toLowerCase() !== viem_1.zeroAddress) {
1094
+ parts.push(`via hook ${split.hook}`);
1095
+ }
1096
+ if (split.preferAddToBalance === true) {
1097
+ parts.push("prefers add-to-balance");
1098
+ }
1099
+ if (typeof split.lockedUntil === "number" && split.lockedUntil > 0) {
1100
+ parts.push(`locked until ${unixTime(split.lockedUntil)}`);
1101
+ }
1102
+ rows.push([
1103
+ `Split ${index + 1} — ${splitPercent(split.percent)}`,
1104
+ parts.join(" | "),
1105
+ ]);
1106
+ }
1107
+ rows.push([
1108
+ "Total",
1109
+ `${splitPercent(total)}${total === constants_js_1.SPLITS_TOTAL_PERCENT ? "" : " — the remainder follows the ruleset's default"}`,
1110
+ ]);
1111
+ steps.push({
1112
+ title: groupLabel,
1113
+ rows: rows.length > 1 ? rows : [["Splits", "none"]],
1114
+ });
1115
+ }
1116
+ return steps.length ? steps : null;
1117
+ }
1118
+ //# sourceMappingURL=decode.js.map