@xgamesfun/solana 0.20.1 → 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 (70) hide show
  1. package/README.md +10 -10
  2. package/examples/coin-flip-arena.json +2 -7
  3. package/examples/elements-set.json +2 -9
  4. package/examples/expedition-guild.json +1 -8
  5. package/examples/guild-missions.json +3 -10
  6. package/examples/halving-seasons.json +4 -9
  7. package/examples/hero-trials.json +1 -4
  8. package/examples/merge-2048.json +5 -5
  9. package/examples/paper-hands-farm.json +4 -16
  10. package/examples/partner-vault.json +1 -4
  11. package/examples/stakers-draw.json +1 -8
  12. package/examples/star-fleet.json +1 -8
  13. package/examples/starforce.json +6 -6
  14. package/js/approve/details.mjs +8 -7
  15. package/js/approve/server.mjs +20 -5
  16. package/js/chain/borsh.js +9 -0
  17. package/js/chain/core.js +14 -2
  18. package/js/chain/lookup.js +8 -2
  19. package/js/chain/rpc.js +4 -2
  20. package/js/client/accounts.js +448 -116
  21. package/js/client/grant.js +125 -66
  22. package/js/client/idl/loot.js +1 -1
  23. package/js/client/idl/loot.json +2360 -2218
  24. package/js/client/index.js +1 -0
  25. package/js/client/ix.js +1204 -473
  26. package/js/client/pda.js +20 -16
  27. package/js/client/policy.js +44 -23
  28. package/js/client/random.js +70 -0
  29. package/js/client/sizes.js +255 -12
  30. package/js/economy/check.mjs +455 -210
  31. package/js/economy/cost.mjs +45 -30
  32. package/js/economy/examples.mjs +2 -2
  33. package/js/economy/explain.mjs +24 -20
  34. package/js/economy/guide.mjs +31 -30
  35. package/js/economy/live.mjs +30 -7
  36. package/js/economy/model.mjs +171 -56
  37. package/js/economy/plan.mjs +380 -112
  38. package/js/economy/schema.json +291 -143
  39. package/js/economy/simulate.mjs +154 -87
  40. package/js/mcp/tools.mjs +14 -8
  41. package/js/play/index.js +1121 -375
  42. package/js/play/keys.js +167 -0
  43. package/package.json +1 -1
  44. package/release.json +5 -5
  45. package/skills/xsolana-economy/SKILL.md +15 -11
  46. package/skills/xsolana-economy/references/after-launch.md +37 -27
  47. package/skills/xsolana-economy/references/apps.md +55 -19
  48. package/skills/xsolana-economy/references/sentence-to-spec.md +30 -22
  49. package/skills/xsolana-economy/references/telling-the-person.md +52 -11
  50. package/types/chain/core.d.ts +3 -1
  51. package/types/chain/lookup.d.ts +5 -2
  52. package/types/chain/rpc.d.ts +3 -1
  53. package/types/client/accounts.d.ts +195 -55
  54. package/types/client/grant.d.ts +72 -27
  55. package/types/client/idl/loot.d.ts +590 -559
  56. package/types/client/index.d.ts +1 -0
  57. package/types/client/ix.d.ts +3862 -234
  58. package/types/client/pda.d.ts +19 -15
  59. package/types/client/policy.d.ts +39 -14
  60. package/types/client/random.d.ts +26 -0
  61. package/types/client/sizes.d.ts +93 -5
  62. package/types/economy/check.d.mts +5 -10
  63. package/types/economy/cost.d.mts +2 -1
  64. package/types/economy/live.d.mts +16 -1
  65. package/types/economy/model.d.mts +105 -37
  66. package/types/economy/plan.d.mts +193 -192
  67. package/types/economy/simulate.d.mts +21 -13
  68. package/types/mcp/tools.d.mts +21 -13
  69. package/types/play/index.d.ts +450 -39
  70. package/types/play/keys.d.ts +51 -0
@@ -7,8 +7,12 @@
7
7
  // which signs the step that creates it. `placeholderContext` fills every address with a stand-in, so a plan can be
8
8
  // sized and shown before anyone connects a wallet.
9
9
  import { encodeBase58 } from '../chain/base58.js';
10
+ import { encodeBorsh } from '../chain/borsh.js';
11
+ import IDL from '../client/idl/loot.js';
10
12
  import { createCollectionInstruction } from '../chain/core.js';
11
- import { NATIVE_MINT, TOKEN_2022_PROGRAM, TOKEN_PROGRAM } from '../chain/pda.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';
12
16
  import { createAssociatedTokenAccountIdempotent, syncNative, transferLamports } from '../chain/programs.js';
13
17
  import { createAccountIx, initializeMint2Ix, MINT_BYTES, mintToIx, setAuthorityIx, transferCheckedIx } from '../chain/token.js';
14
18
  import { compileMessage, transactionSize } from '../chain/tx.js';
@@ -16,7 +20,7 @@ import { sha256 } from '../chain/sha256.js';
16
20
  import { utf8 } from '../chain/bytes.js';
17
21
  import * as loot from '../client/index.js';
18
22
  import { check } from './check.mjs';
19
- import { ACCOUNT_OVERHEAD, boxBuys, boxRolls, compressedTree, destination, effectOf, gateOf, LIMITS, listOf, lotterySplit, model, MOST_RENT_PER_BYTE, outcomeBurns, outcomeParts, perWalletOf, placeOf, prizeLine, prizePool, prizeRolls, prizeToken, rarityNumber, ruleOf, sharesOf, spendOf, stashNfts, toBy, toEnum, toSeconds, toTime, toUnits, weights, wholeCount, windowOf } from './model.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';
20
24
  import { BUDGET } from './txsize.mjs';
21
25
 
22
26
  const owner = 'owner';
@@ -31,10 +35,10 @@ const tokenProgramOf = (t) => (t?.program === 'token-2022' ? TOKEN_2022_PROGRAM
31
35
 
32
36
  // ---------------------------------------------------------------- spec parts → the program's args
33
37
 
34
- export function itemArgs(it) {
38
+ export function itemArgs(it, m) {
35
39
  const kind = it.kind === 'normal' ? 'Normal' : 'Collectible';
36
- // A collectible is minted into its own `collection`, or the one the setup makes ("@collection" until it exists).
37
- const life = it.life && kind !== 'Normal' ? { ready_after: toSeconds(it.life.readyAfter), lasts: toSeconds(it.life.lasts), uses: it.life.uses ?? 0, cooldown: toSeconds(it.life.cooldown) } : null;
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`).
38
42
  return {
39
43
  kind,
40
44
  rarity: rarityNumber(it.rarity),
@@ -43,19 +47,51 @@ export function itemArgs(it) {
43
47
  name: it.name,
44
48
  uri: it.uri ?? '',
45
49
  max_supply: it.supply ?? 0,
46
- life,
47
50
  active: it.active !== false,
48
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) } } })),
49
54
  };
50
55
  }
51
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
+
52
87
  /** The program's `Prize` for one part of a box outcome (`model.outcomeParts`). */
53
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' } };
54
91
  if (o.gives === 'item') return { Item: [m.item.get(o.item)?.id ?? 0] };
55
92
  if (o.gives === 'stash') return { Stash: [m.stash.get(o.stash)?.id ?? 0] };
56
- if (o.gives === 'pay') return { Tokens: { from: { Pool: [m.pool.get(o.pool)?.index ?? 0] }, amount: { Fixed: [amount(o.amount, m.poolToken(o.pool))] } } };
57
- if (o.gives === 'share') return { Tokens: { from: { Pool: [m.pool.get(o.pool)?.index ?? 0] }, amount: { Bps: [Math.round((o.percent ?? 0) * 100)] } } };
58
- if (o.gives === 'mint') return { Tokens: { from: { Mint: [`@mint:${o.currency ?? m.currencyKey}`] }, amount: { Fixed: [amount(o.amount, m.tokens.get(o.currency ?? m.currencyKey))] } } };
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)) } };
59
95
  return prizeArgs(o, m);
60
96
  }
61
97
 
@@ -68,10 +104,14 @@ export function boxArgs(b, m) {
68
104
  rise: rise(b.rise, t),
69
105
  fall: fall(b.fall, t, m.now),
70
106
  stock_odds: !!b.stockOdds,
107
+ when_out: b.whenOut === 'drawLeft' ? 'DrawLeft' : 'StopSales',
71
108
  active: b.active !== false,
72
109
  shares: shares(b.split, m),
73
- // An entry gives its prizes (a bundle up to three), and may burn the payment in place of the split.
74
- entries: (b.outcomes ?? []).map((o, i) => ({ prizes: outcomeParts(o).map((part) => partPrize(part, m)), burns: outcomeBurns(o), weight: w[i] })),
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] })),
75
115
  gate: gateOf(b.gate, m),
76
116
  odds: (b.odds ?? []).map((o) => ({ entry: o.outcome, boost: { rule: ruleOf(o.when, m), effect: effectOf(o.add) } })),
77
117
  window: windowOf(b, m.now),
@@ -82,24 +122,19 @@ export function boxArgs(b, m) {
82
122
  const shifts = (o.shifts ?? []).map((x) => ({ key: x.attribute, by: toBy(x.by), most: toSeconds(x.upTo ?? 0), from_now: Boolean(x.fromNow) }));
83
123
  // An outcome's own `then`, or the box's: an NFT taken as its price goes there whatever the roll gives.
84
124
  const then = thenOf(o.then ?? b.then, m);
85
- return shifts.length || then !== 'Back' ? [{ entry: i, shifts, then }] : [];
125
+ return shifts.length || then !== null ? [{ entry: i, shifts, then }] : [];
86
126
  }),
87
127
  };
88
128
  }
89
129
 
90
- /** Where an outcome sends the NFT a box rolls on: back, burned, given (the treasury's wallet is filled in when the economy goes live), or into a stash. */
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. */
91
131
  export const thenOf = (then, m) => {
92
- if (!then || then === 'back') return 'Back';
132
+ if (!then || then === 'back') return null;
93
133
  if (then === 'burn') return 'Burn';
94
134
  if (then.stash) return { Stash: [m?.stash.get(then.stash)?.id ?? 0] };
95
135
  return { Give: [then === 'treasury' ? '@treasury' : then.giveTo] };
96
136
  };
97
137
 
98
- /** The most a raise names (a u32): a farm’s time moved as written, with no cap of its own until 2106. */
99
- const NO_CAP = 4_294_967_295;
100
- /** Whether `key` is a time a farm of the spec keeps on what it stakes (its lock, its upkeep). */
101
- const farmTime = (key, m) => m.farms.some((f) => f.lockAttribute === key || f.upkeepAttribute === key);
102
-
103
138
  export function recipeArgs(r, m) {
104
139
  const tok = (key) => m.tokens.get(key ?? m.currencyKey);
105
140
  const inputs = (r.in ?? []).map((l) => {
@@ -107,21 +142,24 @@ export function recipeArgs(r, m) {
107
142
  if (l.takes === 'use') return { Use: { item: m.item.get(l.item)?.id ?? 0 } };
108
143
  // A staked one names the farm it sits in (craft.rs): the craft takes it only from its seat there.
109
144
  if (l.takes === 'staked') return { Staked: { item: m.item.get(l.item)?.id ?? 0, farm: m.farm.get(l.farm)?.id ?? 0 } };
110
- if (l.takes === 'renew') return { Renew: { item: m.item.get(l.item)?.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 } };
111
147
  if (l.takes === 'show') return { Show: { collection: l.collection ?? '@collection' } };
112
148
  // A trade-in: one NFT of `collection`, any kind, burned, given on, or put in a stash.
113
149
  if (l.takes === 'trade') return { Trade: { collection: l.collection ?? '@collection', then: spendOf(l.then, m) } };
114
150
  return { Pay: { mint: `@mint:${l.currency ?? m.currencyKey}`, amount: amount(l.amount, tok(l.currency)), to: toEnum(l.to, m) } };
115
151
  });
152
+ const life = lifeLines(r, m);
116
153
  // A line is a prize (prize.rs), or a roll on a collectible it keeps (`on`).
117
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)] }));
118
155
  return {
119
156
  active: r.active !== false,
120
157
  inputs,
121
158
  outputs,
122
- needs: (r.needs ?? []).map((n) => ({ place: n.place, key: n.key, at_least: n.atLeast })),
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],
123
161
  keeps: (r.keeps ?? []).map((k) => ({ key: k.key, from: k.from, to: k.to })),
124
- raises: (r.raises ?? []).map((x) => ({ place: x.place, key: x.key, by: toBy(x.by), most: x.upTo === undefined && farmTime(x.key, m) && !x.fromNow ? NO_CAP : toSeconds(x.upTo ?? 0), from_now: Boolean(x.fromNow) })),
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],
125
163
  window: windowOf(r, m.now),
126
164
  per_wallet: perWalletOf(r),
127
165
  };
@@ -132,8 +170,10 @@ export function scheduleArgs(s, m) {
132
170
  const poolOf = (key) => m.pool.get(key)?.index ?? 0;
133
171
  let action;
134
172
  if (s.does === 'issue') {
135
- const t = m.tokens.get(s.currency ?? m.pool.get(s.pool)?.currency);
136
- action = { from: { Mint: [`@mint:${t?.key}`] }, to: { Pool: [poolOf(s.pool)] }, amount: { Fixed: [amount(s.amount, t)] }, halve_every: s.halveEvery ?? 0 };
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 };
137
177
  } else if (s.sealed) {
138
178
  // A sealed schedule pays from its own vault, in `currency`.
139
179
  const t = m.tokens.get(s.currency ?? m.currencyKey);
@@ -159,12 +199,13 @@ export function prizeArgs(p, m) {
159
199
  if (line.gives === 'item') return { Item: [m.item.get(line.item)?.id ?? 0] };
160
200
  // An NFT a lottery's place holds in the lottery's own stash (a raffle), or one of a project stash.
161
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';
162
204
  if (line.gives === 'nft') return { Nft: { stash: m.stash.get(line.stash)?.id ?? 0, nft: line.nft } };
163
205
  if (line.gives === 'roll') return { Roll: [m.box.get(line.box)?.id ?? 0] };
164
206
  const pool = prizePool(line);
165
207
  const from = pool ? { Pool: [m.pool.get(pool)?.index ?? 0] } : { Mint: [`@mint:${line.currency ?? m.currencyKey}`] };
166
- if (line.gives === 'share') return { Tokens: { from, amount: { Bps: [Math.round((line.percent ?? 0) * 100)] } } };
167
- return { Tokens: { from, amount: { Fixed: [amount(line.amount, prizeToken(line, m))] } } };
208
+ return { Tokens: { from, amount: amount(line.amount, prizeToken(line, m)) } };
168
209
  }
169
210
 
170
211
  /**
@@ -187,25 +228,82 @@ export function lotteryArgs(l, m) {
187
228
  const ticket = l.ticketCurrency ? m.tokens.get(l.ticketCurrency) : potToken;
188
229
  const every = toSeconds(l.every);
189
230
  return {
190
- price: l.ticketNft ? 0n : amount(l.price, ticket),
191
- mint: `@mint:${potKey}`,
192
- shares: shares(lotterySplit(l), m),
193
- // A place takes a share of the pot, a prize for its winner, or both.
194
- places: (l.places ?? []).map(placeOf).map((p) => ({ bps: Math.round(p.percent * 100), prize: prizeOrNull(p.prize, m) })),
195
- ticket_mint: l.ticketCurrency ? `@mint:${l.ticketCurrency}` : '11111111111111111111111111111111',
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
+ },
196
248
  every: BigInt(every),
197
- draws_at: BigInt(l.firstDraw ? toTime(l.firstDraw, m.now) : m.now + every),
198
249
  gate: gateOf(l.gate, m),
199
250
  per_wallet: l.perWallet ?? 0,
200
- // A ticket paid with one NFT that passes the rule, in place of tokens.
201
- nft_ticket: l.ticketNft ? { rule: ruleOf(l.ticketNft.rule, m), then: spendOf(l.ticketNft.then, m) } : null,
202
- // No loss: every ticket goes back to its buyer once the round is drawn; the winners take the pot alone.
203
- returns: Boolean(l.noLoss),
204
- // A least sale: a round that sold fewer tickets is cancelled at its draw and refunded.
205
- min_sold: BigInt(l.minSold ?? 0),
206
251
  };
207
252
  }
208
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
+
209
307
  /**
210
308
  * The args the authority opens a contest with, `at` (Unix seconds) its opening: its sides, when entries close and the
211
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
@@ -232,8 +330,10 @@ export function contestArgs(c, m, at = m.now) {
232
330
  prize: prizeOrNull(c.prize, m),
233
331
  // A token prize set aside whole at the decision and split among the winners by what each put in.
234
332
  prize_split: Boolean(c.prize?.split),
235
- // Each entry stakes one NFT that passes this rule, and the NFTs are the pot.
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).
236
335
  nft: c.stakeNft ? ruleOf(c.stakeNft, m) : null,
336
+ cap: c.stakeNft && c.losersEach ? c.losersEach : null,
237
337
  };
238
338
  }
239
339
 
@@ -245,37 +345,42 @@ export function farmArgs(f, m) {
245
345
  roll: t.roll ? m.box.get(t.roll)?.id ?? 0 : null,
246
346
  // A token seat may leave before this tier's lock by leaving this share of what it takes out.
247
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,
248
350
  }));
249
351
  // Every rule of its gate; with none, a farm of one item's collectibles takes that item.
250
352
  const named = listOf(f.gate);
251
353
  const gates = f.stakes !== 'collectibles' ? [] : (named.length ? named : f.item ? [{ item: f.item, attribute: 'item_id' }] : []).map((r) => ruleOf(r, m));
252
354
  // A rate is per NFT_UNIT of weight: one collectible, or 1,000,000 base units of a staked token.
253
- const rewards = m.poolToken(f.rewards);
355
+ const rewards = m.tokens.get(f.rewardsToken);
254
356
  const staked = f.stakes === 'collectibles' ? null : m.tokens.get(f.stakes);
255
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);
256
360
  return {
257
361
  custody: { freeze: 'Freeze', watch: 'Watch' }[f.custody] ?? 'Escrow',
258
- rewards_pool: m.pool.get(f.rewards)?.index ?? 0,
259
362
  tiers,
260
363
  boosts: (f.boosts ?? []).map((b) => ({ rule: ruleOf(b.when, m), effect: effectOf(b.add) })),
261
364
  max_boost_bps: Math.round((f.maxBoostPercent ?? 0) * 100),
262
365
  gates,
263
- rate: f.rate ? { per_day: staked ? (perDay * 1_000_000n) / 10n ** BigInt(staked.decimals ?? 0) : perDay, mints: Boolean(f.rate.minted) } : null,
264
- 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) })),
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 })),
265
368
  // A farm that runs for a time ends that long after it goes live.
266
369
  ends_at: f.runs ? BigInt(m.now + toSeconds(f.runs)) : 0n,
267
370
  // The times it keeps on what is staked: its lock, and an upkeep.
268
371
  lock_key: f.lockAttribute ?? '',
269
372
  upkeep_key: f.upkeepAttribute ?? '',
270
- // Where an early exit's fee goes, how long a stake waits on its way out, and a toll on every payout of rewards.
373
+ // Where an early exit's fee goes.
271
374
  exit_to: f.exitFeeTo ? sinkOf(f.exitFeeTo, m) : null,
272
- unbond: f.unbond ? toSeconds(f.unbond) : 0,
273
- toll: f.toll ? { bps: Math.round(f.toll.percent * 100), to: sinkOf(f.toll.to, m) } : null,
274
375
  };
275
376
  }
276
377
 
277
- /** A fee's sink (`Sink`): "burn", or "pool:<key>", one of the project's pools. */
278
- export const sinkOf = (to, m) => (to === 'burn' ? 'Burn' : { Pool: [m.pool.get(String(to).replace(/^pool:/, ''))?.index ?? 0] });
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);
279
384
 
280
385
  /** An auction's args: its lot, reserve, least raise, times and split. "start" is when it goes live unless it says. */
281
386
  export function auctionArgs(a, m) {
@@ -317,12 +422,14 @@ export function grantArgs(g, m) {
317
422
  wallet: perWalletOf(l),
318
423
  per_person: l.perPerson ?? 0,
319
424
  per_key: l.perKey ?? 0,
320
- per_day: l.perDay ?? 0,
321
- total: l.total ?? 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 },
322
427
  window: windowOf({ opens: l.opens, closes: l.until }, m.now),
323
428
  },
324
429
  active: g.active !== false,
325
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),
326
433
  };
327
434
  }
328
435
 
@@ -330,7 +437,8 @@ export function grantArgs(g, m) {
330
437
 
331
438
  /**
332
439
  * Replaces "@mint:<key>" strings anywhere in `value` with the mint `ctx` names, "@ata:<wallet>:<key>" with that
333
- * wallet's token account for it, "@pot:<id>" with a lottery's pot and "@farm:<id>" with a farm's address.
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.
334
442
  */
335
443
  function resolve(value, ctx) {
336
444
  if (value === '@treasury') {
@@ -355,6 +463,10 @@ function resolve(value, ctx) {
355
463
  if (!ctx.project) throw new Error('No project yet for a stake gate’s farm.');
356
464
  return loot.pda.farm(ctx.project, Number(value.slice(6)));
357
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
+ }
358
470
  if (typeof value === 'string' && value.startsWith('@pot:')) {
359
471
  if (!ctx.project) throw new Error('No project yet for a lottery’s pot.');
360
472
  return loot.pda.pot(loot.pda.lottery(ctx.project, Number(value.slice(5))));
@@ -381,8 +493,8 @@ const placeholderKey = (name) => encodeBase58(sha256(utf8(`loot-plan:${name}`)))
381
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,
382
494
  * farms, auctions, then the seeds and the grants' funds. `only` limits it to these part ids (a change to a live economy).
383
495
  *
384
- * A farm opens before the seed of its rewards pool lands, and anyone may stake in between. That is safe: a sharing
385
- * farm credits nothing at once, and releases what lands in its pool over its longest lock (a day at least) to the
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
386
498
  * seats there as it is released, its clock stopped while nobody is in. A seed that lands
387
499
  * before the first stake or after it is released the same way from that stake on, so seeding first would change nothing.
388
500
  */
@@ -474,6 +586,39 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
474
586
  ],
475
587
  });
476
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
+
477
622
  for (const p of m.pools) {
478
623
  const t = m.tokens.get(p.currency);
479
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] })] });
@@ -481,30 +626,57 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
481
626
  const record = (part, def, says, kind, what, options = () => ({})) => {
482
627
  const [variant] = Object.keys(def);
483
628
  const bytes = recordBytesOf(kind, def[variant]);
484
- // A lottery's first write opens its pot: a token account as well as the record.
485
- const pot = kind === 'Lottery' ? [{ what: `${what}’s pot`, bytes: 165 }, { what: `${what}’s ticket escrow`, bytes: 165 }] : def.Schedule?.args.action.from.Vest ? [{ what: `${what}’s sealed vault`, bytes: 165 }] : [];
486
- add({ id: `${part}:${kind}`, part, says, creates: [{ what, bytes }, ...pot], build: (ctx) => [loot.upsertIx({ owner: ctx.owner, project: project(ctx), def: resolve(def, ctx), ...options(ctx) })] });
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
+ });
487
635
  };
488
- for (const it of m.items) record(`item:${it.key}`, { Item: { id: it.id, args: itemArgs(it) } }, `${it.active === false ? 'Switch off' : 'Write'} the item “${it.name}”`, 'Item', `the item “${it.name}”`);
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}”`);
489
652
  // A box's line names its odds, so the owner sees what they approve (above all when a live box's odds change).
490
653
  const odds = (b) => {
491
654
  const ws = weights(b);
492
655
  const total = ws.reduce((n, w) => n + w, 0) || 1;
493
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}”` : '');
494
- const what = (o) => outcomeParts(o).map(part).join(' + ') || (outcomeBurns(o) ? 'a burn' : 'nothing');
657
+ const what = (o) => outcomeParts(o).map(part).join(' + ') || 'nothing';
495
658
  return (b.outcomes ?? []).map((o, i) => `${what(o)} ${Math.round((ws[i] / total) * 10_000) / 100}%`).join(', ');
496
659
  };
497
660
  const wasLive = new Set((live?.published?.boxes ?? []).map((b) => b.key));
498
- for (const b of m.allBoxes) {
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) {
499
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)}`}`;
500
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}”`);
501
676
  }
502
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}”`);
503
678
  for (const s of m.schedules) {
504
- // A sealed schedule that pays a lottery's pot names the lottery when it is written (the pot is the project's own).
505
- const pays = destination(s.to ?? '');
506
- const lottery = s.sealed && pays.kind === 'lottery' ? m.lottery.get(pays.key) : null;
507
- 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}”`, (ctx) => (lottery ? { lottery: loot.pda.lottery(project(ctx), lottery.id) } : {}));
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}”`);
508
680
  // A sealed schedule's vault is funded once, at go-live: from the owner's wallet, minted, or from a pool.
509
681
  const id = `fund:${s.key}`;
510
682
  if (!s.sealed || !Number(s.fund ?? 0) || (live?.landed ?? []).includes(id) || live?.published?.schedules?.some((x) => x.key === s.key)) continue;
@@ -532,29 +704,32 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
532
704
  });
533
705
  }
534
706
  for (const l of m.lotteries) {
535
- // A raffle's NFT goes into the lottery's own stash first: the lottery is written naming an NFT already there.
536
- for (const p of (l.places ?? []).map(placeOf).filter((p) => p.prize?.held)) {
537
- const id = `stash:lottery-${l.key}:${p.prize.nft}`;
538
- if ((live?.landed ?? []).includes(id) || live?.published?.lotteries?.some((x) => x.key === l.key)) continue;
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;
539
713
  add({
540
- id,
714
+ id: `stash:lottery-${l.key}:${group.map((n) => n.nft).join(',')}`,
541
715
  part: `lottery:${l.key}`,
542
- says: `Put your NFT ${p.prize.nft} in the lottery “${l.name}”: it stays there until its winner takes it, and nobody, you included, can take it out before`,
543
- creates: [{ what: `the lottery “${l.name}”’s stash`, bytes: STASH_HEAD_BYTES + 32 }],
544
- build: (ctx) => [loot.stashIx({ signer: ctx.owner, project: project(ctx), lottery: l.id, asset: p.prize.nft, collection: p.prize.collection ?? ctx.collection })],
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 })) })],
545
719
  });
546
- }
720
+ });
547
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}”`);
548
722
  }
549
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). */
550
725
  for (const f of m.farms) {
551
726
  if (live?.farms?.includes(f.key)) continue;
552
- const rewards = m.poolToken(f.rewards);
727
+ const rewards = m.tokens.get(f.rewardsToken);
553
728
  add({
554
729
  id: `farm:${f.key}`,
555
730
  part: `farm:${f.key}`,
556
- says: `Open the farm “${f.name}”: stake ${f.stakes === 'collectibles' ? 'collectibles' : m.tokens.get(f.stakes)?.symbol}, earn ${rewards?.symbol} from “${f.rewards}”`,
557
- creates: [{ what: `the farm “${f.name}”`, bytes: encodedSize('Farm', { project: NOBODY, id: 0, farmed: f.stakes === 'collectibles' ? 'Collectibles' : { Tokens: { mint: NOBODY } }, ...placeholders(farmArgs(f, m)), rewards_mint: NOBODY, rate_at: 0n, rate_carry: 0n, total_weight: 0n, reward_per_weight: 0n, reward_remainder: 0n, given: 0n, unreleased: 0n, released_at: 0n, release_ends: 0n, release_from: 0n, release_base: 0n }) }, { what: 'its rewards vault', bytes: 165 }, ...(f.stakes === 'collectibles' ? [] : [{ what: 'its stake vault', bytes: 165 }])],
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 }] : [])],
558
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] })],
559
734
  });
560
735
  }
@@ -577,29 +752,34 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
577
752
  // What is in a live stash already: what its last publish listed, and the steps of this one that landed.
578
753
  const stashed = new Set([
579
754
  ...(live?.published?.stashes ?? []).flatMap((s) => stashNfts(s).map((n) => `${s.key}:${n.asset}`)),
580
- ...(live?.landed ?? []).filter((id) => id.startsWith('stash:')).map((id) => id.slice(6)),
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
+ }),
581
760
  ]);
582
761
  for (const s of m.stashes) {
583
762
  const nfts = stashNfts(s);
584
763
  const putting = nfts.filter((n) => n.kind !== 'compressed' && !stashed.has(`${s.key}:${n.asset}`));
585
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.
586
765
  const fresh = !nfts.some((n) => stashed.has(`${s.key}:${n.asset}`));
587
- for (const [i, n] of putting.entries()) {
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()) {
588
771
  add({
589
- id: `stash:${s.key}:${n.asset}`,
772
+ id: `stash:${s.key}:${group.map((n) => n.asset).join(',')}`,
590
773
  part: `stash:${s.key}`,
591
- says: `Put your NFT ${n.asset} in the stash “${s.name}”: the project holds it until a prize or a roll gives it away`,
592
- creates: i > 0 ? [] : fresh ? [{ what: `the stash “${s.name}”, with ${putting.length} NFT${putting.length === 1 ? '' : 's'}`, bytes: STASH_HEAD_BYTES + 32 * putting.length }] : [{ what: `room for ${putting.length} more NFT${putting.length === 1 ? '' : 's'} in “${s.name}”`, bytes: 32 * putting.length, grows: true }],
593
- build: (ctx) => [
594
- loot.stashIx({
595
- signer: ctx.owner,
596
- project: project(ctx),
597
- id: s.id,
598
- asset: n.asset,
599
- collection: n.kind === 'core' ? n.collection ?? ctx.collection : undefined,
600
- tokenMetadata: n.kind === 'token-metadata' ? { programmable: n.programmable, ruleSet: n.ruleSet ?? undefined } : undefined,
601
- }),
602
- ],
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
+ },
603
783
  });
604
784
  }
605
785
  }
@@ -621,7 +801,8 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
621
801
  : a.lot?.roll
622
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})`
623
803
  : `Open the auction “${a.name}” of one new “${m.item.get(a.lot?.item)?.name ?? a.lot?.item}” (${terms})`,
624
- 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, serial: 0, reserve: 0n, min_raise_bps: 0, starts_at: 0n, ends_at: 0n, extend: 0, high_bidder: NOBODY, high: 0n, last_bid_at: 0n, shares: args.shares, gate: args.gate, nft_bids: placeholders(args).nft_bids, held: [] }) }],
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) }],
625
806
  build: (ctx) => {
626
807
  const seller = auctionSeller(a, ctx);
627
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) })];
@@ -655,6 +836,24 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
655
836
  });
656
837
  }
657
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
+ }
658
857
  // A lottery's seed: the owner puts it straight into the lottery's pot.
659
858
  for (const l of m.lotteries) {
660
859
  if (!(Number(l.seed ?? 0) > 0) || live?.seeded?.includes(`lottery:${l.key}`)) continue;
@@ -673,16 +872,72 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
673
872
  },
674
873
  });
675
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
+ }
676
885
  for (const g of m.grants) {
677
886
  if (!(Number(g.fund ?? 0) > 0) || live?.funded?.includes(g.key)) continue;
678
887
  const lamports = toUnits(g.fund, 9);
679
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)] });
680
889
  }
681
- return { steps, model: m };
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);
682
931
  }
683
932
 
684
- /** A stash's bytes before its NFTs: its tag, its project, its id and its count (`stash.rs`); each NFT adds 32. */
685
- export const STASH_HEAD_BYTES = 41;
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);
686
941
 
687
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. */
688
943
  export const auctionSeller = (a, ctx) => (a.lot?.asset ? ctx.owner : ctx.authority);
@@ -690,17 +945,24 @@ export const auctionSeller = (a, ctx) => (a.lot?.asset ? ctx.owner : ctx.authori
690
945
  export const recordBytesOf = (kind, body) => {
691
946
  const args = body.args;
692
947
  const base = {
693
- Item: () => ({ project: NOBODY, item_id: 0, ...placeholders(args), issued: 0, voided: 0 }),
694
- LootBox: () => ({ project: NOBODY, box_id: 0, ...placeholders(args), version: 0 }),
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 }),
695
950
  Recipe: () => ({ project: NOBODY, id: 0, version: 0, ...placeholders(args) }),
696
951
  Schedule: () => ({ project: NOBODY, id: 0, ...placeholders(args), runs: 0 }),
697
- Lottery: () => ({ project: NOBODY, id: 0, pot_bump: 0, funds_bump: 0, incarnation: 0n, revision: 0, pending: 0, ...placeholders(args), places: [...(args.places ?? []).map((p) => (typeof p === 'object' ? { prize: null, ...p } : { bps: p, prize: null })), ...Array(LIMITS.places).fill({ bps: 0, prize: null })].slice(0, LIMITS.places), round: 0, status: 'Open', sold: 0n, asked_first: 0n, asked_slot: 0n, first_ticket: 0n, paid: 0, pot_at_payout: 0n, given: 0 }),
698
- Grant: () => ({ project: NOBODY, id: 0, incarnation: 0n, ...placeholders(args), counters: { claimed: 0, today: 0, day: 0n } }),
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 } }),
699
955
  }[kind];
700
956
  return encodedSize(kind, base());
701
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
+ });
702
964
  /** Exact initial contest record size for its configured shares, gate, sides and deciders. */
703
- export const contestBytes = (contest, m) => encodedSize('Contest', { project: NOBODY, id: 0, incarnation: 0n, opener: NOBODY, mint: NOBODY, ...placeholders(contestArgs(contest, m)), verdict: 'Open', winner: 0, entries: 0, unclaimed: 0n, sides: Array(contest.sides).fill(0n), losers: 0 });
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 });
704
966
  /** Args with "@mint:" names standing in as addresses, for sizing. */
705
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)}`) }) });
706
968
  /** The token program of each Token-2022 mint a context names (the program's account lists name it). */
@@ -725,6 +987,7 @@ export function placeholderContext(m, { rent = (bytes) => (BigInt(bytes) + ACCOU
725
987
  mints,
726
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}`)])) },
727
989
  rent,
990
+ slot: 0n,
728
991
  };
729
992
  }
730
993
 
@@ -791,8 +1054,11 @@ export function pack(steps, ctx) {
791
1054
  const sizeOf = (instructions) => stepBytes(instructions, ctx);
792
1055
  for (const step of steps) {
793
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;
794
1059
  const signerKind = step.signers.includes('authority') ? 'authority' : 'owner';
795
- if (current && current.signer === signerKind) {
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) {
796
1062
  const next = [...current.instructions, ...instructions];
797
1063
  if (sizeOf(next) <= BUDGET) {
798
1064
  current.instructions = next;
@@ -805,7 +1071,7 @@ export function pack(steps, ctx) {
805
1071
  const size = sizeOf(instructions);
806
1072
  if (size > BUDGET) throw new Error(`The step “${step.says}” is ${size} bytes: too big for one transaction.`);
807
1073
  // `built`: each step's own instructions, in the order of `steps`.
808
- current = { signer: signerKind, steps: [step], built: [instructions], instructions, signers: [...step.signers] };
1074
+ current = { signer: signerKind, steps: [step], built: [instructions], instructions, signers: [...step.signers], alone: Boolean(step.alone) };
809
1075
  transactions.push(current);
810
1076
  }
811
1077
  for (const t of transactions) t.bytes = sizeOf(t.instructions);
@@ -828,7 +1094,8 @@ export function plan(economy, { now = Math.floor(Date.now() / 1000), ctx = null,
828
1094
  warnings: checked.warnings,
829
1095
  steps,
830
1096
  transactions,
831
- approvals: Math.max(1, Math.ceil(transactions.filter((t) => t.signer === 'owner').length / PER_APPROVAL)) + (transactions.some((t) => t.signer === 'authority') ? 1 : 0),
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),
832
1099
  freshKeys: [...new Set(steps.flatMap((s) => s.signers.filter((x) => x.startsWith('new:'))))],
833
1100
  };
834
1101
  }
@@ -844,35 +1111,36 @@ export function playSteps(economy) {
844
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`);
845
1112
  }
846
1113
  for (const b of m.boxes) {
847
- if (boxBuys(b)) {
1114
+ if (boxBuys(b, m)) {
848
1115
  add(`box:${b.key}`, 'buy', 'player', `buy “${b.name}”: what it gives, at once`);
849
1116
  continue;
850
1117
  }
851
1118
  add(`box:${b.key}`, 'open_box', 'player', `open “${b.name}”`);
852
- if (boxRolls(b)) add(`box:${b.key}`, 'callback_roll', 'the oracle', 'roll it');
853
- add(`box:${b.key}`, 'settle', 'anyone', 'carry out what it rolled');
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');
854
1121
  }
855
1122
  for (const p of m.payments) add(`payment:${p.key}`, 'pay', 'player', `pay “${p.name}”`);
856
1123
  add('payments', 'pay_nft', 'player', 'pay with an NFT (burned, or given to a wallet): the receipt is the record');
857
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`);
858
1125
  for (const r of prizeRolls(m).filter((r) => !['mission', 'recipe'].includes(r.route))) {
859
- add(r.by, 'ask_roll', 'anyone, who pays the oracle', `ask for the roll of “${m.box.get(r.box)?.name ?? r.box}” a winner got (${r.route})`);
860
- add(r.by, 'settle', 'anyone', 'carry out what it rolled');
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');
861
1128
  }
862
1129
  for (const r of m.recipes) add(`recipe:${r.key}`, 'craft', 'player', `craft “${r.name}”`);
863
- for (const s of m.schedules) add(`schedule:${s.key}`, 'run_schedule', 'anyone', `run “${s.name ?? s.key}” when it is due`);
864
- 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}`, 'finish_lottery_contribution', 'anyone', 'finish each purchase, then pay each winner');
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');
865
1132
  for (const f of m.farms) {
866
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');
867
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');
868
1135
  add(`farm:${f.key}`, 'harvest', 'player', 'collect rewards');
869
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');
870
1138
  }
871
1139
  for (const c of m.contests) {
872
1140
  add(`contest:${c.key}`, 'open_contest', 'authority', `open “${c.name}”`);
873
1141
  add(`contest:${c.key}`, 'enter', 'player', 'back a side');
874
- add(`contest:${c.key}`, 'decide', 'authority (or anyone, to call it off once overdue)', 'name the side that won');
875
- add(`contest:${c.key}`, 'claim', 'anyone', 'pay an entry');
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');
876
1144
  }
877
1145
  for (const a of m.auctions) {
878
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)`);