@xgamesfun/solana 0.24.0 → 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 (92) hide show
  1. package/README.md +94 -51
  2. package/bin/xsolana.mjs +23 -8
  3. package/examples/blind-forge.json +2 -4
  4. package/examples/coin-flip-arena.json +2 -5
  5. package/examples/duel-arena.json +1 -4
  6. package/examples/fair-presale.json +1 -3
  7. package/examples/hero-trials.json +14 -2
  8. package/examples/holder-den.json +38 -0
  9. package/examples/holders-drop.json +105 -0
  10. package/examples/ideas.json +788 -788
  11. package/examples/match-predictions.json +24 -0
  12. package/examples/nft-raffle.json +1 -0
  13. package/examples/pack-league.json +108 -0
  14. package/examples/partner-vault.json +19 -3
  15. package/examples/sacrifice-raffle.json +2 -10
  16. package/examples/shelter-fair.json +1 -0
  17. package/examples/stakers-draw.json +3 -7
  18. package/examples/stride-sneakers.json +164 -0
  19. package/examples/trade-in-shop.json +2 -3
  20. package/js/approve/details.mjs +117 -48
  21. package/js/approve/ed25519.mjs +5 -1
  22. package/js/approve/live-check.mjs +83 -26
  23. package/js/approve/page.html +217 -78
  24. package/js/approve/page.js +43 -15
  25. package/js/approve/server.mjs +276 -74
  26. package/js/chain/base58.js +7 -1
  27. package/js/chain/borsh.js +15 -11
  28. package/js/chain/bytes.js +11 -8
  29. package/js/chain/core.js +21 -5
  30. package/js/chain/curve.js +1 -1
  31. package/js/chain/lookup.js +118 -36
  32. package/js/chain/mint.js +6 -2
  33. package/js/chain/pda.js +2 -2
  34. package/js/chain/programs.js +6 -6
  35. package/js/chain/rpc.js +40 -14
  36. package/js/chain/token.js +13 -2
  37. package/js/chain/token2022.js +55 -16
  38. package/js/chain/tx.js +32 -22
  39. package/js/client/accounts.js +270 -80
  40. package/js/client/compressed.js +4 -3
  41. package/js/client/grant.js +165 -40
  42. package/js/client/idl/loot.js +1 -1
  43. package/js/client/idl/loot.json +104 -16
  44. package/js/client/ix.js +2121 -457
  45. package/js/client/pda.js +20 -18
  46. package/js/client/policy.js +12 -5
  47. package/js/client/random.js +1 -1
  48. package/js/client/sizes.js +220 -65
  49. package/js/client/traits.js +14 -5
  50. package/js/connect/connect.mjs +59 -17
  51. package/js/economy/check.mjs +3873 -690
  52. package/js/economy/cost.mjs +380 -56
  53. package/js/economy/examples.mjs +176 -35
  54. package/js/economy/explain.mjs +461 -110
  55. package/js/economy/guide.mjs +282 -59
  56. package/js/economy/legal.mjs +12 -6
  57. package/js/economy/live.mjs +60 -19
  58. package/js/economy/model.mjs +320 -97
  59. package/js/economy/plan.mjs +1022 -203
  60. package/js/economy/schema.json +127 -369
  61. package/js/economy/secrets.mjs +17 -7
  62. package/js/economy/simulate.mjs +771 -178
  63. package/js/economy/txsize.mjs +15 -3
  64. package/js/economy/validate.mjs +61 -24
  65. package/js/mcp/server.mjs +42 -14
  66. package/js/mcp/tools.mjs +293 -80
  67. package/js/play/index.js +1960 -461
  68. package/js/play/keys.js +35 -9
  69. package/js/results/index.js +223 -30
  70. package/js/results/server.js +230 -0
  71. package/js/results/wire.js +64 -16
  72. package/package.json +6 -4
  73. package/release.json +5 -5
  74. package/skills/xsolana-economy/SKILL.md +30 -16
  75. package/skills/xsolana-economy/references/after-launch.md +30 -20
  76. package/skills/xsolana-economy/references/apps.md +47 -36
  77. package/skills/xsolana-economy/references/sentence-to-spec.md +148 -129
  78. package/skills/xsolana-economy/references/telling-the-person.md +9 -6
  79. package/types/chain/core.d.ts +1 -1
  80. package/types/chain/lookup.d.ts +1 -1
  81. package/types/client/accounts.d.ts +24 -3
  82. package/types/client/grant.d.ts +2 -2
  83. package/types/client/idl/loot.d.ts +22 -0
  84. package/types/client/ix.d.ts +54 -45
  85. package/types/client/pda.d.ts +3 -1
  86. package/types/economy/guide.d.mts +5 -0
  87. package/types/economy/model.d.mts +48 -3
  88. package/types/economy/plan.d.mts +44 -2
  89. package/types/play/index.d.ts +8 -19
  90. package/types/results/index.d.ts +7 -6
  91. package/types/results/server.d.ts +126 -0
  92. package/types/results/wire.d.ts +1 -1
package/js/client/pda.js CHANGED
@@ -16,13 +16,13 @@ export const BUBBLEGUM_PROGRAM = 'BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY';
16
16
  export const COMPRESSION_PROGRAM = 'mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW';
17
17
  export const NOOP_PROGRAM = 'mnoopTCrg4p8ry25e4bcWA9XZjbNjMTfgYVGGEdRsf3';
18
18
 
19
- const seed = (s) => utf8(s);
20
- const key = (a) => addressBytes(a);
21
- const u8 = (n) => uintLE(n, 1);
22
- const u16 = (n) => uintLE(n, 2);
23
- const u32 = (n) => uintLE(n, 4);
24
- const u64 = (n) => uintLE(n, 8);
25
- const i64 = (n) => intLE(n, 8);
19
+ const seed = s => utf8(s);
20
+ const key = a => addressBytes(a);
21
+ const u8 = n => uintLE(n, 1);
22
+ const u16 = n => uintLE(n, 2);
23
+ const u32 = n => uintLE(n, 4);
24
+ const u64 = n => uintLE(n, 8);
25
+ const i64 = n => intLE(n, 8);
26
26
 
27
27
  /** The PDA of these seeds under the loot program, with its bump. */
28
28
  export const lootAddress = (seeds, program = LOOT_PROGRAM) => findProgramAddress(seeds, program);
@@ -34,7 +34,7 @@ export const pda = {
34
34
  * new owner keeps the address. */
35
35
  project: (creator, id = 0) => at(seed('project'), key(creator), u64(id)),
36
36
  /** The project's fee box: where the platform's fees on its transactions wait. */
37
- fees: (project) => at(seed('fees'), key(project)),
37
+ fees: project => at(seed('fees'), key(project)),
38
38
  pool: (project, index) => at(seed('pool'), key(project), u16(index)),
39
39
  item: (project, id) => at(seed('item'), key(project), u32(id)),
40
40
  box: (project, id) => at(seed('box'), key(project), u32(id)),
@@ -56,8 +56,10 @@ export const pda = {
56
56
  /** Every collectible the program mints of its own accord (an order's, a prize's, an auction's lot): its address
57
57
  * names its item and its serial, minted once at most. */
58
58
  itemAsset: (item, serial) => at(seed('asset'), key(item), u32(serial)),
59
- /** A contest's pot, or a lottery's: `["pot", contest or lottery]`. */
60
- pot: (record) => at(seed('pot'), key(record)),
59
+ /** A contest's pot: `["pot", contest]`. */
60
+ pot: record => at(seed('pot'), key(record)),
61
+ /** A lottery's pot `id` (below `MAX_POTS`), kept from round to round: `["pot", lottery, id]`. */
62
+ lotteryPot: (lottery, id) => at(seed('pot'), key(lottery), u8(id)),
61
63
  /** One round of a lottery: its record, opened by the round's first purchase (`["round", lottery, incarnation, round]`). */
62
64
  round: (lottery, incarnation, round) => at(seed('round'), key(lottery), u64(incarnation), u32(round)),
63
65
  /** A lottery round's own token account of `mint`, which the round's record owns. */
@@ -69,27 +71,27 @@ export const pda = {
69
71
  /** An offer: an id is its maker's own. Its money waits in the project's vault for its mint (`vault`). */
70
72
  offer: (project, maker, id) => at(seed('offer'), key(project), key(maker), u32(id)),
71
73
  /** A lottery's own stash: the NFTs its places promise (`Prize::Held`), which leave only as its prizes. */
72
- lotteryStash: (lottery) => at(seed('lottery_stash'), key(lottery)),
74
+ lotteryStash: lottery => at(seed('lottery_stash'), key(lottery)),
73
75
  /** A contest's prize split by stake, set aside when it is decided: `["prize", contest]`. */
74
- prizeVault: (contest) => at(seed('prize'), key(contest)),
76
+ prizeVault: contest => at(seed('prize'), key(contest)),
75
77
  /** A sealed schedule's own vault, which only its runs pay out of: `["vest", schedule]`. */
76
- vest: (schedule) => at(seed('vest'), key(schedule)),
78
+ vest: schedule => at(seed('vest'), key(schedule)),
77
79
  /** A listing of an NFT for sale or for rent: one per NFT a project holds, so its address names the NFT. */
78
80
  listing: (project, asset) => at(seed('listing'), key(project), key(asset)),
79
- staked: (farm) => at(seed('staked'), key(farm)),
80
- rewards: (farm) => at(seed('rewards'), key(farm)),
81
+ staked: farm => at(seed('staked'), key(farm)),
82
+ rewards: farm => at(seed('rewards'), key(farm)),
81
83
  /** A farm's record of when its seats stop earning, for a farm that keeps one (`Farm.keeps_ends`). */
82
- ends: (farm) => at(seed('ends'), key(farm)),
84
+ ends: farm => at(seed('ends'), key(farm)),
83
85
  /** A seat: a player's in a farm of tokens, a collectible's (its asset) in a farm of collectibles. */
84
86
  seat: (farm, by) => at(seed('seat'), key(farm), key(by)),
85
87
  /** What the project writes about an NFT it has no authority over (its Sheet): `nft` is a Core asset, a Token Metadata mint or a compressed NFT's asset id. */
86
88
  sheet: (project, nft) => at(seed('sheet'), key(project), key(nft)),
87
89
  /** A compressed NFT as last proven (`prove_leaf`), read while the leaf is unchanged and its root is still in the tree's change log. */
88
- leaf: (asset) => at(seed('leaf'), key(asset)),
90
+ leaf: asset => at(seed('leaf'), key(asset)),
89
91
  /** The root of the traits the project vouches for, under its own `key` (a collection's address, by convention). */
90
92
  traits: (project, key_) => at(seed('traits'), key(project), key(key_)),
91
93
  /** Bubblegum's config of a tree: `[tree]`. */
92
- treeConfig: (tree) => findProgramAddress([key(tree)], BUBBLEGUM_PROGRAM)[0],
94
+ treeConfig: tree => findProgramAddress([key(tree)], BUBBLEGUM_PROGRAM)[0],
93
95
  /** A compressed NFT's asset id: Bubblegum's `["asset", tree, nonce]`, `nonce` the count minted before it. */
94
96
  compressedAsset: (tree, nonce) => findProgramAddress([seed('asset'), key(tree), u64(nonce)], BUBBLEGUM_PROGRAM)[0],
95
97
  /** A wallet's counts in a project: how many times it bought from each box, crafted each recipe or held tickets in
@@ -20,7 +20,8 @@ export function orderRefundAt(order) {
20
20
  const slot = BigInt(order.requested_slot);
21
21
  if (order.status === 'Failed') return { slot, at: null };
22
22
  if (order.status === 'Pending') return { slot, at: BigInt(order.asked_at) + STALE_SECONDS };
23
- if (order.status === 'Asked' && (BigInt(order.paid ?? 0) > 0n || order.held)) return { slot: null, at: BigInt(order.first_asked_at) + LOTTERY_TIMEOUT_SECONDS };
23
+ if (order.status === 'Asked' && (BigInt(order.paid ?? 0) > 0n || order.held))
24
+ return { slot: null, at: BigInt(order.first_asked_at) + LOTTERY_TIMEOUT_SECONDS };
24
25
  if (['Rolled', 'Waiting', 'Asked'].includes(order.status)) return null;
25
26
  throw new Error('Unknown order status.');
26
27
  }
@@ -39,7 +40,8 @@ export function orderRefundable(order, slot, now) {
39
40
  * again is asked from the next slot on.
40
41
  */
41
42
  export const rollAskable = (order, slot, now) =>
42
- (order.status === 'Waiting' && (BigInt(order.asked_first ?? 0) === 0n || BigInt(slot) > BigInt(order.requested_slot))) ||
43
+ (order.status === 'Waiting' &&
44
+ (BigInt(order.asked_first ?? 0) === 0n || BigInt(slot) > BigInt(order.requested_slot))) ||
43
45
  (order.status === 'Asked' && BigInt(now) > BigInt(order.asked_at) + STALE_SECONDS);
44
46
 
45
47
  /**
@@ -50,12 +52,16 @@ export function contestOff(contest, now) {
50
52
  if (contest.verdict === 'Off') return true;
51
53
  if (contest.verdict !== 'Open') return false;
52
54
  const total = contest.sides.reduce((sum, side) => sum + BigInt(side), 0n);
53
- return BigInt(now) > BigInt(contest.result_by) || (BigInt(now) >= BigInt(contest.closes_at) && total < BigInt(contest.min_pot ?? 0));
55
+ return (
56
+ BigInt(now) > BigInt(contest.result_by) ||
57
+ (BigInt(now) >= BigInt(contest.closes_at) && total < BigInt(contest.min_pot ?? 0))
58
+ );
54
59
  }
55
60
 
56
61
  /** Whether a lottery round's draw (`round`: its decoded record) was asked for and the oracle has not answered within
57
62
  * `STALE_SECONDS` by `now` (Unix seconds, the chain's clock): anyone may ask again. */
58
- export const lotteryDrawStuck = (round, now) => round?.status === 'Drawing' && BigInt(now) > BigInt(round.asked_at) + STALE_SECONDS;
63
+ export const lotteryDrawStuck = (round, now) =>
64
+ round?.status === 'Drawing' && BigInt(now) > BigInt(round.asked_at) + STALE_SECONDS;
59
65
 
60
66
  /**
61
67
  * Whether a lottery round's draw (`round`: its decoded record) was asked for and the oracle has not answered within
@@ -63,7 +69,8 @@ export const lotteryDrawStuck = (round, now) => round?.status === 'Drawing' && B
63
69
  * the next ask of that round (`drawLotteryIx` with its address) cancels it, and every entry is refunded. A round nobody
64
70
  * asked to draw is never cancelled.
65
71
  */
66
- export const lotteryCancellable = (round, now) => round?.status === 'Drawing' && BigInt(now) > BigInt(round.first_asked_at) + LOTTERY_TIMEOUT_SECONDS;
72
+ export const lotteryCancellable = (round, now) =>
73
+ round?.status === 'Drawing' && BigInt(now) > BigInt(round.first_asked_at) + LOTTERY_TIMEOUT_SECONDS;
67
74
 
68
75
  /** A farm seat's `earns_until` when nothing ends its earning (i64::MAX): no tier term and no fuel it reads. */
69
76
  export const NO_END = (1n << 63n) - 1n;
@@ -15,7 +15,7 @@ const PREFIX = new TextEncoder().encode('loot-roll-v1');
15
15
  /** A stream of the oracle's 32-byte `answer` at `path` (whole numbers): seed = sha256("loot-roll-v1" | answer | path as
16
16
  * u32 little-endian words); its nth number the first 8 bytes, little-endian, of sha256(seed | n as u64 little-endian). */
17
17
  export function stream(answer, path = []) {
18
- const words = path.slice(0, MAX_PATH).map((w) => uintLE(w, 4));
18
+ const words = path.slice(0, MAX_PATH).map(w => uintLE(w, 4));
19
19
  const seed = sha256(concat(PREFIX, Uint8Array.from(answer), ...words));
20
20
  let counter = 0n;
21
21
  return {
@@ -23,15 +23,18 @@ 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
  }
@@ -59,7 +62,7 @@ export const MAX_CALLBACK_ACCOUNTS = 21;
59
62
  export function callbackAccounts(entries) {
60
63
  const sources = new Set();
61
64
  let named = 0;
62
- for (const prize of entries.flatMap((e) => e.prizes)) {
65
+ for (const prize of entries.flatMap(e => e.prizes)) {
63
66
  if (prize.Item || prize.Compressed || prize.Nft || prize.Stash) named += 1;
64
67
  else if (prize.Tokens) {
65
68
  if (prize.Tokens.from.Pool) named += 1;
@@ -73,9 +76,9 @@ export function callbackAccounts(entries) {
73
76
  * How many fresh items' table accounts the oracle's answer to a roll of `entries` can carry (`order::table_budget`):
74
77
  * what the box's own accounts leave of `MAX_CALLBACK_ACCOUNTS`. Negative: the box can't be rolled at all.
75
78
  */
76
- export const tableBudget = (entries) => MAX_CALLBACK_ACCOUNTS - callbackAccounts(entries);
79
+ export const tableBudget = entries => MAX_CALLBACK_ACCOUNTS - callbackAccounts(entries);
77
80
 
78
- const draws = (lootBox) => (lootBox.slots?.length ? lootBox.slots.reduce((n, s) => n + Number(s.count), 0) : 1);
81
+ const draws = lootBox => (lootBox.slots?.length ? lootBox.slots.reduce((n, s) => n + Number(s.count), 0) : 1);
79
82
  const ZERO = '11111111111111111111111111111111';
80
83
 
81
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
@@ -84,8 +87,8 @@ export const MAX_ORDER_BYTES = 10_240;
84
87
 
85
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,
86
89
  * a roll, a held NFT or a raffled place nothing. */
87
- const wonBytes = (p) => (p.Item || p.Compressed ? 14 : p.Tokens || p.Nft || p.Stash ? 41 : 0);
88
- const tableCount = (item) => (item?.traits ?? []).filter((t) => t.from?.Table).length;
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;
89
92
 
90
93
  /**
91
94
  * The rows of fresh items' tables a draw of each entry writes into its order (`order::rows_of`), walked with the box's
@@ -94,13 +97,15 @@ const tableCount = (item) => (item?.traits ?? []).filter((t) => t.from?.Table).l
94
97
  */
95
98
  export function entryRows(entries, items = {}) {
96
99
  let budget = Math.max(0, tableBudget(entries));
97
- return entries.map(({ prizes }) => prizes.reduce((n, p) => {
98
- const id = p.Item?.[0] ?? p.Compressed?.item;
99
- const t = id === undefined ? 0 : tableCount(items[id]);
100
- if (t > budget) return n;
101
- budget -= t;
102
- return n + t;
103
- }, 0));
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
+ );
104
109
  }
105
110
 
106
111
  /**
@@ -113,20 +118,52 @@ export function entryRows(entries, items = {}) {
113
118
  */
114
119
  export function orderBytes(lootBox, shares = 0, opens = 1, items) {
115
120
  const drawn = draws(lootBox) * Math.max(1, opens);
116
- const aDraw = Math.max(0, ...lootBox.entries.map((e) => e.prizes.reduce((n, p) => n + wonBytes(p), 0)));
117
- const gives = lootBox.entries.some((e) => e.prizes.some((p) => p.Item || p.Compressed));
118
- const rows = items ? Math.max(0, ...entryRows(lootBox.entries, items)) : gives ? Math.max(0, tableBudget(lootBox.entries)) : 0;
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;
119
128
  const fates = lootBox.fates ?? [];
120
- const fate = fates.reduce((a, f) => (a && encodeBorsh(IDL, { defined: { name: 'Fate' } }, a).length >= encodeBorsh(IDL, { defined: { name: 'Fate' } }, f).length ? a : f), null);
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
+ );
121
138
  const empty = {
122
- project: ZERO, player: ZERO, mint: ZERO, loot_box: ZERO, box_version: 0,
139
+ project: ZERO,
140
+ player: ZERO,
141
+ mint: ZERO,
142
+ loot_box: ZERO,
143
+ box_version: 0,
123
144
  odds_bps: new Array(lootBox.odds?.length ?? 0).fill(0),
124
145
  status: 'Waiting',
125
146
  won: [],
126
- paid: 0n, fee: 0n,
147
+ paid: 0n,
148
+ fee: 0n,
127
149
  shares: new Array(shares).fill({ to: { Account: [ZERO] }, bps: 0 }),
128
- opens: 0, settled: 0, undrawn: 0, back: 0n, settler: ZERO, drew: 0, answer: new Uint8Array(32), drawn: [], reserved: 0n,
129
- requested_slot: 0n, asked_first: 0n, asked_at: 0n, first_asked_at: 0n, counted: 0, subject: lootBox.subject ? ZERO : null, held: false, fate,
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,
130
167
  };
131
168
  return encodeAccount(IDL, 'Order', empty).length + drawn * (aDraw + rows * 4);
132
169
  }
@@ -136,42 +173,105 @@ export function orderBytes(lootBox, shares = 0, opens = 1, items) {
136
173
  // answer, less a margin.
137
174
  const ROLL_BUDGET = 300_000n;
138
175
  const ROLL_HEAP = 32n * 1024n - 512n;
139
- const COST = { fixed: 20_000n, entry: 3_500n, blank: 1_400n, weigh: 1_000n, weighEntry: 40n, pair: 150n, item: 1_500n, tokens: 6_000n, draw: 9_000n, group: 250n, reread: 1_000n, finish: 15_000n, won: 150n, drawn: 50n, odds: 1_000n, share: 1_000n, fate: 1_000n, slot: 500n, slotEntry: 350n, openSlot: 150n };
140
- const BYTES = { start: 6_000n, prizeRead: 600n, entryRead: 180n, minted: 700n, pooled: 1_000n, prize: 200n, draw: 40n, finish: 2_000n, won: 120n, boxByte: 2n, gateByte: 3n, odds: 100n, fate: 60n, slot: 64n };
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
+ };
141
214
 
142
215
  /** The Borsh length of `value` as IDL type `type`, an address counted as 32 bytes whatever stands in for it (a spec's
143
216
  * `@mint:…` before it goes live). */
144
217
  function borshLength(type, value) {
145
- if (typeof type === 'string') return type === 'string' ? 4 + new TextEncoder().encode(String(value)).length : type === 'bytes' ? 4 + value.length : scalar[type];
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];
146
224
  if (type.array) return type.array[1] * (type.array[0] === 'u8' ? 1 : borshLength(type.array[0], value?.[0]));
147
225
  if (type.vec) return 4 + (value ?? []).reduce((n, v) => n + borshLength(type.vec, v), 0);
148
226
  if (type.option) return value == null ? 1 : 1 + borshLength(type.option, value);
149
- const definition = IDL.types.find((entry) => entry.name === type.defined.name).type;
227
+ const definition = IDL.types.find(entry => entry.name === type.defined.name).type;
150
228
  if (definition.kind === 'enum') {
151
229
  const name = typeof value === 'string' ? value : Object.keys(value)[0];
152
- const fields = definition.variants.find((v) => v.name === name)?.fields ?? [];
230
+ const fields = definition.variants.find(v => v.name === name)?.fields ?? [];
153
231
  const payload = typeof value === 'string' ? [] : value[name];
154
- return 1 + fields.reduce((n, f, i) => n + (typeof f === 'object' && 'name' in f ? borshLength(f.type, payload[f.name]) : borshLength(f, payload[i])), 0);
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
+ );
155
241
  }
156
242
  return definition.fields.reduce((n, f) => n + borshLength(f.type, value[f.name]), 0);
157
243
  }
158
244
 
159
- const defined = (name) => ({ defined: { name } });
245
+ const defined = name => ({ defined: { name } });
160
246
 
161
247
  /**
162
248
  * The heap reading a box takes beyond its entries (`order::box_bytes`): its odds, gate, shares, subject, slots and
163
249
  * fates, read whole with it, by their bytes and their number. `box`: the box's args, as `upsertBoxIx` takes them.
164
250
  */
165
251
  export function boxBytes({ odds = [], gate = null, shares = [], subject = null, slots = [], fates = [] }) {
166
- const slotsWritten = slots.map((s) => ({ only: [], distinct: false, share: 0, ...s }));
167
- const fatesWritten = fates.map((f) => ({ then: null, ...f, shifts: (f.shifts ?? []).map((x) => ({ from_now: false, ...x })) }));
168
- const bytes = borshLength({ vec: defined('Odds') }, odds) + borshLength({ vec: defined('Share') }, shares) + borshLength({ option: defined('Rule') }, subject)
169
- + borshLength({ vec: defined('Slot') }, slotsWritten) + borshLength({ vec: defined('Fate') }, fatesWritten);
170
- return BYTES.boxByte * BigInt(bytes) + BYTES.gateByte * BigInt(borshLength({ option: defined('Gate') }, gate)) + BYTES.odds * BigInt(odds.length) + BYTES.fate * BigInt(fates.length) + BYTES.slot * BigInt(slots.length);
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
+ );
171
271
  }
172
272
 
173
273
  /** The account a prize reads, as a string, when it reads one the box can name: entries naming the same share it. */
174
- const accountOf = (p) => {
274
+ const accountOf = p => {
175
275
  if (p.Item != null) return `item ${p.Item[0] ?? p.Item}`;
176
276
  if (p.Compressed) return `item ${p.Compressed.item}`;
177
277
  if (p.Tokens?.from?.Pool) return `pool ${p.Tokens.from.Pool[0] ?? p.Tokens.from.Pool}`;
@@ -189,41 +289,80 @@ const accountOf = (p) => {
189
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
190
290
  * hold (`PackTooLarge`).
191
291
  */
192
- export function drawsFit(entries, slots, opens = 1, { odds = 0, shares = 0, fates = 0, bytes = 0n, oracle = false } = {}) {
292
+ export function drawsFit(
293
+ entries,
294
+ slots,
295
+ opens = 1,
296
+ { odds = 0, shares = 0, fates = 0, bytes = 0n, oracle = false } = {},
297
+ ) {
193
298
  const list = typeof slots === 'number' || typeof slots === 'bigint' ? [{ count: slots, only: [] }] : (slots ?? []);
194
299
  const draws = list.reduce((n, s) => n + BigInt(s.count), 0n) * BigInt(opens);
195
300
  if (draws <= 1n) return true;
196
301
  const n = BigInt(entries.length);
197
302
  const weigh = COST.weigh + COST.weighEntry * n;
198
- const reads = entries.map((e) => e.prizes.map(accountOf));
199
- const cost = entries.map((e) => e.prizes.reduce((c, p) => {
200
- if (p.Item != null || p.Compressed || p.Nft) return { set: c.set + COST.item, again: c.again + COST.reread, bytes: c.bytes + BYTES.prize };
201
- if (p.Tokens) return { set: c.set + COST.tokens, again: c.again + (p.Tokens.from.Pool ? COST.reread : 0n), bytes: c.bytes + (p.Tokens.from.Pool ? BYTES.pooled : BYTES.minted) };
202
- if (p.Stash != null) return { set: c.set + COST.draw + COST.group, again: c.again + COST.reread, bytes: c.bytes + BYTES.prize };
203
- return { set: c.set + COST.item, again: c.again, bytes: c.bytes + BYTES.prize };
204
- }, { set: 0n, again: 0n, bytes: BYTES.draw }));
205
- const next = entries.map((_, i) => weigh + cost[i].set + cost.reduce((sum, c, j) => (j === i || reads[i].some((a) => a && reads[j].includes(a)) ? sum + c.again : sum), 0n));
206
- const most = BigInt(Math.max(1, ...entries.map((e) => e.prizes.length)));
207
- let spent = COST.fixed + COST.pair * n * n * most * most + entries.reduce((c, e) => c + (e.prizes.length ? COST.entry : COST.blank), 0n);
208
- let used = BYTES.start + BigInt(bytes) + BYTES.entryRead * n + BYTES.prizeRead * BigInt(entries.reduce((c, e) => c + e.prizes.length, 0));
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));
209
338
  let won = 0n;
210
- spent += (COST.slot + COST.slotEntry * n) * BigInt(Math.max(1, list.length)) + COST.openSlot * BigInt(list.length) * BigInt(opens) + COST.odds * BigInt(odds) + COST.share * BigInt(shares) + COST.fate * BigInt(fates);
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);
211
345
  for (const s of list) {
212
346
  const count = BigInt(s.count) * BigInt(opens);
213
- const among = entries.map((_, i) => i).filter((i) => !(s.only ?? []).length || (s.only ?? []).includes(i));
214
- const dearest = (f) => among.reduce((m, i) => (f(i) > m ? f(i) : m), 0n);
215
- spent += count * dearest((i) => next[i]);
216
- used += count * dearest((i) => cost[i].bytes);
217
- won += count * dearest((i) => BigInt(entries[i].prizes.length));
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));
218
352
  }
219
- const rolls = oracle || entries.length > 1 || entries.some((e) => e.prizes.some((p) => p.Stash != null));
220
- return spent + COST.finish + COST.won * won + COST.drawn * draws <= ROLL_BUDGET && (!rolls || used + BYTES.finish + BYTES.won * won + draws <= ROLL_HEAP);
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
+ );
221
358
  }
222
359
 
223
360
  // What one `buy` takes, as the program counts it at the box's write (`catalog::buy_fits`): a transaction holds 64
224
361
  // accounts, 64 instructions and 1,400,000 compute units.
225
362
  const BUY = { accounts: 15, calls: 7, cu: 30_000, most: { accounts: 64, calls: 64, cu: 1_400_000 } };
226
- const sameJson = (a, b) => JSON.stringify(a, (_, v) => (typeof v === 'bigint' ? String(v) : v)) === JSON.stringify(b, (_, v) => (typeof v === 'bigint' ? String(v) : v));
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));
227
366
 
228
367
  /**
229
368
  * Whether one `buy` of a box that doesn't roll (one entry, given `draws` times: its slots' counts) fits one transaction
@@ -231,13 +370,14 @@ const sameJson = (a, b) => JSON.stringify(a, (_, v) => (typeof v === 'bigint' ?
231
370
  * is given once at most. A box that doesn't fit sells through the oracle (`open_box`), its answer's heap counted
232
371
  * (`drawsFit` with `oracle`), and can't give a roll (`PackTooLarge`). `box`: the box's args.
233
372
  */
234
- export function buyFits(box, draws = (box.slots?.length ? box.slots.reduce((n, s) => n + Number(s.count), 0) : 1)) {
373
+ export function buyFits(box, draws = box.slots?.length ? box.slots.reduce((n, s) => n + Number(s.count), 0) : 1) {
235
374
  const entry = box.entries?.[0];
236
375
  if (!entry) return true;
237
376
  let { accounts, calls, cu } = BUY;
238
377
  for (const share of box.shares ?? []) {
239
378
  const to = typeof share.to === 'string' ? share.to : Object.keys(share.to)[0];
240
- const [a, c] = to === 'Burn' ? [0, 1] : to === 'Pool' || to === 'Account' ? [1, 1] : to === 'Holder' ? [3, 6] : [2, 6];
379
+ const [a, c] =
380
+ to === 'Burn' ? [0, 1] : to === 'Pool' || to === 'Account' ? [1, 1] : to === 'Holder' ? [3, 6] : [2, 6];
241
381
  accounts += a;
242
382
  calls += c;
243
383
  cu += 30_000;
@@ -247,11 +387,16 @@ export function buyFits(box, draws = (box.slots?.length ? box.slots.reduce((n, s
247
387
  const limit = box.per_wallet ?? {};
248
388
  if (Number(limit.total ?? 0) > 0 || Number(limit.per_window ?? 0) > 0) calls += 1;
249
389
  // A collectible's collection and Metaplex Core, named once for all of them.
250
- if (entry.prizes.some((p) => p.Item != null)) accounts += 2;
390
+ if (entry.prizes.some(p => p.Item != null)) accounts += 2;
251
391
  for (const [i, p] of entry.prizes.entries()) {
252
- const first = !entry.prizes.slice(0, i).some((q) => (accountOf(q) && accountOf(q) === accountOf(p)) || sameJson(q, p));
392
+ const first = !entry.prizes
393
+ .slice(0, i)
394
+ .some(q => (accountOf(q) && accountOf(q) === accountOf(p)) || sameJson(q, p));
253
395
  const gives = p.Tokens ? 1 : draws;
254
- let once = 0, each = 0, c = 0, units = 0;
396
+ let once = 0,
397
+ each = 0,
398
+ c = 0,
399
+ units = 0;
255
400
  if (p.Tokens) [once, c, units] = [p.Tokens.from.Pool ? 3 : 2, 5, 30_000];
256
401
  else if (p.Item != null) [once, each, c, units] = [1, 1, 4, 40_000];
257
402
  else if (p.Compressed) [once, c, units] = [9, 6, 150_000];
@@ -280,8 +425,18 @@ const PLAYER_FEE = 2_000_000n;
280
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
281
426
  * orders, each its own answer.
282
427
  */
283
- export function opensAnOrder(lootBox, { shares = (lootBox.shares ?? []).length, items = {}, rentPerByte = RENT_PER_BYTE } = {}) {
284
- const fits = (n) => drawsFit(lootBox.entries, lootBox.slots?.length ? lootBox.slots : 1, n, { odds: (lootBox.odds ?? []).length, shares, fates: (lootBox.fates ?? []).length, bytes: boxBytes(lootBox), oracle: true }) && (BigInt(orderBytes(lootBox, shares, n, items)) + 128n) * rentPerByte + PLAYER_FEE <= ORDER_COST_CAP;
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;
285
440
  let n = 1;
286
441
  while (n < 4096 && fits(n + 1)) n += 1;
287
442
  return n;
@@ -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]));