@xgamesfun/solana 0.0.0-stage → 0.24.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 (138) hide show
  1. package/DISCLAIMER.md +41 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +13 -0
  4. package/README.md +172 -2
  5. package/bin/xsolana.mjs +189 -0
  6. package/examples/artwork-auction.json +51 -0
  7. package/examples/blind-forge.json +128 -0
  8. package/examples/coin-flip-arena.json +103 -0
  9. package/examples/duel-arena.json +137 -0
  10. package/examples/elements-set.json +274 -0
  11. package/examples/event-badges.json +67 -0
  12. package/examples/expedition-guild.json +154 -0
  13. package/examples/fair-presale.json +48 -0
  14. package/examples/falling-drop.json +90 -0
  15. package/examples/guild-missions.json +137 -0
  16. package/examples/halving-seasons.json +107 -0
  17. package/examples/hero-trials.json +93 -0
  18. package/examples/ideas.json +789 -0
  19. package/examples/level-ladder.json +65 -0
  20. package/examples/merge-2048.json +128 -0
  21. package/examples/nft-raffle.json +33 -0
  22. package/examples/paper-hands-farm.json +44 -0
  23. package/examples/partner-vault.json +112 -0
  24. package/examples/sacrifice-raffle.json +118 -0
  25. package/examples/scorched-treasury.json +84 -0
  26. package/examples/shelter-fair.json +164 -0
  27. package/examples/spark-pity.json +153 -0
  28. package/examples/stakers-draw.json +118 -0
  29. package/examples/star-fleet.json +150 -0
  30. package/examples/starforce.json +116 -0
  31. package/examples/stud-farm.json +134 -0
  32. package/examples/team-vesting.json +70 -0
  33. package/examples/trade-in-shop.json +168 -0
  34. package/js/approve/details.mjs +263 -0
  35. package/js/approve/ed25519.mjs +36 -0
  36. package/js/approve/live-check.mjs +144 -0
  37. package/js/approve/page.html +86 -0
  38. package/js/approve/page.js +203 -0
  39. package/js/approve/server.mjs +726 -0
  40. package/js/chain/base58.js +73 -0
  41. package/js/chain/borsh.js +270 -0
  42. package/js/chain/bytes.js +141 -0
  43. package/js/chain/core.js +181 -0
  44. package/js/chain/curve.js +52 -0
  45. package/js/chain/lookup.js +279 -0
  46. package/js/chain/mint.js +96 -0
  47. package/js/chain/pda.js +66 -0
  48. package/js/chain/programs.js +102 -0
  49. package/js/chain/rpc.js +219 -0
  50. package/js/chain/sha256.js +68 -0
  51. package/js/chain/token.js +85 -0
  52. package/js/chain/token2022.js +247 -0
  53. package/js/chain/tx.js +224 -0
  54. package/js/client/accounts.js +679 -0
  55. package/js/client/compressed.js +46 -0
  56. package/js/client/grant.js +299 -0
  57. package/js/client/idl/loot.js +2 -0
  58. package/js/client/idl/loot.json +17206 -0
  59. package/js/client/index.js +10 -0
  60. package/js/client/ix.js +2887 -0
  61. package/js/client/pda.js +114 -0
  62. package/js/client/policy.js +69 -0
  63. package/js/client/random.js +70 -0
  64. package/js/client/sizes.js +297 -0
  65. package/js/client/traits.js +44 -0
  66. package/js/connect/connect.mjs +182 -0
  67. package/js/economy/check.mjs +2236 -0
  68. package/js/economy/cost.mjs +185 -0
  69. package/js/economy/examples.mjs +114 -0
  70. package/js/economy/explain.mjs +239 -0
  71. package/js/economy/guide.mjs +232 -0
  72. package/js/economy/legal.mjs +15 -0
  73. package/js/economy/live.mjs +130 -0
  74. package/js/economy/model.mjs +698 -0
  75. package/js/economy/plan.mjs +1155 -0
  76. package/js/economy/schema.json +3071 -0
  77. package/js/economy/secrets.mjs +60 -0
  78. package/js/economy/simulate.mjs +1019 -0
  79. package/js/economy/txsize.mjs +45 -0
  80. package/js/economy/validate.mjs +85 -0
  81. package/js/index.js +10 -0
  82. package/js/mcp/server.mjs +113 -0
  83. package/js/mcp/tools.mjs +485 -0
  84. package/js/play/index.js +2855 -0
  85. package/js/play/keys.js +167 -0
  86. package/js/results/index.js +184 -0
  87. package/js/results/wire.js +90 -0
  88. package/package.json +100 -4
  89. package/release.json +8 -0
  90. package/skills/xsolana-economy/SKILL.md +109 -0
  91. package/skills/xsolana-economy/references/after-launch.md +124 -0
  92. package/skills/xsolana-economy/references/apps.md +193 -0
  93. package/skills/xsolana-economy/references/sentence-to-spec.md +180 -0
  94. package/skills/xsolana-economy/references/telling-the-person.md +134 -0
  95. package/types/chain/base58.d.ts +10 -0
  96. package/types/chain/borsh.d.ts +23 -0
  97. package/types/chain/bytes.d.ts +45 -0
  98. package/types/chain/core.d.ts +52 -0
  99. package/types/chain/curve.d.ts +1 -0
  100. package/types/chain/lookup.d.ts +194 -0
  101. package/types/chain/mint.d.ts +25 -0
  102. package/types/chain/pda.d.ts +25 -0
  103. package/types/chain/programs.d.ts +65 -0
  104. package/types/chain/rpc.d.ts +68 -0
  105. package/types/chain/sha256.d.ts +1 -0
  106. package/types/chain/token.d.ts +141 -0
  107. package/types/chain/token2022.d.ts +53 -0
  108. package/types/chain/tx.d.ts +60 -0
  109. package/types/client/accounts.d.ts +318 -0
  110. package/types/client/compressed.d.ts +29 -0
  111. package/types/client/grant.d.ts +170 -0
  112. package/types/client/idl/loot.d.ts +4498 -0
  113. package/types/client/index.d.ts +9 -0
  114. package/types/client/ix.d.ts +5315 -0
  115. package/types/client/pda.d.ts +95 -0
  116. package/types/client/policy.d.ts +48 -0
  117. package/types/client/random.d.ts +26 -0
  118. package/types/client/sizes.d.ts +101 -0
  119. package/types/client/traits.d.ts +12 -0
  120. package/types/economy/check.d.mts +100 -0
  121. package/types/economy/cost.d.mts +69 -0
  122. package/types/economy/examples.d.mts +27 -0
  123. package/types/economy/explain.d.mts +19 -0
  124. package/types/economy/guide.d.mts +140 -0
  125. package/types/economy/legal.d.mts +12 -0
  126. package/types/economy/live.d.mts +124 -0
  127. package/types/economy/model.d.mts +431 -0
  128. package/types/economy/plan.d.mts +1014 -0
  129. package/types/economy/secrets.d.mts +4 -0
  130. package/types/economy/simulate.d.mts +164 -0
  131. package/types/economy/txsize.d.mts +17 -0
  132. package/types/economy/validate.d.mts +3 -0
  133. package/types/index.d.ts +9 -0
  134. package/types/mcp/tools.d.mts +635 -0
  135. package/types/play/index.d.ts +880 -0
  136. package/types/play/keys.d.ts +51 -0
  137. package/types/results/index.d.ts +155 -0
  138. package/types/results/wire.d.ts +122 -0
@@ -0,0 +1,1155 @@
1
+ // The setup plan: every instruction that puts an economy live on loot, in order, built with the client and
2
+ // packed into transactions that fit. Nothing here signs or talks to the chain. Pure.
3
+ //
4
+ // A step is { id, part, says, signers, creates: [{ what, bytes }], build(ctx) → instructions }. `ctx` names the
5
+ // addresses a step needs: { owner, authority, project, collection, tree, treasury, mints: { tokenKey: mint }, keys: {
6
+ // name: address } } where `keys` are the fresh keys this setup makes ("new:collection", "new:tree", "new:mint/points"), each of
7
+ // which signs the step that creates it. `placeholderContext` fills every address with a stand-in, so a plan can be
8
+ // sized and shown before anyone connects a wallet.
9
+ import { encodeBase58 } from '../chain/base58.js';
10
+ import { encodeBorsh } from '../chain/borsh.js';
11
+ import IDL from '../client/idl/loot.js';
12
+ import { createCollectionInstruction } from '../chain/core.js';
13
+ import { ASSOCIATED_TOKEN_PROGRAM, NATIVE_MINT, SYSTEM_PROGRAM, SYSVAR_INSTRUCTIONS, SYSVAR_SLOT_HASHES, TOKEN_2022_PROGRAM, TOKEN_PROGRAM } from '../chain/pda.js';
14
+ import { createTableIx, extendTableIx } from '../chain/lookup.js';
15
+ import { MEMO_PROGRAM } from '../chain/token2022.js';
16
+ import { createAssociatedTokenAccountIdempotent, syncNative, transferLamports } from '../chain/programs.js';
17
+ import { createAccountIx, initializeMint2Ix, MINT_BYTES, mintToIx, setAuthorityIx, transferCheckedIx } from '../chain/token.js';
18
+ import { compileMessage, transactionSize } from '../chain/tx.js';
19
+ import { sha256 } from '../chain/sha256.js';
20
+ import { utf8 } from '../chain/bytes.js';
21
+ import * as loot from '../client/index.js';
22
+ import { check } from './check.mjs';
23
+ import { ACCOUNT_OVERHEAD, boxBuys, rent, RENT_PER_BYTE, boxRolls, compressedTree, destination, effectOf, gateOf, listOf, lotterySplit, model, MOST_RENT_PER_BYTE, outcomeParts, perWalletOf, placeOf, lifeLines, prizeLine, prizePool, prizeRolls, prizeToken, rarityNumber, ruleOf, sharesOf, spendOf, stashNfts, toBy, toEnum, toSeconds, toTime, toUnits, weights, wholeCount, windowOf } from './model.mjs';
24
+ import { BUDGET } from './txsize.mjs';
25
+
26
+ const owner = 'owner';
27
+ /** Most transactions one wallet approval carries. */
28
+ export const PER_APPROVAL = 12;
29
+
30
+ const amount = (value, t) => toUnits(value ?? 0, t?.decimals ?? 0);
31
+ const shares = (split, m) => sharesOf(split).map((s) => ({ to: toEnum(s.to, m), bps: s.bps ?? 0 }));
32
+ const rise = (r, t) => (r ? { step: amount(r.step ?? 0, t), growth_bps: Math.round((r.growthPercent ?? 0) * 100) } : null);
33
+ const fall = (f, t, now) => (f ? { step: amount(f.step, t), every: toSeconds(f.every), floor: amount(f.floor ?? 0, t), since: BigInt(toTime(f.start, now)), shared: f.clock === 'shared' } : null);
34
+ const tokenProgramOf = (t) => (t?.program === 'token-2022' ? TOKEN_2022_PROGRAM : TOKEN_PROGRAM);
35
+
36
+ // ---------------------------------------------------------------- spec parts → the program's args
37
+
38
+ export function itemArgs(it, m) {
39
+ const kind = it.kind === 'normal' ? 'Normal' : 'Collectible';
40
+ // A collectible is minted into its own `collection`, or the one the setup makes ("@collection" until it exists). Its
41
+ // life is written into each recipe that takes it (`lifeLines`).
42
+ return {
43
+ kind,
44
+ rarity: rarityNumber(it.rarity),
45
+ soulbound: !!it.soulbound,
46
+ burnable: !!it.burnable,
47
+ name: it.name,
48
+ uri: it.uri ?? '',
49
+ max_supply: it.supply ?? 0,
50
+ active: it.active !== false,
51
+ collection: kind === 'Normal' ? NOBODY : it.collection ?? '@collection',
52
+ // A fresh collectible's traits, each drawn per unit (a table's row, or a number in a range).
53
+ traits: kind === 'Normal' ? [] : (it.drawn ?? []).map((t) => ({ key: t.attribute, from: t.table !== undefined ? { Table: [m?.table.get(t.table)?.id ?? 0] } : { Range: { min: BigInt(t.min ?? 0), max: BigInt(t.max ?? 0) } } })),
54
+ };
55
+ }
56
+
57
+ /**
58
+ * The bytes of an order of box `b` (`orderBytes`), its spec's names standing in as addresses, with the shares its split
59
+ * stores. `opened`: an open's (`open_box`, `Order::sized`: each draw at its dearest outcome's prizes as they are kept,
60
+ * rows only for the tables its fresh items draw); otherwise a roll made inside another call (a craft, a grant's claim,
61
+ * a mission, a lottery's place, a roll lot: `Order::space_for`, the box's whole table budget a draw).
62
+ */
63
+ export const boxOrderBytes = (b, m, opens = 1, { opened = false } = {}) => {
64
+ const args = boxArgs(b, m);
65
+ const items = opened ? Object.fromEntries(m.items.map((it) => [it.id, itemArgs(it, m)])) : undefined;
66
+ return loot.orderBytes(placeholders(args), (args.shares ?? []).length, opens, items);
67
+ };
68
+
69
+ /** The most bytes of rows one table write carries: what a 1,232-byte transaction leaves after the upsert's accounts,
70
+ * its signature and its own bytes. */
71
+ export const TABLE_BATCH_BYTES = 800;
72
+
73
+ /** A spec table's rows as the program takes them ({ weight, left, label }), in batches that fit a transaction each. */
74
+ export function tableBatches(t) {
75
+ const rows = t.rows.map((r) => ({ weight: r.weight, left: r.stock ?? 4294967295, label: t.kind === 'numbers' ? '' : String(r.label ?? '') }));
76
+ const batches = [[]];
77
+ let bytes = 0;
78
+ for (const r of rows) {
79
+ const size = 12 + new TextEncoder().encode(r.label).length;
80
+ if (bytes + size > TABLE_BATCH_BYTES && batches.at(-1).length) batches.push([]), (bytes = 0);
81
+ batches.at(-1).push(r);
82
+ bytes += size;
83
+ }
84
+ return batches;
85
+ }
86
+
87
+ /** The program's `Prize` for one part of a box outcome (`model.outcomeParts`). */
88
+ export function partPrize(o, m) {
89
+ // An item, or a compressed NFT of it, minted at the settle into the project's tree.
90
+ if (o.gives === 'item' && o.compressed) return { Compressed: { item: m.item.get(o.item)?.id ?? 0, tree: '@tree' } };
91
+ if (o.gives === 'item') return { Item: [m.item.get(o.item)?.id ?? 0] };
92
+ if (o.gives === 'stash') return { Stash: [m.stash.get(o.stash)?.id ?? 0] };
93
+ if (o.gives === 'pay') return { Tokens: { from: { Pool: [m.pool.get(o.pool)?.index ?? 0] }, amount: amount(o.amount, m.poolToken(o.pool)) } };
94
+ if (o.gives === 'mint') return { Tokens: { from: { Mint: [`@mint:${o.currency ?? m.currencyKey}`] }, amount: amount(o.amount, m.tokens.get(o.currency ?? m.currencyKey)) } };
95
+ return prizeArgs(o, m);
96
+ }
97
+
98
+ export function boxArgs(b, m) {
99
+ const t = m.priceToken(b);
100
+ const w = weights(b);
101
+ return {
102
+ price: amount(b.price, t),
103
+ currency: `@mint:${b.currency ?? m.currencyKey}`,
104
+ rise: rise(b.rise, t),
105
+ fall: fall(b.fall, t, m.now),
106
+ stock_odds: !!b.stockOdds,
107
+ when_out: b.whenOut === 'drawLeft' ? 'DrawLeft' : 'StopSales',
108
+ active: b.active !== false,
109
+ shares: shares(b.split, m),
110
+ // What one open draws: each slot so many draws among the outcomes it names (all, when none), no outcome twice
111
+ // when distinct. None is one draw among every outcome.
112
+ slots: (b.slots ?? []).map((s) => ({ count: s.count, only: s.from ?? [], distinct: Boolean(s.distinct), share: Math.round((s.share ?? 0) * 100) })),
113
+ // An entry gives its prizes (a bundle of any number), and may burn the payment in place of the split.
114
+ entries: (b.outcomes ?? []).map((o, i) => ({ prizes: outcomeParts(o).map((part) => partPrize(part, m)), weight: w[i] })),
115
+ gate: gateOf(b.gate, m),
116
+ odds: (b.odds ?? []).map((o) => ({ entry: o.outcome, boost: { rule: ruleOf(o.when, m), effect: effectOf(o.add) } })),
117
+ window: windowOf(b, m.now),
118
+ per_wallet: perWalletOf(b),
119
+ // A box that rolls on an NFT: the rule it must pass, and what each outcome does to it.
120
+ subject: b.rollsOn ? ruleOf(b.rollsOn, m) : null,
121
+ fates: (b.outcomes ?? []).flatMap((o, i) => {
122
+ const shifts = (o.shifts ?? []).map((x) => ({ key: x.attribute, by: toBy(x.by), most: toSeconds(x.upTo ?? 0), from_now: Boolean(x.fromNow) }));
123
+ // An outcome's own `then`, or the box's: an NFT taken as its price goes there whatever the roll gives.
124
+ const then = thenOf(o.then ?? b.then, m);
125
+ return shifts.length || then !== null ? [{ entry: i, shifts, then }] : [];
126
+ }),
127
+ };
128
+ }
129
+
130
+ /** Where an outcome sends the NFT a box rolls on: back (null), burned, given (the treasury's wallet is filled in when the economy goes live), or into a stash. */
131
+ export const thenOf = (then, m) => {
132
+ if (!then || then === 'back') return null;
133
+ if (then === 'burn') return 'Burn';
134
+ if (then.stash) return { Stash: [m?.stash.get(then.stash)?.id ?? 0] };
135
+ return { Give: [then === 'treasury' ? '@treasury' : then.giveTo] };
136
+ };
137
+
138
+ export function recipeArgs(r, m) {
139
+ const tok = (key) => m.tokens.get(key ?? m.currencyKey);
140
+ const inputs = (r.in ?? []).map((l) => {
141
+ if (l.takes === 'burn') return { Burn: { item: m.item.get(l.item)?.id ?? 0, count: l.count ?? 1 } };
142
+ if (l.takes === 'use') return { Use: { item: m.item.get(l.item)?.id ?? 0 } };
143
+ // A staked one names the farm it sits in (craft.rs): the craft takes it only from its seat there.
144
+ if (l.takes === 'staked') return { Staked: { item: m.item.get(l.item)?.id ?? 0, farm: m.farm.get(l.farm)?.id ?? 0 } };
145
+ // A renew keeps the collectible as a use does; its life's raise counts its uses from 0 again (`lifeLines`).
146
+ if (l.takes === 'renew') return { Use: { item: m.item.get(l.item)?.id ?? 0 } };
147
+ if (l.takes === 'show') return { Show: { collection: l.collection ?? '@collection' } };
148
+ // A trade-in: one NFT of `collection`, any kind, burned, given on, or put in a stash.
149
+ if (l.takes === 'trade') return { Trade: { collection: l.collection ?? '@collection', then: spendOf(l.then, m) } };
150
+ return { Pay: { mint: `@mint:${l.currency ?? m.currencyKey}`, amount: amount(l.amount, tok(l.currency)), to: toEnum(l.to, m) } };
151
+ });
152
+ const life = lifeLines(r, m);
153
+ // A line is a prize (prize.rs), or a roll on a collectible it keeps (`on`).
154
+ const outputs = (r.out ?? []).map((l) => (l.gives === 'roll' && l.on !== undefined ? { RollOn: { box_id: m.box.get(l.box)?.id ?? 0, place: l.on } } : { Prize: [prizeArgs(l, m)] }));
155
+ return {
156
+ active: r.active !== false,
157
+ inputs,
158
+ outputs,
159
+ // The recipe's own needs and raises, then what the life of each collectible it takes adds (`lifeLines`).
160
+ needs: [...(r.needs ?? []).map((n) => ({ place: n.place, key: n.key, test: { AtLeast: [n.atLeast ?? 0] } })), ...life.needs],
161
+ keeps: (r.keeps ?? []).map((k) => ({ key: k.key, from: k.from, to: k.to })),
162
+ raises: [...(r.raises ?? []).map((x) => ({ place: x.place, shift: { key: x.key, by: toBy(x.by), most: toSeconds(x.upTo ?? 0), from_now: Boolean(x.fromNow) } })), ...life.raises],
163
+ window: windowOf(r, m.now),
164
+ per_wallet: perWalletOf(r),
165
+ };
166
+ }
167
+
168
+ export function scheduleArgs(s, m) {
169
+ const every = toSeconds(s.every);
170
+ const poolOf = (key) => m.pool.get(key)?.index ?? 0;
171
+ let action;
172
+ if (s.does === 'issue') {
173
+ // Minted into a pool, or into a farm's rewards vault ("to": "farm:<key>"): an emission its stakers share.
174
+ const farm = destination(s.to ?? '').kind === 'farm' ? m.farm.get(destination(s.to).key) : null;
175
+ const t = m.tokens.get(s.currency ?? (farm ? farm.rewardsToken : m.pool.get(s.pool)?.currency));
176
+ action = { from: { Mint: [`@mint:${t?.key}`] }, to: farm ? toEnum(s.to, m) : { Pool: [poolOf(s.pool)] }, amount: { Fixed: [amount(s.amount, t)] }, halve_every: s.halveEvery ?? 0 };
177
+ } else if (s.sealed) {
178
+ // A sealed schedule pays from its own vault, in `currency`.
179
+ const t = m.tokens.get(s.currency ?? m.currencyKey);
180
+ const how = s.percent !== undefined ? { Bps: [Math.round(s.percent * 100)] } : { Fixed: [amount(s.amount, t)] };
181
+ action = { from: { Vest: [`@mint:${t?.key}`] }, to: toEnum(s.to, m), amount: how, halve_every: s.halveEvery ?? 0 };
182
+ } else {
183
+ const from = s.does === 'burn' ? s.pool : s.from;
184
+ const t = m.poolToken(from);
185
+ const to = s.does === 'burn' ? 'Burn' : s.does === 'move' ? { Pool: [poolOf(s.to)] } : toEnum(s.to, m);
186
+ const how = s.percent !== undefined ? { Bps: [Math.round(s.percent * 100)] } : { Fixed: [amount(s.amount, t)] };
187
+ action = { from: { Pool: [poolOf(from)] }, to, amount: how, halve_every: s.halveEvery ?? 0 };
188
+ }
189
+ return { active: s.active !== false, action, every: BigInt(every), next_at: BigInt(s.start ? toTime(s.start, m.now) : m.now + every), max_runs: s.maxRuns ?? 0 };
190
+ }
191
+
192
+ /**
193
+ * The program's `Prize` for a prize line (`model.prizeLine`): tokens minted or from a pool (a fixed amount, or a share
194
+ * of the pool), an item (a compressed NFT of it into the project's tree, "@tree"), one NFT of a stash, or a roll of a box.
195
+ */
196
+ export function prizeArgs(p, m) {
197
+ const line = prizeLine(p);
198
+ if (line.gives === 'item' && line.compressed) return { Compressed: { item: m.item.get(line.item)?.id ?? 0, tree: '@tree' } };
199
+ if (line.gives === 'item') return { Item: [m.item.get(line.item)?.id ?? 0] };
200
+ // An NFT a lottery's place holds in the lottery's own stash (a raffle), or one of a project stash.
201
+ if (line.gives === 'nft' && line.held) return { Held: [line.nft] };
202
+ // An NFT drawn at random from the lottery's own stash (a raffle of many NFTs).
203
+ if (line.gives === 'raffled') return 'Raffled';
204
+ if (line.gives === 'nft') return { Nft: { stash: m.stash.get(line.stash)?.id ?? 0, nft: line.nft } };
205
+ if (line.gives === 'roll') return { Roll: [m.box.get(line.box)?.id ?? 0] };
206
+ const pool = prizePool(line);
207
+ const from = pool ? { Pool: [m.pool.get(pool)?.index ?? 0] } : { Mint: [`@mint:${line.currency ?? m.currencyKey}`] };
208
+ return { Tokens: { from, amount: amount(line.amount, prizeToken(line, m)) } };
209
+ }
210
+
211
+ /**
212
+ * A grant list's leaves: a plain address is the grant's amount in round 0; an entry { wallet, amount, round } names
213
+ * its own amount (whole units of `t`, the grant's token) and round, so one wallet listed under several rounds claims
214
+ * each once (monthly vesting, a season's airdrop).
215
+ */
216
+ export const listLeaves = (list, t) => list.map((e) => (typeof e === 'string' ? loot.listLeaf(e, null) : loot.listLeaf(e.wallet, null, loot.listRound(e.round ?? 0), e.amount !== undefined ? amount(e.amount, t) : 0n)));
217
+
218
+ /** A prize line's `Prize`, or null for none. */
219
+ const prizeOrNull = (p, m) => (prizeLine(p) ? prizeArgs(p, m) : null);
220
+
221
+ /**
222
+ * A lottery's args: its own pot in `currency` (the project's by default), ticket money split by `split` (and
223
+ * `cutPercent` to the treasury) with the rest into the pot. No `every` draws once, at `firstDraw`.
224
+ */
225
+ export function lotteryArgs(l, m) {
226
+ const potKey = l.currency ?? m.currencyKey;
227
+ const potToken = m.tokens.get(potKey);
228
+ const ticket = l.ticketCurrency ? m.tokens.get(l.ticketCurrency) : potToken;
229
+ const every = toSeconds(l.every);
230
+ return {
231
+ // What a round copies at its first purchase.
232
+ terms: {
233
+ price: l.ticketNft ? 0n : amount(l.price, ticket),
234
+ mint: `@mint:${potKey}`,
235
+ shares: shares(lotterySplit(l), m),
236
+ // A place takes a share of the pot, a prize for its winner, or both.
237
+ // A run of `times` places alike: "places 2 to 201 each take 1%" is one.
238
+ places: (l.places ?? []).map(placeOf).map((p) => ({ bps: Math.round(p.percent * 100), prize: prizeOrNull(p.prize, m), times: p.times })),
239
+ ticket_mint: l.ticketCurrency ? `@mint:${l.ticketCurrency}` : '11111111111111111111111111111111',
240
+ draws_at: BigInt(l.firstDraw ? toTime(l.firstDraw, m.now) : m.now + every),
241
+ // A ticket paid with one NFT that passes the rule, in place of tokens.
242
+ nft_ticket: l.ticketNft ? { rule: ruleOf(l.ticketNft.rule, m), then: spendOf(l.ticketNft.then, m) } : null,
243
+ // No loss: every ticket goes back to its buyer once the round is drawn; the winners take the pot alone.
244
+ returns: Boolean(l.noLoss),
245
+ // A least sale: a round that sold fewer tickets is cancelled at its draw and refunded.
246
+ min_sold: BigInt(l.minSold ?? 0),
247
+ },
248
+ every: BigInt(every),
249
+ gate: gateOf(l.gate, m),
250
+ per_wallet: l.perWallet ?? 0,
251
+ };
252
+ }
253
+
254
+ /**
255
+ * A lottery round's header as its first purchase sizes it (`LotteryRound::new`: its `head`), and the token accounts a
256
+ * round may open (`accounts`: the payment mint's, the pot's, one for each run whose prize comes from a pool). The
257
+ * header holds a copy of the terms, a total for each share, two bits a place, a record for each run of tokens and an
258
+ * NFT slot for each place of the lottery's own stash.
259
+ */
260
+ export function roundSize(l, m) {
261
+ const { terms } = placeholders(lotteryArgs(l, m));
262
+ const places = terms.places.reduce((n, p) => n + Number(p.times ?? 1), 0);
263
+ const tokens = terms.places.filter((p) => p.prize?.Tokens).length;
264
+ const pools = terms.places.filter((p) => p.prize?.Tokens?.from.Pool).length;
265
+ const own = terms.places.filter((p) => p.prize?.Held || p.prize === 'Raffled' || p.prize?.Raffled !== undefined).reduce((n, p) => n + Number(p.times ?? 1), 0);
266
+ const termsBytes = encodeBorsh(IDL, { defined: { name: 'LotteryTerms' } }, terms).length;
267
+ // The tag; project, lottery, id, incarnation, round, bump, opener, lent, head, status, revision; the terms; fee, sold,
268
+ // to_pot; common; owns, owed, the draw's slots and times, answer and first ticket, the pot's four numbers; paid; prizes (59
269
+ // bytes each); accounts (64 each); nfts; assigned.
270
+ const head = 1 + 130 + termsBytes + 17 + (4 + 8 * terms.shares.length) + 112 + (4 + Math.ceil((2 * places) / 8)) + (4 + 59 * tokens) + (4 + 64 * (2 + pools)) + (4 + 32 * own) + 4;
271
+ const payment = l.ticketCurrency ?? l.currency ?? m.currencyKey;
272
+ const accounts = (l.ticketNft ? 1 : payment === (l.currency ?? m.currencyKey) ? 1 : 2) + pools;
273
+ return { head, accounts };
274
+ }
275
+
276
+ /** How many rounds of a lottery are open or still paying out at once, and how many purchases a round takes, as the
277
+ * spec expects them (`expect`): by default two rounds of a lottery that draws again (one selling, one paying out) and
278
+ * one of a lottery that draws once; a round's purchases from the scenario's purchases a day over a round, else 100. */
279
+ export function roundsExpected(l, m) {
280
+ const every = toSeconds(l.every ?? 0);
281
+ const perDay = Number(m.scenario?.perDay?.[`lottery:${l.key}`] ?? 0);
282
+ const days = every > 0 ? every / 86_400 : l.firstDraw ? Math.max(1, (toTime(l.firstDraw, m.now) - m.now) / 86_400) : 1;
283
+ const rounds = l.expect?.rounds ?? (every > 0 ? 2 : 1);
284
+ const entries = l.expect?.entries ?? (perDay > 0 ? Math.ceil(perDay * days) : 100);
285
+ return { rounds, entries };
286
+ }
287
+
288
+ /**
289
+ * The SOL, in lamports, a lottery holds for its rounds' rent: the spec's `roundRent` (0 turns it off), or by default
290
+ * what its expected rounds take at once (`roundsExpected`), each its header, its token accounts and its expected
291
+ * entries' bytes (`LotteryRound.lent`: in a round the lottery opened, its spare SOL pays each entry while it covers
292
+ * them, so buyers pay no program rent for a ticket), at devnet's and mainnet's rent, rounded up to 0.001 SOL. Short
293
+ * of it (a busier round, a cluster that charges more), the first buyer fronts the header and later buyers pay their
294
+ * own entries. Each round gives back what it took when it closes; the lottery gives the rest back to the owner when it closes.
295
+ */
296
+ export function roundRentOf(l, m) {
297
+ if (l.roundRent !== undefined) return toUnits(l.roundRent, 9);
298
+ const { head, accounts } = roundSize(l, m);
299
+ const { rounds, entries } = roundsExpected(l, m);
300
+ const width = loot.entryWidth(lotterySplit(l).some((s) => String(s.to).startsWith('chosen')), Boolean(l.ticketNft));
301
+ // The header and its entries' list head (6 bytes), each token account, each entry's bytes.
302
+ const one = rent(head + 6) + BigInt(accounts) * rent(165) + BigInt(entries * width) * RENT_PER_BYTE;
303
+ const all = BigInt(rounds) * one;
304
+ return ((all + 999_999n) / 1_000_000n) * 1_000_000n;
305
+ }
306
+
307
+ /**
308
+ * The args the authority opens a contest with, `at` (Unix seconds) its opening: its sides, when entries close and the
309
+ * result is due, its split, its gate (a list of wallets for a duel), the keys that may decide it, the least pot (an
310
+ * all-or-nothing crowdfund) and the stake every entry puts in.
311
+ */
312
+ export function contestArgs(c, m, at = m.now) {
313
+ const t = m.priceToken(c);
314
+ // A decider's verdict is taken once entries close, so with deciders entries close before the result is due (the
315
+ // program refuses them at once): with no "entriesClose", an hour before the result is due (halfway for a contest
316
+ // shorter than two hours); with no "resultDue", an hour after entries close.
317
+ const deciders = (c.deciders ?? []).length > 0;
318
+ const due = c.resultDue === undefined && c.entriesClose !== undefined && deciders ? toSeconds(c.entriesClose) + 3600 : toSeconds(c.resultDue ?? c.entriesClose ?? '1d');
319
+ const closes = c.entriesClose !== undefined ? toSeconds(c.entriesClose) : deciders ? due - Math.min(3600, Math.floor(due / 2)) : due;
320
+ return {
321
+ sides: c.sides,
322
+ closes_at: BigInt(at + closes),
323
+ result_by: BigInt(at + due),
324
+ shares: shares(c.split, m),
325
+ gate: gateOf(c.gate, m),
326
+ deciders: c.deciders ?? [],
327
+ // A contest of NFTs counts its pot in NFTs.
328
+ min_pot: c.stakeNft ? wholeCount(c.minPot) ?? 0n : amount(c.minPot ?? 0, t),
329
+ stake: amount(c.stake ?? 0, t),
330
+ prize: prizeOrNull(c.prize, m),
331
+ // A token prize set aside whole at the decision and split among the winners by what each put in.
332
+ prize_split: Boolean(c.prize?.split),
333
+ // Each entry stakes one NFT that passes this rule, and the NFTs are the pot: each winner shares the losers', at most
334
+ // `losersEach` of them (none: no cap).
335
+ nft: c.stakeNft ? ruleOf(c.stakeNft, m) : null,
336
+ cap: c.stakeNft && c.losersEach ? c.losersEach : null,
337
+ };
338
+ }
339
+
340
+ export function farmArgs(f, m) {
341
+ const tiers = (f.tiers ?? [{ lock: 0, multiplier: 1 }]).map((t) => ({
342
+ lock: toSeconds(t.lock),
343
+ bps: Math.round((t.multiplier ?? 1) * 10_000),
344
+ prize: prizeOrNull(t.prize, m),
345
+ roll: t.roll ? m.box.get(t.roll)?.id ?? 0 : null,
346
+ // A token seat may leave before this tier's lock by leaving this share of what it takes out.
347
+ exit_bps: Math.round((t.exitFee ?? 0) * 100),
348
+ // How long a seat of this tier earns and grows from when its lock started (0: no end): a term.
349
+ earns_for: t.earnsFor ? toSeconds(t.earnsFor) : 0,
350
+ }));
351
+ // Every rule of its gate; with none, a farm of one item's collectibles takes that item.
352
+ const named = listOf(f.gate);
353
+ const gates = f.stakes !== 'collectibles' ? [] : (named.length ? named : f.item ? [{ item: f.item, attribute: 'item_id' }] : []).map((r) => ruleOf(r, m));
354
+ // A rate is per NFT_UNIT of weight: one collectible, or 1,000,000 base units of a staked token.
355
+ const rewards = m.tokens.get(f.rewardsToken);
356
+ const staked = f.stakes === 'collectibles' ? null : m.tokens.get(f.stakes);
357
+ const perDay = f.rate ? amount(f.rate.perDay, rewards) : 0n;
358
+ // A shared rate is the whole farm's a day, shared by weight; any other is for each unit of weight.
359
+ const shared = Boolean(f.rate?.shared);
360
+ return {
361
+ custody: { freeze: 'Freeze', watch: 'Watch' }[f.custody] ?? 'Escrow',
362
+ tiers,
363
+ boosts: (f.boosts ?? []).map((b) => ({ rule: ruleOf(b.when, m), effect: effectOf(b.add) })),
364
+ max_boost_bps: Math.round((f.maxBoostPercent ?? 0) * 100),
365
+ gates,
366
+ rate: f.rate ? { per_day: staked && !shared ? (perDay * 1_000_000n) / 10n ** BigInt(staked.decimals ?? 0) : perDay, mints: Boolean(f.rate.minted), shared, halve_every: f.rate.halveEvery ? toSeconds(f.rate.halveEvery) : 0 } : null,
367
+ growth: listOf(f.growth).map((g) => ({ key: g.attribute ?? 'xp', per_day: g.perDay, level: (g.levels ?? []).length ? g.level ?? 'level' : '', steps: g.levels ?? [], streak: Boolean(g.streak), profile: false, at_least: 0n })),
368
+ // A farm that runs for a time ends that long after it goes live.
369
+ ends_at: f.runs ? BigInt(m.now + toSeconds(f.runs)) : 0n,
370
+ // The times it keeps on what is staked: its lock, and an upkeep.
371
+ lock_key: f.lockAttribute ?? '',
372
+ upkeep_key: f.upkeepAttribute ?? '',
373
+ // Where an early exit's fee goes.
374
+ exit_to: f.exitFeeTo ? sinkOf(f.exitFeeTo, m) : null,
375
+ };
376
+ }
377
+
378
+ /** A fee's sink (`Sink`): "burn", "rewards" (the farm's own vault), or "pool:<key>", one of the project's pools. */
379
+ export const sinkOf = (to, m) => (to === 'burn' ? 'Burn' : to === 'rewards' ? 'Rewards' : { Pool: [m.pool.get(String(to).replace(/^pool:/, ''))?.index ?? 0] });
380
+
381
+ /** Whether a spec's farm keeps a record of when its seats stop earning (`loot.farmKeepsEnds`): a tier's term or an upkeep,
382
+ * and it pays something. Each different time waiting costs whoever writes it `loot.END_BYTES` of rent. */
383
+ export const farmEnds = (f) => ((f.tiers ?? []).some((t) => t.earnsFor) || Boolean(f.upkeepAttribute)) && (!f.rate || Number(f.rate.perDay) > 0);
384
+
385
+ /** An auction's args: its lot, reserve, least raise, times and split. "start" is when it goes live unless it says. */
386
+ export function auctionArgs(a, m) {
387
+ const t = m.priceToken(a);
388
+ const startsAt = toTime(a.start, m.now);
389
+ return {
390
+ lot: a.lot?.asset ? { Asset: [a.lot.asset] } : a.lot?.roll ? { Roll: [rollOf(a.lot.roll, m)] } : { Item: [m.item.get(a.lot?.item)?.id ?? 0] },
391
+ // With NFT bids, the reserve counts NFTs.
392
+ reserve: a.bidNfts ? BigInt(a.reserve ?? 1) : amount(a.reserve ?? 0, t),
393
+ min_raise_bps: Math.round((a.minRaise ?? 5) * 100),
394
+ starts_at: BigInt(startsAt),
395
+ ends_at: BigInt(startsAt + toSeconds(a.runs)),
396
+ extend: toSeconds(a.extendBy ?? '10m'),
397
+ shares: shares(a.split, m),
398
+ gate: gateOf(a.gate, m),
399
+ // A bid is a bundle of NFTs that pass this rule, in place of tokens.
400
+ nft_bids: a.bidNfts ? ruleOf(a.bidNfts, m) : null,
401
+ };
402
+ }
403
+
404
+ const NOBODY = '11111111111111111111111111111111';
405
+ /** The program id of the box a prize rolls (a box key), or null for none. */
406
+ const rollOf = (box, m) => (box ? m.box.get(box)?.id ?? 0 : null);
407
+
408
+ export function grantArgs(g, m) {
409
+ const gives = prizeArgs(g.prize ?? g.gives, m);
410
+ const vouchers = [...(g.vouchers ?? []), NOBODY, NOBODY, NOBODY].slice(0, 3);
411
+ const list = g.list ?? [];
412
+ const keys = g.keys ?? [];
413
+ const l = g.limits ?? {};
414
+ return {
415
+ gives,
416
+ vouchers,
417
+ list_root: list.length ? loot.merkleRoot(listLeaves(list, prizeToken(prizeLine(g.prize ?? g.gives), m))) : new Uint8Array(32),
418
+ key_root: keys.length ? loot.merkleRoot(keys.map((k) => loot.keyLeaf(k))) : new Uint8Array(32),
419
+ // A grant that names no voucher key and no list is one any wallet may claim, within its limits.
420
+ open: !(g.vouchers ?? []).length && !list.length && !keys.length,
421
+ limits: {
422
+ wallet: perWalletOf(l),
423
+ per_person: l.perPerson ?? 0,
424
+ per_key: l.perKey ?? 0,
425
+ // Every wallet's claims together: in all, and a day (a window of 86,400 seconds, from the unix epoch).
426
+ all: { total: l.total ?? 0, per_window: l.perDay ?? 0, window: l.perDay ? 86_400 : 0 },
427
+ window: windowOf({ opens: l.opens, closes: l.until }, m.now),
428
+ },
429
+ active: g.active !== false,
430
+ gate: gateOf(g.gate, m),
431
+ // Its records in 2^bits lists, the fewest that keep each under ~100,000 over its life (wallets × rounds).
432
+ shard_bits: loot.shardBitsFor(g.wallets ?? list.length ?? 0, g.rounds ?? 1),
433
+ };
434
+ }
435
+
436
+ // ---------------------------------------------------------------- resolving "@" names at build time
437
+
438
+ /**
439
+ * Replaces "@mint:<key>" strings anywhere in `value` with the mint `ctx` names, "@ata:<wallet>:<key>" with that
440
+ * wallet's token account for it, "@pot:<id>" with a lottery's pot, "@farm:<id>" with a farm's address and "@rewards:<id>"
441
+ * with a farm's rewards vault.
442
+ */
443
+ function resolve(value, ctx) {
444
+ if (value === '@treasury') {
445
+ if (!ctx.treasury) throw new Error('No treasury yet for a roll that gives an NFT to it.');
446
+ return ctx.treasury;
447
+ }
448
+ if (value === '@collection') {
449
+ if (!ctx.collection) throw new Error('No collection yet for a rule or an item about the project’s collection.');
450
+ return ctx.collection;
451
+ }
452
+ if (value === '@tree') {
453
+ if (!ctx.tree) throw new Error('No tree yet for a compressed prize.');
454
+ return ctx.tree;
455
+ }
456
+ if (typeof value === 'string' && value.startsWith('@mint:')) {
457
+ const key = value.slice(6);
458
+ const mint = ctx.mints[key];
459
+ if (!mint) throw new Error(`No mint yet for the token “${key}”.`);
460
+ return mint;
461
+ }
462
+ if (typeof value === 'string' && value.startsWith('@farm:')) {
463
+ if (!ctx.project) throw new Error('No project yet for a stake gate’s farm.');
464
+ return loot.pda.farm(ctx.project, Number(value.slice(6)));
465
+ }
466
+ if (typeof value === 'string' && value.startsWith('@rewards:')) {
467
+ if (!ctx.project) throw new Error('No project yet for a farm’s rewards vault.');
468
+ return loot.pda.rewards(loot.pda.farm(ctx.project, Number(value.slice(9))));
469
+ }
470
+ if (typeof value === 'string' && value.startsWith('@pot:')) {
471
+ if (!ctx.project) throw new Error('No project yet for a lottery’s pot.');
472
+ return loot.pda.pot(loot.pda.lottery(ctx.project, Number(value.slice(5))));
473
+ }
474
+ if (typeof value === 'string' && value.startsWith('@ata:')) {
475
+ const [, wallet, key, program] = value.split(':');
476
+ const mint = ctx.mints[key];
477
+ if (!mint) throw new Error(`No mint yet for the token “${key}”.`);
478
+ return loot.ata(wallet, mint, program === 'token-2022' ? TOKEN_2022_PROGRAM : TOKEN_PROGRAM);
479
+ }
480
+ if (Array.isArray(value)) return value.map((v) => resolve(v, ctx));
481
+ if (value instanceof Uint8Array || typeof value !== 'object' || value === null) return value;
482
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, resolve(v, ctx)]));
483
+ }
484
+
485
+ // ---------------------------------------------------------------- the steps
486
+
487
+ const encodedSize = loot.accountBytes;
488
+ /** Bubblegum's config of a tree. */
489
+ const TREE_CONFIG_BYTES = 96;
490
+ const placeholderKey = (name) => encodeBase58(sha256(utf8(`loot-plan:${name}`)));
491
+
492
+ /**
493
+ * Every step that sets the economy up, in order: the tokens the project mints, the collection, the project, the pools, items, boxes (the shop's too), recipes, schedules, lotteries, grants,
494
+ * farms, auctions, then the seeds and the grants' funds. `only` limits it to these part ids (a change to a live economy).
495
+ *
496
+ * A farm opens before its seed lands in its rewards vault, and anyone may stake in between. That is safe: a sharing
497
+ * farm credits nothing at once, and releases what lands in its vault over its longest lock (a day at least) to the
498
+ * seats there as it is released, its clock stopped while nobody is in. A seed that lands
499
+ * before the first stake or after it is released the same way from that stake on, so seeding first would change nothing.
500
+ */
501
+ export function setupSteps(economy, { now = 0, only = null, live: given = null } = {}) {
502
+ const m = model(economy, { now });
503
+ const live = madeSoFar(given);
504
+ const steps = [];
505
+ const want = (id) => !only || only.has(id);
506
+ const landed = new Set(live?.landed ?? []);
507
+ const add = (step) => want(step.part) && !landed.has(step.id) && steps.push({ creates: [], signers: [owner], ...step });
508
+ const project = (ctx) => ctx.project;
509
+ const fresh = !live?.project;
510
+
511
+ // Tokens the project mints: a classic mint with the owner as its authority, handed to the project right after.
512
+ for (const t of m.tokens.values()) {
513
+ if (t.source !== 'game' || live?.mints?.[t.key] || t.mint && !t.mint.startsWith('@')) continue;
514
+ const key = `new:mint/${t.key}`;
515
+ if (t.fixedSupply) {
516
+ const supply = amount(t.supply, t);
517
+ add({
518
+ id: `token:${t.key}`,
519
+ part: `token:${t.key}`,
520
+ says: `Make the token ${t.symbol} (${t.decimals} decimals) with a fixed supply of ${t.supply}, all of it to your wallet, then turn its minting off for good`,
521
+ signers: [owner, key],
522
+ creates: [{ what: `the mint of ${t.symbol}`, bytes: MINT_BYTES }, { what: `your ${t.symbol} account`, bytes: 165 }],
523
+ build: (ctx) => [
524
+ createAccountIx({ from: ctx.owner, address: ctx.keys[key], lamports: ctx.rent(MINT_BYTES), space: MINT_BYTES, owner: TOKEN_PROGRAM }),
525
+ initializeMint2Ix({ mint: ctx.keys[key], decimals: t.decimals, authority: ctx.owner }),
526
+ createAssociatedTokenAccountIdempotent(ctx.owner, ctx.owner, ctx.keys[key]),
527
+ mintToIx({ mint: ctx.keys[key], to: loot.ata(ctx.owner, ctx.keys[key]), authority: ctx.owner, amount: supply }),
528
+ setAuthorityIx({ account: ctx.keys[key], current: ctx.owner, newAuthority: null }),
529
+ ],
530
+ });
531
+ continue;
532
+ }
533
+ add({
534
+ id: `token:${t.key}`,
535
+ part: `token:${t.key}`,
536
+ says: `Make the token ${t.symbol} (${t.decimals} decimals), then hand its minting to the project`,
537
+ signers: [owner, key],
538
+ creates: [{ what: `the mint of ${t.symbol}`, bytes: MINT_BYTES }],
539
+ build: (ctx) => [
540
+ createAccountIx({ from: ctx.owner, address: ctx.keys[key], lamports: ctx.rent(MINT_BYTES), space: MINT_BYTES, owner: TOKEN_PROGRAM }),
541
+ initializeMint2Ix({ mint: ctx.keys[key], decimals: t.decimals, authority: ctx.owner }),
542
+ setAuthorityIx({ account: ctx.keys[key], current: ctx.owner, newAuthority: ctx.project }),
543
+ ],
544
+ });
545
+ }
546
+
547
+ // The collection and the project. A spec that gives compressed NFTs makes its collection with Bubblegum's plugin,
548
+ // which only a new collection can carry.
549
+ const tree = compressedTree(m);
550
+ if (fresh) {
551
+ const c = m.collection;
552
+ if (c)
553
+ add({
554
+ id: 'collection',
555
+ part: 'project',
556
+ says: `Make the collection “${c.name}”, with ${c.royaltyPercent ?? 5}% resale royalty to you; the project may mint into it${tree ? ', as Core NFTs or compressed ones' : ''}`,
557
+ signers: [owner, 'new:collection'],
558
+ creates: [{ what: 'the collection', bytes: 1 + 32 + 4 + c.name.length + 4 + (c.uri ?? '').length + 140 + (tree ? 3 : 0), approx: true }],
559
+ build: (ctx) => [createCollectionInstruction({ collection: ctx.keys['new:collection'], creator: ctx.owner, project: ctx.project, name: c.name, uri: c.uri ?? '', royaltyBps: Math.round((c.royaltyPercent ?? 5) * 100), compressed: Boolean(tree) })],
560
+ });
561
+ // The treasury is a wallet: the remainder of every split goes to its account of that mint, which the program
562
+ // opens when it is not there. The project's vaults, and every account a share pays, open the same way when first
563
+ // paid, so the setup opens none of them.
564
+ const collections = (ctx) => [...(ctx.collection ? [ctx.collection] : []), ...m.collections];
565
+ const count = (c ? 1 : 0) + m.collections.length;
566
+ add({
567
+ id: 'project',
568
+ part: 'project',
569
+ says: `Open the project on the loot program${count ? ` with ${count === 1 ? 'its collection' : `its ${count} collections`}` : ', with no collection'}; ${m.authority === 'owner' ? 'your wallet' : m.authority} signs its decisions, and ${m.treasuryWallet ?? 'your wallet'} is its treasury`,
570
+ creates: [{ what: 'the project', bytes: loot.projectBytes(count) }, { what: 'the project’s fee box', bytes: 0 }],
571
+ build: (ctx) => [loot.createProjectIx({ owner: ctx.owner, id: m.projectNumber, collections: collections(ctx), treasury: ctx.treasury, authority: ctx.authority })],
572
+ });
573
+ }
574
+
575
+ // The tree compressed prizes are minted into: made empty by the owner, then opened with the project as its creator.
576
+ if (tree && !live?.tree)
577
+ add({
578
+ id: 'tree',
579
+ part: 'tree',
580
+ says: `Open the project’s tree of compressed NFTs, for ${(2 ** tree.depth).toLocaleString('en-US')} of them in all`,
581
+ signers: [owner, 'new:tree'],
582
+ creates: [{ what: 'the tree of compressed NFTs', bytes: tree.bytes }, { what: 'the tree’s config (Bubblegum’s)', bytes: TREE_CONFIG_BYTES }],
583
+ build: (ctx) => [
584
+ createAccountIx({ from: ctx.owner, address: ctx.keys['new:tree'], lamports: ctx.rent(tree.bytes), space: tree.bytes, owner: loot.COMPRESSION_PROGRAM }),
585
+ loot.openTreeIx({ signer: ctx.owner, project: project(ctx), tree: ctx.keys['new:tree'], maxDepth: tree.depth, maxBufferSize: tree.buffer }),
586
+ ],
587
+ });
588
+
589
+ // The token accounts players' purchases would otherwise open (the owner pays their rent now, about 0.002 SOL each,
590
+ // so no player's purchase pays another wallet's): the treasury's of each coin the economy takes, the project's vault
591
+ // of each coin a box or an auction holds, and each fixed wallet's a split pays.
592
+ const accounts = goLiveAccounts(m);
593
+ if (fresh && accounts.length)
594
+ add({
595
+ id: 'accounts',
596
+ part: 'project',
597
+ says: `Open ${accounts.length} token account${accounts.length === 1 ? '' : 's'} the economy pays into (the treasury’s, the project’s vaults, the wallets its splits name), so no player’s purchase opens them`,
598
+ creates: accounts.map((x) => ({ what: `${x.who}’s account of ${m.tokens.get(x.token)?.symbol ?? x.token}`, bytes: 165 })),
599
+ build: (ctx) => accounts.map((x) => createAssociatedTokenAccountIdempotent(ctx.owner, x.who === 'the project' ? ctx.project : x.who === 'the treasury' ? ctx.treasury : x.who, ctx.mints[x.token], programsOf(ctx)[ctx.mints[x.token]] ?? TOKEN_PROGRAM)),
600
+ });
601
+
602
+ // The project's standing lookup table (the owner its authority): the addresses its transactions name most, so a
603
+ // transaction too big for a legacy one goes as v0 with it in one approval, never with a table of its own. Made with
604
+ // the first of its addresses, then extended a group a step.
605
+ if (fresh) {
606
+ const named = standingAddresses(m);
607
+ groupsOf(named, TABLE_GROUP).forEach((group, i) =>
608
+ add({
609
+ id: i ? `table:${i}` : 'table',
610
+ part: 'project',
611
+ says: i ? `Add ${group.length} more addresses to the project’s lookup table` : `Make the project’s lookup table of the ${named.length} addresses its transactions name most, so a large one takes one approval`,
612
+ creates: i ? [{ what: `room for ${group.length} addresses in the lookup table`, bytes: 32 * group.length, grows: true }] : [{ what: 'the project’s lookup table', bytes: 56 + 32 * group.length }],
613
+ build: (ctx) => {
614
+ // With no recent slot known (a plan built offline), there is no table to make.
615
+ if (ctx.slot == null) return [];
616
+ const { address: table, instruction } = createTableIx({ authority: ctx.owner, slot: ctx.slot });
617
+ return [...(i ? [] : [instruction]), extendTableIx({ table, authority: ctx.owner, addresses: group.map((at) => at(ctx)).filter(Boolean) })];
618
+ },
619
+ }));
620
+ }
621
+
622
+ for (const p of m.pools) {
623
+ const t = m.tokens.get(p.currency);
624
+ add({ id: `pool:${p.key}`, part: `pool:${p.key}`, says: `Open the pool “${p.key}” for ${t?.symbol}`, creates: [{ what: `the pool “${p.key}”`, bytes: 165 }], build: (ctx) => [loot.openPoolIx({ owner: ctx.owner, project: project(ctx), index: p.index, mint: ctx.mints[p.currency] })] });
625
+ }
626
+ const record = (part, def, says, kind, what, options = () => ({})) => {
627
+ const [variant] = Object.keys(def);
628
+ const bytes = recordBytesOf(kind, def[variant]);
629
+ // A lottery's first write opens its pot: a token account as well as the record (each round holds its own money).
630
+ const pot = kind === 'Lottery' ? [{ what: `${what}’s pot`, bytes: 165 }] : def.Schedule?.args.action.from.Vest ? [{ what: `${what}’s sealed vault`, bytes: 165 }] : [];
631
+ add({
632
+ id: `${part}:${kind}`, part, says, creates: [{ what, bytes }, ...pot],
633
+ build: (ctx) => [loot.upsertIx({ owner: ctx.owner, project: project(ctx), def: resolve(def, ctx), ...options(ctx) })],
634
+ });
635
+ };
636
+ // Each table, a batch of rows a transaction (`TABLE_BATCH_BYTES` of rows at most), sealed with its last batch.
637
+ for (const t of m.tables) {
638
+ const batches = tableBatches(t);
639
+ const len = t.kind === 'numbers' ? 8 : 40;
640
+ batches.forEach((rows, i) => {
641
+ const seal = i === batches.length - 1;
642
+ add({
643
+ id: `table:${t.key}:${i}`,
644
+ part: `table:${t.key}`,
645
+ says: `Write ${rows.length} row${rows.length === 1 ? '' : 's'} of the table “${t.key}”${seal ? ', and seal it: its weights never change from now on' : ''}`,
646
+ creates: i === 0 ? [{ what: `the table “${t.key}”, ${t.rows.length} rows`, bytes: loot.tableRowAt(len, t.rows.length - 1) + len }] : [],
647
+ build: (ctx) => [loot.upsertIx({ owner: ctx.owner, project: project(ctx), def: { Table: { id: t.id, args: { payload: t.kind === 'numbers' ? 'Number' : 'Label', rows, seal } } } })],
648
+ });
649
+ });
650
+ }
651
+ for (const it of m.items) record(`item:${it.key}`, { Item: { id: it.id, args: itemArgs(it, m) } }, `${it.active === false ? 'Switch off' : 'Write'} the item “${it.name}”`, 'Item', `the item “${it.name}”`);
652
+ // A box's line names its odds, so the owner sees what they approve (above all when a live box's odds change).
653
+ const odds = (b) => {
654
+ const ws = weights(b);
655
+ const total = ws.reduce((n, w) => n + w, 0) || 1;
656
+ const part = (o) => (o.gives === 'item' ? m.item.get(o.item)?.name ?? o.item : o.gives === 'stash' ? `an NFT of “${m.stash.get(o.stash)?.name ?? o.stash}”` : o.gives === 'pay' ? `${o.amount} ${m.poolToken(o.pool)?.symbol ?? ''}`.trim() : o.gives === 'share' ? `${o.percent}% of “${o.pool}”` : o.gives === 'mint' ? `${o.amount} ${m.tokens.get(o.currency ?? m.currencyKey)?.symbol ?? ''}`.trim() : o.gives === 'nft' ? `the NFT ${String(o.nft).slice(0, 4)}…` : o.gives === 'roll' ? `a roll of “${m.box.get(o.box)?.name ?? o.box}”` : '');
657
+ const what = (o) => outcomeParts(o).map(part).join(' + ') || 'nothing';
658
+ return (b.outcomes ?? []).map((o, i) => `${what(o)} ${Math.round((ws[i] / total) * 10_000) / 100}%`).join(', ');
659
+ };
660
+ const wasLive = new Set((live?.published?.boxes ?? []).map((b) => b.key));
661
+ // A box is written before any box that names it to roll (one may name itself): in that order.
662
+ const boxOrder = [];
663
+ const visit = (b, path = new Set()) => {
664
+ if (boxOrder.includes(b) || path.has(b)) return;
665
+ path.add(b);
666
+ for (const id of loot.defRolls({ Box: { args: boxArgs(b, m) } })) {
667
+ const named = m.allBoxes.find((x) => x.id === id);
668
+ if (named && named !== b) visit(named, path);
669
+ }
670
+ boxOrder.push(b);
671
+ };
672
+ for (const b of m.allBoxes) visit(b);
673
+ for (const b of boxOrder) {
674
+ const says = b.shopOf ? `Put “${b.name}” in the shop at ${b.price} ${m.priceToken(b)?.symbol}` : `${b.active === false ? 'Switch off' : wasLive.has(b.key) ? 'Change' : 'Put on sale'} the box “${b.name}” at ${b.price} ${m.priceToken(b)?.symbol}${b.active === false ? '' : `: ${odds(b)}`}`;
675
+ record(b.shopOf ? `item:${b.shopOf}` : `box:${b.key}`, { Box: { id: b.id, args: boxArgs(b, m) } }, says, 'LootBox', b.shopOf ? `the shop listing of “${b.name}”` : `the box “${b.name}”`);
676
+ }
677
+ for (const r of m.recipes) record(`recipe:${r.key}`, { Recipe: { id: r.id, args: recipeArgs(r, m) } }, `Write the recipe “${r.name}”`, 'Recipe', `the recipe “${r.name}”`);
678
+ for (const s of m.schedules) {
679
+ record(`schedule:${s.key}`, { Schedule: { id: s.id, args: scheduleArgs(s, m) } }, `${s.active === false ? 'Stop' : 'Start'} the ${s.sealed ? 'sealed ' : ''}schedule “${s.name ?? s.key}”`, 'Schedule', `the schedule “${s.name ?? s.key}”`);
680
+ // A sealed schedule's vault is funded once, at go-live: from the owner's wallet, minted, or from a pool.
681
+ const id = `fund:${s.key}`;
682
+ if (!s.sealed || !Number(s.fund ?? 0) || (live?.landed ?? []).includes(id) || live?.published?.schedules?.some((x) => x.key === s.key)) continue;
683
+ const t = m.tokens.get(s.currency ?? m.currencyKey);
684
+ const from = s.fundFrom ?? 'wallet';
685
+ add({
686
+ id,
687
+ part: `schedule:${s.key}`,
688
+ signers: from === 'wallet' || m.authority === 'owner' ? [owner] : ['authority'],
689
+ says: `Put ${s.fund} ${t?.symbol ?? ''} in the sealed schedule “${s.name ?? s.key}”${from === 'wallet' ? ' from your wallet' : from === 'mint' ? ', minted' : ` from “${from.slice(5)}”`}: nobody can take it back`,
690
+ creates: [],
691
+ build: (ctx) => {
692
+ const mint = ctx.mints[t.key];
693
+ const vest = loot.pda.vest(loot.pda.schedule(project(ctx), s.id));
694
+ const value = amount(s.fund, t);
695
+ if (from === 'wallet') {
696
+ const tokenProgram = programsOf(ctx)[mint] ?? TOKEN_PROGRAM;
697
+ // SOL is wrapped first, as a seed is: the owner's wallet holds it as plain SOL, the vault takes wrapped SOL.
698
+ const wrap = t.wsol ? [createAssociatedTokenAccountIdempotent(ctx.owner, ctx.owner, NATIVE_MINT), transferLamports(ctx.owner, loot.ata(ctx.owner, NATIVE_MINT), value), syncNative(loot.ata(ctx.owner, NATIVE_MINT))] : [];
699
+ return [...wrap, transferCheckedIx({ from: loot.ata(ctx.owner, mint, tokenProgram), mint, to: vest, owner: ctx.owner, amount: value, decimals: t.decimals ?? 0, tokenProgram })];
700
+ }
701
+ const source = from === 'mint' ? { Mint: [mint] } : { Pool: [m.pool.get(from.slice(5))?.index ?? 0] };
702
+ return [loot.moveTokensIx({ authority: ctx.authority, project: project(ctx), mint, from: source, to: { Account: [vest] }, amount: { Fixed: [value] }, programs: programsOf(ctx) })];
703
+ },
704
+ });
705
+ }
706
+ for (const l of m.lotteries) {
707
+ // A raffle's NFTs go into the lottery's own stash first, several a call: the lottery is written naming NFTs already
708
+ // there, or drawing from as many as its places.
709
+ const raffled = [...(l.places ?? []).map(placeOf).filter((p) => p.prize?.held).map((p) => ({ nft: p.prize.nft, collection: p.prize.collection })), ...(l.nfts ?? []).map((n) => (typeof n === 'string' ? { nft: n } : n))];
710
+ const opened = live?.published?.lotteries?.some((x) => x.key === l.key);
711
+ groupsOf(raffled.filter((n) => !(live?.landed ?? []).includes(`stash:lottery-${l.key}:${n.nft}`)), STASH_GROUP).forEach((group, i) => {
712
+ if (opened) return;
713
+ add({
714
+ id: `stash:lottery-${l.key}:${group.map((n) => n.nft).join(',')}`,
715
+ part: `lottery:${l.key}`,
716
+ says: `Put your NFT${group.length === 1 ? ` ${group[0].nft}` : `s ${group.map((n) => n.nft).join(', ')}`} in the lottery “${l.name}”: ${group.length === 1 ? 'it stays' : 'they stay'} there until ${group.length === 1 ? 'its winner takes it' : 'their winners take them'}, and nobody, you included, can take ${group.length === 1 ? 'it' : 'them'} out before`,
717
+ creates: i === 0 ? [{ what: `the lottery “${l.name}”’s stash`, bytes: stashBytes(raffled.length) }] : [],
718
+ build: (ctx) => [loot.stashIx({ signer: ctx.owner, project: project(ctx), lottery: l.id, asset: group[0].nft, collection: group[0].collection ?? ctx.collection, more: group.slice(1).map((n) => ({ asset: n.nft, collection: n.collection ?? ctx.collection })) })],
719
+ });
720
+ });
721
+ record(`lottery:${l.key}`, { Lottery: { id: l.id, args: lotteryArgs(l, m) } }, `Start the ${l.raffle ? 'raffle' : 'lottery'} “${l.name}”`, 'Lottery', `the lottery “${l.name}”`);
722
+ }
723
+ for (const g of m.grants) record(`grant:${g.key}`, { Grant: { id: g.id, args: grantArgs(g, m) } }, `Write the grant “${g.name}”`, 'Grant', `the grant “${g.name}”`);
724
+ /** The boxes a farm's tiers roll, tier by tier (its roll, then a prize that is a roll). */
725
+ for (const f of m.farms) {
726
+ if (live?.farms?.includes(f.key)) continue;
727
+ const rewards = m.tokens.get(f.rewardsToken);
728
+ add({
729
+ id: `farm:${f.key}`,
730
+ part: `farm:${f.key}`,
731
+ says: `Open the farm “${f.name}”: stake ${f.stakes === 'collectibles' ? 'collectibles' : m.tokens.get(f.stakes)?.symbol}, earn ${rewards?.symbol}${f.rate?.minted ? ', minted' : ' from its own rewards vault'}`,
732
+ creates: [{ what: `the farm “${f.name}”`, bytes: farmBytes(f, m) }, { what: 'its rewards vault', bytes: 165 }, ...(f.stakes === 'collectibles' ? [] : [{ what: 'its stake vault', bytes: 165 }]), ...(farmEnds(f) ? [{ what: 'its record of when seats stop earning', bytes: loot.ENDS_HEAD_BYTES }] : [])],
733
+ build: (ctx) => [loot.openFarmIx({ owner: ctx.owner, project: project(ctx), id: f.id, args: resolve(farmArgs(f, m), ctx), rewardsMint: ctx.mints[rewards.key], stakedMint: f.stakes === 'collectibles' ? null : ctx.mints[f.stakes] })],
734
+ });
735
+ }
736
+
737
+ // Traits the project vouches for from NFTs' off-chain metadata: one Merkle root per collection, signed once.
738
+ const vouched = new Map((live?.published?.traits ?? []).map((t) => [t.collection, t.root]));
739
+ for (const t of m.traits) {
740
+ if (vouched.get(t.collection) === t.root) continue;
741
+ add({
742
+ id: `traits:${t.collection}`,
743
+ part: `traits:${t.collection}`,
744
+ says: `Vouch for the traits of the NFTs of ${t.collection} (a root of their off-chain metadata)`,
745
+ creates: [{ what: `the traits of ${t.collection}`, bytes: 1 + 32 * 3 }],
746
+ build: (ctx) => [loot.setTraitsIx({ signer: ctx.owner, project: project(ctx), key: t.collection, root: Uint8Array.from(t.root.match(/../g).map((h) => parseInt(h, 16))) })],
747
+ });
748
+ }
749
+
750
+ // Stashes: the owner's wallet hands each NFT it lists to the project, one instruction each (`stash`). A compressed NFT
751
+ // needs a proof fetched when it is signed, so the checker leaves those to `loot.stashIx` after go-live.
752
+ // What is in a live stash already: what its last publish listed, and the steps of this one that landed.
753
+ const stashed = new Set([
754
+ ...(live?.published?.stashes ?? []).flatMap((s) => stashNfts(s).map((n) => `${s.key}:${n.asset}`)),
755
+ // A step puts in a group of NFTs: `stash:<key>:<asset>,<asset>…`.
756
+ ...(live?.landed ?? []).filter((id) => id.startsWith('stash:')).flatMap((id) => {
757
+ const [key, assets = ''] = id.slice(6).split(/:(.*)/s);
758
+ return assets.split(',').map((asset) => `${key}:${asset}`);
759
+ }),
760
+ ]);
761
+ for (const s of m.stashes) {
762
+ const nfts = stashNfts(s);
763
+ const putting = nfts.filter((n) => n.kind !== 'compressed' && !stashed.has(`${s.key}:${n.asset}`));
764
+ // The first NFT this plan puts in names the stash's rent: all of it for a new stash, what it grows by for one live already.
765
+ const fresh = !nfts.some((n) => stashed.has(`${s.key}:${n.asset}`));
766
+ // Several NFTs a call, each with its weight: Core NFTs a group of `STASH_GROUP`, Token Metadata ones two (each takes
767
+ // more accounts).
768
+ const kindOf = (n) => ({ asset: n.asset, weight: n.weight, collection: n.kind === 'core' ? n.collection : undefined, tokenMetadata: n.kind === 'token-metadata' ? { programmable: n.programmable, ruleSet: n.ruleSet ?? undefined } : undefined });
769
+ const groups = [...groupsOf(putting.filter((n) => n.kind === 'core'), STASH_GROUP), ...groupsOf(putting.filter((n) => n.kind !== 'core'), 2)];
770
+ for (const [i, group] of groups.entries()) {
771
+ add({
772
+ id: `stash:${s.key}:${group.map((n) => n.asset).join(',')}`,
773
+ part: `stash:${s.key}`,
774
+ says: `Put your NFT${group.length === 1 ? ` ${group[0].asset}` : `s ${group.map((n) => n.asset).join(', ')}`} in the stash “${s.name}”: the project holds ${group.length === 1 ? 'it' : 'them'} until a prize or a roll gives ${group.length === 1 ? 'it' : 'them'} away`,
775
+ creates: i > 0 ? [] : fresh ? [{ what: `the stash “${s.name}”, with ${putting.length} NFT${putting.length === 1 ? '' : 's'}`, bytes: stashBytes(putting.length) }] : [{ what: `room for ${putting.length} more NFT${putting.length === 1 ? '' : 's'} in “${s.name}”`, bytes: 36 * putting.length, grows: true }],
776
+ build: (ctx) => {
777
+ const [first, ...more] = group.map((n) => {
778
+ const k = kindOf(n);
779
+ return { ...k, collection: n.kind === 'core' ? k.collection ?? ctx.collection : undefined };
780
+ });
781
+ return [loot.stashIx({ signer: ctx.owner, project: project(ctx), id: s.id, ...first, more })];
782
+ },
783
+ });
784
+ }
785
+ }
786
+
787
+ // Auctions, each opened by its seller alone: a new unit of an item by the authority, a collectible by the owner who
788
+ // holds it. Its address names the seller (`auctionSeller`).
789
+ for (const a of m.auctions) {
790
+ const t = m.priceToken(a);
791
+ const args = auctionArgs(a, m);
792
+ const asset = Boolean(a.lot?.asset);
793
+ const signer = asset || m.authority === 'owner' ? owner : 'authority';
794
+ const terms = `bids in ${t?.symbol} from ${a.reserve ?? 'any amount'}, for ${a.runs}`;
795
+ add({
796
+ id: `auction:${a.key}`,
797
+ part: `auction:${a.key}`,
798
+ signers: [signer],
799
+ says: asset
800
+ ? `Put your NFT up for auction in “${a.name}”: it stays with the project until someone wins it or the auction ends (${terms})`
801
+ : a.lot?.roll
802
+ ? `Open the auction “${a.name}” of a roll of the box “${m.box.get(a.lot.roll)?.name ?? a.lot.roll}”, made for its winner at the end (${terms})`
803
+ : `Open the auction “${a.name}” of one new “${m.item.get(a.lot?.item)?.name ?? a.lot?.item}” (${terms})`,
804
+ // An auction of NFT bids is made with room for the most a bid holds (`MAX_BID_NFTS`), its seller's rent.
805
+ creates: [{ what: `the auction “${a.name}” (back when it ends)`, bytes: encodedSize('Auction', { project: NOBODY, id: 0, seller: NOBODY, mint: NOBODY, lot: args.lot, ours: false, pin: 0, reserve: 0n, min_raise_bps: 0, starts_at: 0n, ends_at: 0n, extend: 0, shares: args.shares, gate: args.gate, nft_bids: placeholders(args).nft_bids, high_bidder: NOBODY, high: 0n, last_bid_at: 0n, proceeds_owed: 0n, held: [] }) + (args.nft_bids ? 32 * loot.MAX_BID_NFTS : 0) }],
806
+ build: (ctx) => {
807
+ const seller = auctionSeller(a, ctx);
808
+ return [loot.openAuctionIx({ seller, project: project(ctx), collection: a.lot?.collection ?? ctx.collection, id: a.id, mint: ctx.mints[t.key], args: resolve(args, ctx) })];
809
+ },
810
+ });
811
+ }
812
+
813
+ // Seeds: what the owner puts in each pool. SOL is wrapped into the owner's account first.
814
+ for (const p of m.pools) {
815
+ if (!(Number(p.seed ?? 0) > 0) || live?.seeded?.includes(p.key)) continue;
816
+ const t = m.tokens.get(p.currency);
817
+ const units = amount(p.seed, t);
818
+ if (t.gameMints) {
819
+ const signer = m.authority === 'owner' ? owner : 'authority';
820
+ add({
821
+ id: `seed:${p.key}`,
822
+ part: `seed:${p.key}`,
823
+ signers: [signer],
824
+ says: `Mint ${p.seed} ${t.symbol} into “${p.key}”`,
825
+ build: (ctx) => [loot.moveTokensIx({ authority: ctx.authority, project: project(ctx), mint: ctx.mints[t.key], from: { Mint: [ctx.mints[t.key]] }, to: { Pool: [p.index] }, amount: { Fixed: [units] }, reference: `seed ${p.key}` })],
826
+ });
827
+ } else {
828
+ add({
829
+ id: `seed:${p.key}`,
830
+ part: `seed:${p.key}`,
831
+ says: `Put ${p.seed} ${t.symbol} into “${p.key}”`,
832
+ build: (ctx) => [
833
+ ...(t.wsol ? [createAssociatedTokenAccountIdempotent(ctx.owner, ctx.owner, NATIVE_MINT), transferLamports(ctx.owner, loot.ata(ctx.owner, NATIVE_MINT), units), syncNative(loot.ata(ctx.owner, NATIVE_MINT))] : []),
834
+ loot.payIx({ player: ctx.owner, project: project(ctx), mint: ctx.mints[t.key], treasury: ctx.treasury, amount: units, shares: [{ to: { Pool: [p.index] }, bps: 10_000 }], reference: `seed ${p.key}`, programs: { [ctx.mints[t.key]]: tokenProgramOf(t) } }),
835
+ ],
836
+ });
837
+ }
838
+ }
839
+ // A farm's seed: the owner puts it straight into the farm's rewards vault, once the farm is open.
840
+ for (const f of m.farms) {
841
+ if (!(Number(f.seed ?? 0) > 0) || live?.seeded?.includes(`farm:${f.key}`)) continue;
842
+ const t = m.tokens.get(f.rewardsToken);
843
+ const units = amount(f.seed, t);
844
+ add({
845
+ id: `seed:farm:${f.key}`,
846
+ part: `farm:${f.key}`,
847
+ says: `Put ${f.seed} ${t.symbol} into the rewards of “${f.name}”`,
848
+ build: (ctx) => {
849
+ const vault = loot.pda.rewards(loot.pda.farm(project(ctx), f.id));
850
+ return [
851
+ ...(t.wsol ? [createAssociatedTokenAccountIdempotent(ctx.owner, ctx.owner, NATIVE_MINT), transferLamports(ctx.owner, loot.ata(ctx.owner, NATIVE_MINT), units), syncNative(loot.ata(ctx.owner, NATIVE_MINT))] : []),
852
+ loot.payIx({ player: ctx.owner, project: project(ctx), mint: ctx.mints[t.key], treasury: ctx.treasury, amount: units, shares: [{ to: { Account: [vault] }, bps: 10_000 }], reference: `seed ${f.key}`, programs: programsOf(ctx) }),
853
+ ];
854
+ },
855
+ });
856
+ }
857
+ // A lottery's seed: the owner puts it straight into the lottery's pot.
858
+ for (const l of m.lotteries) {
859
+ if (!(Number(l.seed ?? 0) > 0) || live?.seeded?.includes(`lottery:${l.key}`)) continue;
860
+ const t = m.tokens.get(l.currency ?? m.currencyKey);
861
+ const units = amount(l.seed, t);
862
+ add({
863
+ id: `seed:lottery:${l.key}`,
864
+ part: `lottery:${l.key}`,
865
+ says: `Put ${l.seed} ${t.symbol} into the pot of “${l.name}”`,
866
+ build: (ctx) => {
867
+ const pot = loot.pda.pot(loot.pda.lottery(project(ctx), l.id));
868
+ return [
869
+ ...(t.wsol ? [createAssociatedTokenAccountIdempotent(ctx.owner, ctx.owner, NATIVE_MINT), transferLamports(ctx.owner, loot.ata(ctx.owner, NATIVE_MINT), units), syncNative(loot.ata(ctx.owner, NATIVE_MINT))] : []),
870
+ loot.payIx({ player: ctx.owner, project: project(ctx), mint: ctx.mints[t.key], treasury: ctx.treasury, amount: units, shares: [{ to: { Account: [pot] }, bps: 10_000 }], reference: `seed ${l.key}`, programs: programsOf(ctx) }),
871
+ ];
872
+ },
873
+ });
874
+ }
875
+ // SOL a lottery holds for its rounds' rent (`roundRentOf`: the spec's `roundRent`, or by default what its expected
876
+ // rounds and their entries take): each round borrows it at its first purchase (its header and payment account), each
877
+ // purchase's entry, and its ask (the pot's and pools' accounts), back at its close. A lottery live already keeps what
878
+ // it holds unless the spec names a `roundRent` to add.
879
+ for (const l of m.lotteries) {
880
+ const lamports = roundRentOf(l, m);
881
+ if (lamports <= 0n || (l.roundRent === undefined && (live?.published?.lotteries ?? []).some((x) => x.key === l.key))) continue;
882
+ const sol = Number(lamports) / 1e9;
883
+ add({ id: `rent:lottery:${l.key}`, part: `lottery:${l.key}`, says: `Put ${sol} SOL in “${l.name}” for its rounds’ rent and its tickets’ entries${l.roundRent === undefined ? ` (sized for ${roundsExpected(l, m).rounds} round${roundsExpected(l, m).rounds === 1 ? '' : 's'} of ${roundsExpected(l, m).entries} purchases at once)` : ''}, so no buyer pays program rent; each round gives it back when it closes`, build: (ctx) => [transferLamports(ctx.owner, loot.pda.lottery(project(ctx), l.id), lamports)] });
884
+ }
885
+ for (const g of m.grants) {
886
+ if (!(Number(g.fund ?? 0) > 0) || live?.funded?.includes(g.key)) continue;
887
+ const lamports = toUnits(g.fund, 9);
888
+ add({ id: `fund:${g.key}`, part: `fund:${g.key}`, says: `Put ${g.fund} SOL in the funds of the grant “${g.name}” (they pay each claim’s rent)`, build: (ctx) => [transferLamports(ctx.owner, loot.pda.grant(project(ctx), g.id), lamports)] });
889
+ }
890
+ // The authority's steps go last: the owner approves theirs in one batch, the authority theirs in another.
891
+ return { steps: [...steps.filter((x) => !x.signers.includes('authority')), ...steps.filter((x) => x.signers.includes('authority'))], model: m };
892
+ }
893
+
894
+ /** The token accounts going live opens (`setupSteps`): [{ who ('the treasury', 'the project' or a wallet), token }], each
895
+ * once, for the coins the economy takes in. */
896
+ export function goLiveAccounts(m) {
897
+ const out = new Map();
898
+ const add = (who, token) => token && out.set(`${who}|${token}`, { who, token });
899
+ const takes = (part, token = part.currency ?? m.currencyKey) => {
900
+ add('the treasury', token);
901
+ for (const s of sharesOf(part.split)) if (s.kind === 'wallet') add(s.key, token);
902
+ };
903
+ for (const b of m.allBoxes) takes(b), add('the project', b.currency ?? m.currencyKey);
904
+ for (const p of m.payments) takes(p);
905
+ for (const l of m.lotteries) if (!l.ticketNft) takes(l, l.ticketCurrency ?? l.currency ?? m.currencyKey);
906
+ for (const a of m.auctions) if (!a.bidNfts) takes(a), add('the project', a.currency ?? m.currencyKey);
907
+ return [...out.values()];
908
+ }
909
+
910
+ /** How many addresses one step adds to the project's lookup table (each extension its own transaction). */
911
+ const TABLE_GROUP = 24;
912
+
913
+ /**
914
+ * The addresses a project's transactions name most, for its standing lookup table: loot's own fixed accounts (the
915
+ * platform, the oracle's), the project and its fee box, its treasury and collections, each coin's mint, its vault and
916
+ * the treasury's account of it, then each pool, item, box and stash. Each a function of the go-live context.
917
+ */
918
+ export function standingAddresses(m) {
919
+ const fixed = [loot.LOOT_PROGRAM, loot.pda.platform(), loot.pda.identity(), loot.VRF_PROGRAM, loot.VRF_QUEUE, loot.MPL_CORE_PROGRAM, TOKEN_PROGRAM, TOKEN_2022_PROGRAM, ASSOCIATED_TOKEN_PROGRAM, SYSTEM_PROGRAM, SYSVAR_SLOT_HASHES, SYSVAR_INSTRUCTIONS, MEMO_PROGRAM].map((a) => () => a);
920
+ const programOf = (ctx, key) => programsOf(ctx)[ctx.mints[key]] ?? TOKEN_PROGRAM;
921
+ return [
922
+ ...fixed,
923
+ (ctx) => ctx.project, (ctx) => loot.pda.fees(ctx.project), (ctx) => ctx.treasury,
924
+ ...(m.collection ? [(ctx) => ctx.collection] : []),
925
+ ...[...m.tokens.values()].flatMap((t) => [(ctx) => ctx.mints[t.key], (ctx) => ctx.mints[t.key] && loot.ata(ctx.project, ctx.mints[t.key], programOf(ctx, t.key)), (ctx) => ctx.mints[t.key] && loot.ata(ctx.treasury, ctx.mints[t.key], programOf(ctx, t.key))]),
926
+ ...m.pools.map((p) => (ctx) => loot.pda.pool(ctx.project, p.index)),
927
+ ...m.items.map((it) => (ctx) => loot.pda.item(ctx.project, it.id)),
928
+ ...m.allBoxes.map((b) => (ctx) => loot.pda.box(ctx.project, b.id)),
929
+ ...m.stashes.map((s) => (ctx) => loot.pda.stash(ctx.project, s.id)),
930
+ ].slice(0, 256);
931
+ }
932
+
933
+ /** How many Core NFTs one `stash` call puts in (`Piece::another`: three accounts each after the first). */
934
+ const STASH_GROUP = 6;
935
+ /** `list` in groups of `n`. */
936
+ const groupsOf = (list, n) => Array.from({ length: Math.ceil(list.length / n) }, (_, i) => list.slice(i * n, i * n + n));
937
+
938
+ /** A stash's bytes with `n` NFTs: a table of NFTs (`table.rs`): its 51-byte header, a total for each group of 4,096
939
+ * rows and each block of 64, and 36 bytes an NFT (its weight and its address). */
940
+ export const stashBytes = (n) => (n ? loot.tableRowAt(36, n - 1) + 36 : 51);
941
+
942
+ /** Who puts a spec's auction up, and so is named in its address: the owner for a collectible it holds, the authority for a new unit of an item. */
943
+ export const auctionSeller = (a, ctx) => (a.lot?.asset ? ctx.owner : ctx.authority);
944
+
945
+ export const recordBytesOf = (kind, body) => {
946
+ const args = body.args;
947
+ const base = {
948
+ Item: () => ({ project: NOBODY, item_id: 0, ...placeholders(args), issued: 0, voided: 0, compressed: false }),
949
+ LootBox: () => ({ project: NOBODY, box_id: 0, ...placeholders(args), in_flight: 0n, version: 0 }),
950
+ Recipe: () => ({ project: NOBODY, id: 0, version: 0, ...placeholders(args) }),
951
+ Schedule: () => ({ project: NOBODY, id: 0, ...placeholders(args), runs: 0 }),
952
+ // Its terms and its round number: each round keeps the rest in a record of its own.
953
+ Lottery: () => ({ project: NOBODY, id: 0, incarnation: 0n, revision: 0, ...placeholders(loot.withDefaults({ Lottery: { args } }).Lottery.args), round: 0 }),
954
+ Grant: () => ({ project: NOBODY, id: 0, incarnation: 0n, ...placeholders(args), counted: { count: 0, window: 0, in_window: 0 } }),
955
+ }[kind];
956
+ return encodedSize(kind, base());
957
+ };
958
+ /** A farm's bytes as `open_farm` writes it: its args and its numbers. */
959
+ export const farmBytes = (f, m) => encodedSize('Farm', {
960
+ project: NOBODY, id: 0, farmed: f.stakes === 'collectibles' ? 'Collectibles' : { Tokens: { mint: NOBODY } }, ...placeholders(farmArgs(f, m)), rewards_mint: NOBODY,
961
+ rate_carry: 0n, opened_at: 0n, total_weight: 0n, reward_per_weight: 0n, reward_remainder: 0n, owed: 0n, clock: 0n, unreleased: 0n, release_ends: 0n, release_from: 0n, release_base: 0n,
962
+ keeps_ends: farmEnds(f),
963
+ });
964
+ /** Exact initial contest record size for its configured shares, gate, sides and deciders. */
965
+ export const contestBytes = (contest, m) => encodedSize('Contest', { project: NOBODY, id: 0, incarnation: 0n, opener: NOBODY, mint: NOBODY, decided_at: 0n, ...placeholders(contestArgs(contest, m)), verdict: 'Open', winner: 0, owed: 0n, unclaimed: 0n, sides: Array(contest.sides).fill(0n), share: 0, extras: 0, picks: 0 });
966
+ /** Args with "@mint:" names standing in as addresses, for sizing. */
967
+ const placeholders = (args) => resolve(args, { project: placeholderKey('project'), collection: placeholderKey('new:collection'), tree: placeholderKey('new:tree'), treasury: placeholderKey('treasury'), mints: new Proxy({}, { get: (_, k) => placeholderKey(`mint/${String(k)}`) }) });
968
+ /** The token program of each Token-2022 mint a context names (the program's account lists name it). */
969
+ const programsOf = (ctx) => ctx.tokenPrograms ?? {};
970
+
971
+ // ---------------------------------------------------------------- contexts
972
+
973
+ /** Stand-in addresses for every name a plan uses, so it can be built and sized without a wallet. */
974
+ export function placeholderContext(m, { rent = (bytes) => (BigInt(bytes) + ACCOUNT_OVERHEAD) * MOST_RENT_PER_BYTE } = {}) {
975
+ const ownerKey = placeholderKey('owner');
976
+ const collection = m.collection ? placeholderKey('new:collection') : null;
977
+ const tree = compressedTree(m) ? placeholderKey('new:tree') : null;
978
+ const mints = {};
979
+ for (const t of m.tokens.values()) mints[t.key] = t.mint?.startsWith('@') ? placeholderKey(`new:mint/${t.key}`) : t.mint;
980
+ return {
981
+ owner: ownerKey,
982
+ authority: m.authority === 'owner' ? ownerKey : m.authority,
983
+ collection,
984
+ tree,
985
+ project: loot.pda.project(ownerKey, m.projectNumber),
986
+ treasury: m.treasuryWallet ?? ownerKey,
987
+ mints,
988
+ keys: { ...(collection ? { 'new:collection': collection } : {}), ...(tree ? { 'new:tree': tree } : {}), ...Object.fromEntries([...m.tokens.values()].map((t) => [`new:mint/${t.key}`, placeholderKey(`new:mint/${t.key}`)])) },
989
+ rent,
990
+ slot: 0n,
991
+ };
992
+ }
993
+
994
+ /**
995
+ * What of `live` is made. While a setup is landing (`partial`), the project, its collection, its tree and each mint the
996
+ * setup makes count only once the step that makes it landed (`landed`): a setup cut short makes the rest again, with
997
+ * this session's fresh keys, instead of naming an account that was never made. What was live before a change, and
998
+ * that the change does not make, counts as it is. The treasury and authority count only with the project. The approval
999
+ * page holds what is left against the chain before it builds on it (`js/approve/live-check.mjs`).
1000
+ */
1001
+ export function madeSoFar(live) {
1002
+ if (!live?.partial) return live;
1003
+ const landed = new Set(live.landed ?? []);
1004
+ const changing = new Set(live.changing ?? []);
1005
+ // A new setup makes everything; a change makes only the parts it names.
1006
+ const made = (part, id) => landed.has(id) || (Boolean(live.published) && !changing.has(part));
1007
+ const out = { ...live };
1008
+ // The treasury and authority are the project's: named only once it is made, never from the file before then.
1009
+ if (!made('project', 'project')) {
1010
+ delete out.project;
1011
+ delete out.treasury;
1012
+ delete out.authority;
1013
+ }
1014
+ if (!made('project', 'collection')) delete out.collection;
1015
+ if (!made('tree', 'tree')) delete out.tree;
1016
+ if (live.mints) out.mints = Object.fromEntries(Object.entries(live.mints).filter(([key]) => made(`token:${key}`, `token:${key}`)));
1017
+ return out;
1018
+ }
1019
+
1020
+ /** The context of a real setup: the owner's wallet, fresh keys for what it creates, and what is live already. */
1021
+ export function setupContext(m, { owner: ownerKey, keys, live: given = {}, rent }) {
1022
+ const live = madeSoFar(given) ?? {};
1023
+ // A setup goes on only in the owner's own project: never in one a file names for them.
1024
+ const own = loot.pda.project(ownerKey, m.projectNumber);
1025
+ if (live.project && live.project !== own) throw new Error(`The live section names the project ${live.project}, which is not the project ${own} of the wallet ${ownerKey}.`);
1026
+ const collection = live.collection ?? keys['new:collection'] ?? null;
1027
+ const tree = live.tree ?? keys['new:tree'] ?? null;
1028
+ const mints = {};
1029
+ // A mint the spec names by its address is that one, whatever the live section says; one the setup makes is the
1030
+ // live section's once it landed (checked against the chain by the approval page), else this setup's fresh key.
1031
+ for (const t of m.tokens.values()) mints[t.key] = t.mint && !t.mint.startsWith('@') ? t.mint : live.mints?.[t.key] ?? keys[`new:mint/${t.key}`];
1032
+ return {
1033
+ owner: ownerKey,
1034
+ authority: m.authority === 'owner' ? ownerKey : m.authority,
1035
+ collection,
1036
+ tree,
1037
+ project: own,
1038
+ treasury: live.treasury ?? m.treasuryWallet ?? ownerKey,
1039
+ mints,
1040
+ keys,
1041
+ rent,
1042
+ };
1043
+ }
1044
+
1045
+ // ---------------------------------------------------------------- packing
1046
+
1047
+ /** The bytes of a transaction of `instructions` the owner pays for: what `pack` holds against the budget. */
1048
+ export const stepBytes = (instructions, ctx) => transactionSize(compileMessage({ payer: ctx.owner, blockhash: placeholderKey('blockhash'), instructions }));
1049
+
1050
+ /** Packs steps into transactions under the size budget, one signer set at a time; each step whole. */
1051
+ export function pack(steps, ctx) {
1052
+ const transactions = [];
1053
+ let current = null;
1054
+ const sizeOf = (instructions) => stepBytes(instructions, ctx);
1055
+ for (const step of steps) {
1056
+ const instructions = step.build(ctx);
1057
+ // A step with nothing to send (the lookup table of a plan built with no recent slot) is left out.
1058
+ if (!instructions.length) continue;
1059
+ const signerKind = step.signers.includes('authority') ? 'authority' : 'owner';
1060
+ // A write that names boxes to roll takes all the compute a transaction has: alone.
1061
+ if (current && current.signer === signerKind && !step.alone && !current.alone) {
1062
+ const next = [...current.instructions, ...instructions];
1063
+ if (sizeOf(next) <= BUDGET) {
1064
+ current.instructions = next;
1065
+ current.steps.push(step);
1066
+ current.built.push(instructions);
1067
+ current.signers = [...new Set([...current.signers, ...step.signers])];
1068
+ continue;
1069
+ }
1070
+ }
1071
+ const size = sizeOf(instructions);
1072
+ if (size > BUDGET) throw new Error(`The step “${step.says}” is ${size} bytes: too big for one transaction.`);
1073
+ // `built`: each step's own instructions, in the order of `steps`.
1074
+ current = { signer: signerKind, steps: [step], built: [instructions], instructions, signers: [...step.signers], alone: Boolean(step.alone) };
1075
+ transactions.push(current);
1076
+ }
1077
+ for (const t of transactions) t.bytes = sizeOf(t.instructions);
1078
+ return transactions;
1079
+ }
1080
+
1081
+ /**
1082
+ * The plan for a spec: refusals, or every step packed into transactions. With `ctx` the transactions carry the
1083
+ * real instructions; without it they are built on stand-in addresses (for sizing, cost and showing).
1084
+ */
1085
+ export function plan(economy, { now = Math.floor(Date.now() / 1000), ctx = null, only = null, live = null } = {}) {
1086
+ const checked = check(economy, { now });
1087
+ if (!checked.ok) return { ok: false, refusals: checked.refusals, warnings: checked.warnings };
1088
+ const { steps, model: m } = setupSteps(economy, { now, only, live });
1089
+ if (ctx) ctx.tokenPrograms ??= Object.fromEntries([...m.tokens.values()].filter((t) => t.program === 'token-2022' && ctx.mints[t.key]).map((t) => [ctx.mints[t.key], TOKEN_2022_PROGRAM]));
1090
+ const context = ctx ?? placeholderContext(m);
1091
+ const transactions = pack(steps, context);
1092
+ return {
1093
+ ok: true,
1094
+ warnings: checked.warnings,
1095
+ steps,
1096
+ transactions,
1097
+ // A wallet approves up to PER_APPROVAL transactions at once: the owner's, then the authority's (its steps go last).
1098
+ approvals: Math.max(1, Math.ceil(transactions.filter((t) => t.signer === 'owner').length / PER_APPROVAL)) + Math.ceil(transactions.filter((t) => t.signer === 'authority').length / PER_APPROVAL),
1099
+ freshKeys: [...new Set(steps.flatMap((s) => s.signers.filter((x) => x.startsWith('new:'))))],
1100
+ };
1101
+ }
1102
+
1103
+ /** What each part needs while the economy runs: the instruction, and who signs it. */
1104
+ export function playSteps(economy) {
1105
+ const m = model(economy);
1106
+ const rows = [];
1107
+ const add = (part, ix, signer, says) => rows.push({ part, ix, signer, says });
1108
+ // A shop item, and any box with one outcome and nothing random, sells in one instruction: price split, prize given.
1109
+ for (const b of m.shop) {
1110
+ const it = m.item.get(b.shopOf);
1111
+ add(`item:${it.key}`, 'buy', 'player', it.kind === 'normal' ? `buy “${it.name}”: the project’s backend delivers it from the receipt` : `buy “${it.name}”: minted to them at once`);
1112
+ }
1113
+ for (const b of m.boxes) {
1114
+ if (boxBuys(b, m)) {
1115
+ add(`box:${b.key}`, 'buy', 'player', `buy “${b.name}”: what it gives, at once`);
1116
+ continue;
1117
+ }
1118
+ add(`box:${b.key}`, 'open_box', 'player', `open “${b.name}”`);
1119
+ if (boxRolls(b, m)) add(`box:${b.key}`, 'callback_roll', 'the oracle', 'roll it');
1120
+ add(`box:${b.key}`, 'settle', 'the player, a key they named, or the project’s keys', 'carry out what it rolled');
1121
+ }
1122
+ for (const p of m.payments) add(`payment:${p.key}`, 'pay', 'player', `pay “${p.name}”`);
1123
+ add('payments', 'pay_nft', 'player', 'pay with an NFT (burned, or given to a wallet): the receipt is the record');
1124
+ for (const s of m.stashes) add(`stash:${s.key}`, 'stash', 'the owner or the authority', `put an NFT it holds in “${s.name}”, or take one out to any wallet`);
1125
+ for (const r of prizeRolls(m).filter((r) => !['mission', 'recipe'].includes(r.route))) {
1126
+ add(r.by, 'ask_roll', 'the winner or the project’s keys, who pay the oracle', `ask for the roll of “${m.box.get(r.box)?.name ?? r.box}” a winner got (${r.route})`);
1127
+ add(r.by, 'settle', 'the winner, a key they named, or the project’s keys', 'carry out what it rolled');
1128
+ }
1129
+ for (const r of m.recipes) add(`recipe:${r.key}`, 'craft', 'player', `craft “${r.name}”`);
1130
+ for (const s of m.schedules) add(`schedule:${s.key}`, 'run_schedule', 'the project’s keys, or who it pays', `run “${s.name ?? s.key}” when it is due`);
1131
+ for (const l of m.lotteries) add(`lottery:${l.key}`, 'buy_tickets', 'player', 'buy tickets'), add(`lottery:${l.key}`, 'draw_lottery', 'anyone', 'ask for the draw when it is due'), add(`lottery:${l.key}`, 'pull', 'anyone', 'pay one part: a refund, a place’s part of the pot or its prize, a share owed in common');
1132
+ for (const f of m.farms) {
1133
+ if (f.stakes === 'collectibles') add(`farm:${f.key}`, 'stake_nft', 'player', 'stake a collectible'), add(`farm:${f.key}`, 'unstake_nft', 'player', 'take it out once its lock has run');
1134
+ else add(`farm:${f.key}`, 'stake_tokens', 'player', 'stake'), add(`farm:${f.key}`, 'unstake_tokens', 'player', 'unstake or collect'), add(`farm:${f.key}`, 'stake_nft', 'player', 'put a collectible on as a booster');
1135
+ add(`farm:${f.key}`, 'harvest', 'player', 'collect rewards');
1136
+ add(`farm:${f.key}`, 'refresh_seat', 'anyone', 'read a seat’s boosts again');
1137
+ if (farmEnds(f)) add(`farm:${f.key}`, 'walk_farm', 'anyone', 'catch the farm up, in parts, when it has passed more end times than a move can walk');
1138
+ }
1139
+ for (const c of m.contests) {
1140
+ add(`contest:${c.key}`, 'open_contest', 'authority', `open “${c.name}”`);
1141
+ add(`contest:${c.key}`, 'enter', 'player', 'back a side');
1142
+ add(`contest:${c.key}`, 'decide', 'authority (or anyone sending a decider’s verdict)', 'name the side that won, or call it off before its result is due');
1143
+ add(`contest:${c.key}`, 'claim', c.stakeNft || c.prize ? 'the winner or the project’s keys' : 'anyone', 'pay an entry');
1144
+ }
1145
+ for (const a of m.auctions) {
1146
+ add(`auction:${a.key}`, 'open_auction', a.lot?.asset ? 'the owner, alone (a collectible of theirs)' : 'the authority, alone (a new unit of an item)', `put the lot up (at go-live)`);
1147
+ add(`auction:${a.key}`, 'bid', 'player', 'bid: the bid it tops goes back at once');
1148
+ add(`auction:${a.key}`, 'end_auction', 'anyone once it is over; its seller, to call off one with no bid', 'sell to the high bidder, or send the lot home');
1149
+ }
1150
+ for (const h of m.holds) add(`hold:${h.key}`, 'hold', 'player', `hand in a “${h.item}” until ${h.until ?? `${h.for ?? 'the time they choose'} from then`}`), add(`hold:${h.key}`, 'release', 'authority while it runs, anyone after', 'say where it goes, or send it home');
1151
+ for (const g of m.grants) add(`grant:${g.key}`, 'claim_grant', 'the winning wallet, which signs it (with a voucher, a list proof, or an open grant); anyone may pay its fees', `claim “${g.name}”`);
1152
+ for (const d of m.decisions) add(`decision:${d.key}`, { payout: 'move_tokens', burn: 'move_tokens', issue: 'move_tokens', make: 'collectible', unmake: 'collectible', set: 'collectible', award: 'release', decide: 'decide', roll: 'roll', open_contest: 'open_contest' }[d.does], 'authority', d.when);
1153
+ return rows;
1154
+ }
1155
+