@xgamesfun/solana 0.20.1 → 0.25.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 (112) hide show
  1. package/README.md +95 -52
  2. package/bin/xsolana.mjs +23 -8
  3. package/examples/blind-forge.json +2 -4
  4. package/examples/coin-flip-arena.json +4 -12
  5. package/examples/duel-arena.json +1 -4
  6. package/examples/elements-set.json +2 -9
  7. package/examples/expedition-guild.json +1 -8
  8. package/examples/fair-presale.json +1 -3
  9. package/examples/guild-missions.json +3 -10
  10. package/examples/halving-seasons.json +4 -9
  11. package/examples/hero-trials.json +15 -6
  12. package/examples/holder-den.json +38 -0
  13. package/examples/holders-drop.json +105 -0
  14. package/examples/ideas.json +788 -788
  15. package/examples/match-predictions.json +24 -0
  16. package/examples/merge-2048.json +5 -5
  17. package/examples/nft-raffle.json +1 -0
  18. package/examples/pack-league.json +108 -0
  19. package/examples/paper-hands-farm.json +4 -16
  20. package/examples/partner-vault.json +20 -7
  21. package/examples/sacrifice-raffle.json +2 -10
  22. package/examples/shelter-fair.json +1 -0
  23. package/examples/stakers-draw.json +4 -15
  24. package/examples/star-fleet.json +1 -8
  25. package/examples/starforce.json +6 -6
  26. package/examples/stride-sneakers.json +164 -0
  27. package/examples/trade-in-shop.json +2 -3
  28. package/js/approve/details.mjs +122 -52
  29. package/js/approve/ed25519.mjs +5 -1
  30. package/js/approve/live-check.mjs +83 -26
  31. package/js/approve/page.html +217 -78
  32. package/js/approve/page.js +43 -15
  33. package/js/approve/server.mjs +290 -73
  34. package/js/chain/base58.js +7 -1
  35. package/js/chain/borsh.js +24 -11
  36. package/js/chain/bytes.js +11 -8
  37. package/js/chain/core.js +35 -7
  38. package/js/chain/curve.js +1 -1
  39. package/js/chain/lookup.js +124 -36
  40. package/js/chain/mint.js +6 -2
  41. package/js/chain/pda.js +2 -2
  42. package/js/chain/programs.js +6 -6
  43. package/js/chain/rpc.js +44 -16
  44. package/js/chain/token.js +13 -2
  45. package/js/chain/token2022.js +55 -16
  46. package/js/chain/tx.js +32 -22
  47. package/js/client/accounts.js +653 -131
  48. package/js/client/compressed.js +4 -3
  49. package/js/client/grant.js +267 -83
  50. package/js/client/idl/loot.js +1 -1
  51. package/js/client/idl/loot.json +2406 -2176
  52. package/js/client/index.js +1 -0
  53. package/js/client/ix.js +3059 -664
  54. package/js/client/pda.js +39 -33
  55. package/js/client/policy.js +52 -24
  56. package/js/client/random.js +70 -0
  57. package/js/client/sizes.js +414 -16
  58. package/js/client/traits.js +14 -5
  59. package/js/connect/connect.mjs +59 -17
  60. package/js/economy/check.mjs +4153 -725
  61. package/js/economy/cost.mjs +403 -64
  62. package/js/economy/examples.mjs +176 -35
  63. package/js/economy/explain.mjs +466 -111
  64. package/js/economy/guide.mjs +296 -72
  65. package/js/economy/legal.mjs +12 -6
  66. package/js/economy/live.mjs +83 -19
  67. package/js/economy/model.mjs +462 -124
  68. package/js/economy/plan.mjs +1306 -219
  69. package/js/economy/schema.json +390 -484
  70. package/js/economy/secrets.mjs +17 -7
  71. package/js/economy/simulate.mjs +883 -223
  72. package/js/economy/txsize.mjs +15 -3
  73. package/js/economy/validate.mjs +61 -24
  74. package/js/mcp/server.mjs +42 -14
  75. package/js/mcp/tools.mjs +307 -88
  76. package/js/play/index.js +2864 -619
  77. package/js/play/keys.js +193 -0
  78. package/js/results/index.js +223 -30
  79. package/js/results/server.js +230 -0
  80. package/js/results/wire.js +64 -16
  81. package/package.json +6 -4
  82. package/release.json +5 -5
  83. package/skills/xsolana-economy/SKILL.md +39 -21
  84. package/skills/xsolana-economy/references/after-launch.md +58 -38
  85. package/skills/xsolana-economy/references/apps.md +97 -50
  86. package/skills/xsolana-economy/references/sentence-to-spec.md +148 -121
  87. package/skills/xsolana-economy/references/telling-the-person.md +61 -17
  88. package/types/chain/core.d.ts +4 -2
  89. package/types/chain/lookup.d.ts +5 -2
  90. package/types/chain/rpc.d.ts +3 -1
  91. package/types/client/accounts.d.ts +217 -56
  92. package/types/client/grant.d.ts +72 -27
  93. package/types/client/idl/loot.d.ts +602 -549
  94. package/types/client/index.d.ts +1 -0
  95. package/types/client/ix.d.ts +3884 -247
  96. package/types/client/pda.d.ts +22 -16
  97. package/types/client/policy.d.ts +39 -14
  98. package/types/client/random.d.ts +26 -0
  99. package/types/client/sizes.d.ts +93 -5
  100. package/types/economy/check.d.mts +5 -10
  101. package/types/economy/cost.d.mts +2 -1
  102. package/types/economy/guide.d.mts +5 -0
  103. package/types/economy/live.d.mts +16 -1
  104. package/types/economy/model.d.mts +151 -38
  105. package/types/economy/plan.d.mts +237 -194
  106. package/types/economy/simulate.d.mts +21 -13
  107. package/types/mcp/tools.d.mts +21 -13
  108. package/types/play/index.d.ts +453 -53
  109. package/types/play/keys.d.ts +51 -0
  110. package/types/results/index.d.ts +7 -6
  111. package/types/results/server.d.ts +126 -0
  112. package/types/results/wire.d.ts +1 -1
@@ -1,11 +1,11 @@
1
1
  // Rent sizes from the current IDL. Fixed allocations reserve the largest enum and option forms,
2
2
  // matching Anchor InitSpace. Dynamic records use their actual encoded fields.
3
- import { encodeAccount } from '../chain/borsh.js';
3
+ import { encodeAccount, encodeBorsh } from '../chain/borsh.js';
4
4
  import IDL from './idl/loot.js';
5
5
 
6
6
  const scalar = { bool: 1, u8: 1, u16: 2, u32: 4, i32: 4, u64: 8, i64: 8, u128: 16, pubkey: 32 };
7
- // The most an order holds: its shares, and a roll's fate (`MAX_SHIFTS` numbers, each key at most 32 bytes).
8
- const bounds = { 'Order.shares': 4, 'Order.won': 3, 'Fate.shifts': 2, 'Project.collections': 8 };
7
+ // What a fixed-size account reserves for its lists and strings: a roll's fate (`Fate.shifts`, each key at most 32 bytes).
8
+ const bounds = { 'Fate.shifts': 2 };
9
9
  const strings = { 'Shift.key': 32 };
10
10
  function maximum(type, path) {
11
11
  if (type === 'string') {
@@ -23,32 +23,430 @@ function maximum(type, path) {
23
23
  return 4 + bounds[path] * maximum(type.vec, path);
24
24
  }
25
25
  const name = type.defined.name;
26
- const definition = IDL.types.find((entry) => entry.name === name)?.type;
26
+ const definition = IDL.types.find(entry => entry.name === name)?.type;
27
27
  if (!definition) throw new Error(`Missing current IDL type ${name}`);
28
- const fields = (list) => (list ?? []).reduce((n, field) => n + maximum(field.type ?? field, `${name}.${field.name ?? ''}`), 0);
29
- return definition.kind === 'enum' ? 1 + Math.max(...definition.variants.map((variant) => fields(variant.fields))) : fields(definition.fields);
28
+ const fields = list =>
29
+ (list ?? []).reduce((n, field) => n + maximum(field.type ?? field, `${name}.${field.name ?? ''}`), 0);
30
+ return definition.kind === 'enum'
31
+ ? 1 + Math.max(...definition.variants.map(variant => fields(variant.fields)))
32
+ : fields(definition.fields);
30
33
  }
31
34
 
32
35
  /** The space reserved by a fixed-size account, including its discriminator. */
33
36
  export function allocationBytes(name) {
34
- const account = IDL.accounts.find((entry) => entry.name === name);
37
+ const account = IDL.accounts.find(entry => entry.name === name);
35
38
  if (!account) throw new Error(`Missing current IDL account ${name}`);
36
39
  return account.discriminator.length + maximum({ defined: { name } }, name);
37
40
  }
38
41
 
39
42
  /** The exact bytes of a dynamically allocated account. */
40
43
  export const accountBytes = (name, value) => encodeAccount(IDL, name, value).length;
41
- export const ACCOUNT_BYTES = Object.freeze({ ...Object.fromEntries(['Order', 'Seat', 'Hold', 'Entry', 'LotteryContribution'].map((name) => [name, allocationBytes(name)])) });
44
+ /** A seat; a loan's hold (with room for its debt); a hold a player asks for, made without one (`Hold::PLAIN`). */
45
+ export const ACCOUNT_BYTES = Object.freeze({
46
+ Seat: allocationBytes('Seat'),
47
+ Loan: allocationBytes('Hold'),
48
+ Hold: allocationBytes('Hold') - maximum({ defined: { name: 'Debt' } }, 'Debt'),
49
+ });
42
50
 
43
51
  /** A project's bytes with `collections` collections, as the program sizes the account. */
44
52
  export const projectBytes = (collections = 0) => 153 + 32 * collections;
45
53
 
46
- // A receipt is not in the IDL and is read by hand: a 17-byte head (its tag, counts and how many rounds), then 8 bytes
47
- // for each round it holds (see docs/program.md).
48
- export const MAX_RECEIPT_ROUNDS = Number(IDL.constants.find((entry) => entry.name === 'MAX_RECEIPT_ROUNDS').value);
49
- export const RECEIPT_BASE_BYTES = 17;
50
- export const RECEIPT_ROUND_BYTES = 8;
51
- export function receiptBytes(rounds = 0) {
52
- if (!Number.isInteger(rounds) || rounds < 0 || rounds > MAX_RECEIPT_ROUNDS) throw new RangeError(`A receipt retains at most ${MAX_RECEIPT_ROUNDS} distinct voucher rounds`);
53
- return RECEIPT_BASE_BYTES + RECEIPT_ROUND_BYTES * rounds;
54
+ /** The most accounts the oracle's answer to a roll carries (`MAX_CALLBACK_ACCOUNTS`). */
55
+ export const MAX_CALLBACK_ACCOUNTS = 21;
56
+
57
+ /**
58
+ * The most accounts a roll of `entries` hands the oracle (`catalog::callback_accounts`): the order, the box and the
59
+ * ticket; one for each prize that needs one at the roll (an item, a pool, a stash); and with tokens, the two token
60
+ * programs, the project, and a mint and a vault for each source of tokens.
61
+ */
62
+ export function callbackAccounts(entries) {
63
+ const sources = new Set();
64
+ let named = 0;
65
+ for (const prize of entries.flatMap(e => e.prizes)) {
66
+ if (prize.Item || prize.Compressed || prize.Nft || prize.Stash) named += 1;
67
+ else if (prize.Tokens) {
68
+ if (prize.Tokens.from.Pool) named += 1;
69
+ sources.add(JSON.stringify(prize.Tokens.from));
70
+ }
71
+ }
72
+ return 3 + named + (sources.size ? 3 + 2 * sources.size : 0);
73
+ }
74
+
75
+ /**
76
+ * How many fresh items' table accounts the oracle's answer to a roll of `entries` can carry (`order::table_budget`):
77
+ * what the box's own accounts leave of `MAX_CALLBACK_ACCOUNTS`. Negative: the box can't be rolled at all.
78
+ */
79
+ export const tableBudget = entries => MAX_CALLBACK_ACCOUNTS - callbackAccounts(entries);
80
+
81
+ const draws = lootBox => (lootBox.slots?.length ? lootBox.slots.reduce((n, s) => n + Number(s.count), 0) : 1);
82
+ const ZERO = '11111111111111111111111111111111';
83
+
84
+ /** The most an order can be (`MAX_ORDER_BYTES`, not in the IDL): a box whose one open's order would be more is refused
85
+ * at its upsert (`BadEntries`). In practice a pack is bounded first by what one oracle answer draws (`drawsFit`). */
86
+ export const MAX_ORDER_BYTES = 10_240;
87
+
88
+ /** The bytes a draw sets aside for a prize, as the `Won` it becomes (`Order::sized`): an item 14, tokens or an NFT 41,
89
+ * a roll, a held NFT or a raffled place nothing. */
90
+ const wonBytes = p => (p.Item || p.Compressed ? 14 : p.Tokens || p.Nft || p.Stash ? 41 : 0);
91
+ const tableCount = item => (item?.traits ?? []).filter(t => t.from?.Table).length;
92
+
93
+ /**
94
+ * The rows of fresh items' tables a draw of each entry writes into its order (`order::rows_of`), walked with the box's
95
+ * table budget as the roll's accounts are (`entryAccounts`: an item whose tables don't fit what is left draws none).
96
+ * `items` maps an item's id to its decoded Item; one left out is taken as not fresh.
97
+ */
98
+ export function entryRows(entries, items = {}) {
99
+ let budget = Math.max(0, tableBudget(entries));
100
+ return entries.map(({ prizes }) =>
101
+ prizes.reduce((n, p) => {
102
+ const id = p.Item?.[0] ?? p.Compressed?.item;
103
+ const t = id === undefined ? 0 : tableCount(items[id]);
104
+ if (t > budget) return n;
105
+ budget -= t;
106
+ return n + t;
107
+ }, 0),
108
+ );
109
+ }
110
+
111
+ /**
112
+ * The bytes of an order of `lootBox` with `shares` shares and `opens` opens. With `items` (an item's id to its decoded
113
+ * Item), as `open_box` sizes it (`Order::sized`): each draw at its dearest entry's prizes as the `Won` they become (an
114
+ * item 14 bytes, tokens or an NFT 41, a roll none) and 4 bytes for each table row its fresh items draw now
115
+ * (`entryRows`); its odds and its largest fate. Without `items`, as a roll made inside another call sizes it (a craft,
116
+ * a grant's claim, a mission, a roll lot: `Order::space_for`): the box's whole table budget a draw for a box that
117
+ * gives items. A pack's order grows with its cards.
118
+ */
119
+ export function orderBytes(lootBox, shares = 0, opens = 1, items) {
120
+ const drawn = draws(lootBox) * Math.max(1, opens);
121
+ const aDraw = Math.max(0, ...lootBox.entries.map(e => e.prizes.reduce((n, p) => n + wonBytes(p), 0)));
122
+ const gives = lootBox.entries.some(e => e.prizes.some(p => p.Item || p.Compressed));
123
+ const rows = items
124
+ ? Math.max(0, ...entryRows(lootBox.entries, items))
125
+ : gives
126
+ ? Math.max(0, tableBudget(lootBox.entries))
127
+ : 0;
128
+ const fates = lootBox.fates ?? [];
129
+ const fate = fates.reduce(
130
+ (a, f) =>
131
+ a &&
132
+ encodeBorsh(IDL, { defined: { name: 'Fate' } }, a).length >=
133
+ encodeBorsh(IDL, { defined: { name: 'Fate' } }, f).length
134
+ ? a
135
+ : f,
136
+ null,
137
+ );
138
+ const empty = {
139
+ project: ZERO,
140
+ player: ZERO,
141
+ mint: ZERO,
142
+ loot_box: ZERO,
143
+ box_version: 0,
144
+ odds_bps: new Array(lootBox.odds?.length ?? 0).fill(0),
145
+ status: 'Waiting',
146
+ won: [],
147
+ paid: 0n,
148
+ fee: 0n,
149
+ shares: new Array(shares).fill({ to: { Account: [ZERO] }, bps: 0 }),
150
+ opens: 0,
151
+ settled: 0,
152
+ undrawn: 0,
153
+ back: 0n,
154
+ settler: ZERO,
155
+ drew: 0,
156
+ answer: new Uint8Array(32),
157
+ drawn: [],
158
+ reserved: 0n,
159
+ requested_slot: 0n,
160
+ asked_first: 0n,
161
+ asked_at: 0n,
162
+ first_asked_at: 0n,
163
+ counted: 0,
164
+ subject: lootBox.subject ? ZERO : null,
165
+ held: false,
166
+ fate,
167
+ };
168
+ return encodeAccount(IDL, 'Order', empty).length + drawn * (aDraw + rows * 4);
169
+ }
170
+
171
+ // What one oracle answer's roll spends, counted at no less than each part costs (`order.rs`'s `RollCost`, measured by
172
+ // the program's `tests/roll_costs.rs` and `tests/roll_heap.rs`), against the compute and the heap the oracle gives its
173
+ // answer, less a margin.
174
+ const ROLL_BUDGET = 300_000n;
175
+ const ROLL_HEAP = 32n * 1024n - 512n;
176
+ const COST = {
177
+ fixed: 20_000n,
178
+ entry: 3_500n,
179
+ blank: 1_400n,
180
+ weigh: 1_000n,
181
+ weighEntry: 40n,
182
+ pair: 150n,
183
+ item: 1_500n,
184
+ tokens: 6_000n,
185
+ draw: 9_000n,
186
+ group: 250n,
187
+ reread: 1_000n,
188
+ finish: 15_000n,
189
+ won: 150n,
190
+ drawn: 50n,
191
+ odds: 1_000n,
192
+ share: 1_000n,
193
+ fate: 1_000n,
194
+ slot: 500n,
195
+ slotEntry: 350n,
196
+ openSlot: 150n,
197
+ };
198
+ const BYTES = {
199
+ start: 6_000n,
200
+ prizeRead: 600n,
201
+ entryRead: 180n,
202
+ minted: 700n,
203
+ pooled: 1_000n,
204
+ prize: 200n,
205
+ draw: 40n,
206
+ finish: 2_000n,
207
+ won: 120n,
208
+ boxByte: 2n,
209
+ gateByte: 3n,
210
+ odds: 100n,
211
+ fate: 60n,
212
+ slot: 64n,
213
+ };
214
+
215
+ /** The Borsh length of `value` as IDL type `type`, an address counted as 32 bytes whatever stands in for it (a spec's
216
+ * `@mint:…` before it goes live). */
217
+ function borshLength(type, value) {
218
+ if (typeof type === 'string')
219
+ return type === 'string'
220
+ ? 4 + new TextEncoder().encode(String(value)).length
221
+ : type === 'bytes'
222
+ ? 4 + value.length
223
+ : scalar[type];
224
+ if (type.array) return type.array[1] * (type.array[0] === 'u8' ? 1 : borshLength(type.array[0], value?.[0]));
225
+ if (type.vec) return 4 + (value ?? []).reduce((n, v) => n + borshLength(type.vec, v), 0);
226
+ if (type.option) return value == null ? 1 : 1 + borshLength(type.option, value);
227
+ const definition = IDL.types.find(entry => entry.name === type.defined.name).type;
228
+ if (definition.kind === 'enum') {
229
+ const name = typeof value === 'string' ? value : Object.keys(value)[0];
230
+ const fields = definition.variants.find(v => v.name === name)?.fields ?? [];
231
+ const payload = typeof value === 'string' ? [] : value[name];
232
+ return (
233
+ 1 +
234
+ fields.reduce(
235
+ (n, f, i) =>
236
+ n +
237
+ (typeof f === 'object' && 'name' in f ? borshLength(f.type, payload[f.name]) : borshLength(f, payload[i])),
238
+ 0,
239
+ )
240
+ );
241
+ }
242
+ return definition.fields.reduce((n, f) => n + borshLength(f.type, value[f.name]), 0);
243
+ }
244
+
245
+ const defined = name => ({ defined: { name } });
246
+
247
+ /**
248
+ * The heap reading a box takes beyond its entries (`order::box_bytes`): its odds, gate, shares, subject, slots and
249
+ * fates, read whole with it, by their bytes and their number. `box`: the box's args, as `upsertBoxIx` takes them.
250
+ */
251
+ export function boxBytes({ odds = [], gate = null, shares = [], subject = null, slots = [], fates = [] }) {
252
+ const slotsWritten = slots.map(s => ({ only: [], distinct: false, share: 0, ...s }));
253
+ const fatesWritten = fates.map(f => ({
254
+ then: null,
255
+ ...f,
256
+ shifts: (f.shifts ?? []).map(x => ({ from_now: false, ...x })),
257
+ }));
258
+ const bytes =
259
+ borshLength({ vec: defined('Odds') }, odds) +
260
+ borshLength({ vec: defined('Share') }, shares) +
261
+ borshLength({ option: defined('Rule') }, subject) +
262
+ borshLength({ vec: defined('Slot') }, slotsWritten) +
263
+ borshLength({ vec: defined('Fate') }, fatesWritten);
264
+ return (
265
+ BYTES.boxByte * BigInt(bytes) +
266
+ BYTES.gateByte * BigInt(borshLength({ option: defined('Gate') }, gate)) +
267
+ BYTES.odds * BigInt(odds.length) +
268
+ BYTES.fate * BigInt(fates.length) +
269
+ BYTES.slot * BigInt(slots.length)
270
+ );
271
+ }
272
+
273
+ /** The account a prize reads, as a string, when it reads one the box can name: entries naming the same share it. */
274
+ const accountOf = p => {
275
+ if (p.Item != null) return `item ${p.Item[0] ?? p.Item}`;
276
+ if (p.Compressed) return `item ${p.Compressed.item}`;
277
+ if (p.Tokens?.from?.Pool) return `pool ${p.Tokens.from.Pool[0] ?? p.Tokens.from.Pool}`;
278
+ if (p.Stash != null) return `stash ${p.Stash[0] ?? p.Stash}`;
279
+ if (p.Nft) return `stash ${p.Nft.stash}`;
280
+ return null;
281
+ };
282
+
283
+ /**
284
+ * Whether all of `opens` opens of `entries` by `slots` fit one oracle answer (`order::draws_fit`), each draw at the
285
+ * dearest its slot could make, with what is known from the box alone (no table rows, a stash of one group), its `odds`,
286
+ * `shares` and `fates` counted, and `bytes` (`boxBytes`) of heap reading the box takes beyond its entries. `slots` may
287
+ * be a number: that many draws among every entry. The answer's heap counts for a box that rolls, or one sold through
288
+ * the oracle (`oracle`: a box that doesn't roll but one `buy` can't carry, `buyFits`). The program refuses a box whose
289
+ * one open doesn't fit at its upsert, and a purchase that doesn't at `open_box`, where it also reads what the accounts
290
+ * hold (`PackTooLarge`).
291
+ */
292
+ export function drawsFit(
293
+ entries,
294
+ slots,
295
+ opens = 1,
296
+ { odds = 0, shares = 0, fates = 0, bytes = 0n, oracle = false } = {},
297
+ ) {
298
+ const list = typeof slots === 'number' || typeof slots === 'bigint' ? [{ count: slots, only: [] }] : (slots ?? []);
299
+ const draws = list.reduce((n, s) => n + BigInt(s.count), 0n) * BigInt(opens);
300
+ if (draws <= 1n) return true;
301
+ const n = BigInt(entries.length);
302
+ const weigh = COST.weigh + COST.weighEntry * n;
303
+ const reads = entries.map(e => e.prizes.map(accountOf));
304
+ const cost = entries.map(e =>
305
+ e.prizes.reduce(
306
+ (c, p) => {
307
+ if (p.Item != null || p.Compressed || p.Nft)
308
+ return { set: c.set + COST.item, again: c.again + COST.reread, bytes: c.bytes + BYTES.prize };
309
+ if (p.Tokens)
310
+ return {
311
+ set: c.set + COST.tokens,
312
+ again: c.again + (p.Tokens.from.Pool ? COST.reread : 0n),
313
+ bytes: c.bytes + (p.Tokens.from.Pool ? BYTES.pooled : BYTES.minted),
314
+ };
315
+ if (p.Stash != null)
316
+ return { set: c.set + COST.draw + COST.group, again: c.again + COST.reread, bytes: c.bytes + BYTES.prize };
317
+ return { set: c.set + COST.item, again: c.again, bytes: c.bytes + BYTES.prize };
318
+ },
319
+ { set: 0n, again: 0n, bytes: BYTES.draw },
320
+ ),
321
+ );
322
+ const next = entries.map(
323
+ (_, i) =>
324
+ weigh +
325
+ cost[i].set +
326
+ cost.reduce((sum, c, j) => (j === i || reads[i].some(a => a && reads[j].includes(a)) ? sum + c.again : sum), 0n),
327
+ );
328
+ const most = BigInt(Math.max(1, ...entries.map(e => e.prizes.length)));
329
+ let spent =
330
+ COST.fixed +
331
+ COST.pair * n * n * most * most +
332
+ entries.reduce((c, e) => c + (e.prizes.length ? COST.entry : COST.blank), 0n);
333
+ let used =
334
+ BYTES.start +
335
+ BigInt(bytes) +
336
+ BYTES.entryRead * n +
337
+ BYTES.prizeRead * BigInt(entries.reduce((c, e) => c + e.prizes.length, 0));
338
+ let won = 0n;
339
+ spent +=
340
+ (COST.slot + COST.slotEntry * n) * BigInt(Math.max(1, list.length)) +
341
+ COST.openSlot * BigInt(list.length) * BigInt(opens) +
342
+ COST.odds * BigInt(odds) +
343
+ COST.share * BigInt(shares) +
344
+ COST.fate * BigInt(fates);
345
+ for (const s of list) {
346
+ const count = BigInt(s.count) * BigInt(opens);
347
+ const among = entries.map((_, i) => i).filter(i => !(s.only ?? []).length || (s.only ?? []).includes(i));
348
+ const dearest = f => among.reduce((m, i) => (f(i) > m ? f(i) : m), 0n);
349
+ spent += count * dearest(i => next[i]);
350
+ used += count * dearest(i => cost[i].bytes);
351
+ won += count * dearest(i => BigInt(entries[i].prizes.length));
352
+ }
353
+ const rolls = oracle || entries.length > 1 || entries.some(e => e.prizes.some(p => p.Stash != null));
354
+ return (
355
+ spent + COST.finish + COST.won * won + COST.drawn * draws <= ROLL_BUDGET &&
356
+ (!rolls || used + BYTES.finish + BYTES.won * won + draws <= ROLL_HEAP)
357
+ );
54
358
  }
359
+
360
+ // What one `buy` takes, as the program counts it at the box's write (`catalog::buy_fits`): a transaction holds 64
361
+ // accounts, 64 instructions and 1,400,000 compute units.
362
+ const BUY = { accounts: 15, calls: 7, cu: 30_000, most: { accounts: 64, calls: 64, cu: 1_400_000 } };
363
+ const sameJson = (a, b) =>
364
+ JSON.stringify(a, (_, v) => (typeof v === 'bigint' ? String(v) : v)) ===
365
+ JSON.stringify(b, (_, v) => (typeof v === 'bigint' ? String(v) : v));
366
+
367
+ /**
368
+ * Whether one `buy` of a box that doesn't roll (one entry, given `draws` times: its slots' counts) fits one transaction
369
+ * (`catalog::buy_fits`): its accounts, its instructions and its compute, each prize's at the most it takes. A named NFT
370
+ * is given once at most. A box that doesn't fit sells through the oracle (`open_box`), its answer's heap counted
371
+ * (`drawsFit` with `oracle`), and can't give a roll (`PackTooLarge`). `box`: the box's args.
372
+ */
373
+ export function buyFits(box, draws = box.slots?.length ? box.slots.reduce((n, s) => n + Number(s.count), 0) : 1) {
374
+ const entry = box.entries?.[0];
375
+ if (!entry) return true;
376
+ let { accounts, calls, cu } = BUY;
377
+ for (const share of box.shares ?? []) {
378
+ const to = typeof share.to === 'string' ? share.to : Object.keys(share.to)[0];
379
+ const [a, c] =
380
+ to === 'Burn' ? [0, 1] : to === 'Pool' || to === 'Account' ? [1, 1] : to === 'Holder' ? [3, 6] : [2, 6];
381
+ accounts += a;
382
+ calls += c;
383
+ cu += 30_000;
384
+ }
385
+ const gate = box.gate ? Object.keys(box.gate)[0] : null;
386
+ accounts += gate === 'List' ? Number(box.gate.List.depth) : gate === 'Staked' ? 2 : gate === 'Holds' ? 5 : 0;
387
+ const limit = box.per_wallet ?? {};
388
+ if (Number(limit.total ?? 0) > 0 || Number(limit.per_window ?? 0) > 0) calls += 1;
389
+ // A collectible's collection and Metaplex Core, named once for all of them.
390
+ if (entry.prizes.some(p => p.Item != null)) accounts += 2;
391
+ for (const [i, p] of entry.prizes.entries()) {
392
+ const first = !entry.prizes
393
+ .slice(0, i)
394
+ .some(q => (accountOf(q) && accountOf(q) === accountOf(p)) || sameJson(q, p));
395
+ const gives = p.Tokens ? 1 : draws;
396
+ let once = 0,
397
+ each = 0,
398
+ c = 0,
399
+ units = 0;
400
+ if (p.Tokens) [once, c, units] = [p.Tokens.from.Pool ? 3 : 2, 5, 30_000];
401
+ else if (p.Item != null) [once, each, c, units] = [1, 1, 4, 40_000];
402
+ else if (p.Compressed) [once, c, units] = [9, 6, 150_000];
403
+ else if (p.Roll != null) [once, each, c, units] = [1, 1, 1, 20_000];
404
+ else if (p.Nft) {
405
+ if (draws > 1) return false;
406
+ [once, c, units] = [8, 6, 60_000];
407
+ }
408
+ accounts += (first ? once : 0) + each * gives;
409
+ calls += c * gives;
410
+ cu += units * gives;
411
+ }
412
+ return accounts <= BUY.most.accounts && calls <= BUY.most.calls && cu <= BUY.most.cu;
413
+ }
414
+
415
+ /** The most program rent and fee one order takes from its player, in lamports (0.01 SOL): a client cap (`opensAnOrder`). */
416
+ export const ORDER_COST_CAP = 10_000_000n;
417
+ /** Rent a byte, as devnet and mainnet-beta charge it ((bytes + 128) × 5,080 lamports). */
418
+ const RENT_PER_BYTE = 5_080n;
419
+ /** The player's fee for one order (`fee::PLAYER_FEE`, 0.002 SOL). */
420
+ const PLAYER_FEE = 2_000_000n;
421
+
422
+ /**
423
+ * The most opens of `lootBox` a client puts in one order: what one oracle answer draws (`drawsFit`), and what keeps the
424
+ * order's rent and the player's fee within `ORDER_COST_CAP` (`orderBytes` with `items`, so with the box's shares). At
425
+ * least one: a box whose one open is over the cap is still bought one open an order. A purchase of more is several
426
+ * orders, each its own answer.
427
+ */
428
+ export function opensAnOrder(
429
+ lootBox,
430
+ { shares = (lootBox.shares ?? []).length, items = {}, rentPerByte = RENT_PER_BYTE } = {},
431
+ ) {
432
+ const fits = n =>
433
+ drawsFit(lootBox.entries, lootBox.slots?.length ? lootBox.slots : 1, n, {
434
+ odds: (lootBox.odds ?? []).length,
435
+ shares,
436
+ fates: (lootBox.fates ?? []).length,
437
+ bytes: boxBytes(lootBox),
438
+ oracle: true,
439
+ }) && (BigInt(orderBytes(lootBox, shares, n, items)) + 128n) * rentPerByte + PLAYER_FEE <= ORDER_COST_CAP;
440
+ let n = 1;
441
+ while (n < 4096 && fits(n + 1)) n += 1;
442
+ return n;
443
+ }
444
+
445
+ // Lists of records (tag 26, `list.rs`): a tag byte and a 6-byte header, then fixed-width records.
446
+ export const LIST_HEAD_BYTES = 7;
447
+ /** A limit record (a wallet's count of a box, a recipe or a lottery round; a grant's claim count): key, count, window, in it. */
448
+ export const LIMIT_RECORD_BYTES = 28;
449
+ /** A round a grant paid a wallet: its key alone. */
450
+ export const ROUND_RECORD_BYTES = 16;
451
+ /** A contest's entry: of tokens, and of NFTs (the NFT, its place on its side, the picks taken). */
452
+ export const CONTEST_ENTRY_BYTES = Object.freeze({ tokens: 43, nfts: 83 });
@@ -5,25 +5,34 @@ import { addressBytes } from '../chain/base58.js';
5
5
  import { compareBytes, concat, uintLE, utf8 } from '../chain/bytes.js';
6
6
  import { sha256 } from '../chain/sha256.js';
7
7
 
8
- const text = (s) => concat(uintLE(utf8(s).length, 4), utf8(s));
8
+ const text = s => concat(uintLE(utf8(s).length, 4), utf8(s));
9
9
 
10
10
  /** A leaf: an NFT's address and its traits ([{ key, value }], in order). */
11
11
  export const traitsLeaf = (nft, attributes) =>
12
- sha256(concat(Uint8Array.of(0), addressBytes(nft), uintLE(attributes.length, 4), ...attributes.map((a) => concat(text(a.key), text(a.value)))));
12
+ sha256(
13
+ concat(
14
+ Uint8Array.of(0),
15
+ addressBytes(nft),
16
+ uintLE(attributes.length, 4),
17
+ ...attributes.map(a => concat(text(a.key), text(a.value))),
18
+ ),
19
+ );
13
20
 
14
21
  /** Two nodes joined, the smaller first, so a proof needs no left or right. */
15
- export const traitsNode = (a, b) => (compareBytes(a, b) <= 0 ? sha256(concat(Uint8Array.of(1), a, b)) : sha256(concat(Uint8Array.of(1), b, a)));
22
+ export const traitsNode = (a, b) =>
23
+ compareBytes(a, b) <= 0 ? sha256(concat(Uint8Array.of(1), a, b)) : sha256(concat(Uint8Array.of(1), b, a));
16
24
 
17
25
  /**
18
26
  * The tree of `entries` ([{ nft, attributes }]): { root, proof(nft) → [node, …] bottom up }. A node left without a pair
19
27
  * at a level goes up as it is.
20
28
  */
21
29
  export function traitsTree(entries) {
22
- const levels = [entries.map((e) => traitsLeaf(e.nft, e.attributes))];
30
+ const levels = [entries.map(e => traitsLeaf(e.nft, e.attributes))];
23
31
  while (levels.at(-1).length > 1) {
24
32
  const below = levels.at(-1);
25
33
  const up = [];
26
- for (let i = 0; i < below.length; i += 2) up.push(i + 1 < below.length ? traitsNode(below[i], below[i + 1]) : below[i]);
34
+ for (let i = 0; i < below.length; i += 2)
35
+ up.push(i + 1 < below.length ? traitsNode(below[i], below[i + 1]) : below[i]);
27
36
  levels.push(up);
28
37
  }
29
38
  const at = new Map(entries.map((e, i) => [e.nft, i]));
@@ -25,11 +25,24 @@ export const AGENTS = {
25
25
  name: 'Claude Code',
26
26
  bin: 'claude',
27
27
  scopes: ['user', 'project'],
28
- add: (command, args, scope) => ['mcp', 'add', '--transport', 'stdio', '--scope', scope, NAME, '--', command, ...args],
29
- remove: (scope) => ['mcp', 'remove', '--scope', scope, NAME],
28
+ add: (command, args, scope) => [
29
+ 'mcp',
30
+ 'add',
31
+ '--transport',
32
+ 'stdio',
33
+ '--scope',
34
+ scope,
35
+ NAME,
36
+ '--',
37
+ command,
38
+ ...args,
39
+ ],
40
+ remove: scope => ['mcp', 'remove', '--scope', scope, NAME],
30
41
  get: ['mcp', 'get', NAME],
31
- skills: (scope, cwd) => (scope === 'project' ? path.join(cwd, '.claude', 'skills') : path.join(os.homedir(), '.claude', 'skills')),
32
- plugin: 'Or install the plugin: /plugin marketplace add XGamesFun/solana-kit, then /plugin install xsolana@xgamesfun.',
42
+ skills: (scope, cwd) =>
43
+ scope === 'project' ? path.join(cwd, '.claude', 'skills') : path.join(os.homedir(), '.claude', 'skills'),
44
+ plugin:
45
+ 'Or install the plugin: /plugin marketplace add XGamesFun/solana-kit, then /plugin install xsolana@xgamesfun.',
33
46
  next: 'Start a new Claude Code session (or /mcp) to load the xsolana tools.',
34
47
  },
35
48
  codex: {
@@ -39,23 +52,30 @@ export const AGENTS = {
39
52
  add: (command, args) => ['mcp', 'add', NAME, '--', command, ...args],
40
53
  remove: () => ['mcp', 'remove', NAME],
41
54
  get: ['mcp', 'get', NAME],
42
- skills: (scope, cwd) => (scope === 'project' ? path.join(cwd, '.agents', 'skills') : path.join(os.homedir(), '.agents', 'skills')),
43
- plugin: 'Or install the plugin: codex plugin marketplace add XGamesFun/solana-kit, then codex plugin add xsolana@xgamesfun.',
55
+ skills: (scope, cwd) =>
56
+ scope === 'project' ? path.join(cwd, '.agents', 'skills') : path.join(os.homedir(), '.agents', 'skills'),
57
+ plugin:
58
+ 'Or install the plugin: codex plugin marketplace add XGamesFun/solana-kit, then codex plugin add xsolana@xgamesfun.',
44
59
  next: 'Start a new Codex session so it picks up the xsolana tools.',
45
60
  },
46
61
  };
47
62
 
48
63
  /** One word for cmd.exe: as it is when it is plain, else in double quotes (a quote inside doubled). */
49
- const winWord = (a) => (/^[\w@+=:,./\\-]+$/.test(a) ? a : `"${String(a).replace(/"/g, '""')}"`);
64
+ const winWord = a => (/^[\w@+=:,./\\-]+$/.test(a) ? a : `"${String(a).replace(/"/g, '""')}"`);
50
65
 
51
66
  /**
52
67
  * Runs an agent's command and answers { code, out }. On Windows the agents are installed as .cmd shims, which only a
53
68
  * shell can start: there the command goes to cmd.exe as one line, each word quoted as it needs.
54
69
  */
55
70
  export function runAgent(bin, args, { platform = process.platform, execFileImpl = execFile, execImpl = exec } = {}) {
56
- return new Promise((resolve) => {
57
- const done = (error, stdout, stderr) => resolve({ code: error ? (typeof error.code === 'number' ? error.code : 1) : 0, out: `${stdout ?? ''}${stderr ?? ''}`.trim() });
58
- if (platform === 'win32') execImpl([bin, ...args].map(winWord).join(' '), { timeout: 60_000, windowsHide: true }, done);
71
+ return new Promise(resolve => {
72
+ const done = (error, stdout, stderr) =>
73
+ resolve({
74
+ code: error ? (typeof error.code === 'number' ? error.code : 1) : 0,
75
+ out: `${stdout ?? ''}${stderr ?? ''}`.trim(),
76
+ });
77
+ if (platform === 'win32')
78
+ execImpl([bin, ...args].map(winWord).join(' '), { timeout: 60_000, windowsHide: true }, done);
59
79
  else execFileImpl(bin, args, { timeout: 60_000 }, done);
60
80
  });
61
81
  }
@@ -91,7 +111,16 @@ export async function skillFolder(dir) {
91
111
  }
92
112
 
93
113
  /** Connects (or with `remove`, disconnects) each agent found. Returns an exit code. */
94
- export async function connect({ agent, local = false, remove = false, dryRun = false, scope = 'user', cwd = process.cwd(), log = (...a) => console.log(...a), run = runAgent } = {}) {
114
+ export async function connect({
115
+ agent,
116
+ local = false,
117
+ remove = false,
118
+ dryRun = false,
119
+ scope = 'user',
120
+ cwd = process.cwd(),
121
+ log = (...a) => console.log(...a),
122
+ run = runAgent,
123
+ } = {}) {
95
124
  if (!SCOPES.includes(scope)) {
96
125
  log(`No scope called “${scope}” (${SCOPES.join(', ')}).`);
97
126
  return 1;
@@ -113,7 +142,9 @@ export async function connect({ agent, local = false, remove = false, dryRun = f
113
142
  }
114
143
  found++;
115
144
  if (!a.scopes.includes(scope)) {
116
- log(`${a.name}: adds MCP servers only for the whole user (--scope ${a.scopes.join(' or ')}), so it was left as it is. Run \`xsolana connect --agent ${id}\` for that.`);
145
+ log(
146
+ `${a.name}: adds MCP servers only for the whole user (--scope ${a.scopes.join(' or ')}), so it was left as it is. Run \`xsolana connect --agent ${id}\` for that.`,
147
+ );
117
148
  if (agent) failed++;
118
149
  continue;
119
150
  }
@@ -146,7 +177,9 @@ export async function connect({ agent, local = false, remove = false, dryRun = f
146
177
  const there = await skillFolder(skillDir);
147
178
  let skillSaid;
148
179
  if (there === 'other') {
149
- log(`${a.name}: ${skillDir} is there already and is not xsolana's ${SKILL} skill, so it was left as it is${remove ? '' : ' and the skill was not copied'}. Move it away, then run \`xsolana connect\` again.`);
180
+ log(
181
+ `${a.name}: ${skillDir} is there already and is not xsolana's ${SKILL} skill, so it was left as it is${remove ? '' : ' and the skill was not copied'}. Move it away, then run \`xsolana connect\` again.`,
182
+ );
150
183
  failed++;
151
184
  ok = false;
152
185
  } else if (remove) {
@@ -154,7 +187,8 @@ export async function connect({ agent, local = false, remove = false, dryRun = f
154
187
  else if (there === 'ours') await rm(skillDir, { recursive: true, force: true });
155
188
  skillSaid = there === 'ours' ? `; the ${SKILL} skill removed from ${skillDir}` : '';
156
189
  } else {
157
- if (dryRun) log(`${there === 'ours' ? 'replace the xsolana skill at' : 'copy skills/xsolana-economy to'} ${skillDir}`);
190
+ if (dryRun)
191
+ log(`${there === 'ours' ? 'replace the xsolana skill at' : 'copy skills/xsolana-economy to'} ${skillDir}`);
158
192
  else {
159
193
  await mkdir(skills, { recursive: true });
160
194
  if (there === 'ours') await rm(skillDir, { recursive: true, force: true });
@@ -167,15 +201,23 @@ export async function connect({ agent, local = false, remove = false, dryRun = f
167
201
  // Registered, but not at this scope: it is still there, and the person asked for it gone.
168
202
  const stays = had.code === 0 && !removed;
169
203
  if (stays) failed++;
170
- const server = removed ? 'xsolana removed' : stays ? `xsolana’s server was not found at the ${scope} scope, so it was left where it is (\`${a.bin} mcp get ${NAME}\` shows where)` : 'xsolana’s server was not registered';
204
+ const server = removed
205
+ ? 'xsolana removed'
206
+ : stays
207
+ ? `xsolana’s server was not found at the ${scope} scope, so it was left where it is (\`${a.bin} mcp get ${NAME}\` shows where)`
208
+ : 'xsolana’s server was not registered';
171
209
  log(`${a.name}: ${server}${skillSaid ?? ''}.`);
172
210
  } else if (ok) {
173
- log(`${a.name}: xsolana tools added (${local ? 'from this checkout' : 'from npm'}, ${scope === 'project' ? `for ${cwd} only` : 'for every project'})${skillSaid}. ${a.next}`);
211
+ log(
212
+ `${a.name}: xsolana tools added (${local ? 'from this checkout' : 'from npm'}, ${scope === 'project' ? `for ${cwd} only` : 'for every project'})${skillSaid}. ${a.next}`,
213
+ );
174
214
  log(` ${a.plugin}`);
175
215
  }
176
216
  }
177
217
  if (!found) {
178
- log('No coding agent found (Claude Code: npm install -g @anthropic-ai/claude-code; Codex: npm install -g @openai/codex).');
218
+ log(
219
+ 'No coding agent found (Claude Code: npm install -g @anthropic-ai/claude-code; Codex: npm install -g @openai/codex).',
220
+ );
179
221
  return 1;
180
222
  }
181
223
  return failed ? 1 : 0;