@xgamesfun/solana 0.20.1 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +95 -52
  2. package/bin/xsolana.mjs +23 -8
  3. package/examples/blind-forge.json +2 -4
  4. package/examples/coin-flip-arena.json +4 -12
  5. package/examples/duel-arena.json +1 -4
  6. package/examples/elements-set.json +2 -9
  7. package/examples/expedition-guild.json +1 -8
  8. package/examples/fair-presale.json +1 -3
  9. package/examples/guild-missions.json +3 -10
  10. package/examples/halving-seasons.json +4 -9
  11. package/examples/hero-trials.json +15 -6
  12. package/examples/holder-den.json +38 -0
  13. package/examples/holders-drop.json +105 -0
  14. package/examples/ideas.json +788 -788
  15. package/examples/match-predictions.json +24 -0
  16. package/examples/merge-2048.json +5 -5
  17. package/examples/nft-raffle.json +1 -0
  18. package/examples/pack-league.json +108 -0
  19. package/examples/paper-hands-farm.json +4 -16
  20. package/examples/partner-vault.json +20 -7
  21. package/examples/sacrifice-raffle.json +2 -10
  22. package/examples/shelter-fair.json +1 -0
  23. package/examples/stakers-draw.json +4 -15
  24. package/examples/star-fleet.json +1 -8
  25. package/examples/starforce.json +6 -6
  26. package/examples/stride-sneakers.json +164 -0
  27. package/examples/trade-in-shop.json +2 -3
  28. package/js/approve/details.mjs +122 -52
  29. package/js/approve/ed25519.mjs +5 -1
  30. package/js/approve/live-check.mjs +83 -26
  31. package/js/approve/page.html +217 -78
  32. package/js/approve/page.js +43 -15
  33. package/js/approve/server.mjs +290 -73
  34. package/js/chain/base58.js +7 -1
  35. package/js/chain/borsh.js +24 -11
  36. package/js/chain/bytes.js +11 -8
  37. package/js/chain/core.js +35 -7
  38. package/js/chain/curve.js +1 -1
  39. package/js/chain/lookup.js +124 -36
  40. package/js/chain/mint.js +6 -2
  41. package/js/chain/pda.js +2 -2
  42. package/js/chain/programs.js +6 -6
  43. package/js/chain/rpc.js +44 -16
  44. package/js/chain/token.js +13 -2
  45. package/js/chain/token2022.js +55 -16
  46. package/js/chain/tx.js +32 -22
  47. package/js/client/accounts.js +653 -131
  48. package/js/client/compressed.js +4 -3
  49. package/js/client/grant.js +267 -83
  50. package/js/client/idl/loot.js +1 -1
  51. package/js/client/idl/loot.json +2406 -2176
  52. package/js/client/index.js +1 -0
  53. package/js/client/ix.js +3059 -664
  54. package/js/client/pda.js +39 -33
  55. package/js/client/policy.js +52 -24
  56. package/js/client/random.js +70 -0
  57. package/js/client/sizes.js +414 -16
  58. package/js/client/traits.js +14 -5
  59. package/js/connect/connect.mjs +59 -17
  60. package/js/economy/check.mjs +4153 -725
  61. package/js/economy/cost.mjs +403 -64
  62. package/js/economy/examples.mjs +176 -35
  63. package/js/economy/explain.mjs +466 -111
  64. package/js/economy/guide.mjs +296 -72
  65. package/js/economy/legal.mjs +12 -6
  66. package/js/economy/live.mjs +83 -19
  67. package/js/economy/model.mjs +462 -124
  68. package/js/economy/plan.mjs +1306 -219
  69. package/js/economy/schema.json +390 -484
  70. package/js/economy/secrets.mjs +17 -7
  71. package/js/economy/simulate.mjs +883 -223
  72. package/js/economy/txsize.mjs +15 -3
  73. package/js/economy/validate.mjs +61 -24
  74. package/js/mcp/server.mjs +42 -14
  75. package/js/mcp/tools.mjs +307 -88
  76. package/js/play/index.js +2864 -619
  77. package/js/play/keys.js +193 -0
  78. package/js/results/index.js +223 -30
  79. package/js/results/server.js +230 -0
  80. package/js/results/wire.js +64 -16
  81. package/package.json +6 -4
  82. package/release.json +5 -5
  83. package/skills/xsolana-economy/SKILL.md +39 -21
  84. package/skills/xsolana-economy/references/after-launch.md +58 -38
  85. package/skills/xsolana-economy/references/apps.md +97 -50
  86. package/skills/xsolana-economy/references/sentence-to-spec.md +148 -121
  87. package/skills/xsolana-economy/references/telling-the-person.md +61 -17
  88. package/types/chain/core.d.ts +4 -2
  89. package/types/chain/lookup.d.ts +5 -2
  90. package/types/chain/rpc.d.ts +3 -1
  91. package/types/client/accounts.d.ts +217 -56
  92. package/types/client/grant.d.ts +72 -27
  93. package/types/client/idl/loot.d.ts +602 -549
  94. package/types/client/index.d.ts +1 -0
  95. package/types/client/ix.d.ts +3884 -247
  96. package/types/client/pda.d.ts +22 -16
  97. package/types/client/policy.d.ts +39 -14
  98. package/types/client/random.d.ts +26 -0
  99. package/types/client/sizes.d.ts +93 -5
  100. package/types/economy/check.d.mts +5 -10
  101. package/types/economy/cost.d.mts +2 -1
  102. package/types/economy/guide.d.mts +5 -0
  103. package/types/economy/live.d.mts +16 -1
  104. package/types/economy/model.d.mts +151 -38
  105. package/types/economy/plan.d.mts +237 -194
  106. package/types/economy/simulate.d.mts +21 -13
  107. package/types/mcp/tools.d.mts +21 -13
  108. package/types/play/index.d.ts +453 -53
  109. package/types/play/keys.d.ts +51 -0
  110. package/types/results/index.d.ts +7 -6
  111. package/types/results/server.d.ts +126 -0
  112. package/types/results/wire.d.ts +1 -1
@@ -7,16 +7,71 @@
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 {
14
+ ASSOCIATED_TOKEN_PROGRAM,
15
+ NATIVE_MINT,
16
+ SYSTEM_PROGRAM,
17
+ SYSVAR_INSTRUCTIONS,
18
+ SYSVAR_SLOT_HASHES,
19
+ TOKEN_2022_PROGRAM,
20
+ TOKEN_PROGRAM,
21
+ } from '../chain/pda.js';
22
+ import { createTableIx, extendTableIx } from '../chain/lookup.js';
23
+ import { MEMO_PROGRAM } from '../chain/token2022.js';
12
24
  import { createAssociatedTokenAccountIdempotent, syncNative, transferLamports } from '../chain/programs.js';
13
- import { createAccountIx, initializeMint2Ix, MINT_BYTES, mintToIx, setAuthorityIx, transferCheckedIx } from '../chain/token.js';
25
+ import {
26
+ createAccountIx,
27
+ initializeMint2Ix,
28
+ MINT_BYTES,
29
+ mintToIx,
30
+ setAuthorityIx,
31
+ transferCheckedIx,
32
+ } from '../chain/token.js';
14
33
  import { compileMessage, transactionSize } from '../chain/tx.js';
15
34
  import { sha256 } from '../chain/sha256.js';
16
35
  import { utf8 } from '../chain/bytes.js';
17
36
  import * as loot from '../client/index.js';
18
37
  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';
38
+ import {
39
+ ACCOUNT_OVERHEAD,
40
+ boxBuys,
41
+ rent,
42
+ RENT_PER_BYTE,
43
+ boxRolls,
44
+ compressedTree,
45
+ destination,
46
+ effectOf,
47
+ gateOf,
48
+ listOf,
49
+ lotterySplit,
50
+ model,
51
+ MOST_RENT_PER_BYTE,
52
+ paysFromOf,
53
+ outcomeParts,
54
+ perWalletOf,
55
+ placeOf,
56
+ lifeLines,
57
+ prizeLine,
58
+ prizePool,
59
+ prizeRolls,
60
+ prizeToken,
61
+ rarityNumber,
62
+ ruleOf,
63
+ sharesOf,
64
+ spendOf,
65
+ stashNfts,
66
+ toBy,
67
+ toEnum,
68
+ toSeconds,
69
+ toTime,
70
+ toUnits,
71
+ weights,
72
+ wholeCount,
73
+ windowOf,
74
+ } from './model.mjs';
20
75
  import { BUDGET } from './txsize.mjs';
21
76
 
22
77
  const owner = 'owner';
@@ -24,17 +79,27 @@ const owner = 'owner';
24
79
  export const PER_APPROVAL = 12;
25
80
 
26
81
  const amount = (value, t) => toUnits(value ?? 0, t?.decimals ?? 0);
27
- const shares = (split, m) => sharesOf(split).map((s) => ({ to: toEnum(s.to, m), bps: s.bps ?? 0 }));
28
- const rise = (r, t) => (r ? { step: amount(r.step ?? 0, t), growth_bps: Math.round((r.growthPercent ?? 0) * 100) } : null);
29
- 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);
30
- const tokenProgramOf = (t) => (t?.program === 'token-2022' ? TOKEN_2022_PROGRAM : TOKEN_PROGRAM);
82
+ const shares = (split, m) => sharesOf(split).map(s => ({ to: toEnum(s.to, m), bps: s.bps ?? 0 }));
83
+ const rise = (r, t) =>
84
+ r ? { step: amount(r.step ?? 0, t), growth_bps: Math.round((r.growthPercent ?? 0) * 100) } : null;
85
+ const fall = (f, t, now) =>
86
+ f
87
+ ? {
88
+ step: amount(f.step, t),
89
+ every: toSeconds(f.every),
90
+ floor: amount(f.floor ?? 0, t),
91
+ since: BigInt(toTime(f.start, now)),
92
+ shared: f.clock === 'shared',
93
+ }
94
+ : null;
95
+ const tokenProgramOf = t => (t?.program === 'token-2022' ? TOKEN_2022_PROGRAM : TOKEN_PROGRAM);
31
96
 
32
97
  // ---------------------------------------------------------------- spec parts → the program's args
33
98
 
34
- export function itemArgs(it) {
99
+ export function itemArgs(it, m) {
35
100
  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;
101
+ // A collectible is minted into its own `collection`, or the one the setup makes ("@collection" until it exists). Its
102
+ // life is written into each recipe that takes it (`lifeLines`).
38
103
  return {
39
104
  kind,
40
105
  rarity: rarityNumber(it.rarity),
@@ -43,19 +108,73 @@ export function itemArgs(it) {
43
108
  name: it.name,
44
109
  uri: it.uri ?? '',
45
110
  max_supply: it.supply ?? 0,
46
- life,
47
111
  active: it.active !== false,
48
- collection: kind === 'Normal' ? NOBODY : it.collection ?? '@collection',
112
+ collection: kind === 'Normal' ? NOBODY : (it.collection ?? '@collection'),
113
+ // A fresh collectible's traits, each drawn per unit (a table's row, or a number in a range).
114
+ traits:
115
+ kind === 'Normal'
116
+ ? []
117
+ : (it.drawn ?? []).map(t => ({
118
+ key: t.attribute,
119
+ from:
120
+ t.table !== undefined
121
+ ? { Table: [m?.table.get(t.table)?.id ?? 0] }
122
+ : { Range: { min: BigInt(t.min ?? 0), max: BigInt(t.max ?? 0) } },
123
+ })),
49
124
  };
50
125
  }
51
126
 
127
+ /**
128
+ * The bytes of an order of box `b` (`orderBytes`), its spec's names standing in as addresses, with the shares its split
129
+ * stores. `opened`: an open's (`open_box`, `Order::sized`: each draw at its dearest outcome's prizes as they are kept,
130
+ * rows only for the tables its fresh items draw); otherwise a roll made inside another call (a craft, a grant's claim,
131
+ * a mission, a lottery's place, a roll lot: `Order::space_for`, the box's whole table budget a draw).
132
+ */
133
+ export const boxOrderBytes = (b, m, opens = 1, { opened = false } = {}) => {
134
+ const args = boxArgs(b, m);
135
+ const items = opened ? Object.fromEntries(m.items.map(it => [it.id, itemArgs(it, m)])) : undefined;
136
+ return loot.orderBytes(placeholders(args), (args.shares ?? []).length, opens, items);
137
+ };
138
+
139
+ /** The most bytes of rows one table write carries: what a 1,232-byte transaction leaves after the upsert's accounts,
140
+ * its signature and its own bytes. */
141
+ export const TABLE_BATCH_BYTES = 800;
142
+
143
+ /** A spec table's rows as the program takes them ({ weight, left, label }), in batches that fit a transaction each. */
144
+ export function tableBatches(t) {
145
+ const rows = t.rows.map(r => ({
146
+ weight: r.weight,
147
+ left: r.stock ?? 4294967295,
148
+ label: t.kind === 'numbers' ? '' : String(r.label ?? ''),
149
+ }));
150
+ const batches = [[]];
151
+ let bytes = 0;
152
+ for (const r of rows) {
153
+ const size = 12 + new TextEncoder().encode(r.label).length;
154
+ if (bytes + size > TABLE_BATCH_BYTES && batches.at(-1).length) (batches.push([]), (bytes = 0));
155
+ batches.at(-1).push(r);
156
+ bytes += size;
157
+ }
158
+ return batches;
159
+ }
160
+
52
161
  /** The program's `Prize` for one part of a box outcome (`model.outcomeParts`). */
53
162
  export function partPrize(o, m) {
163
+ // An item, or a compressed NFT of it, minted at the settle into the project's tree.
164
+ if (o.gives === 'item' && o.compressed) return { Compressed: { item: m.item.get(o.item)?.id ?? 0, tree: '@tree' } };
54
165
  if (o.gives === 'item') return { Item: [m.item.get(o.item)?.id ?? 0] };
55
166
  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))] } } };
167
+ if (o.gives === 'pay')
168
+ return {
169
+ Tokens: { from: { Pool: [m.pool.get(o.pool)?.index ?? 0] }, amount: amount(o.amount, m.poolToken(o.pool)) },
170
+ };
171
+ if (o.gives === 'mint')
172
+ return {
173
+ Tokens: {
174
+ from: { Mint: [`@mint:${o.currency ?? m.currencyKey}`] },
175
+ amount: amount(o.amount, m.tokens.get(o.currency ?? m.currencyKey)),
176
+ },
177
+ };
59
178
  return prizeArgs(o, m);
60
179
  }
61
180
 
@@ -68,60 +187,95 @@ export function boxArgs(b, m) {
68
187
  rise: rise(b.rise, t),
69
188
  fall: fall(b.fall, t, m.now),
70
189
  stock_odds: !!b.stockOdds,
190
+ when_out: b.whenOut === 'drawLeft' ? 'DrawLeft' : 'StopSales',
71
191
  active: b.active !== false,
72
192
  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] })),
193
+ // What one open draws: each slot so many draws among the outcomes it names (all, when none), no outcome twice
194
+ // when distinct. None is one draw among every outcome.
195
+ slots: (b.slots ?? []).map(s => ({
196
+ count: s.count,
197
+ only: s.from ?? [],
198
+ distinct: Boolean(s.distinct),
199
+ share: Math.round((s.share ?? 0) * 100),
200
+ })),
201
+ // An entry gives its prizes (a bundle of any number), and may burn the payment in place of the split.
202
+ entries: (b.outcomes ?? []).map((o, i) => ({
203
+ prizes: outcomeParts(o).map(part => partPrize(part, m)),
204
+ weight: w[i],
205
+ })),
75
206
  gate: gateOf(b.gate, m),
76
- odds: (b.odds ?? []).map((o) => ({ entry: o.outcome, boost: { rule: ruleOf(o.when, m), effect: effectOf(o.add) } })),
207
+ odds: (b.odds ?? []).map(o => ({ entry: o.outcome, boost: { rule: ruleOf(o.when, m), effect: effectOf(o.add) } })),
77
208
  window: windowOf(b, m.now),
78
209
  per_wallet: perWalletOf(b),
79
210
  // A box that rolls on an NFT: the rule it must pass, and what each outcome does to it.
80
211
  subject: b.rollsOn ? ruleOf(b.rollsOn, m) : null,
81
212
  fates: (b.outcomes ?? []).flatMap((o, i) => {
82
- const shifts = (o.shifts ?? []).map((x) => ({ key: x.attribute, by: toBy(x.by), most: toSeconds(x.upTo ?? 0), from_now: Boolean(x.fromNow) }));
213
+ const shifts = (o.shifts ?? []).map(x => ({
214
+ key: x.attribute,
215
+ by: toBy(x.by),
216
+ most: toSeconds(x.upTo ?? 0),
217
+ from_now: Boolean(x.fromNow),
218
+ }));
83
219
  // An outcome's own `then`, or the box's: an NFT taken as its price goes there whatever the roll gives.
84
220
  const then = thenOf(o.then ?? b.then, m);
85
- return shifts.length || then !== 'Back' ? [{ entry: i, shifts, then }] : [];
221
+ return shifts.length || then !== null ? [{ entry: i, shifts, then }] : [];
86
222
  }),
87
223
  };
88
224
  }
89
225
 
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. */
226
+ /** 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
227
  export const thenOf = (then, m) => {
92
- if (!then || then === 'back') return 'Back';
228
+ if (!then || then === 'back') return null;
93
229
  if (then === 'burn') return 'Burn';
94
230
  if (then.stash) return { Stash: [m?.stash.get(then.stash)?.id ?? 0] };
95
231
  return { Give: [then === 'treasury' ? '@treasury' : then.giveTo] };
96
232
  };
97
233
 
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
234
  export function recipeArgs(r, m) {
104
- const tok = (key) => m.tokens.get(key ?? m.currencyKey);
105
- const inputs = (r.in ?? []).map((l) => {
235
+ const tok = key => m.tokens.get(key ?? m.currencyKey);
236
+ const inputs = (r.in ?? []).map(l => {
106
237
  if (l.takes === 'burn') return { Burn: { item: m.item.get(l.item)?.id ?? 0, count: l.count ?? 1 } };
107
238
  if (l.takes === 'use') return { Use: { item: m.item.get(l.item)?.id ?? 0 } };
108
239
  // A staked one names the farm it sits in (craft.rs): the craft takes it only from its seat there.
109
- 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 } };
240
+ if (l.takes === 'staked')
241
+ return { Staked: { item: m.item.get(l.item)?.id ?? 0, farm: m.farm.get(l.farm)?.id ?? 0 } };
242
+ // A renew keeps the collectible as a use does; its life's raise counts its uses from 0 again (`lifeLines`).
243
+ if (l.takes === 'renew') return { Use: { item: m.item.get(l.item)?.id ?? 0 } };
111
244
  if (l.takes === 'show') return { Show: { collection: l.collection ?? '@collection' } };
112
245
  // A trade-in: one NFT of `collection`, any kind, burned, given on, or put in a stash.
113
246
  if (l.takes === 'trade') return { Trade: { collection: l.collection ?? '@collection', then: spendOf(l.then, m) } };
114
- return { Pay: { mint: `@mint:${l.currency ?? m.currencyKey}`, amount: amount(l.amount, tok(l.currency)), to: toEnum(l.to, m) } };
247
+ return {
248
+ Pay: {
249
+ mint: `@mint:${l.currency ?? m.currencyKey}`,
250
+ amount: amount(l.amount, tok(l.currency)),
251
+ to: toEnum(l.to, m),
252
+ },
253
+ };
115
254
  });
255
+ const life = lifeLines(r, m);
116
256
  // A line is a prize (prize.rs), or a roll on a collectible it keeps (`on`).
117
- 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)] }));
257
+ const outputs = (r.out ?? []).map(l =>
258
+ l.gives === 'roll' && l.on !== undefined
259
+ ? { RollOn: { box_id: m.box.get(l.box)?.id ?? 0, place: l.on } }
260
+ : { Prize: [prizeArgs(l, m)] },
261
+ );
118
262
  return {
119
263
  active: r.active !== false,
120
264
  inputs,
121
265
  outputs,
122
- needs: (r.needs ?? []).map((n) => ({ place: n.place, key: n.key, at_least: n.atLeast })),
123
- 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) })),
266
+ // The recipe's own needs and raises, then what the life of each collectible it takes adds (`lifeLines`).
267
+ needs: [
268
+ ...(r.needs ?? []).map(n => ({ place: n.place, key: n.key, test: { AtLeast: [n.atLeast ?? 0] } })),
269
+ ...life.needs,
270
+ ],
271
+ keeps: (r.keeps ?? []).map(k => ({ key: k.key, from: k.from, to: k.to })),
272
+ raises: [
273
+ ...(r.raises ?? []).map(x => ({
274
+ place: x.place,
275
+ shift: { key: x.key, by: toBy(x.by), most: toSeconds(x.upTo ?? 0), from_now: Boolean(x.fromNow) },
276
+ })),
277
+ ...life.raises,
278
+ ],
125
279
  window: windowOf(r, m.now),
126
280
  per_wallet: perWalletOf(r),
127
281
  };
@@ -129,11 +283,18 @@ export function recipeArgs(r, m) {
129
283
 
130
284
  export function scheduleArgs(s, m) {
131
285
  const every = toSeconds(s.every);
132
- const poolOf = (key) => m.pool.get(key)?.index ?? 0;
286
+ const poolOf = key => m.pool.get(key)?.index ?? 0;
133
287
  let action;
134
288
  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 };
289
+ // Minted into a pool, or into a farm's rewards vault ("to": "farm:<key>"): an emission its stakers share.
290
+ const farm = destination(s.to ?? '').kind === 'farm' ? m.farm.get(destination(s.to).key) : null;
291
+ const t = m.tokens.get(s.currency ?? (farm ? farm.rewardsToken : m.pool.get(s.pool)?.currency));
292
+ action = {
293
+ from: { Mint: [`@mint:${t?.key}`] },
294
+ to: farm ? toEnum(s.to, m) : { Pool: [poolOf(s.pool)] },
295
+ amount: { Fixed: [amount(s.amount, t)] },
296
+ halve_every: s.halveEvery ?? 0,
297
+ };
137
298
  } else if (s.sealed) {
138
299
  // A sealed schedule pays from its own vault, in `currency`.
139
300
  const t = m.tokens.get(s.currency ?? m.currencyKey);
@@ -146,7 +307,13 @@ export function scheduleArgs(s, m) {
146
307
  const how = s.percent !== undefined ? { Bps: [Math.round(s.percent * 100)] } : { Fixed: [amount(s.amount, t)] };
147
308
  action = { from: { Pool: [poolOf(from)] }, to, amount: how, halve_every: s.halveEvery ?? 0 };
148
309
  }
149
- 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 };
310
+ return {
311
+ active: s.active !== false,
312
+ action,
313
+ every: BigInt(every),
314
+ next_at: BigInt(s.start ? toTime(s.start, m.now) : m.now + every),
315
+ max_runs: s.maxRuns ?? 0,
316
+ };
150
317
  }
151
318
 
152
319
  /**
@@ -155,16 +322,18 @@ export function scheduleArgs(s, m) {
155
322
  */
156
323
  export function prizeArgs(p, m) {
157
324
  const line = prizeLine(p);
158
- if (line.gives === 'item' && line.compressed) return { Compressed: { item: m.item.get(line.item)?.id ?? 0, tree: '@tree' } };
325
+ if (line.gives === 'item' && line.compressed)
326
+ return { Compressed: { item: m.item.get(line.item)?.id ?? 0, tree: '@tree' } };
159
327
  if (line.gives === 'item') return { Item: [m.item.get(line.item)?.id ?? 0] };
160
328
  // An NFT a lottery's place holds in the lottery's own stash (a raffle), or one of a project stash.
161
329
  if (line.gives === 'nft' && line.held) return { Held: [line.nft] };
330
+ // An NFT drawn at random from the lottery's own stash (a raffle of many NFTs).
331
+ if (line.gives === 'raffled') return 'Raffled';
162
332
  if (line.gives === 'nft') return { Nft: { stash: m.stash.get(line.stash)?.id ?? 0, nft: line.nft } };
163
333
  if (line.gives === 'roll') return { Roll: [m.box.get(line.box)?.id ?? 0] };
164
334
  const pool = prizePool(line);
165
335
  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))] } } };
336
+ return { Tokens: { from, amount: amount(line.amount, prizeToken(line, m)) } };
168
337
  }
169
338
 
170
339
  /**
@@ -172,14 +341,20 @@ export function prizeArgs(p, m) {
172
341
  * its own amount (whole units of `t`, the grant's token) and round, so one wallet listed under several rounds claims
173
342
  * each once (monthly vesting, a season's airdrop).
174
343
  */
175
- 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)));
344
+ export const listLeaves = (list, t) =>
345
+ list.map(e =>
346
+ typeof e === 'string'
347
+ ? loot.listLeaf(e, null)
348
+ : loot.listLeaf(e.wallet, null, loot.listRound(e.round ?? 0), e.amount !== undefined ? amount(e.amount, t) : 0n),
349
+ );
176
350
 
177
351
  /** A prize line's `Prize`, or null for none. */
178
352
  const prizeOrNull = (p, m) => (prizeLine(p) ? prizeArgs(p, m) : null);
179
353
 
180
354
  /**
181
- * A lottery's args: its own pot in `currency` (the project's by default), ticket money split by `split` (and
182
- * `cutPercent` to the treasury) with the rest into the pot. No `every` draws once, at `firstDraw`.
355
+ * A lottery's args: its money in `currency` (the project's by default), ticket money split by `split` (and
356
+ * `cutPercent` to the treasury) with the rest for the places, which are paid from its pot (`pot`) or from each round's
357
+ * own money, what they leave going to `rest` (`paysFromOf`). No `every` draws once, at `firstDraw`.
183
358
  */
184
359
  export function lotteryArgs(l, m) {
185
360
  const potKey = l.currency ?? m.currencyKey;
@@ -187,25 +362,103 @@ export function lotteryArgs(l, m) {
187
362
  const ticket = l.ticketCurrency ? m.tokens.get(l.ticketCurrency) : potToken;
188
363
  const every = toSeconds(l.every);
189
364
  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',
365
+ // What a round copies at its first purchase.
366
+ terms: {
367
+ price: l.ticketNft ? 0n : amount(l.price, ticket),
368
+ mint: `@mint:${potKey}`,
369
+ // Where the places are paid from: a pot kept from round to round, or each round's own money.
370
+ pays_from: paysFromOf(l, m),
371
+ shares: shares(lotterySplit(l), m),
372
+ // A place takes a share of the pot, a prize for its winner, or both.
373
+ // A run of `times` places alike: "places 2 to 201 each take 1%" is one.
374
+ places: (l.places ?? [])
375
+ .map(placeOf)
376
+ .map(p => ({ bps: Math.round(p.percent * 100), prize: prizeOrNull(p.prize, m), times: p.times })),
377
+ ticket_mint: l.ticketCurrency ? `@mint:${l.ticketCurrency}` : '11111111111111111111111111111111',
378
+ draws_at: BigInt(l.firstDraw ? toTime(l.firstDraw, m.now) : m.now + every),
379
+ // A ticket paid with one NFT that passes the rule, in place of tokens.
380
+ nft_ticket: l.ticketNft ? { rule: ruleOf(l.ticketNft.rule, m), then: spendOf(l.ticketNft.then, m) } : null,
381
+ // No loss: every ticket goes back to its buyer once the round is drawn; the winners take the pot alone.
382
+ returns: Boolean(l.noLoss),
383
+ // A least sale: a round that sold fewer tickets is cancelled at its draw and refunded.
384
+ min_sold: BigInt(l.minSold ?? 0),
385
+ },
196
386
  every: BigInt(every),
197
- draws_at: BigInt(l.firstDraw ? toTime(l.firstDraw, m.now) : m.now + every),
198
387
  gate: gateOf(l.gate, m),
199
388
  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
389
  };
207
390
  }
208
391
 
392
+ /**
393
+ * A lottery round's header as its first purchase sizes it (`LotteryRound::new`: its `head`), and the token accounts a
394
+ * round may open (`accounts`: the payment mint's, the pot's, one for each run whose prize comes from a pool). The
395
+ * 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
396
+ * NFT slot for each place of the lottery's own stash.
397
+ */
398
+ export function roundSize(l, m) {
399
+ const { terms } = placeholders(lotteryArgs(l, m));
400
+ const places = terms.places.reduce((n, p) => n + Number(p.times ?? 1), 0);
401
+ const tokens = terms.places.filter(p => p.prize?.Tokens).length;
402
+ const pools = terms.places.filter(p => p.prize?.Tokens?.from.Pool).length;
403
+ const own = terms.places
404
+ .filter(p => p.prize?.Held || p.prize === 'Raffled' || p.prize?.Raffled !== undefined)
405
+ .reduce((n, p) => n + Number(p.times ?? 1), 0);
406
+ const termsBytes = encodeBorsh(IDL, { defined: { name: 'LotteryTerms' } }, terms).length;
407
+ // The tag; project, lottery, id, incarnation, round, bump, opener, lent, head, status, revision; the terms; fee, sold,
408
+ // to_pot; common; owns, owed, the draw's slots and times, answer and first ticket, the pot's four numbers; paid; prizes (59
409
+ // bytes each); accounts (64 each); nfts; assigned.
410
+ const head =
411
+ 1 +
412
+ 130 +
413
+ termsBytes +
414
+ 17 +
415
+ (4 + 8 * terms.shares.length) +
416
+ 112 +
417
+ (4 + Math.ceil((2 * places) / 8)) +
418
+ (4 + 59 * tokens) +
419
+ (4 + 64 * (2 + pools)) +
420
+ (4 + 32 * own) +
421
+ 4;
422
+ const payment = l.ticketCurrency ?? l.currency ?? m.currencyKey;
423
+ const accounts = (l.ticketNft ? 1 : payment === (l.currency ?? m.currencyKey) ? 1 : 2) + pools;
424
+ return { head, accounts };
425
+ }
426
+
427
+ /** How many rounds of a lottery are open or still paying out at once, and how many purchases a round takes, as the
428
+ * spec expects them (`expect`): by default two rounds of a lottery that draws again (one selling, one paying out) and
429
+ * one of a lottery that draws once; a round's purchases from the scenario's purchases a day over a round, else 100. */
430
+ export function roundsExpected(l, m) {
431
+ const every = toSeconds(l.every ?? 0);
432
+ const perDay = Number(m.scenario?.perDay?.[`lottery:${l.key}`] ?? 0);
433
+ const days =
434
+ every > 0 ? every / 86_400 : l.firstDraw ? Math.max(1, (toTime(l.firstDraw, m.now) - m.now) / 86_400) : 1;
435
+ const rounds = l.expect?.rounds ?? (every > 0 ? 2 : 1);
436
+ const entries = l.expect?.entries ?? (perDay > 0 ? Math.ceil(perDay * days) : 100);
437
+ return { rounds, entries };
438
+ }
439
+
440
+ /**
441
+ * The SOL, in lamports, a lottery holds for its rounds' rent: the spec's `roundRent` (0 turns it off), or by default
442
+ * what its expected rounds take at once (`roundsExpected`), each its header, its token accounts and its expected
443
+ * entries' bytes (`LotteryRound.lent`: in a round the lottery opened, its spare SOL pays each entry while it covers
444
+ * them, so buyers pay no program rent for a ticket), at devnet's and mainnet's rent, rounded up to 0.001 SOL. Short
445
+ * of it (a busier round, a cluster that charges more), the first buyer fronts the header and later buyers pay their
446
+ * own entries. Each round gives back what it took when it closes; the lottery gives the rest back to the owner when it closes.
447
+ */
448
+ export function roundRentOf(l, m) {
449
+ if (l.roundRent !== undefined) return toUnits(l.roundRent, 9);
450
+ const { head, accounts } = roundSize(l, m);
451
+ const { rounds, entries } = roundsExpected(l, m);
452
+ const width = loot.entryWidth(
453
+ lotterySplit(l).some(s => String(s.to).startsWith('chosen')),
454
+ Boolean(l.ticketNft),
455
+ );
456
+ // The header and its entries' list head (6 bytes), each token account, each entry's bytes.
457
+ const one = rent(head + 6) + BigInt(accounts) * rent(165) + BigInt(entries * width) * RENT_PER_BYTE;
458
+ const all = BigInt(rounds) * one;
459
+ return ((all + 999_999n) / 1_000_000n) * 1_000_000n;
460
+ }
461
+
209
462
  /**
210
463
  * The args the authority opens a contest with, `at` (Unix seconds) its opening: its sides, when entries close and the
211
464
  * result is due, its split, its gate (a list of wallets for a duel), the keys that may decide it, the least pot (an
@@ -217,8 +470,16 @@ export function contestArgs(c, m, at = m.now) {
217
470
  // program refuses them at once): with no "entriesClose", an hour before the result is due (halfway for a contest
218
471
  // shorter than two hours); with no "resultDue", an hour after entries close.
219
472
  const deciders = (c.deciders ?? []).length > 0;
220
- const due = c.resultDue === undefined && c.entriesClose !== undefined && deciders ? toSeconds(c.entriesClose) + 3600 : toSeconds(c.resultDue ?? c.entriesClose ?? '1d');
221
- const closes = c.entriesClose !== undefined ? toSeconds(c.entriesClose) : deciders ? due - Math.min(3600, Math.floor(due / 2)) : due;
473
+ const due =
474
+ c.resultDue === undefined && c.entriesClose !== undefined && deciders
475
+ ? toSeconds(c.entriesClose) + 3600
476
+ : toSeconds(c.resultDue ?? c.entriesClose ?? '1d');
477
+ const closes =
478
+ c.entriesClose !== undefined
479
+ ? toSeconds(c.entriesClose)
480
+ : deciders
481
+ ? due - Math.min(3600, Math.floor(due / 2))
482
+ : due;
222
483
  return {
223
484
  sides: c.sides,
224
485
  closes_at: BigInt(at + closes),
@@ -227,62 +488,97 @@ export function contestArgs(c, m, at = m.now) {
227
488
  gate: gateOf(c.gate, m),
228
489
  deciders: c.deciders ?? [],
229
490
  // A contest of NFTs counts its pot in NFTs.
230
- min_pot: c.stakeNft ? wholeCount(c.minPot) ?? 0n : amount(c.minPot ?? 0, t),
491
+ min_pot: c.stakeNft ? (wholeCount(c.minPot) ?? 0n) : amount(c.minPot ?? 0, t),
231
492
  stake: amount(c.stake ?? 0, t),
232
493
  prize: prizeOrNull(c.prize, m),
233
494
  // A token prize set aside whole at the decision and split among the winners by what each put in.
234
495
  prize_split: Boolean(c.prize?.split),
235
- // Each entry stakes one NFT that passes this rule, and the NFTs are the pot.
496
+ // Each entry stakes one NFT that passes this rule, and the NFTs are the pot: each winner shares the losers', at most
497
+ // `losersEach` of them (none: no cap).
236
498
  nft: c.stakeNft ? ruleOf(c.stakeNft, m) : null,
499
+ cap: c.stakeNft && c.losersEach ? c.losersEach : null,
237
500
  };
238
501
  }
239
502
 
240
503
  export function farmArgs(f, m) {
241
- const tiers = (f.tiers ?? [{ lock: 0, multiplier: 1 }]).map((t) => ({
504
+ const tiers = (f.tiers ?? [{ lock: 0, multiplier: 1 }]).map(t => ({
242
505
  lock: toSeconds(t.lock),
243
506
  bps: Math.round((t.multiplier ?? 1) * 10_000),
244
507
  prize: prizeOrNull(t.prize, m),
245
- roll: t.roll ? m.box.get(t.roll)?.id ?? 0 : null,
508
+ roll: t.roll ? (m.box.get(t.roll)?.id ?? 0) : null,
246
509
  // A token seat may leave before this tier's lock by leaving this share of what it takes out.
247
510
  exit_bps: Math.round((t.exitFee ?? 0) * 100),
511
+ // How long a seat of this tier earns and grows from when its lock started (0: no end): a term.
512
+ earns_for: t.earnsFor ? toSeconds(t.earnsFor) : 0,
248
513
  }));
249
514
  // Every rule of its gate; with none, a farm of one item's collectibles takes that item.
250
515
  const named = listOf(f.gate);
251
- const gates = f.stakes !== 'collectibles' ? [] : (named.length ? named : f.item ? [{ item: f.item, attribute: 'item_id' }] : []).map((r) => ruleOf(r, m));
516
+ const gates =
517
+ f.stakes !== 'collectibles'
518
+ ? []
519
+ : (named.length ? named : f.item ? [{ item: f.item, attribute: 'item_id' }] : []).map(r => ruleOf(r, m));
252
520
  // 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);
521
+ const rewards = m.tokens.get(f.rewardsToken);
254
522
  const staked = f.stakes === 'collectibles' ? null : m.tokens.get(f.stakes);
255
523
  const perDay = f.rate ? amount(f.rate.perDay, rewards) : 0n;
524
+ // A shared rate is the whole farm's a day, shared by weight; any other is for each unit of weight.
525
+ const shared = Boolean(f.rate?.shared);
256
526
  return {
257
527
  custody: { freeze: 'Freeze', watch: 'Watch' }[f.custody] ?? 'Escrow',
258
- rewards_pool: m.pool.get(f.rewards)?.index ?? 0,
259
528
  tiers,
260
- boosts: (f.boosts ?? []).map((b) => ({ rule: ruleOf(b.when, m), effect: effectOf(b.add) })),
529
+ boosts: (f.boosts ?? []).map(b => ({ rule: ruleOf(b.when, m), effect: effectOf(b.add) })),
261
530
  max_boost_bps: Math.round((f.maxBoostPercent ?? 0) * 100),
262
531
  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) })),
532
+ rate: f.rate
533
+ ? {
534
+ per_day: staked && !shared ? (perDay * 1_000_000n) / 10n ** BigInt(staked.decimals ?? 0) : perDay,
535
+ mints: Boolean(f.rate.minted),
536
+ shared,
537
+ halve_every: f.rate.halveEvery ? toSeconds(f.rate.halveEvery) : 0,
538
+ }
539
+ : null,
540
+ growth: listOf(f.growth).map(g => ({
541
+ key: g.attribute ?? 'xp',
542
+ per_day: g.perDay,
543
+ level: (g.levels ?? []).length ? (g.level ?? 'level') : '',
544
+ steps: g.levels ?? [],
545
+ streak: Boolean(g.streak),
546
+ profile: false,
547
+ at_least: 0n,
548
+ })),
265
549
  // A farm that runs for a time ends that long after it goes live.
266
550
  ends_at: f.runs ? BigInt(m.now + toSeconds(f.runs)) : 0n,
267
551
  // The times it keeps on what is staked: its lock, and an upkeep.
268
552
  lock_key: f.lockAttribute ?? '',
269
553
  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.
554
+ // Where an early exit's fee goes.
271
555
  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
556
  };
275
557
  }
276
558
 
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] });
559
+ /** A fee's sink (`Sink`): "burn", "rewards" (the farm's own vault), or "pool:<key>", one of the project's pools. */
560
+ export const sinkOf = (to, m) =>
561
+ to === 'burn'
562
+ ? 'Burn'
563
+ : to === 'rewards'
564
+ ? 'Rewards'
565
+ : { Pool: [m.pool.get(String(to).replace(/^pool:/, ''))?.index ?? 0] };
566
+
567
+ /** Whether a spec's farm keeps a record of when its seats stop earning (`loot.farmKeepsEnds`): a tier's term or an upkeep,
568
+ * and it pays something. Each different time waiting costs whoever writes it `loot.END_BYTES` of rent. */
569
+ export const farmEnds = f =>
570
+ ((f.tiers ?? []).some(t => t.earnsFor) || Boolean(f.upkeepAttribute)) && (!f.rate || Number(f.rate.perDay) > 0);
279
571
 
280
572
  /** An auction's args: its lot, reserve, least raise, times and split. "start" is when it goes live unless it says. */
281
573
  export function auctionArgs(a, m) {
282
574
  const t = m.priceToken(a);
283
575
  const startsAt = toTime(a.start, m.now);
284
576
  return {
285
- 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] },
577
+ lot: a.lot?.asset
578
+ ? { Asset: [a.lot.asset] }
579
+ : a.lot?.roll
580
+ ? { Roll: [rollOf(a.lot.roll, m)] }
581
+ : { Item: [m.item.get(a.lot?.item)?.id ?? 0] },
286
582
  // With NFT bids, the reserve counts NFTs.
287
583
  reserve: a.bidNfts ? BigInt(a.reserve ?? 1) : amount(a.reserve ?? 0, t),
288
584
  min_raise_bps: Math.round((a.minRaise ?? 5) * 100),
@@ -298,7 +594,7 @@ export function auctionArgs(a, m) {
298
594
 
299
595
  const NOBODY = '11111111111111111111111111111111';
300
596
  /** The program id of the box a prize rolls (a box key), or null for none. */
301
- const rollOf = (box, m) => (box ? m.box.get(box)?.id ?? 0 : null);
597
+ const rollOf = (box, m) => (box ? (m.box.get(box)?.id ?? 0) : null);
302
598
 
303
599
  export function grantArgs(g, m) {
304
600
  const gives = prizeArgs(g.prize ?? g.gives, m);
@@ -309,20 +605,24 @@ export function grantArgs(g, m) {
309
605
  return {
310
606
  gives,
311
607
  vouchers,
312
- list_root: list.length ? loot.merkleRoot(listLeaves(list, prizeToken(prizeLine(g.prize ?? g.gives), m))) : new Uint8Array(32),
313
- key_root: keys.length ? loot.merkleRoot(keys.map((k) => loot.keyLeaf(k))) : new Uint8Array(32),
608
+ list_root: list.length
609
+ ? loot.merkleRoot(listLeaves(list, prizeToken(prizeLine(g.prize ?? g.gives), m)))
610
+ : new Uint8Array(32),
611
+ key_root: keys.length ? loot.merkleRoot(keys.map(k => loot.keyLeaf(k))) : new Uint8Array(32),
314
612
  // A grant that names no voucher key and no list is one any wallet may claim, within its limits.
315
613
  open: !(g.vouchers ?? []).length && !list.length && !keys.length,
316
614
  limits: {
317
615
  wallet: perWalletOf(l),
318
616
  per_person: l.perPerson ?? 0,
319
617
  per_key: l.perKey ?? 0,
320
- per_day: l.perDay ?? 0,
321
- total: l.total ?? 0,
618
+ // Every wallet's claims together: in all, and a day (a window of 86,400 seconds, from the unix epoch).
619
+ all: { total: l.total ?? 0, per_window: l.perDay ?? 0, window: l.perDay ? 86_400 : 0 },
322
620
  window: windowOf({ opens: l.opens, closes: l.until }, m.now),
323
621
  },
324
622
  active: g.active !== false,
325
623
  gate: gateOf(g.gate, m),
624
+ // Its records in 2^bits lists, the fewest that keep each under ~100,000 over its life (wallets × rounds).
625
+ shard_bits: loot.shardBitsFor(g.wallets ?? list.length ?? 0, g.rounds ?? 1),
326
626
  };
327
627
  }
328
628
 
@@ -330,7 +630,8 @@ export function grantArgs(g, m) {
330
630
 
331
631
  /**
332
632
  * 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.
633
+ * wallet's token account for it, "@pot:<lottery id>:<pot id>" with a lottery's pot, "@farm:<id>" with a farm's address and "@rewards:<id>"
634
+ * with a farm's rewards vault.
334
635
  */
335
636
  function resolve(value, ctx) {
336
637
  if (value === '@treasury') {
@@ -355,9 +656,14 @@ function resolve(value, ctx) {
355
656
  if (!ctx.project) throw new Error('No project yet for a stake gate’s farm.');
356
657
  return loot.pda.farm(ctx.project, Number(value.slice(6)));
357
658
  }
659
+ if (typeof value === 'string' && value.startsWith('@rewards:')) {
660
+ if (!ctx.project) throw new Error('No project yet for a farm’s rewards vault.');
661
+ return loot.pda.rewards(loot.pda.farm(ctx.project, Number(value.slice(9))));
662
+ }
358
663
  if (typeof value === 'string' && value.startsWith('@pot:')) {
359
664
  if (!ctx.project) throw new Error('No project yet for a lottery’s pot.');
360
- return loot.pda.pot(loot.pda.lottery(ctx.project, Number(value.slice(5))));
665
+ const [, lottery, pot] = value.split(':');
666
+ return loot.pda.lotteryPot(loot.pda.lottery(ctx.project, Number(lottery)), Number(pot));
361
667
  }
362
668
  if (typeof value === 'string' && value.startsWith('@ata:')) {
363
669
  const [, wallet, key, program] = value.split(':');
@@ -365,7 +671,7 @@ function resolve(value, ctx) {
365
671
  if (!mint) throw new Error(`No mint yet for the token “${key}”.`);
366
672
  return loot.ata(wallet, mint, program === 'token-2022' ? TOKEN_2022_PROGRAM : TOKEN_PROGRAM);
367
673
  }
368
- if (Array.isArray(value)) return value.map((v) => resolve(v, ctx));
674
+ if (Array.isArray(value)) return value.map(v => resolve(v, ctx));
369
675
  if (value instanceof Uint8Array || typeof value !== 'object' || value === null) return value;
370
676
  return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, resolve(v, ctx)]));
371
677
  }
@@ -375,14 +681,14 @@ function resolve(value, ctx) {
375
681
  const encodedSize = loot.accountBytes;
376
682
  /** Bubblegum's config of a tree. */
377
683
  const TREE_CONFIG_BYTES = 96;
378
- const placeholderKey = (name) => encodeBase58(sha256(utf8(`loot-plan:${name}`)));
684
+ const placeholderKey = name => encodeBase58(sha256(utf8(`loot-plan:${name}`)));
379
685
 
380
686
  /**
381
687
  * 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
688
  * farms, auctions, then the seeds and the grants' funds. `only` limits it to these part ids (a change to a live economy).
383
689
  *
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
690
+ * A farm opens before its seed lands in its rewards vault, and anyone may stake in between. That is safe: a sharing
691
+ * farm credits nothing at once, and releases what lands in its vault over its longest lock (a day at least) to the
386
692
  * seats there as it is released, its clock stopped while nobody is in. A seed that lands
387
693
  * before the first stake or after it is released the same way from that stake on, so seeding first would change nothing.
388
694
  */
@@ -390,15 +696,15 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
390
696
  const m = model(economy, { now });
391
697
  const live = madeSoFar(given);
392
698
  const steps = [];
393
- const want = (id) => !only || only.has(id);
699
+ const want = id => !only || only.has(id);
394
700
  const landed = new Set(live?.landed ?? []);
395
- const add = (step) => want(step.part) && !landed.has(step.id) && steps.push({ creates: [], signers: [owner], ...step });
396
- const project = (ctx) => ctx.project;
701
+ const add = step => want(step.part) && !landed.has(step.id) && steps.push({ creates: [], signers: [owner], ...step });
702
+ const project = ctx => ctx.project;
397
703
  const fresh = !live?.project;
398
704
 
399
705
  // Tokens the project mints: a classic mint with the owner as its authority, handed to the project right after.
400
706
  for (const t of m.tokens.values()) {
401
- if (t.source !== 'game' || live?.mints?.[t.key] || t.mint && !t.mint.startsWith('@')) continue;
707
+ if (t.source !== 'game' || live?.mints?.[t.key] || (t.mint && !t.mint.startsWith('@'))) continue;
402
708
  const key = `new:mint/${t.key}`;
403
709
  if (t.fixedSupply) {
404
710
  const supply = amount(t.supply, t);
@@ -407,12 +713,26 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
407
713
  part: `token:${t.key}`,
408
714
  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`,
409
715
  signers: [owner, key],
410
- creates: [{ what: `the mint of ${t.symbol}`, bytes: MINT_BYTES }, { what: `your ${t.symbol} account`, bytes: 165 }],
411
- build: (ctx) => [
412
- createAccountIx({ from: ctx.owner, address: ctx.keys[key], lamports: ctx.rent(MINT_BYTES), space: MINT_BYTES, owner: TOKEN_PROGRAM }),
716
+ creates: [
717
+ { what: `the mint of ${t.symbol}`, bytes: MINT_BYTES },
718
+ { what: `your ${t.symbol} account`, bytes: 165 },
719
+ ],
720
+ build: ctx => [
721
+ createAccountIx({
722
+ from: ctx.owner,
723
+ address: ctx.keys[key],
724
+ lamports: ctx.rent(MINT_BYTES),
725
+ space: MINT_BYTES,
726
+ owner: TOKEN_PROGRAM,
727
+ }),
413
728
  initializeMint2Ix({ mint: ctx.keys[key], decimals: t.decimals, authority: ctx.owner }),
414
729
  createAssociatedTokenAccountIdempotent(ctx.owner, ctx.owner, ctx.keys[key]),
415
- mintToIx({ mint: ctx.keys[key], to: loot.ata(ctx.owner, ctx.keys[key]), authority: ctx.owner, amount: supply }),
730
+ mintToIx({
731
+ mint: ctx.keys[key],
732
+ to: loot.ata(ctx.owner, ctx.keys[key]),
733
+ authority: ctx.owner,
734
+ amount: supply,
735
+ }),
416
736
  setAuthorityIx({ account: ctx.keys[key], current: ctx.owner, newAuthority: null }),
417
737
  ],
418
738
  });
@@ -424,8 +744,14 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
424
744
  says: `Make the token ${t.symbol} (${t.decimals} decimals), then hand its minting to the project`,
425
745
  signers: [owner, key],
426
746
  creates: [{ what: `the mint of ${t.symbol}`, bytes: MINT_BYTES }],
427
- build: (ctx) => [
428
- createAccountIx({ from: ctx.owner, address: ctx.keys[key], lamports: ctx.rent(MINT_BYTES), space: MINT_BYTES, owner: TOKEN_PROGRAM }),
747
+ build: ctx => [
748
+ createAccountIx({
749
+ from: ctx.owner,
750
+ address: ctx.keys[key],
751
+ lamports: ctx.rent(MINT_BYTES),
752
+ space: MINT_BYTES,
753
+ owner: TOKEN_PROGRAM,
754
+ }),
429
755
  initializeMint2Ix({ mint: ctx.keys[key], decimals: t.decimals, authority: ctx.owner }),
430
756
  setAuthorityIx({ account: ctx.keys[key], current: ctx.owner, newAuthority: ctx.project }),
431
757
  ],
@@ -443,20 +769,47 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
443
769
  part: 'project',
444
770
  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' : ''}`,
445
771
  signers: [owner, 'new:collection'],
446
- creates: [{ what: 'the collection', bytes: 1 + 32 + 4 + c.name.length + 4 + (c.uri ?? '').length + 140 + (tree ? 3 : 0), approx: true }],
447
- 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) })],
772
+ creates: [
773
+ {
774
+ what: 'the collection',
775
+ bytes: 1 + 32 + 4 + c.name.length + 4 + (c.uri ?? '').length + 140 + (tree ? 3 : 0),
776
+ approx: true,
777
+ },
778
+ ],
779
+ build: ctx => [
780
+ createCollectionInstruction({
781
+ collection: ctx.keys['new:collection'],
782
+ creator: ctx.owner,
783
+ project: ctx.project,
784
+ name: c.name,
785
+ uri: c.uri ?? '',
786
+ royaltyBps: Math.round((c.royaltyPercent ?? 5) * 100),
787
+ compressed: Boolean(tree),
788
+ }),
789
+ ],
448
790
  });
449
791
  // The treasury is a wallet: the remainder of every split goes to its account of that mint, which the program
450
792
  // opens when it is not there. The project's vaults, and every account a share pays, open the same way when first
451
793
  // paid, so the setup opens none of them.
452
- const collections = (ctx) => [...(ctx.collection ? [ctx.collection] : []), ...m.collections];
794
+ const collections = ctx => [...(ctx.collection ? [ctx.collection] : []), ...m.collections];
453
795
  const count = (c ? 1 : 0) + m.collections.length;
454
796
  add({
455
797
  id: 'project',
456
798
  part: 'project',
457
799
  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`,
458
- creates: [{ what: 'the project', bytes: loot.projectBytes(count) }, { what: 'the project’s fee box', bytes: 0 }],
459
- build: (ctx) => [loot.createProjectIx({ owner: ctx.owner, id: m.projectNumber, collections: collections(ctx), treasury: ctx.treasury, authority: ctx.authority })],
800
+ creates: [
801
+ { what: 'the project', bytes: loot.projectBytes(count) },
802
+ { what: 'the project’s fee box', bytes: 0 },
803
+ ],
804
+ build: ctx => [
805
+ loot.createProjectIx({
806
+ owner: ctx.owner,
807
+ id: m.projectNumber,
808
+ collections: collections(ctx),
809
+ treasury: ctx.treasury,
810
+ authority: ctx.authority,
811
+ }),
812
+ ],
460
813
  });
461
814
  }
462
815
 
@@ -467,47 +820,222 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
467
820
  part: 'tree',
468
821
  says: `Open the project’s tree of compressed NFTs, for ${(2 ** tree.depth).toLocaleString('en-US')} of them in all`,
469
822
  signers: [owner, 'new:tree'],
470
- creates: [{ what: 'the tree of compressed NFTs', bytes: tree.bytes }, { what: 'the tree’s config (Bubblegum’s)', bytes: TREE_CONFIG_BYTES }],
471
- build: (ctx) => [
472
- createAccountIx({ from: ctx.owner, address: ctx.keys['new:tree'], lamports: ctx.rent(tree.bytes), space: tree.bytes, owner: loot.COMPRESSION_PROGRAM }),
473
- loot.openTreeIx({ signer: ctx.owner, project: project(ctx), tree: ctx.keys['new:tree'], maxDepth: tree.depth, maxBufferSize: tree.buffer }),
823
+ creates: [
824
+ { what: 'the tree of compressed NFTs', bytes: tree.bytes },
825
+ { what: 'the tree’s config (Bubblegum’s)', bytes: TREE_CONFIG_BYTES },
826
+ ],
827
+ build: ctx => [
828
+ createAccountIx({
829
+ from: ctx.owner,
830
+ address: ctx.keys['new:tree'],
831
+ lamports: ctx.rent(tree.bytes),
832
+ space: tree.bytes,
833
+ owner: loot.COMPRESSION_PROGRAM,
834
+ }),
835
+ loot.openTreeIx({
836
+ signer: ctx.owner,
837
+ project: project(ctx),
838
+ tree: ctx.keys['new:tree'],
839
+ maxDepth: tree.depth,
840
+ maxBufferSize: tree.buffer,
841
+ }),
474
842
  ],
475
843
  });
476
844
 
845
+ // The token accounts players' purchases would otherwise open (the owner pays their rent now, about 0.002 SOL each,
846
+ // so no player's purchase pays another wallet's): the treasury's of each coin the economy takes, the project's vault
847
+ // of each coin a box or an auction holds, and each fixed wallet's a split pays.
848
+ const accounts = goLiveAccounts(m);
849
+ if (fresh && accounts.length)
850
+ add({
851
+ id: 'accounts',
852
+ part: 'project',
853
+ 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`,
854
+ creates: accounts.map(x => ({
855
+ what: `${x.who}’s account of ${m.tokens.get(x.token)?.symbol ?? x.token}`,
856
+ bytes: 165,
857
+ })),
858
+ build: ctx =>
859
+ accounts.map(x =>
860
+ createAssociatedTokenAccountIdempotent(
861
+ ctx.owner,
862
+ x.who === 'the project' ? ctx.project : x.who === 'the treasury' ? ctx.treasury : x.who,
863
+ ctx.mints[x.token],
864
+ programsOf(ctx)[ctx.mints[x.token]] ?? TOKEN_PROGRAM,
865
+ ),
866
+ ),
867
+ });
868
+
869
+ // The project's standing lookup table (the owner its authority): the addresses its transactions name most, so a
870
+ // transaction too big for a legacy one goes as v0 with it in one approval, never with a table of its own. Made with
871
+ // the first of its addresses, then extended a group a step.
872
+ if (fresh) {
873
+ const named = standingAddresses(m);
874
+ groupsOf(named, TABLE_GROUP).forEach((group, i) =>
875
+ add({
876
+ id: i ? `table:${i}` : 'table',
877
+ part: 'project',
878
+ says: i
879
+ ? `Add ${group.length} more addresses to the project’s lookup table`
880
+ : `Make the project’s lookup table of the ${named.length} addresses its transactions name most, so a large one takes one approval`,
881
+ creates: i
882
+ ? [{ what: `room for ${group.length} addresses in the lookup table`, bytes: 32 * group.length, grows: true }]
883
+ : [{ what: 'the project’s lookup table', bytes: 56 + 32 * group.length }],
884
+ build: ctx => {
885
+ // With no recent slot known (a plan built offline), there is no table to make.
886
+ if (ctx.slot == null) return [];
887
+ const { address: table, instruction } = createTableIx({ authority: ctx.owner, slot: ctx.slot });
888
+ return [
889
+ ...(i ? [] : [instruction]),
890
+ extendTableIx({ table, authority: ctx.owner, addresses: group.map(at => at(ctx)).filter(Boolean) }),
891
+ ];
892
+ },
893
+ }),
894
+ );
895
+ }
896
+
477
897
  for (const p of m.pools) {
478
898
  const t = m.tokens.get(p.currency);
479
- 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] })] });
899
+ add({
900
+ id: `pool:${p.key}`,
901
+ part: `pool:${p.key}`,
902
+ says: `Open the pool “${p.key}” for ${t?.symbol}`,
903
+ creates: [{ what: `the pool “${p.key}”`, bytes: 165 }],
904
+ build: ctx => [
905
+ loot.openPoolIx({ owner: ctx.owner, project: project(ctx), index: p.index, mint: ctx.mints[p.currency] }),
906
+ ],
907
+ });
480
908
  }
481
909
  const record = (part, def, says, kind, what, options = () => ({})) => {
482
910
  const [variant] = Object.keys(def);
483
911
  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) })] });
912
+ // A lottery's write opens the pot it pays from: a token account as well as the record (each round holds its own
913
+ // money). One paid from each round's own money opens none.
914
+ const pot = def.Lottery?.args.terms.pays_from.Pot
915
+ ? [{ what: `${what}’s pot`, bytes: 165 }]
916
+ : def.Schedule?.args.action.from.Vest
917
+ ? [{ what: `${what}’s sealed vault`, bytes: 165 }]
918
+ : [];
919
+ add({
920
+ id: `${part}:${kind}`,
921
+ part,
922
+ says,
923
+ creates: [{ what, bytes }, ...pot],
924
+ build: ctx => [
925
+ loot.upsertIx({ owner: ctx.owner, project: project(ctx), def: resolve(def, ctx), ...options(ctx) }),
926
+ ],
927
+ });
487
928
  };
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}”`);
929
+ // Each table, a batch of rows a transaction (`TABLE_BATCH_BYTES` of rows at most), sealed with its last batch.
930
+ for (const t of m.tables) {
931
+ const batches = tableBatches(t);
932
+ const len = t.kind === 'numbers' ? 8 : 40;
933
+ batches.forEach((rows, i) => {
934
+ const seal = i === batches.length - 1;
935
+ add({
936
+ id: `table:${t.key}:${i}`,
937
+ part: `table:${t.key}`,
938
+ 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' : ''}`,
939
+ creates:
940
+ i === 0
941
+ ? [
942
+ {
943
+ what: `the table “${t.key}”, ${t.rows.length} rows`,
944
+ bytes: loot.tableRowAt(len, t.rows.length - 1) + len,
945
+ },
946
+ ]
947
+ : [],
948
+ build: ctx => [
949
+ loot.upsertIx({
950
+ owner: ctx.owner,
951
+ project: project(ctx),
952
+ def: { Table: { id: t.id, args: { payload: t.kind === 'numbers' ? 'Number' : 'Label', rows, seal } } },
953
+ }),
954
+ ],
955
+ });
956
+ });
957
+ }
958
+ for (const it of m.items)
959
+ record(
960
+ `item:${it.key}`,
961
+ { Item: { id: it.id, args: itemArgs(it, m) } },
962
+ `${it.active === false ? 'Switch off' : 'Write'} the item “${it.name}”`,
963
+ 'Item',
964
+ `the item “${it.name}”`,
965
+ );
489
966
  // A box's line names its odds, so the owner sees what they approve (above all when a live box's odds change).
490
- const odds = (b) => {
967
+ const odds = b => {
491
968
  const ws = weights(b);
492
969
  const total = ws.reduce((n, w) => n + w, 0) || 1;
493
- 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');
970
+ const part = o =>
971
+ o.gives === 'item'
972
+ ? (m.item.get(o.item)?.name ?? o.item)
973
+ : o.gives === 'stash'
974
+ ? `an NFT of “${m.stash.get(o.stash)?.name ?? o.stash}”`
975
+ : o.gives === 'pay'
976
+ ? `${o.amount} ${m.poolToken(o.pool)?.symbol ?? ''}`.trim()
977
+ : o.gives === 'share'
978
+ ? `${o.percent}% of “${o.pool}”`
979
+ : o.gives === 'mint'
980
+ ? `${o.amount} ${m.tokens.get(o.currency ?? m.currencyKey)?.symbol ?? ''}`.trim()
981
+ : o.gives === 'nft'
982
+ ? `the NFT ${String(o.nft).slice(0, 4)}…`
983
+ : o.gives === 'roll'
984
+ ? `a roll of “${m.box.get(o.box)?.name ?? o.box}”`
985
+ : '';
986
+ const what = o => outcomeParts(o).map(part).join(' + ') || 'nothing';
495
987
  return (b.outcomes ?? []).map((o, i) => `${what(o)} ${Math.round((ws[i] / total) * 10_000) / 100}%`).join(', ');
496
988
  };
497
- const wasLive = new Set((live?.published?.boxes ?? []).map((b) => b.key));
498
- for (const b of m.allBoxes) {
499
- 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
- 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}”`);
989
+ const wasLive = new Set((live?.published?.boxes ?? []).map(b => b.key));
990
+ // A box is written before any box that names it to roll (one may name itself): in that order.
991
+ const boxOrder = [];
992
+ const visit = (b, path = new Set()) => {
993
+ if (boxOrder.includes(b) || path.has(b)) return;
994
+ path.add(b);
995
+ for (const id of loot.defRolls({ Box: { args: boxArgs(b, m) } })) {
996
+ const named = m.allBoxes.find(x => x.id === id);
997
+ if (named && named !== b) visit(named, path);
998
+ }
999
+ boxOrder.push(b);
1000
+ };
1001
+ for (const b of m.allBoxes) visit(b);
1002
+ for (const b of boxOrder) {
1003
+ const says = b.shopOf
1004
+ ? `Put “${b.name}” in the shop at ${b.price} ${m.priceToken(b)?.symbol}`
1005
+ : `${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)}`}`;
1006
+ record(
1007
+ b.shopOf ? `item:${b.shopOf}` : `box:${b.key}`,
1008
+ { Box: { id: b.id, args: boxArgs(b, m) } },
1009
+ says,
1010
+ 'LootBox',
1011
+ b.shopOf ? `the shop listing of “${b.name}”` : `the box “${b.name}”`,
1012
+ );
501
1013
  }
502
- 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}”`);
1014
+ for (const r of m.recipes)
1015
+ record(
1016
+ `recipe:${r.key}`,
1017
+ { Recipe: { id: r.id, args: recipeArgs(r, m) } },
1018
+ `Write the recipe “${r.name}”`,
1019
+ 'Recipe',
1020
+ `the recipe “${r.name}”`,
1021
+ );
503
1022
  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) } : {}));
1023
+ record(
1024
+ `schedule:${s.key}`,
1025
+ { Schedule: { id: s.id, args: scheduleArgs(s, m) } },
1026
+ `${s.active === false ? 'Stop' : 'Start'} the ${s.sealed ? 'sealed ' : ''}schedule “${s.name ?? s.key}”`,
1027
+ 'Schedule',
1028
+ `the schedule “${s.name ?? s.key}”`,
1029
+ );
508
1030
  // A sealed schedule's vault is funded once, at go-live: from the owner's wallet, minted, or from a pool.
509
1031
  const id = `fund:${s.key}`;
510
- if (!s.sealed || !Number(s.fund ?? 0) || (live?.landed ?? []).includes(id) || live?.published?.schedules?.some((x) => x.key === s.key)) continue;
1032
+ if (
1033
+ !s.sealed ||
1034
+ !Number(s.fund ?? 0) ||
1035
+ (live?.landed ?? []).includes(id) ||
1036
+ live?.published?.schedules?.some(x => x.key === s.key)
1037
+ )
1038
+ continue;
511
1039
  const t = m.tokens.get(s.currency ?? m.currencyKey);
512
1040
  const from = s.fundFrom ?? 'wallet';
513
1041
  add({
@@ -516,51 +1044,126 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
516
1044
  signers: from === 'wallet' || m.authority === 'owner' ? [owner] : ['authority'],
517
1045
  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`,
518
1046
  creates: [],
519
- build: (ctx) => {
1047
+ build: ctx => {
520
1048
  const mint = ctx.mints[t.key];
521
1049
  const vest = loot.pda.vest(loot.pda.schedule(project(ctx), s.id));
522
1050
  const value = amount(s.fund, t);
523
1051
  if (from === 'wallet') {
524
1052
  const tokenProgram = programsOf(ctx)[mint] ?? TOKEN_PROGRAM;
525
1053
  // SOL is wrapped first, as a seed is: the owner's wallet holds it as plain SOL, the vault takes wrapped SOL.
526
- 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))] : [];
527
- return [...wrap, transferCheckedIx({ from: loot.ata(ctx.owner, mint, tokenProgram), mint, to: vest, owner: ctx.owner, amount: value, decimals: t.decimals ?? 0, tokenProgram })];
1054
+ const wrap = t.wsol
1055
+ ? [
1056
+ createAssociatedTokenAccountIdempotent(ctx.owner, ctx.owner, NATIVE_MINT),
1057
+ transferLamports(ctx.owner, loot.ata(ctx.owner, NATIVE_MINT), value),
1058
+ syncNative(loot.ata(ctx.owner, NATIVE_MINT)),
1059
+ ]
1060
+ : [];
1061
+ return [
1062
+ ...wrap,
1063
+ transferCheckedIx({
1064
+ from: loot.ata(ctx.owner, mint, tokenProgram),
1065
+ mint,
1066
+ to: vest,
1067
+ owner: ctx.owner,
1068
+ amount: value,
1069
+ decimals: t.decimals ?? 0,
1070
+ tokenProgram,
1071
+ }),
1072
+ ];
528
1073
  }
529
1074
  const source = from === 'mint' ? { Mint: [mint] } : { Pool: [m.pool.get(from.slice(5))?.index ?? 0] };
530
- return [loot.moveTokensIx({ authority: ctx.authority, project: project(ctx), mint, from: source, to: { Account: [vest] }, amount: { Fixed: [value] }, programs: programsOf(ctx) })];
1075
+ return [
1076
+ loot.moveTokensIx({
1077
+ authority: ctx.authority,
1078
+ project: project(ctx),
1079
+ mint,
1080
+ from: source,
1081
+ to: { Account: [vest] },
1082
+ amount: { Fixed: [value] },
1083
+ programs: programsOf(ctx),
1084
+ }),
1085
+ ];
531
1086
  },
532
1087
  });
533
1088
  }
534
1089
  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;
1090
+ // A raffle's NFTs go into the lottery's own stash first, several a call: the lottery is written naming NFTs already
1091
+ // there, or drawing from as many as its places.
1092
+ const raffled = [
1093
+ ...(l.places ?? [])
1094
+ .map(placeOf)
1095
+ .filter(p => p.prize?.held)
1096
+ .map(p => ({ nft: p.prize.nft, collection: p.prize.collection })),
1097
+ ...(l.nfts ?? []).map(n => (typeof n === 'string' ? { nft: n } : n)),
1098
+ ];
1099
+ const opened = live?.published?.lotteries?.some(x => x.key === l.key);
1100
+ groupsOf(
1101
+ raffled.filter(n => !(live?.landed ?? []).includes(`stash:lottery-${l.key}:${n.nft}`)),
1102
+ STASH_GROUP,
1103
+ ).forEach((group, i) => {
1104
+ if (opened) return;
539
1105
  add({
540
- id,
1106
+ id: `stash:lottery-${l.key}:${group.map(n => n.nft).join(',')}`,
541
1107
  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 })],
1108
+ 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`,
1109
+ creates: i === 0 ? [{ what: `the lottery “${l.name}”’s stash`, bytes: stashBytes(raffled.length) }] : [],
1110
+ build: ctx => [
1111
+ loot.stashIx({
1112
+ signer: ctx.owner,
1113
+ project: project(ctx),
1114
+ lottery: l.id,
1115
+ asset: group[0].nft,
1116
+ collection: group[0].collection ?? ctx.collection,
1117
+ more: group.slice(1).map(n => ({ asset: n.nft, collection: n.collection ?? ctx.collection })),
1118
+ }),
1119
+ ],
545
1120
  });
546
- }
547
- 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}”`);
1121
+ });
1122
+ record(
1123
+ `lottery:${l.key}`,
1124
+ { Lottery: { id: l.id, args: lotteryArgs(l, m) } },
1125
+ `Start the ${l.raffle ? 'raffle' : 'lottery'} “${l.name}”`,
1126
+ 'Lottery',
1127
+ `the lottery “${l.name}”`,
1128
+ );
548
1129
  }
549
- 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}”`);
1130
+ for (const g of m.grants)
1131
+ record(
1132
+ `grant:${g.key}`,
1133
+ { Grant: { id: g.id, args: grantArgs(g, m) } },
1134
+ `Write the grant “${g.name}”`,
1135
+ 'Grant',
1136
+ `the grant “${g.name}”`,
1137
+ );
1138
+ /** The boxes a farm's tiers roll, tier by tier (its roll, then a prize that is a roll). */
550
1139
  for (const f of m.farms) {
551
1140
  if (live?.farms?.includes(f.key)) continue;
552
- const rewards = m.poolToken(f.rewards);
1141
+ const rewards = m.tokens.get(f.rewardsToken);
553
1142
  add({
554
1143
  id: `farm:${f.key}`,
555
1144
  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 }])],
558
- 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] })],
1145
+ 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'}`,
1146
+ creates: [
1147
+ { what: `the farm “${f.name}”`, bytes: farmBytes(f, m) },
1148
+ { what: 'its rewards vault', bytes: 165 },
1149
+ ...(f.stakes === 'collectibles' ? [] : [{ what: 'its stake vault', bytes: 165 }]),
1150
+ ...(farmEnds(f) ? [{ what: 'its record of when seats stop earning', bytes: loot.ENDS_HEAD_BYTES }] : []),
1151
+ ],
1152
+ build: ctx => [
1153
+ loot.openFarmIx({
1154
+ owner: ctx.owner,
1155
+ project: project(ctx),
1156
+ id: f.id,
1157
+ args: resolve(farmArgs(f, m), ctx),
1158
+ rewardsMint: ctx.mints[rewards.key],
1159
+ stakedMint: f.stakes === 'collectibles' ? null : ctx.mints[f.stakes],
1160
+ }),
1161
+ ],
559
1162
  });
560
1163
  }
561
1164
 
562
1165
  // Traits the project vouches for from NFTs' off-chain metadata: one Merkle root per collection, signed once.
563
- const vouched = new Map((live?.published?.traits ?? []).map((t) => [t.collection, t.root]));
1166
+ const vouched = new Map((live?.published?.traits ?? []).map(t => [t.collection, t.root]));
564
1167
  for (const t of m.traits) {
565
1168
  if (vouched.get(t.collection) === t.root) continue;
566
1169
  add({
@@ -568,7 +1171,14 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
568
1171
  part: `traits:${t.collection}`,
569
1172
  says: `Vouch for the traits of the NFTs of ${t.collection} (a root of their off-chain metadata)`,
570
1173
  creates: [{ what: `the traits of ${t.collection}`, bytes: 1 + 32 * 3 }],
571
- 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))) })],
1174
+ build: ctx => [
1175
+ loot.setTraitsIx({
1176
+ signer: ctx.owner,
1177
+ project: project(ctx),
1178
+ key: t.collection,
1179
+ root: Uint8Array.from(t.root.match(/../g).map(h => parseInt(h, 16))),
1180
+ }),
1181
+ ],
572
1182
  });
573
1183
  }
574
1184
 
@@ -576,30 +1186,68 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
576
1186
  // needs a proof fetched when it is signed, so the checker leaves those to `loot.stashIx` after go-live.
577
1187
  // What is in a live stash already: what its last publish listed, and the steps of this one that landed.
578
1188
  const stashed = new Set([
579
- ...(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)),
1189
+ ...(live?.published?.stashes ?? []).flatMap(s => stashNfts(s).map(n => `${s.key}:${n.asset}`)),
1190
+ // A step puts in a group of NFTs: `stash:<key>:<asset>,<asset>…`.
1191
+ ...(live?.landed ?? [])
1192
+ .filter(id => id.startsWith('stash:'))
1193
+ .flatMap(id => {
1194
+ const [key, assets = ''] = id.slice(6).split(/:(.*)/s);
1195
+ return assets.split(',').map(asset => `${key}:${asset}`);
1196
+ }),
581
1197
  ]);
582
1198
  for (const s of m.stashes) {
583
1199
  const nfts = stashNfts(s);
584
- const putting = nfts.filter((n) => n.kind !== 'compressed' && !stashed.has(`${s.key}:${n.asset}`));
1200
+ const putting = nfts.filter(n => n.kind !== 'compressed' && !stashed.has(`${s.key}:${n.asset}`));
585
1201
  // 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
- const fresh = !nfts.some((n) => stashed.has(`${s.key}:${n.asset}`));
587
- for (const [i, n] of putting.entries()) {
1202
+ const fresh = !nfts.some(n => stashed.has(`${s.key}:${n.asset}`));
1203
+ // Several NFTs a call, each with its weight: Core NFTs a group of `STASH_GROUP`, Token Metadata ones two (each takes
1204
+ // more accounts).
1205
+ const kindOf = n => ({
1206
+ asset: n.asset,
1207
+ weight: n.weight,
1208
+ collection: n.kind === 'core' ? n.collection : undefined,
1209
+ tokenMetadata:
1210
+ n.kind === 'token-metadata' ? { programmable: n.programmable, ruleSet: n.ruleSet ?? undefined } : undefined,
1211
+ });
1212
+ const groups = [
1213
+ ...groupsOf(
1214
+ putting.filter(n => n.kind === 'core'),
1215
+ STASH_GROUP,
1216
+ ),
1217
+ ...groupsOf(
1218
+ putting.filter(n => n.kind !== 'core'),
1219
+ 2,
1220
+ ),
1221
+ ];
1222
+ for (const [i, group] of groups.entries()) {
588
1223
  add({
589
- id: `stash:${s.key}:${n.asset}`,
1224
+ id: `stash:${s.key}:${group.map(n => n.asset).join(',')}`,
590
1225
  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
- ],
1226
+ 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`,
1227
+ creates:
1228
+ i > 0
1229
+ ? []
1230
+ : fresh
1231
+ ? [
1232
+ {
1233
+ what: `the stash “${s.name}”, with ${putting.length} NFT${putting.length === 1 ? '' : 's'}`,
1234
+ bytes: stashBytes(putting.length),
1235
+ },
1236
+ ]
1237
+ : [
1238
+ {
1239
+ what: `room for ${putting.length} more NFT${putting.length === 1 ? '' : 's'} in “${s.name}”`,
1240
+ bytes: 36 * putting.length,
1241
+ grows: true,
1242
+ },
1243
+ ],
1244
+ build: ctx => {
1245
+ const [first, ...more] = group.map(n => {
1246
+ const k = kindOf(n);
1247
+ return { ...k, collection: n.kind === 'core' ? (k.collection ?? ctx.collection) : undefined };
1248
+ });
1249
+ return [loot.stashIx({ signer: ctx.owner, project: project(ctx), id: s.id, ...first, more })];
1250
+ },
603
1251
  });
604
1252
  }
605
1253
  }
@@ -621,10 +1269,47 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
621
1269
  : a.lot?.roll
622
1270
  ? `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
1271
  : `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: [] }) }],
625
- build: (ctx) => {
1272
+ // An auction of NFT bids is made with room for the most a bid holds (`MAX_BID_NFTS`), its seller's rent.
1273
+ creates: [
1274
+ {
1275
+ what: `the auction “${a.name}” (back when it ends)`,
1276
+ bytes:
1277
+ encodedSize('Auction', {
1278
+ project: NOBODY,
1279
+ id: 0,
1280
+ seller: NOBODY,
1281
+ mint: NOBODY,
1282
+ lot: args.lot,
1283
+ ours: false,
1284
+ pin: 0,
1285
+ reserve: 0n,
1286
+ min_raise_bps: 0,
1287
+ starts_at: 0n,
1288
+ ends_at: 0n,
1289
+ extend: 0,
1290
+ shares: args.shares,
1291
+ gate: args.gate,
1292
+ nft_bids: placeholders(args).nft_bids,
1293
+ high_bidder: NOBODY,
1294
+ high: 0n,
1295
+ last_bid_at: 0n,
1296
+ proceeds_owed: 0n,
1297
+ held: [],
1298
+ }) + (args.nft_bids ? 32 * loot.MAX_BID_NFTS : 0),
1299
+ },
1300
+ ],
1301
+ build: ctx => {
626
1302
  const seller = auctionSeller(a, ctx);
627
- 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) })];
1303
+ return [
1304
+ loot.openAuctionIx({
1305
+ seller,
1306
+ project: project(ctx),
1307
+ collection: a.lot?.collection ?? ctx.collection,
1308
+ id: a.id,
1309
+ mint: ctx.mints[t.key],
1310
+ args: resolve(args, ctx),
1311
+ }),
1312
+ ];
628
1313
  },
629
1314
  });
630
1315
  }
@@ -641,48 +1326,259 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
641
1326
  part: `seed:${p.key}`,
642
1327
  signers: [signer],
643
1328
  says: `Mint ${p.seed} ${t.symbol} into “${p.key}”`,
644
- 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}` })],
1329
+ build: ctx => [
1330
+ loot.moveTokensIx({
1331
+ authority: ctx.authority,
1332
+ project: project(ctx),
1333
+ mint: ctx.mints[t.key],
1334
+ from: { Mint: [ctx.mints[t.key]] },
1335
+ to: { Pool: [p.index] },
1336
+ amount: { Fixed: [units] },
1337
+ reference: `seed ${p.key}`,
1338
+ }),
1339
+ ],
645
1340
  });
646
1341
  } else {
647
1342
  add({
648
1343
  id: `seed:${p.key}`,
649
1344
  part: `seed:${p.key}`,
650
1345
  says: `Put ${p.seed} ${t.symbol} into “${p.key}”`,
651
- build: (ctx) => [
652
- ...(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))] : []),
653
- 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) } }),
1346
+ build: ctx => [
1347
+ ...(t.wsol
1348
+ ? [
1349
+ createAssociatedTokenAccountIdempotent(ctx.owner, ctx.owner, NATIVE_MINT),
1350
+ transferLamports(ctx.owner, loot.ata(ctx.owner, NATIVE_MINT), units),
1351
+ syncNative(loot.ata(ctx.owner, NATIVE_MINT)),
1352
+ ]
1353
+ : []),
1354
+ loot.payIx({
1355
+ player: ctx.owner,
1356
+ project: project(ctx),
1357
+ mint: ctx.mints[t.key],
1358
+ treasury: ctx.treasury,
1359
+ amount: units,
1360
+ shares: [{ to: { Pool: [p.index] }, bps: 10_000 }],
1361
+ reference: `seed ${p.key}`,
1362
+ programs: { [ctx.mints[t.key]]: tokenProgramOf(t) },
1363
+ }),
654
1364
  ],
655
1365
  });
656
1366
  }
657
1367
  }
1368
+ /** A seed in a token the project mints (no fixed supply): the owner's wallet holds none of it, so the authority
1369
+ * mints it straight into its account, as a pool's seed is minted into the pool. */
1370
+ const mintedSeed = (id, part, t, units, says, account) => {
1371
+ const signer = m.authority === 'owner' ? owner : 'authority';
1372
+ add({
1373
+ id,
1374
+ part,
1375
+ signers: [signer],
1376
+ says,
1377
+ build: ctx => [
1378
+ loot.moveTokensIx({
1379
+ authority: ctx.authority,
1380
+ project: project(ctx),
1381
+ mint: ctx.mints[t.key],
1382
+ from: { Mint: [ctx.mints[t.key]] },
1383
+ to: { Account: [account(ctx)] },
1384
+ amount: { Fixed: [units] },
1385
+ reference: id.replace(/^seed:[a-z]+:/, 'seed '),
1386
+ }),
1387
+ ],
1388
+ });
1389
+ };
1390
+ // A farm's seed: the owner puts it straight into the farm's rewards vault, once the farm is open.
1391
+ for (const f of m.farms) {
1392
+ if (!(Number(f.seed ?? 0) > 0) || live?.seeded?.includes(`farm:${f.key}`)) continue;
1393
+ const t = m.tokens.get(f.rewardsToken);
1394
+ const units = amount(f.seed, t);
1395
+ if (t.gameMints) {
1396
+ mintedSeed(
1397
+ `seed:farm:${f.key}`,
1398
+ `farm:${f.key}`,
1399
+ t,
1400
+ units,
1401
+ `Mint ${f.seed} ${t.symbol} into the rewards of “${f.name}”`,
1402
+ ctx => loot.pda.rewards(loot.pda.farm(project(ctx), f.id)),
1403
+ );
1404
+ continue;
1405
+ }
1406
+ add({
1407
+ id: `seed:farm:${f.key}`,
1408
+ part: `farm:${f.key}`,
1409
+ says: `Put ${f.seed} ${t.symbol} into the rewards of “${f.name}”`,
1410
+ build: ctx => {
1411
+ const vault = loot.pda.rewards(loot.pda.farm(project(ctx), f.id));
1412
+ return [
1413
+ ...(t.wsol
1414
+ ? [
1415
+ createAssociatedTokenAccountIdempotent(ctx.owner, ctx.owner, NATIVE_MINT),
1416
+ transferLamports(ctx.owner, loot.ata(ctx.owner, NATIVE_MINT), units),
1417
+ syncNative(loot.ata(ctx.owner, NATIVE_MINT)),
1418
+ ]
1419
+ : []),
1420
+ loot.payIx({
1421
+ player: ctx.owner,
1422
+ project: project(ctx),
1423
+ mint: ctx.mints[t.key],
1424
+ treasury: ctx.treasury,
1425
+ amount: units,
1426
+ shares: [{ to: { Account: [vault] }, bps: 10_000 }],
1427
+ reference: `seed ${f.key}`,
1428
+ programs: programsOf(ctx),
1429
+ }),
1430
+ ];
1431
+ },
1432
+ });
1433
+ }
658
1434
  // A lottery's seed: the owner puts it straight into the lottery's pot.
659
1435
  for (const l of m.lotteries) {
660
1436
  if (!(Number(l.seed ?? 0) > 0) || live?.seeded?.includes(`lottery:${l.key}`)) continue;
661
1437
  const t = m.tokens.get(l.currency ?? m.currencyKey);
662
1438
  const units = amount(l.seed, t);
1439
+ if (t.gameMints) {
1440
+ mintedSeed(
1441
+ `seed:lottery:${l.key}`,
1442
+ `lottery:${l.key}`,
1443
+ t,
1444
+ units,
1445
+ `Mint ${l.seed} ${t.symbol} into the pot of “${l.name}”`,
1446
+ ctx => loot.pda.lotteryPot(loot.pda.lottery(project(ctx), l.id), l.pot),
1447
+ );
1448
+ continue;
1449
+ }
663
1450
  add({
664
1451
  id: `seed:lottery:${l.key}`,
665
1452
  part: `lottery:${l.key}`,
666
1453
  says: `Put ${l.seed} ${t.symbol} into the pot of “${l.name}”`,
667
- build: (ctx) => {
668
- const pot = loot.pda.pot(loot.pda.lottery(project(ctx), l.id));
1454
+ build: ctx => {
1455
+ const pot = loot.pda.lotteryPot(loot.pda.lottery(project(ctx), l.id), l.pot);
669
1456
  return [
670
- ...(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))] : []),
671
- 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) }),
1457
+ ...(t.wsol
1458
+ ? [
1459
+ createAssociatedTokenAccountIdempotent(ctx.owner, ctx.owner, NATIVE_MINT),
1460
+ transferLamports(ctx.owner, loot.ata(ctx.owner, NATIVE_MINT), units),
1461
+ syncNative(loot.ata(ctx.owner, NATIVE_MINT)),
1462
+ ]
1463
+ : []),
1464
+ loot.payIx({
1465
+ player: ctx.owner,
1466
+ project: project(ctx),
1467
+ mint: ctx.mints[t.key],
1468
+ treasury: ctx.treasury,
1469
+ amount: units,
1470
+ shares: [{ to: { Account: [pot] }, bps: 10_000 }],
1471
+ reference: `seed ${l.key}`,
1472
+ programs: programsOf(ctx),
1473
+ }),
672
1474
  ];
673
1475
  },
674
1476
  });
675
1477
  }
1478
+ // SOL a lottery holds for its rounds' rent (`roundRentOf`: the spec's `roundRent`, or by default what its expected
1479
+ // rounds and their entries take): each round borrows it at its first purchase (its header and payment account), each
1480
+ // purchase's entry, and its ask (the pot's and pools' accounts), back at its close. A lottery live already keeps what
1481
+ // it holds unless the spec names a `roundRent` to add.
1482
+ for (const l of m.lotteries) {
1483
+ const lamports = roundRentOf(l, m);
1484
+ if (lamports <= 0n || (l.roundRent === undefined && (live?.published?.lotteries ?? []).some(x => x.key === l.key)))
1485
+ continue;
1486
+ const sol = Number(lamports) / 1e9;
1487
+ add({
1488
+ id: `rent:lottery:${l.key}`,
1489
+ part: `lottery:${l.key}`,
1490
+ 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`,
1491
+ build: ctx => [transferLamports(ctx.owner, loot.pda.lottery(project(ctx), l.id), lamports)],
1492
+ });
1493
+ }
676
1494
  for (const g of m.grants) {
677
1495
  if (!(Number(g.fund ?? 0) > 0) || live?.funded?.includes(g.key)) continue;
678
1496
  const lamports = toUnits(g.fund, 9);
679
- 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)] });
1497
+ add({
1498
+ id: `fund:${g.key}`,
1499
+ part: `fund:${g.key}`,
1500
+ says: `Put ${g.fund} SOL in the funds of the grant “${g.name}” (they pay each claim’s rent)`,
1501
+ build: ctx => [transferLamports(ctx.owner, loot.pda.grant(project(ctx), g.id), lamports)],
1502
+ });
680
1503
  }
681
- return { steps, model: m };
1504
+ // The authority's steps go last: the owner approves theirs in one batch, the authority theirs in another.
1505
+ return {
1506
+ steps: [
1507
+ ...steps.filter(x => !x.signers.includes('authority')),
1508
+ ...steps.filter(x => x.signers.includes('authority')),
1509
+ ],
1510
+ model: m,
1511
+ };
1512
+ }
1513
+
1514
+ /** The token accounts going live opens (`setupSteps`): [{ who ('the treasury', 'the project' or a wallet), token }], each
1515
+ * once, for the coins the economy takes in. */
1516
+ export function goLiveAccounts(m) {
1517
+ const out = new Map();
1518
+ const add = (who, token) => token && out.set(`${who}|${token}`, { who, token });
1519
+ const takes = (part, token = part.currency ?? m.currencyKey) => {
1520
+ add('the treasury', token);
1521
+ for (const s of sharesOf(part.split)) if (s.kind === 'wallet') add(s.key, token);
1522
+ };
1523
+ for (const b of m.allBoxes) (takes(b), add('the project', b.currency ?? m.currencyKey));
1524
+ for (const p of m.payments) takes(p);
1525
+ for (const l of m.lotteries) if (!l.ticketNft) takes(l, l.ticketCurrency ?? l.currency ?? m.currencyKey);
1526
+ for (const a of m.auctions) if (!a.bidNfts) (takes(a), add('the project', a.currency ?? m.currencyKey));
1527
+ return [...out.values()];
1528
+ }
1529
+
1530
+ /** How many addresses one step adds to the project's lookup table (each extension its own transaction). */
1531
+ const TABLE_GROUP = 24;
1532
+
1533
+ /**
1534
+ * The addresses a project's transactions name most, for its standing lookup table: loot's own fixed accounts (the
1535
+ * platform, the oracle's), the project and its fee box, its treasury and collections, each coin's mint, its vault and
1536
+ * the treasury's account of it, then each pool, item, box and stash. Each a function of the go-live context.
1537
+ */
1538
+ export function standingAddresses(m) {
1539
+ const fixed = [
1540
+ loot.LOOT_PROGRAM,
1541
+ loot.pda.platform(),
1542
+ loot.pda.identity(),
1543
+ loot.VRF_PROGRAM,
1544
+ loot.VRF_QUEUE,
1545
+ loot.MPL_CORE_PROGRAM,
1546
+ TOKEN_PROGRAM,
1547
+ TOKEN_2022_PROGRAM,
1548
+ ASSOCIATED_TOKEN_PROGRAM,
1549
+ SYSTEM_PROGRAM,
1550
+ SYSVAR_SLOT_HASHES,
1551
+ SYSVAR_INSTRUCTIONS,
1552
+ MEMO_PROGRAM,
1553
+ ].map(a => () => a);
1554
+ const programOf = (ctx, key) => programsOf(ctx)[ctx.mints[key]] ?? TOKEN_PROGRAM;
1555
+ return [
1556
+ ...fixed,
1557
+ ctx => ctx.project,
1558
+ ctx => loot.pda.fees(ctx.project),
1559
+ ctx => ctx.treasury,
1560
+ ...(m.collection ? [ctx => ctx.collection] : []),
1561
+ ...[...m.tokens.values()].flatMap(t => [
1562
+ ctx => ctx.mints[t.key],
1563
+ ctx => ctx.mints[t.key] && loot.ata(ctx.project, ctx.mints[t.key], programOf(ctx, t.key)),
1564
+ ctx => ctx.mints[t.key] && loot.ata(ctx.treasury, ctx.mints[t.key], programOf(ctx, t.key)),
1565
+ ]),
1566
+ ...m.pools.map(p => ctx => loot.pda.pool(ctx.project, p.index)),
1567
+ ...m.items.map(it => ctx => loot.pda.item(ctx.project, it.id)),
1568
+ ...m.allBoxes.map(b => ctx => loot.pda.box(ctx.project, b.id)),
1569
+ ...m.stashes.map(s => ctx => loot.pda.stash(ctx.project, s.id)),
1570
+ ].slice(0, 256);
682
1571
  }
683
1572
 
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;
1573
+ /** How many Core NFTs one `stash` call puts in (`Piece::another`: three accounts each after the first). */
1574
+ const STASH_GROUP = 6;
1575
+ /** `list` in groups of `n`. */
1576
+ const groupsOf = (list, n) =>
1577
+ Array.from({ length: Math.ceil(list.length / n) }, (_, i) => list.slice(i * n, i * n + n));
1578
+
1579
+ /** 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
1580
+ * rows and each block of 64, and 36 bytes an NFT (its weight and its address). */
1581
+ export const stashBytes = n => (n ? loot.tableRowAt(36, n - 1) + 36 : 51);
686
1582
 
687
1583
  /** 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
1584
  export const auctionSeller = (a, ctx) => (a.lot?.asset ? ctx.owner : ctx.authority);
@@ -690,31 +1586,98 @@ export const auctionSeller = (a, ctx) => (a.lot?.asset ? ctx.owner : ctx.authori
690
1586
  export const recordBytesOf = (kind, body) => {
691
1587
  const args = body.args;
692
1588
  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 }),
1589
+ Item: () => ({ project: NOBODY, item_id: 0, ...placeholders(args), issued: 0, voided: 0, compressed: false }),
1590
+ LootBox: () => ({ project: NOBODY, box_id: 0, ...placeholders(args), in_flight: 0n, version: 0 }),
695
1591
  Recipe: () => ({ project: NOBODY, id: 0, version: 0, ...placeholders(args) }),
696
1592
  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 } }),
1593
+ // Its terms and its round number: each round keeps the rest in a record of its own.
1594
+ Lottery: () => ({
1595
+ project: NOBODY,
1596
+ id: 0,
1597
+ incarnation: 0n,
1598
+ revision: 0,
1599
+ ...placeholders(loot.withDefaults({ Lottery: { args } }).Lottery.args),
1600
+ round: 0,
1601
+ pots: 0,
1602
+ owing: NOBODY,
1603
+ }),
1604
+ Grant: () => ({
1605
+ project: NOBODY,
1606
+ id: 0,
1607
+ incarnation: 0n,
1608
+ ...placeholders(args),
1609
+ counted: { count: 0, window: 0, in_window: 0 },
1610
+ }),
699
1611
  }[kind];
700
1612
  return encodedSize(kind, base());
701
1613
  };
1614
+ /** A farm's bytes as `open_farm` writes it: its args and its numbers. */
1615
+ export const farmBytes = (f, m) =>
1616
+ encodedSize('Farm', {
1617
+ project: NOBODY,
1618
+ id: 0,
1619
+ farmed: f.stakes === 'collectibles' ? 'Collectibles' : { Tokens: { mint: NOBODY } },
1620
+ ...placeholders(farmArgs(f, m)),
1621
+ rewards_mint: NOBODY,
1622
+ rate_carry: 0n,
1623
+ opened_at: 0n,
1624
+ total_weight: 0n,
1625
+ reward_per_weight: 0n,
1626
+ reward_remainder: 0n,
1627
+ owed: 0n,
1628
+ clock: 0n,
1629
+ unreleased: 0n,
1630
+ release_ends: 0n,
1631
+ release_from: 0n,
1632
+ release_base: 0n,
1633
+ keeps_ends: farmEnds(f),
1634
+ });
702
1635
  /** 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 });
1636
+ export const contestBytes = (contest, m) =>
1637
+ encodedSize('Contest', {
1638
+ project: NOBODY,
1639
+ id: 0,
1640
+ incarnation: 0n,
1641
+ opener: NOBODY,
1642
+ mint: NOBODY,
1643
+ decided_at: 0n,
1644
+ ...placeholders(contestArgs(contest, m)),
1645
+ verdict: 'Open',
1646
+ winner: 0,
1647
+ owed: 0n,
1648
+ unclaimed: 0n,
1649
+ pot_won: 0n,
1650
+ prize_won: 0n,
1651
+ sides: Array(contest.sides).fill(0n),
1652
+ share: 0,
1653
+ extras: 0,
1654
+ picks: 0,
1655
+ });
704
1656
  /** Args with "@mint:" names standing in as addresses, for sizing. */
705
- 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)}`) }) });
1657
+ const placeholders = args =>
1658
+ resolve(args, {
1659
+ project: placeholderKey('project'),
1660
+ collection: placeholderKey('new:collection'),
1661
+ tree: placeholderKey('new:tree'),
1662
+ treasury: placeholderKey('treasury'),
1663
+ mints: new Proxy({}, { get: (_, k) => placeholderKey(`mint/${String(k)}`) }),
1664
+ });
706
1665
  /** The token program of each Token-2022 mint a context names (the program's account lists name it). */
707
- const programsOf = (ctx) => ctx.tokenPrograms ?? {};
1666
+ const programsOf = ctx => ctx.tokenPrograms ?? {};
708
1667
 
709
1668
  // ---------------------------------------------------------------- contexts
710
1669
 
711
1670
  /** Stand-in addresses for every name a plan uses, so it can be built and sized without a wallet. */
712
- export function placeholderContext(m, { rent = (bytes) => (BigInt(bytes) + ACCOUNT_OVERHEAD) * MOST_RENT_PER_BYTE } = {}) {
1671
+ export function placeholderContext(
1672
+ m,
1673
+ { rent = bytes => (BigInt(bytes) + ACCOUNT_OVERHEAD) * MOST_RENT_PER_BYTE } = {},
1674
+ ) {
713
1675
  const ownerKey = placeholderKey('owner');
714
1676
  const collection = m.collection ? placeholderKey('new:collection') : null;
715
1677
  const tree = compressedTree(m) ? placeholderKey('new:tree') : null;
716
1678
  const mints = {};
717
- for (const t of m.tokens.values()) mints[t.key] = t.mint?.startsWith('@') ? placeholderKey(`new:mint/${t.key}`) : t.mint;
1679
+ for (const t of m.tokens.values())
1680
+ mints[t.key] = t.mint?.startsWith('@') ? placeholderKey(`new:mint/${t.key}`) : t.mint;
718
1681
  return {
719
1682
  owner: ownerKey,
720
1683
  authority: m.authority === 'owner' ? ownerKey : m.authority,
@@ -723,8 +1686,15 @@ export function placeholderContext(m, { rent = (bytes) => (BigInt(bytes) + ACCOU
723
1686
  project: loot.pda.project(ownerKey, m.projectNumber),
724
1687
  treasury: m.treasuryWallet ?? ownerKey,
725
1688
  mints,
726
- 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}`)])) },
1689
+ keys: {
1690
+ ...(collection ? { 'new:collection': collection } : {}),
1691
+ ...(tree ? { 'new:tree': tree } : {}),
1692
+ ...Object.fromEntries(
1693
+ [...m.tokens.values()].map(t => [`new:mint/${t.key}`, placeholderKey(`new:mint/${t.key}`)]),
1694
+ ),
1695
+ },
727
1696
  rent,
1697
+ slot: 0n,
728
1698
  };
729
1699
  }
730
1700
 
@@ -750,7 +1720,8 @@ export function madeSoFar(live) {
750
1720
  }
751
1721
  if (!made('project', 'collection')) delete out.collection;
752
1722
  if (!made('tree', 'tree')) delete out.tree;
753
- if (live.mints) out.mints = Object.fromEntries(Object.entries(live.mints).filter(([key]) => made(`token:${key}`, `token:${key}`)));
1723
+ if (live.mints)
1724
+ out.mints = Object.fromEntries(Object.entries(live.mints).filter(([key]) => made(`token:${key}`, `token:${key}`)));
754
1725
  return out;
755
1726
  }
756
1727
 
@@ -759,13 +1730,17 @@ export function setupContext(m, { owner: ownerKey, keys, live: given = {}, rent
759
1730
  const live = madeSoFar(given) ?? {};
760
1731
  // A setup goes on only in the owner's own project: never in one a file names for them.
761
1732
  const own = loot.pda.project(ownerKey, m.projectNumber);
762
- 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}.`);
1733
+ if (live.project && live.project !== own)
1734
+ throw new Error(
1735
+ `The live section names the project ${live.project}, which is not the project ${own} of the wallet ${ownerKey}.`,
1736
+ );
763
1737
  const collection = live.collection ?? keys['new:collection'] ?? null;
764
1738
  const tree = live.tree ?? keys['new:tree'] ?? null;
765
1739
  const mints = {};
766
1740
  // A mint the spec names by its address is that one, whatever the live section says; one the setup makes is the
767
1741
  // live section's once it landed (checked against the chain by the approval page), else this setup's fresh key.
768
- 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}`];
1742
+ for (const t of m.tokens.values())
1743
+ mints[t.key] = t.mint && !t.mint.startsWith('@') ? t.mint : (live.mints?.[t.key] ?? keys[`new:mint/${t.key}`]);
769
1744
  return {
770
1745
  owner: ownerKey,
771
1746
  authority: m.authority === 'owner' ? ownerKey : m.authority,
@@ -782,17 +1757,21 @@ export function setupContext(m, { owner: ownerKey, keys, live: given = {}, rent
782
1757
  // ---------------------------------------------------------------- packing
783
1758
 
784
1759
  /** The bytes of a transaction of `instructions` the owner pays for: what `pack` holds against the budget. */
785
- export const stepBytes = (instructions, ctx) => transactionSize(compileMessage({ payer: ctx.owner, blockhash: placeholderKey('blockhash'), instructions }));
1760
+ export const stepBytes = (instructions, ctx) =>
1761
+ transactionSize(compileMessage({ payer: ctx.owner, blockhash: placeholderKey('blockhash'), instructions }));
786
1762
 
787
1763
  /** Packs steps into transactions under the size budget, one signer set at a time; each step whole. */
788
1764
  export function pack(steps, ctx) {
789
1765
  const transactions = [];
790
1766
  let current = null;
791
- const sizeOf = (instructions) => stepBytes(instructions, ctx);
1767
+ const sizeOf = instructions => stepBytes(instructions, ctx);
792
1768
  for (const step of steps) {
793
1769
  const instructions = step.build(ctx);
1770
+ // A step with nothing to send (the lookup table of a plan built with no recent slot) is left out.
1771
+ if (!instructions.length) continue;
794
1772
  const signerKind = step.signers.includes('authority') ? 'authority' : 'owner';
795
- if (current && current.signer === signerKind) {
1773
+ // A write that names boxes to roll takes all the compute a transaction has: alone.
1774
+ if (current && current.signer === signerKind && !step.alone && !current.alone) {
796
1775
  const next = [...current.instructions, ...instructions];
797
1776
  if (sizeOf(next) <= BUDGET) {
798
1777
  current.instructions = next;
@@ -805,7 +1784,14 @@ export function pack(steps, ctx) {
805
1784
  const size = sizeOf(instructions);
806
1785
  if (size > BUDGET) throw new Error(`The step “${step.says}” is ${size} bytes: too big for one transaction.`);
807
1786
  // `built`: each step's own instructions, in the order of `steps`.
808
- current = { signer: signerKind, steps: [step], built: [instructions], instructions, signers: [...step.signers] };
1787
+ current = {
1788
+ signer: signerKind,
1789
+ steps: [step],
1790
+ built: [instructions],
1791
+ instructions,
1792
+ signers: [...step.signers],
1793
+ alone: Boolean(step.alone),
1794
+ };
809
1795
  transactions.push(current);
810
1796
  }
811
1797
  for (const t of transactions) t.bytes = sizeOf(t.instructions);
@@ -820,7 +1806,12 @@ export function plan(economy, { now = Math.floor(Date.now() / 1000), ctx = null,
820
1806
  const checked = check(economy, { now });
821
1807
  if (!checked.ok) return { ok: false, refusals: checked.refusals, warnings: checked.warnings };
822
1808
  const { steps, model: m } = setupSteps(economy, { now, only, live });
823
- 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]));
1809
+ if (ctx)
1810
+ ctx.tokenPrograms ??= Object.fromEntries(
1811
+ [...m.tokens.values()]
1812
+ .filter(t => t.program === 'token-2022' && ctx.mints[t.key])
1813
+ .map(t => [ctx.mints[t.key], TOKEN_2022_PROGRAM]),
1814
+ );
824
1815
  const context = ctx ?? placeholderContext(m);
825
1816
  const transactions = pack(steps, context);
826
1817
  return {
@@ -828,8 +1819,11 @@ export function plan(economy, { now = Math.floor(Date.now() / 1000), ctx = null,
828
1819
  warnings: checked.warnings,
829
1820
  steps,
830
1821
  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),
832
- freshKeys: [...new Set(steps.flatMap((s) => s.signers.filter((x) => x.startsWith('new:'))))],
1822
+ // A wallet approves up to PER_APPROVAL transactions at once: the owner's, then the authority's (its steps go last).
1823
+ approvals:
1824
+ Math.max(1, Math.ceil(transactions.filter(t => t.signer === 'owner').length / PER_APPROVAL)) +
1825
+ Math.ceil(transactions.filter(t => t.signer === 'authority').length / PER_APPROVAL),
1826
+ freshKeys: [...new Set(steps.flatMap(s => s.signers.filter(x => x.startsWith('new:'))))],
833
1827
  };
834
1828
  }
835
1829
 
@@ -841,47 +1835,140 @@ export function playSteps(economy) {
841
1835
  // A shop item, and any box with one outcome and nothing random, sells in one instruction: price split, prize given.
842
1836
  for (const b of m.shop) {
843
1837
  const it = m.item.get(b.shopOf);
844
- 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`);
1838
+ add(
1839
+ `item:${it.key}`,
1840
+ 'buy',
1841
+ 'player',
1842
+ it.kind === 'normal'
1843
+ ? `buy “${it.name}”: the project’s backend delivers it from the receipt`
1844
+ : `buy “${it.name}”: minted to them at once`,
1845
+ );
845
1846
  }
846
1847
  for (const b of m.boxes) {
847
- if (boxBuys(b)) {
1848
+ if (boxBuys(b, m)) {
848
1849
  add(`box:${b.key}`, 'buy', 'player', `buy “${b.name}”: what it gives, at once`);
849
1850
  continue;
850
1851
  }
851
1852
  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');
1853
+ if (boxRolls(b, m)) add(`box:${b.key}`, 'callback_roll', 'the oracle', 'roll it');
1854
+ add(`box:${b.key}`, 'settle', 'the player, a key they named, or the project’s keys', 'carry out what it rolled');
854
1855
  }
855
1856
  for (const p of m.payments) add(`payment:${p.key}`, 'pay', 'player', `pay “${p.name}”`);
856
1857
  add('payments', 'pay_nft', 'player', 'pay with an NFT (burned, or given to a wallet): the receipt is the record');
857
- 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
- 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');
1858
+ for (const s of m.stashes)
1859
+ add(
1860
+ `stash:${s.key}`,
1861
+ 'stash',
1862
+ 'the owner or the authority',
1863
+ `put an NFT it holds in “${s.name}”, or take one out to any wallet`,
1864
+ );
1865
+ for (const r of prizeRolls(m).filter(r => !['mission', 'recipe'].includes(r.route))) {
1866
+ add(
1867
+ r.by,
1868
+ 'ask_roll',
1869
+ 'the winner or the project’s keys, who pay the oracle',
1870
+ `ask for the roll of “${m.box.get(r.box)?.name ?? r.box}” a winner got (${r.route})`,
1871
+ );
1872
+ add(r.by, 'settle', 'the winner, a key they named, or the project’s keys', 'carry out what it rolled');
861
1873
  }
862
1874
  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');
1875
+ for (const s of m.schedules)
1876
+ add(
1877
+ `schedule:${s.key}`,
1878
+ 'run_schedule',
1879
+ 'the project’s keys, or who it pays',
1880
+ `run “${s.name ?? s.key}” when it is due`,
1881
+ );
1882
+ for (const l of m.lotteries)
1883
+ (add(`lottery:${l.key}`, 'buy_tickets', 'player', 'buy tickets'),
1884
+ add(`lottery:${l.key}`, 'draw_lottery', 'anyone', 'ask for the draw when it is due'),
1885
+ add(
1886
+ `lottery:${l.key}`,
1887
+ 'pull',
1888
+ 'anyone',
1889
+ 'pay one part: a refund, a place’s part of the pot or its prize, a share owed in common',
1890
+ ));
865
1891
  for (const f of m.farms) {
866
- 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
- 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');
1892
+ if (f.stakes === 'collectibles')
1893
+ (add(`farm:${f.key}`, 'stake_nft', 'player', 'stake a collectible'),
1894
+ add(`farm:${f.key}`, 'unstake_nft', 'player', 'take it out once its lock has run'));
1895
+ else
1896
+ (add(`farm:${f.key}`, 'stake_tokens', 'player', 'stake'),
1897
+ add(`farm:${f.key}`, 'unstake_tokens', 'player', 'unstake or collect'),
1898
+ add(`farm:${f.key}`, 'stake_nft', 'player', 'put a collectible on as a booster'));
868
1899
  add(`farm:${f.key}`, 'harvest', 'player', 'collect rewards');
869
1900
  add(`farm:${f.key}`, 'refresh_seat', 'anyone', 'read a seat’s boosts again');
1901
+ if (farmEnds(f))
1902
+ add(
1903
+ `farm:${f.key}`,
1904
+ 'walk_farm',
1905
+ 'anyone',
1906
+ 'catch the farm up, in parts, when it has passed more end times than a move can walk',
1907
+ );
870
1908
  }
871
1909
  for (const c of m.contests) {
872
1910
  add(`contest:${c.key}`, 'open_contest', 'authority', `open “${c.name}”`);
873
1911
  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');
1912
+ add(
1913
+ `contest:${c.key}`,
1914
+ 'decide',
1915
+ 'authority (or anyone sending a decider’s verdict)',
1916
+ 'name the side that won, or call it off before its result is due',
1917
+ );
1918
+ add(
1919
+ `contest:${c.key}`,
1920
+ 'claim',
1921
+ c.stakeNft || c.prize ? 'the winner or the project’s keys' : 'anyone',
1922
+ 'pay an entry',
1923
+ );
876
1924
  }
877
1925
  for (const a of m.auctions) {
878
- 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)`);
1926
+ add(
1927
+ `auction:${a.key}`,
1928
+ 'open_auction',
1929
+ a.lot?.asset ? 'the owner, alone (a collectible of theirs)' : 'the authority, alone (a new unit of an item)',
1930
+ `put the lot up (at go-live)`,
1931
+ );
879
1932
  add(`auction:${a.key}`, 'bid', 'player', 'bid: the bid it tops goes back at once');
880
- 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');
1933
+ add(
1934
+ `auction:${a.key}`,
1935
+ 'end_auction',
1936
+ 'anyone once it is over; its seller, to call off one with no bid',
1937
+ 'sell to the high bidder, or send the lot home',
1938
+ );
881
1939
  }
882
- 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');
883
- 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}”`);
884
- 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);
1940
+ for (const h of m.holds)
1941
+ (add(
1942
+ `hold:${h.key}`,
1943
+ 'hold',
1944
+ 'player',
1945
+ `hand in a “${h.item}” until ${h.until ?? `${h.for ?? 'the time they choose'} from then`}`,
1946
+ ),
1947
+ add(`hold:${h.key}`, 'release', 'authority while it runs, anyone after', 'say where it goes, or send it home'));
1948
+ for (const g of m.grants)
1949
+ add(
1950
+ `grant:${g.key}`,
1951
+ 'claim_grant',
1952
+ 'the winning wallet, which signs it (with a voucher, a list proof, or an open grant); anyone may pay its fees',
1953
+ `claim “${g.name}”`,
1954
+ );
1955
+ for (const d of m.decisions)
1956
+ add(
1957
+ `decision:${d.key}`,
1958
+ {
1959
+ payout: 'move_tokens',
1960
+ burn: 'move_tokens',
1961
+ issue: 'move_tokens',
1962
+ make: 'collectible',
1963
+ unmake: 'collectible',
1964
+ set: 'collectible',
1965
+ award: 'release',
1966
+ decide: 'decide',
1967
+ roll: 'roll',
1968
+ open_contest: 'open_contest',
1969
+ }[d.does],
1970
+ 'authority',
1971
+ d.when,
1972
+ );
885
1973
  return rows;
886
1974
  }
887
-