@xgamesfun/solana 0.20.1 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +10 -10
  2. package/examples/coin-flip-arena.json +2 -7
  3. package/examples/elements-set.json +2 -9
  4. package/examples/expedition-guild.json +1 -8
  5. package/examples/guild-missions.json +3 -10
  6. package/examples/halving-seasons.json +4 -9
  7. package/examples/hero-trials.json +1 -4
  8. package/examples/merge-2048.json +5 -5
  9. package/examples/paper-hands-farm.json +4 -16
  10. package/examples/partner-vault.json +1 -4
  11. package/examples/stakers-draw.json +1 -8
  12. package/examples/star-fleet.json +1 -8
  13. package/examples/starforce.json +6 -6
  14. package/js/approve/details.mjs +8 -7
  15. package/js/approve/server.mjs +20 -5
  16. package/js/chain/borsh.js +9 -0
  17. package/js/chain/core.js +14 -2
  18. package/js/chain/lookup.js +8 -2
  19. package/js/chain/rpc.js +4 -2
  20. package/js/client/accounts.js +448 -116
  21. package/js/client/grant.js +125 -66
  22. package/js/client/idl/loot.js +1 -1
  23. package/js/client/idl/loot.json +2360 -2218
  24. package/js/client/index.js +1 -0
  25. package/js/client/ix.js +1204 -473
  26. package/js/client/pda.js +20 -16
  27. package/js/client/policy.js +44 -23
  28. package/js/client/random.js +70 -0
  29. package/js/client/sizes.js +255 -12
  30. package/js/economy/check.mjs +455 -210
  31. package/js/economy/cost.mjs +45 -30
  32. package/js/economy/examples.mjs +2 -2
  33. package/js/economy/explain.mjs +24 -20
  34. package/js/economy/guide.mjs +31 -30
  35. package/js/economy/live.mjs +30 -7
  36. package/js/economy/model.mjs +171 -56
  37. package/js/economy/plan.mjs +380 -112
  38. package/js/economy/schema.json +291 -143
  39. package/js/economy/simulate.mjs +154 -87
  40. package/js/mcp/tools.mjs +14 -8
  41. package/js/play/index.js +1121 -375
  42. package/js/play/keys.js +167 -0
  43. package/package.json +1 -1
  44. package/release.json +5 -5
  45. package/skills/xsolana-economy/SKILL.md +15 -11
  46. package/skills/xsolana-economy/references/after-launch.md +37 -27
  47. package/skills/xsolana-economy/references/apps.md +55 -19
  48. package/skills/xsolana-economy/references/sentence-to-spec.md +30 -22
  49. package/skills/xsolana-economy/references/telling-the-person.md +52 -11
  50. package/types/chain/core.d.ts +3 -1
  51. package/types/chain/lookup.d.ts +5 -2
  52. package/types/chain/rpc.d.ts +3 -1
  53. package/types/client/accounts.d.ts +195 -55
  54. package/types/client/grant.d.ts +72 -27
  55. package/types/client/idl/loot.d.ts +590 -559
  56. package/types/client/index.d.ts +1 -0
  57. package/types/client/ix.d.ts +3862 -234
  58. package/types/client/pda.d.ts +19 -15
  59. package/types/client/policy.d.ts +39 -14
  60. package/types/client/random.d.ts +26 -0
  61. package/types/client/sizes.d.ts +93 -5
  62. package/types/economy/check.d.mts +5 -10
  63. package/types/economy/cost.d.mts +2 -1
  64. package/types/economy/live.d.mts +16 -1
  65. package/types/economy/model.d.mts +105 -37
  66. package/types/economy/plan.d.mts +193 -192
  67. package/types/economy/simulate.d.mts +21 -13
  68. package/types/mcp/tools.d.mts +21 -13
  69. package/types/play/index.d.ts +450 -39
  70. package/types/play/keys.d.ts +51 -0
package/js/client/ix.js CHANGED
@@ -16,14 +16,17 @@
16
16
  // Values are the IDL's own shapes (`chain/borsh.js`): addresses base58 strings, u64/u128/i64 bigints (or numbers),
17
17
  // enums 'Variant', { Variant: [values] } or { Variant: { field: value } }, structs with snake_case fields.
18
18
  import { addressBytes, encodeBase58, NO_ADDRESS } from '../chain/base58.js';
19
- import { encodeBorsh, encodeInstruction } from '../chain/borsh.js';
20
- import { concat, intLE, readUintLE, toBigInt, uintLE, utf8 } from '../chain/bytes.js';
19
+ import { encodeAccount, encodeInstruction } from '../chain/borsh.js';
20
+ import { compareBytes, concat, intLE, readUintLE, toBigInt, uintLE, utf8 } from '../chain/bytes.js';
21
21
  import { addressWithSeed, ASSOCIATED_TOKEN_PROGRAM, findProgramAddress, METAPLEX_METADATA_PROGRAM, SYSTEM_PROGRAM, SYSVAR_INSTRUCTIONS, SYSVAR_SLOT_HASHES, TOKEN_2022_PROGRAM, TOKEN_PROGRAM } from '../chain/pda.js';
22
22
  import { allocateWithSeedIx, createAccountWithSeedIx, initializeAccount3Ix, MINT_BYTES, TOKEN_ACCOUNT_BYTES } from '../chain/token.js';
23
23
  import { createAssociatedTokenAccountIdempotent, meta, transferLamports } from '../chain/programs.js';
24
24
  import { sha256 } from '../chain/sha256.js';
25
+ import { MEMO_PROGRAM } from '../chain/token2022.js';
25
26
  import IDL from './idl/loot.js';
26
- import { lotteryOwed, lotteryStep } from './accounts.js';
27
+ import { chosenAtPurchase, CLAIMED, decode, ENDED, idleRound, nftSlot, ownStash, paymentMint, placesPulled, prizePlacesOwed, roundFinished, roundWinners, shareOf, tokenRun } from './accounts.js';
28
+ import { stream, TAG, weighted } from './random.js';
29
+ import { tableBudget } from './sizes.js';
27
30
  import { ata, BUBBLEGUM_PROGRAM, COMPRESSION_PROGRAM, LOOT_PROGRAM, MPL_CORE_PROGRAM, NOOP_PROGRAM, pda, vault, VRF_QUEUE } from './pda.js';
28
31
 
29
32
  // The options of a call made without any: read-only, and typed so the generated declarations take any option.
@@ -35,6 +38,8 @@ export { IDL, ASSOCIATED_TOKEN_PROGRAM };
35
38
  export const TOKEN_METADATA_PROGRAM = METAPLEX_METADATA_PROGRAM;
36
39
 
37
40
  const ZERO_REFERENCE = new Uint8Array(32);
41
+ /** The default address: names nobody, and nobody signs as it. */
42
+ const NOBODY_KEY = '11111111111111111111111111111111';
38
43
  const hashToAddress = (hash) => encodeBase58(hash);
39
44
 
40
45
  /** 32 bytes a project names a call by (a match id, a payout's reason). Zeros when not given. */
@@ -54,6 +59,24 @@ const instructionDef = (name) => {
54
59
  return def;
55
60
  };
56
61
 
62
+ /** The IDL's instructions, the longest tags first: the oracle's three callbacks keep 8-byte tags, every other
63
+ * instruction has a 1-byte one, which the first byte of an 8-byte tag may equal. */
64
+ const BY_TAG = [...IDL.instructions].sort((a, b) => b.discriminator.length - a.discriminator.length);
65
+
66
+ /** The IDL entry of the loot instruction whose data is `data` (its tag first), or null. */
67
+ export const instructionOf = (data) => BY_TAG.find((i) => i.discriminator.length <= data.length && i.discriminator.every((b, j) => data[j] === b)) ?? null;
68
+
69
+ /**
70
+ * An instruction's tail (`tokens::tail`): the accounts every NFT move hands Core (what a check its collection carries
71
+ * reads, an oracle's account) and every token transfer hands a transfer hook, after the route's own. It starts at the
72
+ * Memo program's account, which serves a transfer into an account that requires a memo; with none, it is empty. So
73
+ * `extra` (hook accounts, Core's adapters) goes after the Memo program, which this puts first.
74
+ */
75
+ export function tailOf(extra = []) {
76
+ if (!extra.length) return [];
77
+ return [meta(MEMO_PROGRAM, false, false), ...extra.filter((m) => m.pubkey !== MEMO_PROGRAM)];
78
+ }
79
+
57
80
  /** The accounts of an instruction in order, each with its group (`checkout`, `fee`, `side`, `piece`) or ''. */
58
81
  function leaves(list, group = '') {
59
82
  return list.flatMap((a) => (a.accounts ? leaves(a.accounts, a.name) : [{ ...a, group }]));
@@ -183,6 +206,28 @@ export function destinationAccounts(project, to, mint, { holders = {}, programs,
183
206
  }
184
207
 
185
208
  /** A split's destinations, in order. */
209
+ /** The most shares a split kept to be paid later holds (`MAX_SHARES`, not in the IDL): a box's, a lottery's, a contest's,
210
+ * an auction's, an offer's or a listing's (a ninth is `BadShares`); a payment's own split has no such cap. */
211
+ export const MAX_SHARES = 8;
212
+ /** `shares`, refused here when a split kept to be paid later holds more than `MAX_SHARES`. */
213
+ const stored = (shares = []) => {
214
+ if (shares.length > MAX_SHARES) throw new Error(`A split kept to be paid later holds at most ${MAX_SHARES} shares; this one has ${shares.length} (BadShares).`);
215
+ return shares;
216
+ };
217
+
218
+ /** The most traits a fresh item draws (`MAX_ITEM_TRAITS`, not in the IDL): each is written as its unit is minted, which
219
+ * one transaction must hold. */
220
+ export const MAX_ITEM_TRAITS = 64;
221
+ /** The longest name a fresh item's trait has, in bytes (`MAX_TRAIT_KEY`, not in the IDL). */
222
+ export const MAX_TRAIT_KEY = 64;
223
+ /** An item's `traits`, refused here past the program's caps (`BadItem` when the owner signs). */
224
+ const traitsInCaps = (traits = []) => {
225
+ if (traits.length > MAX_ITEM_TRAITS) throw new Error(`An item draws at most ${MAX_ITEM_TRAITS} traits; this one draws ${traits.length} (BadItem).`);
226
+ const long = traits.find((t) => utf8(String(t.key)).length > MAX_TRAIT_KEY);
227
+ if (long) throw new Error(`A trait's name is at most ${MAX_TRAIT_KEY} bytes; “${String(long.key).slice(0, 24)}…” is ${utf8(String(long.key)).length} (BadItem).`);
228
+ return traits;
229
+ };
230
+
186
231
  export const shareAccounts = (project, shares, mint, options) => shares.flatMap((s) => destinationAccounts(project, s.to, mint, options));
187
232
 
188
233
  /**
@@ -204,11 +249,22 @@ export const sharesBurn = (shares) => shares.some((s) => s.to === 'Burn');
204
249
  /**
205
250
  * The account each prize of a box's entries comes with at the roll, in entry order and each entry's prizes in order:
206
251
  * its item for an item, its pool for tokens from one, its stash for an NFT of one (a named one, or one at random). All
207
- * written: the roll takes from them. Tokens minted and rolls take none here.
252
+ * written: the roll takes from them. Tokens minted and rolls take none here. A fresh item's tables follow it while
253
+ * they fit the box's table budget (`tableBudget`), walked in entry order; an item whose tables don't fit what is left
254
+ * takes none, and is out of stock for that roll.
208
255
  */
209
- export function entryAccounts(project, entries) {
256
+ export function entryAccounts(project, entries, items = {}) {
257
+ let budget = Math.max(0, tableBudget(entries));
210
258
  return entries.flatMap(({ prizes }) => prizes.flatMap((prize) => {
211
- if (prize.Item) return [meta(pda.item(project, prize.Item[0]), false, true)];
259
+ // An item, compressed or not; a fresh one's tables after it, in the order of its traits (`items` maps an item's id
260
+ // to its decoded Item: one left out is taken as not fresh).
261
+ const id = prize.Item?.[0] ?? prize.Compressed?.item;
262
+ if (id !== undefined) {
263
+ const tables = traitTables(items[id]);
264
+ const fits = tables.length <= budget;
265
+ if (fits) budget -= tables.length;
266
+ return [meta(pda.item(project, id), false, true), ...(fits ? tables : []).map((t) => meta(pda.table(project, t), false, true))];
267
+ }
212
268
  if (prize.Tokens?.from.Pool) return [meta(pda.pool(project, prize.Tokens.from.Pool[0]), false, true)];
213
269
  const stash = prize.Nft?.stash ?? prize.Stash?.[0];
214
270
  return stash === undefined ? [] : [meta(pda.stash(project, stash), false, true)];
@@ -248,17 +304,42 @@ function boxPoolMints(box, poolMints) {
248
304
  /** Whether a box leaves its outcome to the oracle: more than one entry, or an entry gives a stash's NFT at random. */
249
305
  export const boxRolls = (entries) => entries.length > 1 || entries.some((e) => e.prizes.some((p) => p.Stash));
250
306
 
251
- /** What an order of `box` holds for the collectibles its roll may mint: a collectible's rent for each item the entry
252
- * with the most of them gives. */
253
- export const orderEscrow = (box) => ESCROW_LAMPORTS * BigInt(Math.max(0, ...box.entries.map((e) => e.prizes.filter((p) => p.Item).length)));
307
+ /** The tables a fresh item's traits draw from, in the order of its traits (none for an item whose units are alike). */
308
+ export const traitTables = (item) => (item?.traits ?? []).flatMap((t) => (t.from?.Table ? [t.from.Table[0]] : []));
309
+
310
+ /** Whether a box gives a fresh item (one with traits): its units' traits are drawn by the oracle (`items` maps an
311
+ * item's id to its decoded Item, as `entryAccounts`). */
312
+ export const boxFresh = (entries, items = {}) => entries.some((e) => e.prizes.some((p) => (items[p.Item?.[0] ?? p.Compressed?.item]?.traits ?? []).length > 0));
313
+
314
+ /**
315
+ * What one prize a settle hands over may open or mint, at most: a collectible's Core asset with the program's plugins
316
+ * (about 0.004 SOL), a stash's NFT's new token account, a fresh compressed NFT's Sheet, a token account of a mint, the
317
+ * NFT rolled on's numbers or its stash row. A client's own figure (the program keeps no reserve): what a throwaway key
318
+ * that settles is funded with for each, and gives back unspent.
319
+ */
320
+ export const SETTLE_RENT_UNIT = 6_000_000n;
321
+
322
+ /**
323
+ * What a settle of an order of `opens` opens of `box` may open or mint, at most, in lamports: whoever settles pays it
324
+ * (the order lends nothing). For each draw of each open, each prize of the entry with the most of them that opens an
325
+ * account (a collectible, a stash's NFT, a fresh compressed NFT's Sheet: `items` maps an item's id to its decoded
326
+ * Item); once an order, each source of tokens it may pay, and the fate of the NFT it rolls on; one `SETTLE_RENT_UNIT`
327
+ * each. A throwaway key that settles is funded with it, and gives back what it doesn't spend.
328
+ */
329
+ export function settleRent(box, opens = 1, items = {}) {
330
+ const rents = (p) => Boolean(p.Item || p.Nft || p.Stash || (p.Compressed && (items[p.Compressed.item]?.traits ?? []).length));
331
+ const most = Math.max(0, ...box.entries.map((e) => e.prizes.filter(rents).length));
332
+ const sources = new Set(box.entries.flatMap((e) => e.prizes.flatMap((p) => (p.Tokens ? [JSON.stringify(p.Tokens.from)] : []))));
333
+ return SETTLE_RENT_UNIT * (BigInt(most * boxDraws(box) * Math.max(1, opens)) + BigInt(sources.size) + (box.subject ? 1n : 0n));
334
+ }
254
335
 
255
336
  /**
256
337
  * The nonce the `k`th roll an order won is named by when it settles: the first 8 bytes, little
257
- * endian, of sha256("roll" | order | its requested slot (u64 LE) | k (u8)). Like every prize's roll, its order is at
338
+ * endian, of sha256("roll" | order | its requested slot (u64 LE) | k (u32 LE)). Like every prize's roll, its order is at
258
339
  * `prizeRollNonce` of it.
259
340
  */
260
341
  export function rollNonce(order, requestedSlot, k) {
261
- const hash = sha256(concat(utf8('roll'), addressBytes(order), uintLE(requestedSlot, 8), uintLE(k, 1)));
342
+ const hash = sha256(concat(utf8('roll'), addressBytes(order), uintLE(requestedSlot, 8), uintLE(k, 4)));
262
343
  return readUintLE(hash, 0, 8);
263
344
  }
264
345
 
@@ -272,7 +353,17 @@ export const nonceOn = (asset) => readUintLE(addressBytes(asset), 0, 8);
272
353
 
273
354
  /** Whether a box's roll holds the NFT it rolls on, or moves its numbers: an entry burns it, gives it
274
355
  * away or moves its numbers. Its order is at the NFT's nonce (`nonceOn`). */
275
- export const boxHolds = (box) => (box.fates ?? []).some((f) => f.then !== 'Back' || (f.shifts ?? []).length > 0);
356
+ export const boxHolds = (box) => (box.fates ?? []).some((f) => f.then != null || (f.shifts ?? []).length > 0);
357
+
358
+ /**
359
+ * The nonce of the order a roll a sender names is made in: a craft's roll on a collectible kept (`RollOn`) and a
360
+ * mission's roll on the NFT leaving it. On an NFT its box holds (`boxHolds`), the NFT's own (`nonceOn`), whatever was
361
+ * named; otherwise `prizeRollNonce` of the named one, so no sender reaches the address of a roll an order won.
362
+ */
363
+ export const namedRollNonce = (box, asset, named) => {
364
+ if (!box) throw new Error('A roll on an NFT needs its box, decoded (boxes), to know where its order is made.');
365
+ return boxHolds(box) ? nonceOn(asset) : prizeRollNonce(named);
366
+ };
276
367
 
277
368
  /** Whether a limit for each wallet ({ total, per_window, window }) limits anything. */
278
369
  const limits = (perWallet) => Boolean(perWallet && (Number(perWallet.total) || Number(perWallet.per_window)));
@@ -305,8 +396,8 @@ export const gateAccounts = (project, { gate, wallet, shown, listProof = [] } =
305
396
  ];
306
397
 
307
398
  /**
308
- * A nonce a prize route names (its `nonce` argument), from what names the prize: a grant and its round, a lottery's
309
- * contribution, a contest's entry, an auction and its last bid. Any nonce works (the sender picks it); the roll's order
399
+ * A nonce a prize route names (its `nonce` argument), from what names the prize: a grant, its wallet and rounds, a
400
+ * contest and its player, an auction and its last bid. Any nonce works (the sender picks it); the roll's order
310
401
  * is then at `prizeRollNonce` of it (plus the place where the route says so), which no roll on an NFT is at.
311
402
  */
312
403
  export const prizeNonce = (...parts) => {
@@ -345,17 +436,15 @@ function pieceMetas(p) {
345
436
  * Tokens the mint (written when minted), the pool when they come from one, the winner's account of the mint;
346
437
  * Item the item; for a collectible, its collection (the Item's), its address at the item's next serial, Metaplex Core;
347
438
  * Nft the stash, the NFT as `piece` takes it, then a compressed NFT's proof;
348
- * Roll the box, then the order its roll is made in, at `prizeRollNonce(nonce)`; on a route the player pays or
349
- * asks for (`strict`: a buy, a craft, a grant's claim), then the box's prize mints (`rollMints`);
439
+ * Roll the box, then the order its roll is made in, at `prizeRollNonce(nonce)` (what the roll can give is read
440
+ * when it is asked: `askRollIx`);
350
441
  * Held the lottery's own stash (`lottery`: the lottery's address), the NFT as `piece` takes it, then its proof.
351
442
  * `poolMints` maps a pool's index to its mint (null for a pool never opened, which gives nothing: `projectMint` stands in
352
443
  * for its mint); `items` an item's id to its decoded Item (for its next serial; `made`
353
444
  * collectibles of the same item already made earlier in the instruction); `nft` the NFT's kind, as `piece` takes it
354
- * ({ collection, tokenMetadata, compressed }). A strict roll's `boxes` maps the box's id to its decoded LootBox (null
355
- * for one never written), and `rollPools` each pool its prizes name to its mint (null for one never opened;
356
- * `poolMints` when not given).
445
+ * ({ collection, tokenMetadata, compressed }).
357
446
  */
358
- export function prizeMetas({ project, collection, prize, winner, nonce = 0n, poolMints = {}, items = {}, nft = {}, made = 0, projectMint, programs, lottery, strict = false, boxes = {}, rollPools = poolMints }) {
447
+ export function prizeMetas({ project, collection, prize, winner, nonce = 0n, poolMints = {}, items = {}, nft = {}, made = 0, projectMint, programs, lottery }) {
359
448
  if (prize.Tokens) {
360
449
  const { from } = prize.Tokens;
361
450
  // A pool never opened (its mint given as null) gives nothing: any mint stands in for its place, the project's.
@@ -398,34 +487,47 @@ export function prizeMetas({ project, collection, prize, winner, nonce = 0n, poo
398
487
  meta(BUBBLEGUM_CORE_SIGNER, false, false),
399
488
  ];
400
489
  }
401
- const roll = [meta(pda.box(project, prize.Roll[0]), false, false), meta(pda.order(project, winner, prizeRollNonce(nonce)), false, true)];
402
- return strict ? [...roll, ...strictRollMints(project, prize.Roll[0], boxes, rollPools)] : roll;
490
+ return [meta(pda.box(project, prize.Roll[0]), false, false), meta(pda.order(project, winner, prizeRollNonce(nonce)), false, true)];
403
491
  }
404
492
 
405
493
  /**
406
- * The accounts the program reads after a roll's box and order, to check the roll can be asked for, on a route the
407
- * player pays or asks for (a craft's `Prize::Roll` or `RollOn`, a buy's roll, a grant's claim of one): for each of the
408
- * box's token prizes, in order, its pool and then, for a pool opened, the pool's mint; for tokens minted, their mint. The program refuses the whole
409
- * transaction (PrizeHasHook) when any of those mints has a transfer hook, so the player keeps what the route takes.
410
- * `box` is the decoded LootBox (null for one never written: no roll is made, and nothing follows); `pools` maps each
411
- * pool its prizes name to its mint, or null for a pool never opened (its address alone).
494
+ * The roll accounts of a box, as `open_box`, `ask_roll` and the oracle's answer take them after their own: each entry's
495
+ * prizes' (`entryAccounts`: an item, its tables when it is fresh and they fit the box's table budget; a pool; a stash),
496
+ * then each mint its tokens are in and the project's vault for it (`prizeAccounts`). `box` is the decoded LootBox (null
497
+ * for one never written: none). `rolls`: { items (an item's id to its decoded Item, null for none written), poolMints
498
+ * (a pool's index to its mint, null for one never opened), programs }.
412
499
  */
413
- export function rollMints(project, box, pools = {}) {
500
+ export function rollAccounts(project, box, rolls = {}) {
414
501
  if (!box) return [];
415
- return box.entries.flatMap((e) => e.prizes).flatMap((prize) => {
416
- const from = prize.Tokens?.from;
417
- if (from?.Mint) return [meta(from.Mint[0], false, false)];
418
- if (!from?.Pool) return [];
419
- const index = from.Pool[0];
420
- if (!(index in pools)) throw new Error(`a roll of box ${box.box_id} needs the mint of pool ${index} its prizes name (rollPools; null for a pool never opened)`);
421
- return [meta(pda.pool(project, index), false, false), ...(pools[index] ? [meta(pools[index], false, false)] : [])];
422
- });
502
+ const { items = {}, poolMints = {}, programs } = /** @type {{ items?: any, poolMints?: any, programs?: any }} */ (rolls);
503
+ for (const p of box.entries.flatMap((e) => e.prizes)) {
504
+ const id = p.Item?.[0] ?? p.Compressed?.item;
505
+ if (id !== undefined && items[id] === undefined) throw new Error(`a roll of box ${box.box_id ?? ''} needs item ${id} (rolls.items: its decoded Item, or null for none written), for its tables`);
506
+ const pool = p.Tokens?.from.Pool?.[0];
507
+ if (pool !== undefined && !(pool in poolMints)) throw new Error(`a roll of box ${box.box_id ?? ''} needs the mint of pool ${pool} (rolls.poolMints; null for a pool never opened)`);
508
+ }
509
+ return [...entryAccounts(project, box.entries, items), ...prizeAccounts(project, box.entries, poolMints, programs)];
423
510
  }
424
511
 
425
- /** `rollMints` of box `id` on a strict route: `boxes` must hold it (its decoded LootBox, or null for none written). */
426
- function strictRollMints(project, id, boxes, pools) {
427
- if (!(id in boxes)) throw new Error(`a roll of box ${id} on a route the player pays or asks for needs the box (boxes: its decoded LootBox, or null for one never written), for its prize mints`);
428
- return rollMints(project, boxes[id], pools);
512
+ /**
513
+ * What a write or a route that names boxes to roll later passes for them (`order::rollables`): each box once, in the
514
+ * order they first come, and nothing after it. What a roll can give is read when it is asked.
515
+ */
516
+ export function boxesAccounts(project, ids) {
517
+ return [...new Set(ids)].map((id) => meta(pda.box(project, id), false, false));
518
+ }
519
+
520
+ /** The boxes a record names to roll later (`Def::rolls`), in order: a box's entries' rolls, a recipe's rolls and
521
+ * `RollOn`s, a grant's, a lottery's places'. */
522
+ export function defRolls(def) {
523
+ const [kind] = Object.keys(def ?? {});
524
+ const args = def?.[kind]?.args ?? {};
525
+ const prizes = kind === 'Box' ? (args.entries ?? []).flatMap((e) => e.prizes ?? [])
526
+ : kind === 'Recipe' ? (args.outputs ?? []).map((o) => o.Prize?.[0] ?? (o.RollOn ? { Roll: [o.RollOn.box_id] } : null))
527
+ : kind === 'Grant' ? [args.gives]
528
+ : kind === 'Lottery' ? (args.terms?.places ?? args.places ?? []).map((p) => (typeof p === 'object' ? p.prize : null))
529
+ : [];
530
+ return prizes.flatMap((p) => (p?.Roll ? [p.Roll[0]] : []));
429
531
  }
430
532
 
431
533
  /** Bubblegum's signer for its calls to Core, `["mpl_core_cpi_signer"]`. */
@@ -512,6 +614,8 @@ export function recordAddress(project, def) {
512
614
  return pda.recipe(project, body.id);
513
615
  case 'Grant':
514
616
  return pda.grant(project, body.id);
617
+ case 'Table':
618
+ return pda.table(project, body.id);
515
619
  default:
516
620
  throw new Error(`Unknown record kind ${kind}`);
517
621
  }
@@ -527,93 +631,113 @@ export function withDefaults(def) {
527
631
  const body = def[kind];
528
632
  const args = { ...body.args };
529
633
  if (kind === 'Box' || kind === 'Lottery' || kind === 'Grant') args.gate ??= null;
530
- if (kind === 'Item') args.collection ??= NO_ADDRESS;
634
+ if (kind === 'Item') {
635
+ args.collection ??= NO_ADDRESS;
636
+ // A fresh item's traits (`Item::traits`): none for one whose units are all alike.
637
+ args.traits = traitsInCaps(args.traits ?? []);
638
+ }
531
639
  if (kind === 'Box') {
640
+ stored(args.shares);
532
641
  if (!args.currency) throw new Error('A box names the mint its price is in (`currency`).');
533
- // An entry: up to three prizes (`prizeMetas`), whether it burns the payment, and its weight.
642
+ // What one open draws (`LootBox::slots`): none is one draw among every entry.
643
+ args.slots = (args.slots ?? []).map((s) => ({ only: [], distinct: false, share: 0, ...s }));
644
+ // An entry: its prizes (`prizeMetas`) and its weight. A box burns its price with a share to `Burn`.
534
645
  if (!Array.isArray(args.entries)) throw new Error('A box needs its entries (`entries`): what its roll can give, each with its weight.');
535
- args.entries = args.entries.map((e) => ({ prizes: [], burns: false, ...e }));
646
+ args.entries = args.entries.map((e) => ({ prizes: [], ...e }));
536
647
  args.subject ??= null;
537
- args.fates = (args.fates ?? []).map((f) => ({ ...f, shifts: (f.shifts ?? []).map((s) => ({ from_now: false, ...s })) }));
648
+ // What it does once an entry can't be given (`WhenOut`): stop selling while any entry couldn't cover the draws of
649
+ // a purchase and those waiting for their answer (the default), or draw among what is left ('DrawLeft').
650
+ args.when_out ??= 'StopSales';
651
+ // A fate's `then`: null sends the NFT back to its player; 'Burn', { Give: [wallet] } or { Stash: [id] }.
652
+ args.fates = (args.fates ?? []).map((f) => ({ ...f, then: f.then ?? null, shifts: (f.shifts ?? []).map((s) => ({ from_now: false, ...s })) }));
538
653
  }
539
654
  if (kind === 'Box' || kind === 'Recipe') {
540
655
  args.window ??= NO_WINDOW;
541
656
  args.per_wallet ??= NO_LIMIT;
542
657
  }
543
- if (kind === 'Recipe') args.raises = (args.raises ?? []).map((r) => ({ from_now: false, ...r }));
658
+ if (kind === 'Recipe') {
659
+ // A raise moves a number on a kept collectible (`place`) as a box's fate does (`Shift`); a need tests one (`Test`).
660
+ args.raises = (args.raises ?? []).map(({ place, shift, ...flat }) => ({ place, shift: { from_now: false, most: 0, ...(shift ?? flat) } }));
661
+ args.needs = (args.needs ?? []).map(({ place, key, test, at_least: least }) => ({ place, key, test: test ?? { AtLeast: [least ?? 0] } }));
662
+ }
544
663
  if (kind === 'Box' && args.fall) args.fall = { shared: false, ...args.fall };
545
664
  if (kind === 'Lottery') {
546
- args.per_wallet ??= 0;
547
- args.nft_ticket ??= null;
548
- args.returns ??= false;
549
- args.min_sold ??= 0;
550
- // A place is { bps, roll }; a number is a share of the pot that rolls nothing.
551
- args.places = (args.places ?? []).map((p) => (typeof p === 'object' ? { prize: null, ...p } : { bps: p, prize: null }));
665
+ // Its terms (`LotteryTerms`, what a round copies at its first purchase), given in `terms` or beside `every`.
666
+ const { every, gate, per_wallet: perWallet, terms: given, ...flat } = args;
667
+ const terms = { nft_ticket: null, returns: false, min_sold: 0, ticket_mint: NO_ADDRESS, ...flat, ...given };
668
+ // A run of places alike is { bps, prize, times }; a number is one place's share of the pot that gives nothing else.
669
+ terms.places = (terms.places ?? []).map((p) => (typeof p === 'object' ? { prize: null, times: 1, ...p } : { bps: p, prize: null, times: 1 }));
670
+ terms.shares = stored(terms.shares ?? []);
671
+ return { [kind]: { ...body, args: { terms, every: every ?? 0n, gate: gate ?? null, per_wallet: perWallet ?? 0 } } };
552
672
  }
553
673
  if (kind === 'Grant') {
554
674
  args.key_root ??= new Uint8Array(32);
555
675
  args.open ??= false;
676
+ args.shard_bits ??= 0;
556
677
  }
557
678
  return { [kind]: { ...body, args } };
558
679
  }
559
680
 
560
681
  /**
561
682
  * The owner writes one record (`def`: { Item: { id, args } }, { Box: … }, …), at its own address. A lottery names
562
- * its pot's mint, its pot (which its first write opens), its ticket mint, its
563
- * funds and both token programs.
564
- */
565
- export function upsertIx({ owner, project, def: given, lottery = null }) {
683
+ * its pot's mint, its pot (which its first write opens), its ticket mint, both token programs and the round it waits on
684
+ * (`lotteryState`: the lottery as it is, null for a new one; `idleRound`: its current round's record, or round 0's once
685
+ * it drew once and ended: a lottery made again, edited once ended or made to repeat waits while that round runs or
686
+ * still owes NFTs of its own stash), then its own stash when a place holds or draws an NFT of it. A box, a recipe, a grant or a lottery that
687
+ * names a box to roll later passes that box after (`boxesAccounts`), which must be written: a box is written before
688
+ * anything names it (it may name itself).
689
+ */
690
+ export function upsertIx({ owner, project, def: given, lotteryState = null }) {
566
691
  const def = withDefaults(given);
567
692
  const [kind] = Object.keys(def);
568
693
  const { args } = def[kind];
569
694
  let remaining = [];
695
+ // A collectible names its collection, read for Bubblegum's plugin (`Item::compressed`: its compressed prizes can be
696
+ // minted only when the collection carries it).
697
+ if (kind === 'Item' && args.kind === 'Collectible') remaining = [meta(args.collection, false, false)];
570
698
  // A new sealed schedule opens its own vault: its mint, the vault and both token programs.
571
699
  const vest = kind === 'Schedule' ? args.action?.from?.Vest?.[0] : null;
572
700
  if (vest) {
701
+ // It pays a wallet, the treasury or a burn: no account it pays is named.
573
702
  remaining = [meta(vest, false, false), meta(pda.vest(recordAddress(project, def)), false, true), meta(TOKEN_PROGRAM, false, false), meta(TOKEN_2022_PROGRAM, false, false)];
574
- // A pool or a token account it pays comes next, checked now to be of the vault's mint (`open_vest`).
575
- const to = args.action.to;
576
- if (to?.Pool) remaining.push(meta(pda.pool(project, to.Pool[0]), false, false));
577
- else if (to?.Account) remaining.push(meta(to.Account[0], false, false));
578
- // A lottery's pot (the only account of the project's it may pay) comes with its lottery (`lottery`).
579
- if (to?.Account && lottery) remaining.push(meta(lottery, false, false));
580
703
  }
581
704
  if (kind === 'Lottery') {
582
705
  remaining = [
583
- meta(args.mint, false, false),
706
+ meta(args.terms.mint, false, false),
584
707
  meta(pda.pot(recordAddress(project, def)), false, true),
585
- meta(args.ticket_mint === NO_ADDRESS ? args.mint : args.ticket_mint, false, false),
586
- meta(pda.lotteryFunds(recordAddress(project, def)), false, true),
708
+ meta(paymentMint(args.terms), false, false),
587
709
  meta(TOKEN_PROGRAM, false, false),
588
710
  meta(TOKEN_2022_PROGRAM, false, false),
711
+ meta(lotteryState ? idleRound(recordAddress(project, def), lotteryState) : SYSTEM_PROGRAM, false, false),
589
712
  ];
590
- // A place that holds an NFT (`Prize::Held`): the lottery's own stash, anywhere after them.
591
- if (args.places.some((p) => p.prize?.Held)) remaining.push(meta(pda.lotteryStash(recordAddress(project, def)), false, false));
713
+ // A place that holds or draws an NFT of its own stash (`Prize::Held`, `Prize::Raffled`): that stash, after them.
714
+ if (args.terms.places.some((p) => p.prize?.Held || p.prize === 'Raffled')) remaining.push(meta(pda.lotteryStash(recordAddress(project, def)), false, false));
592
715
  }
716
+ // Each box it names to roll later (a box's, a recipe's, a grant's, a lottery's places'), once.
717
+ remaining.push(...boxesAccounts(project, defRolls(def)));
593
718
  return lootIx('upsert', { def }, { owner, project, target: recordAddress(project, def) }, { remaining });
594
719
  }
595
720
 
596
721
  /**
597
722
  * The owner closes the record at `target`, and its rent comes back: a grant's with the funds it held. A lottery
598
723
  * (`lottery`: its decoded record) sends what its pot holds to `to`, a token account of the pot's mint (the owner's own
599
- * by default), and closes the pot.
724
+ * by default), and closes the pot, once its current round has no record (one that drew once and ended: once its round 0
725
+ * gave out every NFT of its own stash, `idleRound`).
600
726
  */
601
- export function closeRecordIx({ owner, project, target, lottery, schedule, programs, to, paymentTo, extra = [] }) {
727
+ export function closeRecordIx({ owner, project, target, lottery, schedule, programs, to, extra = [] }) {
602
728
  // A sealed schedule never closes (CannotClose): its address, and its vault's, stay bound to its terms.
603
729
  if (schedule?.action?.from?.Vest) throw new Error('A sealed schedule never closes: its vault stays bound to its terms.');
604
730
  if (!lottery) return lootIx('upsert', { def: null }, { owner, project, target });
605
- const tokenProgram = tokenProgramOf(programs, lottery.mint);
606
- const paymentMint = lotteryPaymentMint(lottery);
731
+ const { mint } = lottery.terms;
732
+ const tokenProgram = tokenProgramOf(programs, mint);
607
733
  const remaining = [
734
+ meta(idleRound(target, lottery), false, false),
608
735
  meta(pda.pot(target), false, true),
609
- meta(lottery.mint, false, true),
610
- meta(to ?? ata(owner, lottery.mint, tokenProgram), false, true),
611
- meta(pda.lotteryFunds(target), false, true),
612
- meta(paymentMint, false, true),
613
- meta(paymentTo ?? ata(owner, paymentMint, tokenProgramOf(programs, paymentMint)), false, true),
736
+ meta(mint, false, true),
737
+ meta(to ?? ata(owner, mint, tokenProgram), false, true),
614
738
  meta(TOKEN_PROGRAM, false, false),
615
739
  meta(TOKEN_2022_PROGRAM, false, false),
616
- ...extra,
740
+ ...tailOf(extra),
617
741
  ];
618
742
  return lootIx('upsert', { def: null }, { owner, project, target }, { remaining });
619
743
  }
@@ -630,7 +754,7 @@ export function payIx({ player, project, mint, treasury, amount, shares = [], re
630
754
  'pay',
631
755
  { amount, shares, reference: reference(ref) },
632
756
  { player, project, mint, treasury, treasury_token: ata(treasury, mint, tokenProgram), player_token: playerToken ?? ata(player, mint, tokenProgram) },
633
- { remaining: [...shareAccounts(project, shares, mint, { holders, programs, treasury }), ...extra], writable: sharesBurn(shares) ? ['mint'] : [] },
757
+ { remaining: [...shareAccounts(project, shares, mint, { holders, programs, treasury }), ...tailOf(extra)], writable: sharesBurn(shares) ? ['mint'] : [] },
634
758
  );
635
759
  }
636
760
 
@@ -640,16 +764,18 @@ export function payIx({ player, project, mint, treasury, amount, shares = [], re
640
764
  * prizes take tokens from to its mint (a pool's tokens won wait in the project's vault for that mint); one not given
641
765
  * is taken to be in the box's currency, one given as null is closed. `shown`: an NFT the player shows for the box's
642
766
  * gate or odds; `listProof`: for a list gate, the proof's nodes. `chosenRecipient`: the wallet (or, for a share with an
643
- * item, the collectible, `chosen`) the buyer names for a share the box lets them choose. A box of one entry with
644
- * nothing random is rolled at once and takes no oracle accounts (a box that needs no roll at all sells with `buyIx`).
767
+ * item, the collectible, `chosen`) the buyer names for a share the box lets them choose. Every open asks the oracle,
768
+ * even of a box with nothing random (which sells in one step with `buyIx`, when it rolls on no NFT).
645
769
  * The player opens the project's vault for the price's mint, and for each prize's, when it is not there. A box that
646
- * limits each wallet counts the purchase in the player's receipt (its rent theirs).
770
+ * limits each wallet counts the purchase in the player's counts (`pda.counts`, its rent theirs).
647
771
  */
648
- export function openBoxIx({ player, project, box, boxAddress, nonce, maxPrice, shown, listProof, subject, chosenRecipient = null, chosen = null, poolMints, playerToken, programs, extra = [] }) {
772
+ export function openBoxIx({ player, project, box, boxAddress, nonce, maxPrice, count = 1, items = {}, shown, listProof, subject, chosenRecipient = null, chosen = null, settler = NOBODY_KEY, poolMints, playerToken, programs, extra = [] }) {
649
773
  const mint = box.currency;
650
774
  poolMints = boxPoolMints(box, poolMints);
651
775
  const tokenProgram = tokenProgramOf(programs, mint);
652
- const rolled = boxRolls(box.entries);
776
+ // `count` opens in one purchase (a box that packs), signed for `maxPrice` (the price times `count` when not given:
777
+ // pass the sum for a box whose price rises with each sale); `items` maps an item a prize names to its decoded Item,
778
+ // for a fresh one's tables (a box whose prizes are all alike needs none).
653
779
  const address = boxAddress ?? pda.box(project, box.box_id);
654
780
  // A box that rolls on an NFT takes it (`subject`: { asset, collection, hasAuthority, tokenMetadata, compressed }, as
655
781
  // `piece` takes it); any other, its own address stands in.
@@ -663,7 +789,7 @@ export function openBoxIx({ player, project, box, boxAddress, nonce, maxPrice, s
663
789
  const gate = [...gateAccounts(project, { gate: box.gate, wallet: player, listProof }), ...(box.subject ? on.proof : shownAccounts(project, shown))];
664
790
  return lootIx(
665
791
  'open_box',
666
- { nonce, max_price: maxPrice ?? box.price, chosen_recipient: chosenRecipient, version: box.version },
792
+ { nonce, max_price: maxPrice ?? box.price * BigInt(count), chosen_recipient: chosenRecipient, version: box.version, count, settler },
667
793
  {
668
794
  loot_box: address,
669
795
  player,
@@ -671,56 +797,68 @@ export function openBoxIx({ player, project, box, boxAddress, nonce, maxPrice, s
671
797
  mint,
672
798
  player_token: playerToken ?? ata(player, mint, tokenProgram),
673
799
  vault: vault(project, mint, tokenProgram),
674
- oracle_queue: rolled ? undefined : null,
675
- program_identity: rolled ? undefined : null,
676
- vrf_program: rolled ? undefined : null,
677
- slot_hashes: rolled ? undefined : null,
678
800
  chosen,
679
- receipt: limits(box.per_wallet) ? pda.receipt(address, player) : null,
801
+ counts: limits(box.per_wallet) ? pda.counts(project, player) : null,
680
802
  ...on.accounts,
681
803
  },
682
- { remaining: [...entryAccounts(project, box.entries), ...prizeAccounts(project, box.entries, poolMints, programs), ...gate, ...extra], writable: on.writable },
804
+ { remaining: [...entryAccounts(project, box.entries, items), ...prizeAccounts(project, box.entries, poolMints, programs), ...gate, ...tailOf(extra)], writable: on.writable },
683
805
  );
684
806
  }
685
807
 
686
808
  /**
687
- * Buys a box that needs no roll (one entry, nothing random, no NFT to roll on: `boxRolls` false and no subject) in one
688
- * instruction: the price is split at once (or burned, for an entry that burns) and the entry's prizes are given to
689
- * the player (`prizeMetas`: `items` maps an item's id to its decoded Item, `poolMints` a pool's index to its mint,
690
- * `nfts` an NFT a prize names to its kind; a roll's `boxes` and `rollPools`, for the rolled box's prize mints). No
691
- * order. A prize that can't be given refuses the purchase. `treasury` is
692
- * the project's treasury wallet; `holders` maps a collectible a share pays to its holder; a roll the entry gives is
693
- * made at `nonce` (the next one for each roll after). `shown`/`listProof` pass the box's gate.
809
+ * Buys `count` of a box that needs no roll (one entry, nothing random, no NFT to roll on: `boxRolls` false and no
810
+ * subject) in one instruction, any number of draws each (a fixed pack): each at the price the one before it left, all
811
+ * within `maxPrice` (the price times `count` when not given: pass the sum for a box whose price rises with each sale).
812
+ * The price is split at once (a share may burn it) and the entry's prizes are given to the player: a fixed amount of
813
+ * tokens once, the whole of every draw's, and every other prize once for each draw of each sale, its accounts repeated
814
+ * (`prizeMetas`: `items` maps an item's id to its decoded Item, `poolMints` a pool's index to its mint, `nfts` an NFT a
815
+ * prize names to its kind). No order. A prize that can't be given refuses the purchase. `treasury` is the project's
816
+ * treasury wallet; `holders` maps a collectible a share pays to its holder; each roll the entry gives is made at the
817
+ * next nonce from `nonce` (`buyRolls`). `shown`/`listProof` pass the box's gate.
694
818
  */
695
- export function buyIx({ player, project, box, boxAddress, maxPrice, treasury, chosenRecipient = null, chosen = null, nonce = 0n, shown, listProof, items = {}, poolMints = {}, nfts = {}, boxes = {}, rollPools = poolMints, holders, playerToken, programs, extra = [] }) {
819
+ export function buyIx({ player, project, box, boxAddress, maxPrice, count = 1, treasury, chosenRecipient = null, chosen = null, nonce = 0n, shown, listProof, items = {}, poolMints = {}, nfts = {}, holders, playerToken, programs, extra = [] }) {
696
820
  const mint = box.currency;
697
821
  const tokenProgram = tokenProgramOf(programs, mint);
698
822
  const address = boxAddress ?? pda.box(project, box.box_id);
699
- const [entry] = box.entries;
700
823
  const shares = chosenShares(box.shares, chosenRecipient);
701
824
  const made = {};
702
- const prizes = entry.prizes.flatMap((prize, k) => {
825
+ const prizes = buyGives(box, count).flatMap(({ prize, k }) => {
703
826
  const item = prize.Item?.[0];
704
- const lines = prizeMetas({ project, prize, winner: player, nonce: toBigInt(nonce) + BigInt(k), poolMints, items, nft: nfts[prize.Nft?.nft], made: made[item] ?? 0, projectMint: mint, programs, strict: true, boxes, rollPools });
827
+ const lines = prizeMetas({ project, prize, winner: player, nonce: toBigInt(nonce) + BigInt(k), poolMints, items, nft: nfts[prize.Nft?.nft], made: made[item] ?? 0, projectMint: mint, programs });
705
828
  if (item !== undefined) made[item] = (made[item] ?? 0) + 1;
706
829
  return lines;
707
830
  });
708
831
  return lootIx(
709
832
  'buy',
710
- { max_price: maxPrice ?? box.price, chosen_recipient: chosenRecipient, version: box.version, nonce: toBigInt(nonce) },
833
+ { max_price: maxPrice ?? box.price * BigInt(count), chosen_recipient: chosenRecipient, version: box.version, nonce: toBigInt(nonce), count },
711
834
  {
712
835
  player, project, loot_box: address, mint,
713
836
  player_token: playerToken ?? ata(player, mint, tokenProgram),
714
837
  treasury, treasury_token: ata(treasury, mint, tokenProgram),
715
- chosen, receipt: limits(box.per_wallet) ? pda.receipt(address, player) : null,
838
+ chosen, counts: limits(box.per_wallet) ? pda.counts(project, player) : null,
716
839
  },
717
840
  {
718
- remaining: [...gateAccounts(project, { gate: box.gate, wallet: player, shown, listProof }), ...shareAccounts(project, shares, mint, { holders, programs, treasury }), ...prizes, ...extra],
719
- writable: entry.burns || sharesBurn(box.shares) ? ['mint'] : [],
841
+ remaining: [...gateAccounts(project, { gate: box.gate, wallet: player, shown, listProof }), ...shareAccounts(project, shares, mint, { holders, programs, treasury }), ...prizes, ...tailOf(extra)],
842
+ writable: sharesBurn(box.shares) ? ['mint'] : [],
720
843
  },
721
844
  );
722
845
  }
723
846
 
847
+ /**
848
+ * What a `buy` of `count` of `box` gives, in the order it gives it (`buy.rs`): each prize of its one entry in turn, a
849
+ * fixed amount of tokens once (`times` the box's draws × `count`), any other prize once for each of those draws. `k`
850
+ * counts the gives: a roll given at `k` is made at the buy's `nonce + k` (its order at `prizeRollNonce` of it).
851
+ */
852
+ export function buyGives(box, count = 1) {
853
+ const draws = boxDraws(box) * count;
854
+ const out = [];
855
+ for (const prize of box.entries[0]?.prizes ?? []) {
856
+ const once = Boolean(prize.Tokens);
857
+ for (let i = 0; i < (once ? 1 : draws); i++) out.push({ prize, k: out.length, times: once ? draws : 1 });
858
+ }
859
+ return out;
860
+ }
861
+
724
862
  /**
725
863
  * The oracle's answer to a box order (for a local simulation: on chain MagicBlock's oracle sends it). The accounts
726
864
  * are the ones `open_box` handed the oracle: the order, the box, the roll's ticket (named by its first ask,
@@ -729,38 +867,64 @@ export function buyIx({ player, project, box, boxAddress, maxPrice, treasury, ch
729
867
  * for a prize in a mint with a transfer hook, the hook's accounts (`extra`). The buyer's odds boosts were fixed at
730
868
  * purchase.
731
869
  */
732
- export function callbackRollIx({ order, orderState, box, boxAddress, randomness, project, poolMints, programs, extra = [] }) {
870
+ export function callbackRollIx({ order, orderState, box, boxAddress, randomness, project, poolMints, items = {}, programs, extra = [] }) {
733
871
  const projectAddress = project ?? orderState.project;
734
872
  const keys = [
735
873
  meta(pda.oracleIdentity(), true, false),
736
874
  meta(order, false, true),
737
- meta(boxAddress ?? orderState.loot_box, false, false),
875
+ // Written: the order's draws leave the box's `in_flight`.
876
+ meta(boxAddress ?? orderState.loot_box, false, true),
738
877
  meta(ticketOf(order, orderState.asked_first), false, false),
739
- ...entryAccounts(orderState.project, box.entries),
878
+ ...entryAccounts(orderState.project, box.entries, items),
740
879
  ];
741
880
  const prizes = prizeAccounts(orderState.project, box.entries, boxPoolMints(box, poolMints), programs);
742
881
  if (prizes.length) keys.push(meta(TOKEN_PROGRAM, false, false), meta(TOKEN_2022_PROGRAM, false, false), meta(projectAddress, false, false), ...prizes);
743
- keys.push(...extra);
882
+ keys.push(...tailOf(extra));
744
883
  return { programId: LOOT_PROGRAM, keys, data: encodeInstruction(IDL, 'callback_roll', { randomness: Uint8Array.from(randomness) }) };
745
884
  }
746
885
 
747
886
  /**
748
887
  * The accounts of what an order won (`order.won`), in order, as `settle` hands each over:
749
- * Item the item; for a collectible (`items[id]` its decoded Item), its collection, its address, Metaplex Core
888
+ * Item the item; for a collectible (`items[id]` its decoded Item), its collection, its address, Metaplex Core;
889
+ * for a fresh one (`drawn` set), then the table of each of its table traits
750
890
  * Tokens the mint, the project's vault for it, the player's account of it (`prizeTo`, when given)
751
891
  * Nft the NFT as `piece` takes it (`nfts[nft]` its kind), going to the player, then a compressed NFT's proof
752
- * Roll the box, then the order its roll is made in (`rollNonce`)
892
+ * Compressed the item, the tree's config, the tree, the collection, Bubblegum's programs; for a fresh one, then its
893
+ * tables and the Sheet of the NFT it mints
753
894
  */
754
- export function wonAccounts({ orderAddress, order, items = {}, nfts = {}, prizeTo, putBack = [], programs }) {
895
+ export function wonAccounts({ order, items = {}, nfts = {}, prizeTo, putBack = [], programs, from = 0, upto = order.won.length, tree, treeMinted = 0n, unminted = [] }) {
755
896
  const project = order.project;
756
- return order.won.flatMap((won, k) => {
897
+ // A compressed prize is minted into `tree` (any of the project's with room), the next NFT it mints: after
898
+ // `treeMinted`, and the compressed prizes before it in this part that were minted. One of an item whose collection
899
+ // lacks Bubblegum's plugin (`unminted`: those collections) gives nothing and mints nothing: the next takes its place.
900
+ let minted = BigInt(treeMinted);
901
+ const skips = new Set(unminted);
902
+ return order.won.slice(from, upto).flatMap((won) => {
903
+ if (won.Compressed) {
904
+ const { item: id, drawn } = won.Compressed;
905
+ const address = pda.item(project, id);
906
+ const item = items[id];
907
+ if (!item) throw new Error(`settling item ${id} needs the item (items), for its collection and its traits`);
908
+ const tables = drawn != null ? traitTables(item).map((t) => meta(pda.table(project, t), false, false)) : [];
909
+ if (!tree) throw new Error('settling a compressed prize needs the project tree it is minted into (tree, treeMinted)');
910
+ const asset = pda.compressedAsset(tree, minted);
911
+ if (!skips.has(item.collection)) minted += 1n;
912
+ return [
913
+ meta(address, false, true), meta(pda.treeConfig(tree), false, true), meta(tree, false, true), meta(item.collection, false, true),
914
+ meta(BUBBLEGUM_PROGRAM, false, false), meta(COMPRESSION_PROGRAM, false, false), meta(NOOP_PROGRAM, false, false), meta(MPL_CORE_PROGRAM, false, false), meta(BUBBLEGUM_CORE_SIGNER, false, false),
915
+ // A fresh one's traits are written in the project's Sheet for the NFT it mints.
916
+ ...(drawn != null ? [...tables, meta(pda.sheet(project, asset), false, true)] : []),
917
+ ];
918
+ }
757
919
  if (won.Item) {
758
- const { item: id, serial } = won.Item;
920
+ const { item: id, serial, drawn } = won.Item;
759
921
  const address = pda.item(project, id);
760
922
  const item = items[id];
761
923
  if (!item) throw new Error(`settling item ${id} needs the item (items), for its kind and collection`);
762
924
  if (item.kind !== 'Collectible') return [meta(address, false, true)];
763
- return [meta(address, false, true), meta(item.collection, false, true), meta(pda.itemAsset(address, serial), false, true), meta(MPL_CORE_PROGRAM, false, false)];
925
+ // A fresh unit's traits come from the rows its tables drew at the roll.
926
+ const tables = drawn != null ? traitTables(item).map((t) => meta(pda.table(project, t), false, false)) : [];
927
+ return [meta(address, false, true), meta(item.collection, false, true), meta(pda.itemAsset(address, serial), false, true), meta(MPL_CORE_PROGRAM, false, false), ...tables];
764
928
  }
765
929
  if (won.Tokens) {
766
930
  const { mint } = won.Tokens;
@@ -774,14 +938,57 @@ export function wonAccounts({ orderAddress, order, items = {}, nfts = {}, prizeT
774
938
  const p = piece({ project, asset, wallet: order.player, ...nfts[asset], ...ONLY_MOVES });
775
939
  return [...pieceMetas(p), ...p.proof];
776
940
  }
777
- return [meta(pda.box(project, won.Roll[0]), false, false), meta(pda.order(project, order.player, rollNonce(orderAddress, order.requested_slot, k)), false, true)];
941
+ throw new Error(`settling an order: a prize of a kind this client doesn't know (${Object.keys(won)[0]})`);
778
942
  });
779
943
  }
780
944
 
945
+ /** The calls a transaction may make, its own instructions and every call inside them (Solana's instruction trace). */
946
+ export const TRACE_LIMIT = 64;
947
+ /** The calls a settle makes before its prizes: the compute budget's, its own, and the payment's at most. */
948
+ const SETTLE_BASE_CALLS = 6;
949
+
950
+ /** The calls one prize makes when a settle hands it over, measured on the program (`tests/limits.rs`): a collectible's
951
+ * mint by Core (4, so 14 a transaction), a compressed one's by Bubblegum (5) and with its Sheet (7), tokens (2), an NFT
952
+ * moved (3). `items` maps an item's id to its decoded Item (a normal item makes none). */
953
+ export function prizeCalls(won, items = {}) {
954
+ if (won.Item) return won.Item.drawn == null && items[won.Item.item]?.kind === 'Normal' ? 0 : 4;
955
+ if (won.Compressed) return won.Compressed.drawn != null ? 7 : 5;
956
+ if (won.Tokens) return 2;
957
+ return 3;
958
+ }
959
+
960
+ /** Where a settle part that starts at `from` ends: as many of the order's prizes as fit the instruction trace (an
961
+ * NFT's move asks for the bigger heap, so at most three a part). Each part is its own transaction; anyone sends any. */
962
+ export function settlePartEnd(order, from = Number(order.settled ?? 0), items = {}) {
963
+ let calls = SETTLE_BASE_CALLS;
964
+ let nfts = 0;
965
+ let end = from;
966
+ while (end < order.won.length) {
967
+ const won = order.won[end];
968
+ const next = prizeCalls(won, items);
969
+ if (end > from && (calls + next > TRACE_LIMIT || (won.Nft && nfts >= 3))) break;
970
+ calls += next;
971
+ nfts += won.Nft ? 1 : 0;
972
+ end++;
973
+ }
974
+ return end;
975
+ }
976
+
977
+ /** How many draws one open of `box` makes: its slots' counts, or one. */
978
+ export const boxDraws = (box) => (box.slots?.length ? box.slots.reduce((n, s) => n + Number(s.count), 0) : 1);
979
+
980
+ /** How many settle parts an order of `opens` opens of `box` takes at most: each card, at its most calls, and with
981
+ * shares one more (the first call splits the payment, alone when no prize fits beside its shares: `upto` 0). */
982
+ export function settleParts(box, opens = 1) {
983
+ const draws = box.slots?.length ? box.slots.reduce((n, s) => n + Number(s.count), 0) : 1;
984
+ const cards = Math.max(1, ...box.entries.map((e) => e.prizes.length)) * draws * Math.max(1, opens);
985
+ return Math.max(1, Math.ceil(cards / Math.floor((TRACE_LIMIT - SETTLE_BASE_CALLS) / 7))) + ((box.shares ?? []).length ? 1 : 0);
986
+ }
987
+
781
988
  /** The heap a settle that moves three or more NFTs asks for (`settleHeapFrame`): all a transaction may have, 256 KiB. */
782
989
  export const LARGE_SETTLE_HEAP = 256 * 1024;
783
990
 
784
- /** Whether an order rolled on an NFT: its `subject` names one (the default address for none). */
991
+ /** Whether an order rolled on an NFT: its `subject` names one (null for none). */
785
992
  const rolledOn = (order) => Boolean(order.subject) && order.subject !== NO_ADDRESS;
786
993
 
787
994
  /**
@@ -810,32 +1017,50 @@ export const MAX_COMPUTE_UNITS = 1_400_000;
810
1017
  * them). A limit costs nothing more when no priority fee is set.
811
1018
  */
812
1019
  export function settleComputeUnits(order) {
813
- const moves = (order.won ?? []).some((w) => w.Nft) || rolledOn(order);
1020
+ // An NFT moved, an NFT rolled on, a compressed mint (Bubblegum's 60,000 to 90,000 units each) or a pack's part of
1021
+ // many cards takes all a transaction may have.
1022
+ const won = order.won ?? [];
1023
+ const moves = won.some((w) => w.Nft || w.Compressed) || won.length > 8 || rolledOn(order);
814
1024
  return moves ? MAX_COMPUTE_UNITS : 800_000;
815
1025
  }
816
1026
 
1027
+ /** `settle`'s `upto` for its last part: past the end of any order. The last part gives what is left, pays back the share
1028
+ * of the draws the roll didn't make, carries out the roll on an NFT and closes the order. */
1029
+ export const SETTLE_LAST = 4_294_967_295;
1030
+
817
1031
  /**
818
1032
  * Carries out a rolled order: hands over what its roll set aside (`wonAccounts`: a collectible minted, the project's
819
- * backend delivering a normal item from the receipt; tokens from the project's vault; an NFT taken out of its stash;
820
- * a roll made for the player), then splits the payment, or burns it for an entry that burns. Anyone sends it, for the
821
- * order at `orderAddress`; `signer` opens any account it pays into that is not there, and pays a roll's order. `order`
822
- * is the decoded Order; `projectState` the decoded Project; `items` and `nfts` as `wonAccounts` takes them. `putBack`:
823
- * NFTs won that the player, signing, puts back into their stash in place of taking them (one its collection won't let move).
824
- * `handBack`: the wallet the box's fate gives the NFT rolled on to (`receiverOf`) is an account of the loot program
825
- * (`handsBack` of it as read), so the program gives it back to the player, and a Token Metadata NFT's `token` is the
826
- * player's. `extra`: a hook's or a memo's accounts.
827
- */
828
- export function settleIx({ signer, orderAddress, order, projectState, items, nfts, prizeTo, putBack, subject, handBack = false, holders, programs, extra = [] }) {
1033
+ * backend delivering a normal item from the receipt; tokens from the project's vault; an NFT taken out of its stash),
1034
+ * then splits the payment (a share may burn it). The order's player, its settler or the project's keys send it, for the order at `orderAddress`; `signer` pays all
1035
+ * the settle opens or mints (Core assets, token accounts, Sheets, a stash's row, the fate's writes: the order lends
1036
+ * nothing, `settleRent`). `order` is the decoded Order; `projectState` the decoded Project; `items` and `nfts` as
1037
+ * `wonAccounts` takes them. `putBack`: NFTs won that the player, signing, puts back into their stash in place of taking
1038
+ * them (one its collection won't let move). `handBack`: the wallet the box's fate gives the NFT rolled on to
1039
+ * (`receiverOf`) is an account of the loot program (`handsBack` of it as read), so the program gives it back to the
1040
+ * player, and a Token Metadata NFT's `token` is the player's. `extra`: a hook's or a memo's accounts.
1041
+ *
1042
+ * `upto`, the part (`settle.rs`): `SETTLE_LAST` (the default, past the end) is the last part, all that is left (maybe
1043
+ * nothing); 0 gives no prize and only splits the payment; any other gives the prizes from where the last part stopped
1044
+ * (`settled`) up to it, and nothing more. The first call, whichever it is, splits the payment (its shares' accounts
1045
+ * ride on it).
1046
+ */
1047
+ export function settleIx({ signer, orderAddress, order, projectState, items, nfts, prizeTo, putBack, subject, handBack = false, holders, programs, upto = SETTLE_LAST, tree, treeMinted, unminted, extra = [] }) {
829
1048
  const project = order.project;
1049
+ const from = Number(order.settled ?? 0);
1050
+ const last = Number(upto) > order.won.length;
1051
+ const end = last ? order.won.length : Math.max(from, Number(upto));
1052
+ // The payment is split once, by the first call (`drew` is 0 once it is made).
1053
+ const splits = Number(order.drew ?? 0) > 0;
1054
+ const prizes = wonAccounts({ order, items, nfts, prizeTo, putBack, programs, from, upto: end, tree, treeMinted, unminted });
830
1055
  const tokenProgram = tokenProgramOf(programs, order.mint);
831
- const burns = order.burns || sharesBurn(order.shares);
832
1056
  // The NFT it rolled on, as the settle moves it: given to a wallet that is an account of the loot program, it goes
833
1057
  // back to its player (`handsBack`), into the player's token account for a Token Metadata NFT.
834
- const receiver = receiverOf(order);
835
- const on = orderPiece(project, orderAddress, order, subject, handBack && order.fate?.then?.Give ? order.player : receiver);
1058
+ // A part before the last gives only its prizes (and the first, the split): no NFT rolled on (its piece a stand-in).
1059
+ const receiver = last ? receiverOf(order) : null;
1060
+ const on = last ? orderPiece(project, orderAddress, order, subject, handBack && order.fate?.then?.Give ? order.player : receiver) : standIn(orderAddress);
836
1061
  return lootIx(
837
1062
  'settle',
838
- {},
1063
+ { upto: Number(upto), prize_accounts: prizes.length },
839
1064
  {
840
1065
  signer,
841
1066
  order: orderAddress,
@@ -847,10 +1072,12 @@ export function settleIx({ signer, orderAddress, order, projectState, items, nft
847
1072
  treasury_token: ata(projectState.treasury, order.mint, tokenProgram),
848
1073
  ...on.accounts,
849
1074
  receiver,
1075
+ // The last part of an order whose roll left a draw unmade or part given gives its share of the payment back here.
1076
+ player_token: BigInt(order.back ?? 0) > 0n && last ? ata(order.player, order.mint, tokenProgram) : null,
850
1077
  },
851
1078
  {
852
- remaining: [...on.proof, ...wonAccounts({ orderAddress, order, items, nfts, prizeTo, putBack, programs }), ...shareAccounts(project, order.shares, order.mint, { holders, programs, treasury: projectState.treasury }), ...extra],
853
- writable: [...(burns ? ['mint'] : []), ...on.writable],
1079
+ remaining: [...on.proof, ...prizes, ...(splits ? shareAccounts(project, order.shares, order.mint, { holders, programs, treasury: projectState.treasury }) : []), ...tailOf(extra)],
1080
+ writable: [...(sharesBurn(order.shares) && splits ? ['mint'] : []), ...on.writable],
854
1081
  },
855
1082
  );
856
1083
  }
@@ -861,10 +1088,10 @@ export function settleIx({ signer, orderAddress, order, projectState, items, nft
861
1088
  */
862
1089
  export function receiverOf(order) {
863
1090
  const then = order.fate?.then;
864
- if (!order.held || !then) return null;
1091
+ if (!order.held || then == null) return null;
865
1092
  if (then.Give) return then.Give[0];
866
1093
  if (then.Stash) return pda.stash(order.project, then.Stash[0]);
867
- return then === 'Burn' || then.Burn ? INCINERATOR : null;
1094
+ return then === 'Burn' ? INCINERATOR : null;
868
1095
  }
869
1096
 
870
1097
  /**
@@ -878,7 +1105,7 @@ function orderPiece(project, orderAddress, order, subject = {}, receiver) {
878
1105
  // a stash keeps it where it is.
879
1106
  const shifts = Boolean(order.fate?.shifts?.length);
880
1107
  const then = order.fate?.then;
881
- const burned = order.held && (then === 'Burn' || Boolean(then?.Burn));
1108
+ const burned = order.held && then === 'Burn';
882
1109
  const stays = Boolean(then?.Stash);
883
1110
  return piece({ project, asset: order.subject, wallet: stays ? order.player : receiver ?? order.player, ...subject, moves: order.held && !stays, locks: false, writes: shifts, burns: burned, reads: shifts });
884
1111
  }
@@ -897,8 +1124,9 @@ export const INCINERATOR = '1nc1nerator11111111111111111111111111111111';
897
1124
  /**
898
1125
  * Gives an order's payment back to its player, into their account of the mint (`signer`, anyone, opens it when it is
899
1126
  * not there): an order whose roll failed or never came. A rolled order is settled, never refunded
900
- * (`orderRefundSlot`). A purchase counted against a box's limit for each
901
- * wallet is given back. `extra`: a hook's or a memo's accounts.
1127
+ * (`orderRefundable`). A purchase counted against a box's limit for each
1128
+ * wallet is given back. A purchase that counts its draws in its box's `in_flight` (`order.reserved`, a box that stops
1129
+ * selling when an entry runs out) names the box, whose count it leaves. `extra`: a hook's or a memo's accounts.
902
1130
  */
903
1131
  export function refundIx({ signer, orderAddress, order, subject, programs, extra = [] }) {
904
1132
  const project = order.project;
@@ -918,129 +1146,186 @@ export function refundIx({ signer, orderAddress, order, subject, programs, extra
918
1146
  mint: order.mint,
919
1147
  vault: vault(project, order.mint, tokenProgram),
920
1148
  player_token: ata(order.player, order.mint, tokenProgram),
921
- receipt: order.counted != null ? pda.receipt(order.loot_box, order.player) : null,
1149
+ counts: order.counted != null ? pda.counts(project, order.player) : null,
1150
+ // Required while the order counts draws in its box (`reserved`); an order read without the field names it too.
1151
+ loot_box: order.reserved == null || BigInt(order.reserved) > 0n ? order.loot_box : null,
922
1152
  ...on.accounts,
923
1153
  },
924
- { remaining: [...on.proof, ...extra], writable: on.writable },
1154
+ { remaining: [...on.proof, ...tailOf(extra)], writable: on.writable },
925
1155
  );
926
1156
  }
927
1157
 
928
1158
  /**
929
- * Anyone (`payer`, who pays the oracle) asks for the roll a mission's return, a recipe or a prize made (`order.status`
930
- * Waiting): the box's prizes' accounts, as `openBoxIx` passes them. It lands in `callback_roll`. Returns a list of
931
- * instructions: first `payer` opens the project's vault for each prize mint when it is not there (idempotent; the
932
- * program refuses the ask without them, since tokens won wait there), then the ask.
1159
+ * Its player or the project's keys (`payer`, who pays the oracle) ask for the roll a mission's return, a recipe or a prize made (`order.status`
1160
+ * Waiting): the box's roll accounts, as `openBoxIx` passes them (`items` maps an item a prize names to its decoded
1161
+ * Item, for a fresh one's tables). It lands in `callback_roll`. Returns a list of instructions: first `payer` opens the
1162
+ * project's vault for each prize mint when it is not there (idempotent; the program refuses the ask without them,
1163
+ * since tokens won wait there), then the ask. A box with nothing in stock is refused (`SoldOut`) unless the order's own
1164
+ * player asks in a later slot than the order was made or last asked in: a route that makes a roll and asks for it in
1165
+ * the same transaction fails whole on an empty box, and the player keeps what the route took. The player's own ask on
1166
+ * an empty box ends the roll there, with no oracle: rolled with nothing (an NFT it holds goes home as it was), or failed
1167
+ * for its refund when its winner paid for it (an auction's roll lot). An answer that finds nothing at all in stock puts
1168
+ * a free roll back to waiting (`Waiting`), for a restock or for its player to end it so. Once asked, every later ask
1169
+ * comes in a later slot than the last (`SameSlot`, `rollAskable`).
933
1170
  */
934
- export function askRollIx({ payer, orderAddress, order, box, poolMints, programs }) {
1171
+ export function askRollIx({ payer, orderAddress, order, box, settler = NOBODY_KEY, poolMints, items = {}, programs }) {
935
1172
  poolMints = boxPoolMints(box, poolMints);
936
1173
  const opens = prizeMints(box.entries, poolMints).map(({ mint }) => createAssociatedTokenAccountIdempotent(payer, order.project, mint, tokenProgramOf(programs, mint)));
937
- const ask = lootIx('ask_roll', {}, { payer, order: orderAddress, loot_box: order.loot_box }, {
938
- remaining: [...entryAccounts(order.project, box.entries), ...prizeAccounts(order.project, box.entries, poolMints, programs)],
1174
+ const ask = lootIx('ask_roll', { settler }, { payer, order: orderAddress, loot_box: order.loot_box, project: order.project }, {
1175
+ remaining: [...entryAccounts(order.project, box.entries, items), ...prizeAccounts(order.project, box.entries, poolMints, programs)],
939
1176
  });
940
1177
  return [...opens, ask];
941
1178
  }
942
1179
 
943
- /** The SOL a box order holds for each collectible its roll may mint (`orderEscrow`): its rent, given back unspent. The
944
- * IDL's `ESCROW_LAMPORTS`. */
945
- export const ESCROW_LAMPORTS = BigInt(IDL.constants.find((c) => c.name === 'ESCROW_LAMPORTS').value);
946
-
947
1180
  // ---------------------------------------------------------------- out: the project's tokens move
948
1181
 
949
1182
  /**
950
- * The authority moves the project's tokens: `from` { Pool: [i] } or { Mint: [mint] } (newly minted), `to` a pool, a
951
- * wallet, an account, the treasury (`treasury`, the project's wallet), a collectible's holder or 'Burn', `amount`
952
- * { Fixed: [n] } or { Bps: [bps] } of a pool. The authority opens the account it pays into when it is not there; a
953
- * collectible nobody holds gets nothing.
1183
+ * The authority moves the project's tokens: `from` { Pool: [i] }, { Mint: [mint] } (newly minted) or { Farm: [id] } (what
1184
+ * that farm's rewards vault holds free, beyond what it owes its seats: a fixed amount; `farm`: the farm decoded, so its
1185
+ * record of ends comes when it keeps one), `to` a pool, a wallet, an account, the treasury (`treasury`, the project's
1186
+ * wallet), a collectible's holder or 'Burn', `amount` { Fixed: [n] } or { Bps: [bps] } of a pool. The authority opens the
1187
+ * account it pays into when it is not there; a collectible nobody holds gets nothing.
954
1188
  */
955
- export function moveTokensIx({ authority, project, mint, from, to, amount, reference: ref, holders, programs, treasury, extra = [] }) {
1189
+ export function moveTokensIx({ authority, project, mint, from, to, amount, reference: ref, holders, programs, treasury, farm = null, extra = [] }) {
956
1190
  const writes = Boolean(from.Mint) || to === 'Burn';
957
1191
  return lootIx(
958
1192
  'move_tokens',
959
1193
  { from, to, amount, reference: reference(ref) },
960
- { authority, project, mint, pool: from.Pool ? pda.pool(project, from.Pool[0]) : null },
961
- { remaining: [...destinationAccounts(project, to, mint, { holders, programs, treasury }), ...extra], writable: writes ? ['mint'] : [] },
1194
+ // What a farm holds free comes out of its rewards vault, passed as the pool, with the farm.
1195
+ { authority, project, mint, pool: from.Pool ? pda.pool(project, from.Pool[0]) : from.Farm ? pda.rewards(pda.farm(project, from.Farm[0])) : null, farm: from.Farm ? pda.farm(project, from.Farm[0]) : null, farm_ends: from.Farm && farm?.keeps_ends ? pda.ends(pda.farm(project, from.Farm[0])) : null },
1196
+ { remaining: [...destinationAccounts(project, to, mint, { holders, programs, treasury }), ...tailOf(extra)], writable: writes ? ['mint'] : [] },
962
1197
  );
963
1198
  }
964
1199
 
965
1200
  // ---------------------------------------------------------------- make: collectibles
966
1201
 
967
1202
  /**
968
- * The authority makes, rewrites or burns a collectible. `change`: { Make: { attributes } } (then `item` is the item id
969
- * it makes one of, `asset` is a new key that signs, and `owner` receives it), { Set: [attributes] } or 'Burn'. `collection`: the project's for a Make
970
- * or a Burn; for a Set, the asset's own (`MPL_CORE_PROGRAM` for one in none). `hasAuthority` false for a `Set` on an
971
- * NFT the project has no authority over (a Core asset of another collection, or a Token Metadata NFT's mint): what it
972
- * writes goes in the NFT's Sheet, or on the NFT itself when its Attributes plugin names the project.
1203
+ * The authority rewrites or burns a collectible, or writes a wallet's profile. `change`: { Set: [attributes] }, 'Burn'
1204
+ * (`owner`: whoever holds it, who gets its rent) or { Profile: [attributes] } (`asset` the wallet). `collection`: for a
1205
+ * Burn the project's; for a Set the asset's own (`MPL_CORE_PROGRAM` for one in none). `hasAuthority` false for a `Set`
1206
+ * on an NFT the project has no authority over (a Core asset of another collection, or a Token Metadata NFT's mint):
1207
+ * what it writes goes in the NFT's Sheet, or on the NFT itself when its Attributes plugin names the project. A `Make`
1208
+ * goes to `makeIx`.
973
1209
  */
974
- export function collectibleIx({ authority, project, collection, asset, change, item = null, owner = null, hasAuthority = true, reference: ref }) {
975
- const make = Boolean(change.Make);
976
- const sheet = change.Set && !hasAuthority ? pda.sheet(project, asset) : null;
1210
+ export function collectibleIx({ authority, project, collection, asset, change, owner = null, hasAuthority = true, reference: ref, ...make }) {
1211
+ if (change.Make) return makeIx({ authority, project, ...change.Make, ...make, owners: make.owners ?? [owner], reference: ref });
1212
+ const sheet = change.Set && !hasAuthority ? pda.sheet(project, asset) : change.Profile ? pda.profile(project, asset) : null;
1213
+ const burn = change === 'Burn' || Boolean(change.Burn);
977
1214
  return lootIx(
978
1215
  'collectible',
979
1216
  { change, reference: reference(ref) },
980
- { authority, project, collection, asset, item: make ? pda.item(project, item) : null, owner: make ? owner : null, sheet },
981
- { signer: make ? ['asset'] : [], writable: change.Set || collection === MPL_CORE_PROGRAM ? [] : ['collection'] },
1217
+ { authority, project, collection, asset, owner: burn ? owner : null, sheet },
1218
+ { writable: change.Set || change.Profile || collection === MPL_CORE_PROGRAM ? [] : ['collection'] },
1219
+ );
1220
+ }
1221
+
1222
+ /**
1223
+ * The authority makes one of item `item` (`itemState`: its decoded Item, for its collection and next serial) for each
1224
+ * of `owners`, in serial order (an owner may come more than once), with `attributes` written over the program's own: a
1225
+ * collectible at `pda.itemAsset(item, serial)` (`madeAssets` says which), or with `tree`, a compressed NFT minted into
1226
+ * that tree of the project's (no attributes: they go in its Sheet). A switched-off item still makes; one sold out
1227
+ * refuses the whole call (`PrizeNotCovered`).
1228
+ */
1229
+ export function makeIx({ authority, project, item, itemState, owners, attributes = [], tree = null, reference: ref }) {
1230
+ if (!itemState) throw new Error(`a make of item ${item} needs the item (itemState: its decoded Item), for its collection and next serial`);
1231
+ if (!owners?.length || owners.some((o) => !o)) throw new Error('A make names who gets each one (`owners`).');
1232
+ const prize = tree ? { Compressed: { item, tree } } : { Item: [item] };
1233
+ const remaining = owners.flatMap((owner, made) => [meta(owner, false, false), ...prizeMetas({ project, prize, winner: owner, items: { [item]: itemState }, made })]);
1234
+ return lootIx(
1235
+ 'collectible',
1236
+ { change: { Make: { item, tree, attributes } }, reference: reference(ref) },
1237
+ { authority, project, collection: itemState.collection, asset: pda.item(project, item), owner: null, sheet: null },
1238
+ { remaining, writable: ['collection'] },
982
1239
  );
983
1240
  }
984
1241
 
1242
+ /** The collectibles a `makeIx` of `count` mints (not compressed): at the item's next serials. */
1243
+ export const madeAssets = (project, item, itemState, count = 1) => Array.from({ length: count }, (_, k) => pda.itemAsset(pda.item(project, item), Number(itemState.issued) + 1 + k));
1244
+
985
1245
  // ---------------------------------------------------------------- decide
986
1246
 
987
1247
  /**
988
1248
  * A held NFT goes to `to`: the authority's choice while the hold runs, home to its player (anyone) after; a repaid
989
1249
  * loan's home (anyone, whenever), an unpaid one's to its lender once due. `hold` is the
990
1250
  * decoded Hold: its address names its end (`pda.hold(asset, free_at)`), so a release sent late never lands on a later
991
- * hold. The NFT is any kind
1251
+ * hold; `address` names it when its `free_at` changed since (a repaid loan's is 0, at the address its loan named). The
1252
+ * NFT is any kind
992
1253
  * (`piece`): `asset` and `collection` for a Core asset, with `tokenMetadata` for a Token Metadata NFT, or `compressed`
993
1254
  * (after a `proveLeafIx`).
994
1255
  */
995
- export function releaseIx({ signer, project, collection, asset, tokenMetadata: tm, compressed, hold, to, reference: ref }) {
996
- const receiver = to ?? hold.player;
1256
+ export function releaseIx({ signer, project, collection, asset, tokenMetadata: tm, compressed, hold, address = null, to, treasury, reference: ref, more = [] }) {
1257
+ // A hold whose player is the project's own address is owed to the treasury, as the project names it now (`treasury`,
1258
+ // the decoded Project's).
1259
+ if (!to && hold.player === project && !treasury) throw new Error('This hold is owed to the treasury: pass treasury (the project’s, as it is now).');
1260
+ const receiver = to ?? (hold.player === project ? treasury : hold.player);
997
1261
  // Never to the project's own address, nor any account of the program, where nothing could move it again (ToTheProject).
998
1262
  if (receiver === project) throw new Error('A held NFT can’t go to the project’s own address: name a wallet.');
999
- const p = piece({ project, asset, wallet: receiver, collection, tokenMetadata: tm, compressed, ...ONLY_MOVES });
1000
- return lootIx('release', { reference: reference(ref) }, { signer, project, ...p.accounts, hold: pda.hold(p.accounts.asset, hold.free_at), player: hold.player, to: receiver }, { remaining: p.proof, writable: p.writable });
1263
+ // Several holds of one player's with one end, payer and rent wallet (`more`: each { asset, collection, tokenMetadata,
1264
+ // compressed }) go to one `to` together, in ascending order of their holds' addresses.
1265
+ const [first, ...rest] = ascending([{ asset, collection, tokenMetadata: tm, compressed, address }, ...more].map((n) => {
1266
+ const p = piece({ project, asset: n.asset, wallet: receiver, collection: n.collection, tokenMetadata: n.tokenMetadata, compressed: n.compressed, ...ONLY_MOVES });
1267
+ return { p, hold: n.address ?? pda.hold(p.accounts.asset, hold.free_at) };
1268
+ }), (x) => x.hold);
1269
+ const p = withPrograms(first.p, rest.map((x) => x.p));
1270
+ const groups = rest.flatMap((x) => [meta(x.hold, false, true), ...anotherMetas(x.p)]);
1271
+ // The hold's rent goes back to whoever made it (`payer`), a Token Metadata NFT's emptied account's to whoever paid it.
1272
+ return lootIx('release', { reference: reference(ref) }, { signer, project, ...p.accounts, hold: first.hold, payer: hold.payer, rent_to: hold.rent_to, to: receiver }, { remaining: [...p.proof, ...groups], writable: p.writable });
1001
1273
  }
1002
1274
 
1003
1275
  /** The mint of a contest's split prize (its `prize`, tokens): minted, or a pool's (`poolMint`). */
1004
1276
  const splitMint = (prize, poolMint) => prize?.Tokens?.from.Mint?.[0] ?? poolMint;
1005
1277
 
1006
1278
  /**
1007
- * The authority opens contest `id` in `mint`. `args`: { sides, closes_at, result_by, shares, gate, deciders,
1008
- * min_pot, stake, prize, nft, prize_split }: `prize`, what each winning entry gets when it is claimed (`prizeMetas`);
1009
- * `nft`, a rule an NFT each entry stakes passes, in place of tokens (no shares then, no stake: the NFTs are the pot).
1010
- * With `args.prize_split`, its token prize is set aside whole when it is
1011
- * decided, in the contest's own vault (`pda.prizeVault`, opened now at the authority's rent), and each winning entry
1279
+ * The authority opens contest `id` in `mint` (`projectState`: the decoded Project, whose next incarnation names the
1280
+ * contest's list of entries). `args`: { sides, closes_at, result_by, shares, gate, deciders, min_pot, stake, prize, nft,
1281
+ * prize_split, cap }: `prize`, what each winning entry gets with its first claim (`prizeMetas`); `nft`, a rule an NFT
1282
+ * each entry stakes passes, in place of tokens (no shares then, no stake: the NFTs are the pot), each winner sharing the
1283
+ * losers' NFTs, at most `cap` each (null: no cap). With `args.prize_split`, its token prize is set aside whole when it
1284
+ * is decided, in the contest's own vault (`pda.prizeVault`, opened now at the authority's rent), and each winning entry
1012
1285
  * takes its part by what it put in: `prizeMint` names the prize's mint when it comes from a pool.
1013
1286
  */
1014
- export const openContestIx = ({ authority, project, id, mint, args, prizeMint }) => {
1287
+ export const openContestIx = ({ authority, project, projectState, id, mint, args, prizeMint }) => {
1288
+ stored(args.shares);
1015
1289
  const contest = pda.contest(project, id);
1016
1290
  const split = Boolean(args.prize_split);
1017
- return lootIx('open_contest', { id, args: { deciders: [], min_pot: 0, stake: 0, prize: null, nft: null, prize_split: false, ...args, gate: args.gate ?? null } }, {
1291
+ return lootIx('open_contest', { id, args: { deciders: [], min_pot: 0, stake: 0, prize: null, nft: null, prize_split: false, cap: null, ...args, gate: args.gate ?? null } }, {
1018
1292
  authority, project, mint, contest,
1019
1293
  prize_mint: split ? splitMint(args.prize, prizeMint) : null, prize_vault: split ? pda.prizeVault(contest) : null,
1294
+ entries: pda.entries(contest, BigInt(projectState.incarnations) + 1n),
1295
+ // A split prize from a pool names the pool at its index (`WrongPool` without it; a prize mint not the pool's is
1296
+ // `WrongMint`).
1297
+ prize_pool: split && args.prize?.Tokens?.from?.Pool ? pda.pool(project, args.prize.Tokens.from.Pool[0]) : null,
1298
+ }, {
1299
+ // A prize that is a roll: its box.
1300
+ remaining: args.prize?.Roll ? boxesAccounts(project, [args.prize.Roll[0]]) : [],
1020
1301
  });
1021
1302
  };
1022
1303
 
1304
+ /** A contest's list of entries, from its decoded Contest. */
1305
+ export const contestEntries = (contestAddress, contest) => pda.entries(contestAddress, contest.incarnation);
1306
+
1023
1307
  /** The prefix of a contest's verdict (`verdictBytes`). */
1024
- export const VERDICT_DOMAIN = 'loot-verdict-v3';
1308
+ export const VERDICT_DOMAIN = 'loot-verdict-v4';
1025
1309
 
1026
1310
  /**
1027
- * The bytes a contest's decider key signs: "loot-verdict-v3" | program (32) | contest (32) | incarnation (u64 LE) |
1028
- * side (1; 255 calls it off) | expires (i64 LE), then | recipient (32) when the verdict names the wallet the
1029
- * contest's chosen share pays. `incarnation` is the contest's (the decoded Contest's), so a verdict decides only the
1030
- * contest it was signed for, never one opened later at the same id. `decide` rebuilds them from its own accounts and
1031
- * arguments.
1311
+ * The bytes a contest's decider key signs: "loot-verdict-v4" | sha256(program (32) | contest (32) | incarnation (u64
1312
+ * LE) | side (u16 LE; 65535 calls it off) | expires (i64 LE), then | recipient (32) when the verdict names the wallet
1313
+ * the contest's chosen share pays). `incarnation` is the contest's (the decoded Contest's), so a verdict decides only
1314
+ * the contest it was signed for, never one opened later at the same id. `decide` rebuilds them from its own accounts
1315
+ * and arguments.
1032
1316
  */
1033
1317
  export function verdictBytes({ program = LOOT_PROGRAM, contest, incarnation, side, expires, recipient = null }) {
1034
1318
  if (incarnation == null) throw new Error('A verdict signs the contest’s incarnation (the decoded Contest’s).');
1035
1319
  const named = recipient == null ? new Uint8Array(0) : addressBytes(recipient);
1036
- return concat(utf8(VERDICT_DOMAIN), addressBytes(program), addressBytes(contest), uintLE(incarnation, 8), uintLE(side ?? 255, 1), intLE(expires, 8), named);
1320
+ return concat(utf8(VERDICT_DOMAIN), sha256(concat(addressBytes(program), addressBytes(contest), uintLE(incarnation, 8), uintLE(side ?? 0xffff, 2), intLE(expires, 8), named)));
1037
1321
  }
1038
1322
 
1039
1323
  /**
1040
1324
  * Names the side that won (`side`), or calls the contest off (`side: null`). `signer` is the authority; or anyone
1041
1325
  * sending a decider's verdict (`verdict: { expires }`, taken once entries have closed): then this instruction goes
1042
1326
  * right after the Ed25519 check of the decider's signature over `verdictBytes` (`ed25519Ix` in grant.js), and
1043
- * `signer` pays the fee. Anyone writes down a contest that is off by itself. The contest's incarnation names it, so a
1327
+ * `signer` pays the fee, as the authority does. A contest that is off by itself (overdue) needs no decision, and takes
1328
+ * none (`ResultOverdue`): `claim` closes it. The contest's incarnation names it, so a
1044
1329
  * decision sent late never decides a contest opened later at the same id. A call-off takes no share accounts. A
1045
1330
  * contest with a chosen share is decided naming its `recipient`, a wallet (a bounty's worker); a decider's verdict
1046
1331
  * signs it too (`verdictBytes`).
@@ -1049,23 +1334,22 @@ export function decideIx({ signer, project, projectState, contestAddress, contes
1049
1334
  const address = contestAddress ?? pda.contest(project, contest.id);
1050
1335
  const named = recipient == null ? contest.shares : chosenShares(contest.shares, recipient);
1051
1336
  const shares = side == null ? [] : shareAccounts(project, named, contest.mint, { holders, programs, treasury: projectState?.treasury });
1052
- // A split prize is set aside when a side is named (minted, or from its pool), and its vault closes with a contest
1053
- // called off before anyone entered.
1337
+ // A split prize is set aside when a side is named (minted, or from its pool). A decision never closes the contest:
1338
+ // `claim` does, entered or not.
1054
1339
  const split = Boolean(contest.prize_split);
1055
1340
  const from = contest.prize?.Tokens?.from;
1056
- const closes = side == null && Number(contest.entries) === 0;
1057
1341
  return lootIx(
1058
1342
  'decide',
1059
1343
  { side: side ?? null, expires: toBigInt(verdict?.expires ?? 0), reference: reference(ref), incarnation: contest.incarnation, recipient },
1060
1344
  {
1061
- signer, project, contest: address, opener: contest.opener, mint: contest.mint, instructions: verdict ? SYSVAR_INSTRUCTIONS : null,
1345
+ signer, project, contest: address, mint: contest.mint, instructions: verdict ? SYSVAR_INSTRUCTIONS : null,
1062
1346
  prize_mint: split ? splitMint(contest.prize, prizeMint) : null,
1347
+ // Its pool: the prize set aside from it when a side is named.
1063
1348
  prize_pool: split && side != null && from?.Pool ? pda.pool(project, from.Pool[0]) : null,
1064
1349
  prize_vault: split ? pda.prizeVault(address) : null,
1065
1350
  },
1066
- // The mint is written by a burn share, or by closing the pot of a contest nobody entered; the prize's mint when it
1067
- // is minted, or when its vault closes.
1068
- { remaining: [...shares, ...extra], writable: [...((side == null ? closes : sharesBurn(contest.shares)) ? ['mint'] : []), ...(split && ((side != null && from?.Mint) || closes) ? ['prize_mint'] : [])] },
1351
+ // The mint is written by a burn share; the prize's mint when it is minted.
1352
+ { remaining: [...shares, ...tailOf(extra)], writable: [...(side != null && sharesBurn(contest.shares) ? ['mint'] : []), ...(split && side != null && from?.Mint ? ['prize_mint'] : [])] },
1069
1353
  );
1070
1354
  }
1071
1355
 
@@ -1082,170 +1366,383 @@ export const callbackRandomIx = ({ project, reference: ref, randomness }) => ({
1082
1366
  // ---------------------------------------------------------------- schedules and lotteries
1083
1367
 
1084
1368
  /**
1085
- * Anyone (`payer`) runs a schedule that is due, and opens the account it pays into when that is not there.
1369
+ * The project's keys or who it pays (`payer`; a sealed burn: anyone) run a schedule that is due, and open the account it
1370
+ * pays into when that is not there. A run missed is missed: it pays once, for the latest run due, and the runs before it
1371
+ * lapse; only a sealed schedule's fixed amount pays every run due, up to `upto` runs made (`scheduleCatchesUp`).
1086
1372
  * `poolMint`: the mint of the pool it moves from (when it moves from one). `extra`: a hook's or a memo's accounts.
1087
1373
  */
1088
- export function runScheduleIx({ payer, project, id, schedule, poolMint, holders, programs, treasury, extra = [] }) {
1374
+ export function runScheduleIx({ payer, project, id, schedule, poolMint, holders, programs, treasury, upto = 0, extra = [] }) {
1089
1375
  const { from, to } = schedule.action;
1090
1376
  const address = pda.schedule(project, id);
1091
1377
  // A sealed schedule pays from its own vault (`pda.vest`), passed where a pool would be.
1092
1378
  const mint = from.Mint?.[0] ?? from.Vest?.[0] ?? poolMint;
1093
1379
  const writes = Boolean(from.Mint) || to === 'Burn';
1094
1380
  const pool = from.Pool ? pda.pool(project, from.Pool[0]) : from.Vest ? pda.vest(address) : null;
1381
+ // `upto` (the runs it has plus those due, `runsDue`; 0: no bound) has an effect only on what pays every run due.
1095
1382
  return lootIx(
1096
1383
  'run_schedule',
1097
- {},
1384
+ { upto },
1098
1385
  { payer, project, schedule: address, pool, mint },
1099
- { remaining: [...destinationAccounts(project, to, mint, { holders, programs, treasury }), ...extra], writable: writes ? ['mint'] : [] },
1386
+ { remaining: [...destinationAccounts(project, to, mint, { holders, programs, treasury }), ...tailOf(extra)], writable: writes ? ['mint'] : [] },
1100
1387
  );
1101
1388
  }
1102
1389
 
1103
- export const lotteryPaymentMint = (lottery) => lottery.ticket_mint && lottery.ticket_mint !== NO_ADDRESS ? lottery.ticket_mint : lottery.mint;
1390
+
1391
+ // A lottery's rounds (`accounts.js`: `decodeRound`, `partsOwed`, `commonOwed`): each round is a record of its own, opened
1392
+ // by its first purchase. Its ask (`draw_lottery`) fixes its money and moves the lottery on; after the answer, `pull`
1393
+ // pays one part at a time, in any order, and the pull that leaves nothing owed closes it. Every builder takes the
1394
+ // decoded records it acts on, so each is pure.
1395
+
1396
+ /** The address of a lottery's current round's record (there or not), from its decoded Lottery. */
1397
+ export const currentRound = (lotteryAddress, lottery) => pda.round(lotteryAddress, lottery.incarnation, lottery.round);
1104
1398
 
1105
1399
  /**
1106
- * `player` buys `count` tickets: a contribution of their own at `nonce` (any number they have no contribution at in
1107
- * this lottery), whose rent they pay and get back once it is finished. The ticket money waits in the lottery's
1108
- * escrow until the draw. Whoever holds a collectible a fixed share pays is fixed now, from that collectible. It buys
1109
- * only at the `lottery`'s incarnation and revision (`TermsChanged` once the owner edits or recreates it). A lottery
1110
- * whose ticket costs an NFT (`lottery.nft_ticket`) sells one at a time, for `nft` ({ asset, collection, tokenMetadata,
1111
- * compressed }, as `piece` takes it), which the project holds until the round is drawn (then it goes where its `then`
1112
- * says, `spentTo`) or cancelled (then back to its buyer).
1400
+ * `player` buys `count` tickets of lottery `id`'s current round (`lottery` decoded; `round` its decoded record, null when
1401
+ * nobody bought into it yet: then this purchase opens it, and the lottery's spare SOL pays its rent, or the buyer fronts
1402
+ * it, back at the close). It buys only at the revision the round copied (`TermsChanged` otherwise), and its round's
1403
+ * address names the lottery's incarnation and round (a purchase that lands as the round moves on fails, and is sent
1404
+ * again). A share the buyer chooses is paid now, out of the price, from the buyer's own account: to the wallet they name
1405
+ * (`chosenRecipient`), the holder of the collectible `chosen` (its holder in `holders`), or with no choice the project's
1406
+ * `treasury` wallet. A lottery whose ticket costs an NFT sells one at a time, for `nft` ({ asset, collection,
1407
+ * tokenMetadata, compressed }, as `piece` takes it).
1113
1408
  */
1114
- export function buyTicketsIx({ player, project, id, lottery, mint, count, nonce, shown, listProof, chosenRecipient = null, chosen = null, nft, playerToken, programs, extra = [] }) {
1409
+ export function buyTicketsIx({ player, project, id, lottery, round = null, mint, count, shown, listProof, chosenRecipient = null, chosen = null, nft, holders = {}, treasury, playerToken, programs, extra = [] }) {
1115
1410
  const address = pda.lottery(project, id);
1116
- mint ??= lotteryPaymentMint(lottery);
1117
- const holderAssets = (lottery.shares ?? []).flatMap((s) => (s.to.Holder ? [meta(s.to.Holder[0], false, false)] : []));
1118
- const held = lottery.nft_ticket ? piece({ project, wallet: player, ...nft, ...ONLY_MOVES, reads: true }) : null;
1119
- const paid = held ? [...pieceMetas(held), ...held.proof] : [];
1411
+ const key = currentRound(address, lottery);
1412
+ // The terms the round copied, or for its first purchase the lottery's.
1413
+ const terms = round ? round.terms : lottery.terms;
1414
+ mint ??= paymentMint(terms);
1415
+ const paying = !terms.nft_ticket && BigInt(terms.price) > 0n;
1416
+ const choices = paying ? chosenShares(terms.shares.filter(chosenAtPurchase), chosenRecipient) : [];
1417
+ const recipients = shareAccounts(project, choices, mint, { holders, programs, treasury });
1418
+ const held = terms.nft_ticket ? piece({ project, wallet: player, ...nft, ...ONLY_MOVES, reads: true }) : null;
1120
1419
  return lootIx(
1121
1420
  'buy_tickets',
1122
- { nonce, count, chosen_recipient: chosenRecipient, incarnation: lottery.incarnation, revision: lottery.revision },
1421
+ { count, chosen_recipient: chosenRecipient, revision: round ? round.revision : lottery.revision },
1123
1422
  {
1124
- player, project, lottery: address, contribution: pda.contribution(address, player, nonce),
1423
+ player, project, lottery: address, round: key,
1125
1424
  player_token: playerToken ?? ata(player, mint, tokenProgramOf(programs, mint)),
1126
- payment_vault: pda.lotteryFunds(address), mint, chosen,
1127
- receipt: Number(lottery.per_wallet) ? pda.lotteryReceipt(address, lottery.incarnation, player) : null,
1425
+ payment_vault: pda.roundTokens(key, mint), mint, chosen,
1426
+ counts: Number(lottery.per_wallet) ? pda.counts(project, player) : null,
1427
+ associated_token_program: choices.length ? ASSOCIATED_TOKEN_PROGRAM : null,
1128
1428
  },
1129
- { remaining: [...gateAccounts(project, { gate: lottery.gate, wallet: player, shown, listProof }), ...holderAssets, ...paid, ...extra] },
1429
+ { remaining: [...gateAccounts(project, { gate: lottery.gate, wallet: player, shown, listProof }), ...recipients, ...(held ? [...pieceMetas(held), ...held.proof] : []), ...tailOf(extra)] },
1130
1430
  );
1131
1431
  }
1132
1432
 
1133
- /** Anyone asks for a lottery's draw once it is due. */
1134
- export const drawLotteryIx = ({ payer, project, id }) => lootIx('draw_lottery', {}, { payer, lottery: pda.lottery(project, id) });
1135
-
1136
- /** The oracle's answer to a draw (for a local simulation). Every request of a round names the round's ticket, fixed
1137
- * at its first ask (`asked_first`): a re-ask keeps it, so the first answer to land draws the round. */
1138
- export function callbackLotteryIx({ project, id, lottery, askedFirst = lottery?.asked_first, randomness }) {
1139
- const address = pda.lottery(project, id);
1140
- return {
1141
- programId: LOOT_PROGRAM,
1142
- keys: [meta(pda.oracleIdentity(), true, false), meta(address, false, true), meta(ticketOf(address, askedFirst), false, false)],
1143
- data: encodeInstruction(IDL, 'callback_lottery', { randomness: Uint8Array.from(randomness) }),
1144
- };
1433
+ /**
1434
+ * Anyone asks for the draw of a lottery's round once it is due (`lottery` decoded). `round`: the decoded record of the
1435
+ * round asked for (null when nobody bought into the current one: it moves on, or a lottery that draws once ends, with
1436
+ * no ask), and `roundAddress` its address when it is not the current round's (an older round still waiting for its
1437
+ * answer: asked again after `STALE_SECONDS`, or after the timeout cancelled, with no lottery account, so `lottery` may be
1438
+ * null then; without `round`, the current round's, with the lottery's terms). A round's first ask fixes its money: it takes
1439
+ * the lottery's pot, its mint and the round's account of it, then each run of token prizes in place order, its mint and
1440
+ * for a pool, the pool and the round's account of the mint (`poolMints`: a pool's index to its mint). With `skipHooked`
1441
+ * (the project's authority only) a hooked pool or pot gives the round nothing. `extra`: hook accounts.
1442
+ */
1443
+ export function drawLotteryIx({ payer, project, id, lottery, round = null, roundAddress = null, skipHooked = false, poolMints = {}, extra = [] }) {
1444
+ const address = id === undefined || id === null ? null : pda.lottery(project, id);
1445
+ const key = roundAddress ?? currentRound(address, lottery);
1446
+ const rest = [];
1447
+ // A round with no record moves on and a round asked before is asked again, reading none of these: they are passed
1448
+ // unless the round is known to have been asked.
1449
+ const current = !round || round.status === 'Open';
1450
+ if (current) {
1451
+ const { terms } = round ?? lottery;
1452
+ rest.push(meta(pda.pot(address), false, true), meta(terms.mint, false, false), meta(pda.roundTokens(key, terms.mint), false, true));
1453
+ for (const place of terms.places) {
1454
+ const from = place.prize?.Tokens?.from;
1455
+ if (from?.Mint) rest.push(meta(from.Mint[0], false, false));
1456
+ else if (from?.Pool) {
1457
+ const mint = poolMints[from.Pool[0]];
1458
+ if (!mint) throw new Error(`The ask of a round whose place gives tokens from pool ${from.Pool[0]} needs that pool's mint (poolMints).`);
1459
+ rest.push(meta(mint, false, false), meta(pda.pool(project, from.Pool[0]), false, true), meta(pda.roundTokens(key, mint), false, true));
1460
+ }
1461
+ }
1462
+ }
1463
+ // The lottery goes with its current round only: a round whose draw waits is asked again or cancelled without it, so a
1464
+ // lottery closed since never strands it.
1465
+ return lootIx('draw_lottery', { skip_hooked: skipHooked }, { payer, project, lottery: current ? address : null, round: key }, { remaining: [...rest, ...tailOf(extra)] });
1145
1466
  }
1146
1467
 
1147
- export const cancelLotteryIx = ({ project, id }) => lootIx('cancel_lottery', {}, { lottery: pda.lottery(project, id) });
1148
-
1149
- /** Where a decoded contribution's shares go: as fixed when it was bought. */
1150
- export function lotteryContributionShares(lottery, contribution) {
1151
- return (lottery.shares ?? []).flatMap((share, i) => {
1152
- const recipient = contribution.recipients[i];
1153
- if (recipient.kind === 2) return [];
1154
- const to = recipient.kind === 1 ? { Wallet: [recipient.wallet] } : share.to?.Chosen ? 'Treasury' : share.to;
1155
- return [{ ...share, to }];
1156
- });
1157
- }
1468
+ /** The oracle's answer to a round's draw (for a local simulation): the round's record is its only account. */
1469
+ export const callbackLotteryIx = ({ round, randomness }) => ({
1470
+ programId: LOOT_PROGRAM,
1471
+ keys: [meta(pda.oracleIdentity(), true, false), meta(round, false, true)],
1472
+ data: encodeInstruction(IDL, 'callback_lottery', { randomness: Uint8Array.from(randomness) }),
1473
+ });
1158
1474
 
1159
- /**
1160
- * Anyone (`payer`) sends a contribution's next step (`lotteryStep`; `contributionAddress`, decoded `contribution`):
1161
- * - a drawn round gives every place's prize first: the step of a contribution that won a place with a prize gives
1162
- * the first such prize not given yet to its buyer, one place a call (`prizeMetas`: `collection`, `poolMints`, `items`, `nfts` as it takes them; a roll's
1163
- * order at `nonce` plus the place, `prizeNonce(contributionAddress)` by default), and nothing else moves;
1164
- * - once every prize is given, it is routed through its shares into the pot (or its tickets burned, or its NFT ticket
1165
- * spent); a cancelled round's, or a lottery's that returns its tickets, goes back to its buyer, never waiting;
1166
- * - one that holds a winning ticket stays (`done`): once every contribution is finished, it pays its buyer every
1167
- * place's share of the pot it won, into their account of the pot's mint.
1168
- * `payer` opens any account it pays into that is not there. `treasury`: the project's treasury wallet, for a share
1169
- * that names it. A ticket an NFT paid for passes that NFT (`ticket`: its kind, as `piece` takes it); `handBack` when the
1170
- * wallet its price gives it to is an account of the loot program (`handsBack`), where it goes back to its buyer (a
1171
- * Token Metadata NFT into the buyer's token account).
1172
- */
1173
- export function finishLotteryContributionIx({ payer, project, id, lottery, contributionAddress, contribution, treasury, nonce, collection, poolMints, items, nfts = {}, ticket = {}, handBack = false, programs, extra = [] }) {
1174
- const address = pda.lottery(project, id);
1175
- const step = lotteryStep(lottery, contribution) ?? (contribution.done ? 'pay' : lottery.status === 'Drawn' ? 'finish' : 'refund');
1176
- nonce = toBigInt(nonce ?? prizeNonce(contributionAddress));
1177
- const accounts = { payer, project, lottery: address, contribution: contributionAddress, payment_vault: pda.lotteryFunds(address), pot: pda.pot(address), player: contribution.player };
1178
- if (step === 'prizes') {
1179
- // One place a call: the first it won whose prize is not given yet. The places go in order, so collectibles of one
1180
- // item are made in place order, each at the next serial.
1181
- const [place] = lotteryOwed(lottery, contribution);
1182
- const prize = lottery.places[place].prize;
1183
- const prizes = prizeMetas({ project, collection, prize, winner: contribution.player, nonce: BigInt.asUintN(64, nonce + BigInt(place)), poolMints, items, nft: nfts[prize.Nft?.nft ?? prize.Held?.[0]], programs, lottery: address });
1184
- return lootIx('finish_lottery_contribution', { nonce }, { ...accounts, to: null, mint: lottery.mint }, { remaining: [...prizes, ...extra] });
1185
- }
1186
- if (step === 'pay') {
1187
- return lootIx('finish_lottery_contribution', { nonce }, { ...accounts, to: ata(contribution.player, lottery.mint, tokenProgramOf(programs, lottery.mint)), mint: lottery.mint }, { remaining: extra });
1475
+ /** The accounts a round's close takes after a pull's own: each of its token accounts, its mint and who opened it (the
1476
+ * lottery, which gets its spare back, or a buyer who fronted it; `owner` for one the lottery opened, once it is closed). */
1477
+ export const roundCloseMetas = (key, r, owner = null) =>
1478
+ r.accounts.filter((o) => o.mint !== NO_ADDRESS).flatMap((o) => [meta(pda.roundTokens(key, o.mint), false, true), meta(o.mint, false, true), meta(owner && o.by === r.lottery ? owner : o.by, false, true)]);
1479
+
1480
+ /** The NFT a raffle's place draws from the lottery's own stash (`stash` decoded, as it is now) with the round's answer. */
1481
+ export const raffledNft = (stash, answer, place) => stash?.nfts[weighted(stream(answer, [TAG.LOTTERY, place]), stash.weights)] ?? null;
1482
+
1483
+ /**
1484
+ * The NFT of the lottery's own stash each place of a drawn round that holds or draws one gets, in place order, as the
1485
+ * pulls take them out (`LotteryRound::assign`): the ones already taken out (`r.nfts`), then the rest as the stash
1486
+ * (`stash`, decoded as it is now) gives them, a named one found or one drawn by weight with the round's answer, each
1487
+ * taken out before the next. null for a place whose NFT isn't there (it gets nothing).
1488
+ */
1489
+ export function stashNfts(r, stash) {
1490
+ const nfts = r.nfts.map((n, k) => (k < Number(r.assigned) ? (n === NO_ADDRESS ? null : n) : undefined));
1491
+ const rows = { nfts: [...(stash?.nfts ?? [])], weights: [...(stash?.weights ?? [])] };
1492
+ const take = (row) => {
1493
+ if (row == null || row < 0) return null;
1494
+ const nft = rows.nfts[row];
1495
+ const last = rows.nfts.length - 1;
1496
+ [rows.nfts[row], rows.weights[row]] = [rows.nfts[last], rows.weights[last]];
1497
+ rows.nfts.pop();
1498
+ rows.weights.pop();
1499
+ return nft;
1500
+ };
1501
+ const w = roundWinners(r);
1502
+ let [k, first] = [0, 0];
1503
+ for (const p of r.terms.places) {
1504
+ const times = Number(p.times);
1505
+ if (ownStash(p)) {
1506
+ for (let place = first; place < first + times && place < w; place++, k++) {
1507
+ if (nfts[k] !== undefined) continue;
1508
+ nfts[k] = p.prize?.Held ? take(rows.nfts.indexOf(p.prize.Held[0])) : take(weighted(stream(r.answer, [TAG.LOTTERY, place]), rows.weights));
1509
+ }
1510
+ k += Math.max(0, first + times - Math.max(first, w));
1511
+ }
1512
+ first += times;
1188
1513
  }
1189
- const mint = lotteryPaymentMint(lottery);
1190
- // Drawn, its tickets go through the shares into the pot; in a lottery that returns them, back to the buyer.
1191
- const routed = step === 'finish';
1192
- const burns = routed && lottery.ticket_mint !== NO_ADDRESS;
1193
- // Burning the tickets, or a share that burns, writes the mint's supply.
1194
- const writesMint = burns || (routed && sharesBurn(lottery.shares ?? []));
1195
- const nft = contribution.asset && contribution.asset !== NO_ADDRESS;
1196
- let remaining = routed && !burns ? shareAccounts(project, lotteryContributionShares(lottery, contribution), mint, { programs, treasury }) : [];
1197
- if (nft) {
1198
- // Drawn: where its price sends it (it stays with the project in a stash); cancelled: back to its buyer.
1199
- const then = routed ? lottery.nft_ticket?.then : null;
1200
- const receiver = then ? spentTo(project, then) : null;
1201
- const burned = then === 'Burn' || Boolean(then?.Burn);
1202
- // Given to a wallet that is an account of the loot program (`handBack`), it goes back to its buyer instead.
1203
- const holder = then?.Stash || (handBack && then?.Give) ? contribution.player : receiver ?? contribution.player;
1204
- const p = piece({ project, asset: contribution.asset, wallet: holder, ...ticket, moves: !then?.Stash, locks: false, burns: burned, reads: false });
1205
- remaining = [...pieceMetas(p), ...p.proof, ...(receiver ? [meta(receiver, false, Boolean(then.Stash))] : [])];
1514
+ return nfts.map((n) => n ?? null);
1515
+ }
1516
+
1517
+ /** The program's rule for where a part nobody can take goes: the treasury's wallet, then its account of the mint. */
1518
+ const treasuryAccounts = (treasury, mint, programs) => walletAccounts(treasury, mint, programs);
1519
+
1520
+ /**
1521
+ * Anyone (`payer`) pulls `part` of the round at `roundAddress` (`round` decoded; `partsOwed` and `commonOwed` say what is
1522
+ * owed):
1523
+ * 'Own' entry `entry`'s own part: a refund, a no-loss ticket's price, or its NFT ticket back or where its
1524
+ * price sends it (`ticket` its kind as `piece` takes it; `handBack` when the wallet its price gives
1525
+ * it to is an account of the loot program; `hold`: made a hold for whoever it is owed to instead);
1526
+ * { Pot: [run] } the entry's places in the run, their part of the pot, paid into the buyer's account;
1527
+ * { Prize: [run] } their prize: tokens into the buyer's account, or each place's prize (`prize`: { stash, the lottery's
1528
+ * own stash decoded, for NFTs not taken out yet (`stashNfts`); stashes, a project stash's id to its
1529
+ * decoded stash; nfts, an NFT's kind by its asset; hold, NFTs made holds; most, the most places a
1530
+ * pull carries; and what `prizeMetas` takes: collection, poolMints, items, projectMint }), a roll's
1531
+ * order at `prizeRollNonce(nonce + place)` (ask it in the same transaction: `pullRolls`);
1532
+ * { Common: [i] } a part owed in common: share `i`, the pot's (`i` = the shares' count), a pool's prize back after a
1533
+ * cancel (past it), paid where it goes (`holders`, `programs`); `toTreasury` when that can't take it
1534
+ * (a pool never opened, the pot closed since or made again in another mint, an account gone: a
1535
+ * holder nobody is is found here).
1536
+ * The pull that leaves nothing owed closes the round (`projectState`: its treasury and owner; `lotteryClosed` when the
1537
+ * lottery is gone, so rent it lent goes to the owner). A round that owes nothing and waits only for its close is closed
1538
+ * by any pull.
1539
+ */
1540
+ export function pullIx({ payer, project, roundAddress: key, round: r, entry = 0, part = 'Own', projectState, ticket = {}, handBack = false, hold = false, nonce = 0n, prize = {}, holders = {}, toTreasury = false, lotteryClosed = false, programs, extra = [] }) {
1541
+ const rest = [];
1542
+ let [mint, to, settled, buyer, partial] = [null, null, 1, payer, false];
1543
+ const account = (m, wallet) => ata(wallet, m, tokenProgramOf(programs, m));
1544
+ const finished = roundFinished(r);
1545
+ const e = part.Common || finished ? null : r.entries[entry];
1546
+ if (e) buyer = e.buyer;
1547
+ if (finished) settled = 0;
1548
+ else if (part === 'Own' && e.nft !== NO_ADDRESS) {
1549
+ const back = r.status === 'Cancelled' || r.terms.returns;
1550
+ const then = back ? null : r.terms.nft_ticket?.then;
1551
+ if (hold) {
1552
+ rest.push(meta(pda.hold(e.nft, 0), false, true));
1553
+ if (then?.Give) rest.push(meta(then.Give[0], false, true));
1554
+ } else {
1555
+ // Where its price sends it (a stash keeps it where it is), or back to its buyer.
1556
+ const receiver = then ? spentTo(project, then) : null;
1557
+ const holder = then?.Stash || (handBack && then?.Give) ? e.buyer : receiver ?? e.buyer;
1558
+ const p = piece({ project, asset: e.nft, wallet: holder, ...ticket, moves: !then?.Stash, locks: false, burns: then === 'Burn' || Boolean(then?.Burn), reads: false });
1559
+ rest.push(...pieceMetas(p), ...p.proof, ...(receiver ? [meta(receiver, false, true)] : []));
1560
+ }
1561
+ } else if (part === 'Own') [mint, to] = [paymentMint(r.terms), account(paymentMint(r.terms), e.buyer)];
1562
+ else if (part.Common) {
1563
+ const i = Number(part.Common[0]);
1564
+ const n = r.terms.shares.length;
1565
+ let dest;
1566
+ if (i < n) [mint, dest] = [paymentMint(r.terms), r.terms.shares[i].to];
1567
+ else if (i === n) [mint, dest] = [r.terms.mint, { Account: [pda.pot(r.lottery)] }];
1568
+ else {
1569
+ const p = r.prizes[i - n - 1];
1570
+ [mint, dest] = [p.mint, r.terms.places[Number(p.run)].prize.Tokens.from];
1571
+ }
1572
+ rest.push(...destinationAccounts(project, dest, mint, { holders, programs, treasury: projectState?.treasury }));
1573
+ const nobody = dest.Holder && !holders[dest.Holder[0]];
1574
+ if (toTreasury || nobody) rest.push(...treasuryAccounts(projectState?.treasury, mint, programs));
1575
+ } else if (part.Pot || tokenRun(r, Number(part.Prize[0]))) {
1576
+ const run = Number((part.Pot ?? part.Prize)[0]);
1577
+ const p = part.Prize ? r.prizes.find((x) => Number(x.run) === run) : null;
1578
+ mint = p ? p.mint : r.terms.mint;
1579
+ to = account(mint, e.buyer);
1580
+ settled = placesPulled(r, entry, run, Boolean(part.Prize));
1581
+ } else {
1582
+ // Each of the entry's places in the run whose prize is not given yet, lowest first.
1583
+ const run = Number(part.Prize[0]);
1584
+ const given = r.terms.places[run].prize;
1585
+ const places = prizePlacesOwed(r, entry, run);
1586
+ const own = ownStash(r.terms.places[run]);
1587
+ const nfts = own ? stashNfts(r, prize.stash) : [];
1588
+ if (own) rest.push(meta(pda.lotteryStash(r.lottery), false, true));
1589
+ const handed = (asset) => (prize.hold ? [meta(pda.hold(asset, 0), false, true)] : (() => {
1590
+ const p = piece({ project, asset, wallet: e.buyer, ...(prize.nfts?.[asset] ?? {}), ...ONLY_MOVES });
1591
+ return [...pieceMetas(p), ...p.proof];
1592
+ })());
1593
+ let [carried, made, stashed] = [0, 0, false];
1594
+ for (const place of places) {
1595
+ if (own && nfts[nftSlot(r, place)] == null) continue;
1596
+ if (prize.most != null && carried >= prize.most) {
1597
+ partial = true;
1598
+ break;
1599
+ }
1600
+ carried += 1;
1601
+ if (own) rest.push(...handed(nfts[nftSlot(r, place)]));
1602
+ else if (given.Nft) {
1603
+ // A project stash's NFT, while the stash holds it: its stash, then the NFT (once: one place takes it).
1604
+ const { stash, nft: asset } = given.Nft;
1605
+ const holds = prize.stashes?.[stash] ? prize.stashes[stash].nfts.includes(asset) : true;
1606
+ rest.push(meta(pda.stash(project, stash), false, true));
1607
+ if (holds && !stashed) rest.push(...handed(asset));
1608
+ stashed ||= holds;
1609
+ } else {
1610
+ rest.push(...prizeMetas({ project, collection: prize.collection, prize: given, winner: e.buyer, nonce: BigInt.asUintN(64, toBigInt(nonce) + BigInt(place)), poolMints: prize.poolMints, items: prize.items, nft: prize.nfts?.[given.Item?.[0]], made, projectMint: prize.projectMint, programs }));
1611
+ if (given.Item) made += 1;
1612
+ }
1613
+ }
1614
+ settled = places.length;
1206
1615
  }
1207
- // A free ticket gives nothing back, and needs no account to give it into.
1208
- const back = !routed && !nft && BigInt(contribution.amount ?? 0) > 0n;
1209
- return lootIx('finish_lottery_contribution', { nonce }, { ...accounts, to: back ? ata(contribution.player, mint, tokenProgramOf(programs, mint)) : null, mint }, { remaining: [...remaining, ...extra], writable: writesMint ? ['mint'] : [] });
1616
+ const closes = finished || (!partial && (r.status === 'Drawn' || r.status === 'Cancelled') && Number(r.owed) <= settled);
1617
+ const owner = lotteryClosed ? projectState?.owner : null;
1618
+ if (closes) rest.push(...roundCloseMetas(key, r, owner));
1619
+ const opener = owner && r.opener === r.lottery ? owner : r.opener;
1620
+ return lootIx(
1621
+ 'pull',
1622
+ { entry: part.Common || finished ? 0 : entry, part, nonce: toBigInt(nonce) },
1623
+ { payer, project, round: key, buyer, mint, vault: mint ? pda.roundTokens(key, mint) : null, to, opener: closes ? opener : null, treasury: closes ? projectState?.treasury : null },
1624
+ { remaining: [...rest, ...tailOf(extra)] },
1625
+ );
1626
+ }
1627
+
1628
+ /** The orders the rolls a `{ Prize: [run] }` pull of entry `entry` makes (a place's prize that is a roll), each with its
1629
+ * box: [{ order, box }], at `prizeRollNonce(nonce + place)` for the entry's buyer. Ask each in the pull's transaction. */
1630
+ export function pullRolls({ project, round: r, entry, part, nonce = 0n }) {
1631
+ const run = Number(part?.Prize?.[0] ?? -1);
1632
+ const given = r.terms.places[run]?.prize;
1633
+ if (!given?.Roll) return [];
1634
+ const buyer = r.entries[entry].buyer;
1635
+ return prizePlacesOwed(r, entry, run).map((place) => ({ order: pda.order(project, buyer, prizeRollNonce(BigInt.asUintN(64, toBigInt(nonce) + BigInt(place)))), box: given.Roll[0] }));
1210
1636
  }
1211
1637
 
1212
1638
  // ---------------------------------------------------------------- farms
1213
1639
 
1214
1640
  /**
1215
- * The owner opens farm `id`: of `stakedMint`'s tokens, or of collectibles when it is null. Its rewards come from pool
1216
- * `args.rewards_pool`, whose mint is `rewardsMint`: all it receives, shared, or with `args.rate` ({ per_day, mints })
1217
- * a fixed rate, minted or from that pool (a rate of 0 pays nothing: a farm that only grows or gives prizes).
1218
- * `args.gates`: the rules an NFT must all pass. `args.growth`: up to 4 of { key, per_day, level, steps, streak }, each
1219
- * writing points (and a level) on what is staked; tiers' `prize` is paid from `args.prize` ({ Mint: [mint] } or
1220
- * { Pool: [index] }), whose mint is `prizeMint`. `args.ends_at`: when it ends (0: never). For a farm of collectibles,
1221
- * `args.lock_key`: the attribute a staked NFT's lock is written under, as the time it ends, so recipes and rolls can
1222
- * move it; `args.upkeep_key`: the attribute that says until when it earns and grows (fuel). Empty: none.
1641
+ * The owner opens farm `id`: of `stakedMint`'s tokens, or of collectibles when it is null. Its rewards are `rewardsMint`'s,
1642
+ * in its own rewards vault (`pda.rewards(farm)`), which anything funds by a plain transfer (a share or a schedule paying
1643
+ * `{ Account: [vault] }`): all that lands there, shared, or with `args.rate` ({ per_day, mints, shared, halve_every }) a
1644
+ * fixed rate, for each seat at x1 or with `shared` the whole farm's shared by weight, halving every `halve_every`
1645
+ * seconds from the farm's opening (0: never), minted when a seat is paid or paid from what the vault holds free while it
1646
+ * lasts (a rate of 0 pays nothing: a farm that only grows or gives prizes). `args.gates`: the rules an NFT must all
1647
+ * pass. `args.growth`: any number of { key, per_day, level, steps, streak, profile, at_least }, each writing points (and
1648
+ * a level) on what is staked, or with `profile` on the staker's profile (a token seat only while it stakes `at_least`);
1649
+ * a tier's `earns_for`: seconds a seat earns and grows after its lock started (0: no end). `args.ends_at`: when it ends
1650
+ * (0: never). For a farm of collectibles, `args.lock_key`: the attribute a staked NFT's lock is written under, as the
1651
+ * time it ends, so recipes and rolls can move it; `args.upkeep_key`: the attribute that says until when it earns and
1652
+ * grows (fuel). Empty: none. `args.exit_to`: where early-exit fees go, 'Burn', { Pool: [index] } or 'Rewards' (the
1653
+ * farm's own vault: a farm whose staked token is its rewards'). A farm whose seats can stop earning and that pays keeps a
1654
+ * record of when (`farmKeepsEnds`, `pda.ends`), made here at the owner's rent; each new time costs its writer `END_BYTES`.
1223
1655
  */
1224
1656
  export const openFarmIx = ({ owner, project, id, args, rewardsMint, stakedMint = null }) => {
1225
1657
  if (!Array.isArray(args?.tiers)) throw new Error('A farm needs its lock tiers (`args.tiers`: [{ lock, bps }, …]).');
1226
- const full = { gates: [], rate: null, growth: [], ends_at: 0, lock_key: '', upkeep_key: '', exit_to: null, unbond: 0, toll: null, ...args, tiers: args.tiers.map((t) => ({ prize: null, roll: null, exit_bps: 0, ...t })) };
1658
+ const full = {
1659
+ gates: [], ends_at: 0, lock_key: '', upkeep_key: '', exit_to: null, ...args,
1660
+ rate: args.rate ? { mints: false, shared: false, halve_every: 0, ...args.rate } : null,
1661
+ tiers: args.tiers.map((t) => ({ prize: null, roll: null, exit_bps: 0, earns_for: 0, ...t })),
1662
+ growth: (args.growth ?? []).map((g) => ({ profile: false, at_least: 0n, ...g })),
1663
+ };
1227
1664
  return lootIx('open_farm', { id, args: full }, {
1228
- owner, project, rewards_pool: pda.pool(project, args.rewards_pool), rewards_mint: rewardsMint, staked_mint: stakedMint,
1665
+ owner, project, rewards_mint: rewardsMint, staked_mint: stakedMint,
1229
1666
  staked_vault: stakedMint ? pda.staked(pda.farm(project, id)) : null,
1230
- exit_pool: sinkPool(project, full.exit_to), toll_pool: sinkPool(project, full.toll?.to),
1667
+ exit_pool: sinkPool(project, full.exit_to),
1668
+ ends: farmKeepsEnds(full) ? pda.ends(pda.farm(project, id)) : null,
1669
+ }, {
1670
+ // Each box its tiers name to roll, tier by tier (its roll, then a prize that is a roll), once.
1671
+ remaining: boxesAccounts(project, full.tiers.flatMap((t) => [t.roll, t.prize?.Roll?.[0]].filter((x) => x != null))),
1231
1672
  });
1232
1673
  };
1233
1674
 
1234
- /** Where a farm's fee goes (`Sink`): the pool's address for `{ Pool: [index] }`, null for a burn or none. */
1675
+ /**
1676
+ * Whether a farm with these args keeps a record of when its seats stop earning (`Farm::keeps_ends`): a tier has a term
1677
+ * (`earns_for`) or the farm reads an upkeep (`upkeep_key`), and it pays something (no rate, or a rate above 0). The farm
1678
+ * and every move of it then pass the record (`WrongEnds` otherwise).
1679
+ */
1680
+ export const farmKeepsEnds = (args) =>
1681
+ ((args.tiers ?? []).some((t) => BigInt(t.earns_for ?? 0) > 0n) || Boolean(args.upkeep_key)) && (!args.rate || BigInt(args.rate.per_day ?? 0) > 0n);
1682
+
1683
+ /** Where a farm's fee goes (`Sink`): the pool's address for `{ Pool: [index] }`, null for a burn, its own vault or none. */
1235
1684
  const sinkPool = (project, sink) => (sink?.Pool ? pda.pool(project, sink.Pool[0]) : null);
1236
- /** The pools a farm's early-exit fees and toll go to, when they go to one (`exit_pool`, `toll_pool`). */
1237
- const farmSinks = (project, farm) => ({ exit_pool: sinkPool(project, farm.exit_to), toll_pool: sinkPool(project, farm.toll?.to) });
1238
- /** A toll that burns writes the rewards' mint. */
1239
- const tollWrites = (farm) => (farm.toll?.to === 'Burn' ? ['rewards_mint'] : []);
1240
1685
 
1241
1686
  const farmSide = (project, id, farm) => ({
1242
1687
  project,
1243
1688
  farm: pda.farm(project, id),
1244
- rewards_pool: pda.pool(project, farm.rewards_pool),
1245
1689
  rewards_mint: farm.rewards_mint,
1690
+ ends: farm.keeps_ends ? pda.ends(pda.farm(project, id)) : null,
1246
1691
  });
1247
- /** What every move of a farm writes: its rewards' mint when it mints at a fixed rate, its rewards pool otherwise. */
1248
- const farmWrites = (farm) => (farm.rate?.mints ? ['rewards_mint'] : ['rewards_pool']);
1692
+
1693
+ /**
1694
+ * What a farm has to walk (`Farm::walk`): the times of its record of ends (`ends`, from `decodeEnds`; an empty list for none)
1695
+ * past its clock, up to `now` (Unix seconds, the chain's clock) or the farm's end. Every move walks all of them first, each a
1696
+ * little compute, so a farm idle past many ends may leave a move no room: `farmNeedsWalk`.
1697
+ */
1698
+ export function endsToWalk(farm, ends, now) {
1699
+ const clock = BigInt(farm.clock ?? 0);
1700
+ const over = BigInt(farm.ends_at ?? 0);
1701
+ const stop = over > 0n && over < BigInt(now) ? over : BigInt(now);
1702
+ return ends.filter((e) => e.at > clock && e.at <= stop).length;
1703
+ }
1704
+
1705
+ /** How many ends `walk_farm` walks in one transaction at `WALK_COMPUTE_UNITS`: an end takes at most some 2,300 units, with a
1706
+ * halving rate (`js/test/farm-ends.test.mjs` measures it). */
1707
+ export const WALK_PER_TX = 500;
1708
+ /** The compute units a transaction that walks ends asks for: `walk_farm`, or a move with ends to walk first. */
1709
+ export const WALK_COMPUTE_UNITS = 1_400_000;
1710
+ /** How many ends a move can walk first at `WALK_COMPUTE_UNITS` and still do its own work: past this, the farm is walked
1711
+ * first (`farmNeedsWalk`). */
1712
+ export const MOVE_WALK_ROOM = 400;
1713
+
1714
+ /**
1715
+ * Whether a farm has passed more ends than a move can walk in its own transaction (`endsToWalk`, `MOVE_WALK_ROOM`): such
1716
+ * a move fails on compute until `walk_farm` has caught the farm up. Nothing is lost by waiting: what a seat earned by
1717
+ * its end is written when the farm walks past it, however late.
1718
+ */
1719
+ export const farmNeedsWalk = (farm, ends, now) => endsToWalk(farm, ends, now) > MOVE_WALK_ROOM;
1720
+
1721
+ /**
1722
+ * The transactions that walk a farm up to now, oldest ends first: one `walkFarmIx` each, `perTx` ends at most, `[]` when
1723
+ * it has nothing to walk. Anyone may send them, in order; each is free but for its network fee.
1724
+ */
1725
+ export function walkFarmParts({ project, id, farm, ends, now, perTx = WALK_PER_TX }) {
1726
+ const left = endsToWalk(farm, ends, now);
1727
+ const parts = [];
1728
+ for (let done = 0; done < left; done += perTx) parts.push(walkFarmIx({ project, id, farm, most: Math.min(perTx, left - done) }));
1729
+ return parts;
1730
+ }
1731
+
1732
+ /**
1733
+ * Credits a farm up to at most `most` of the times its seats stopped earning. Anyone may send it, signing nothing but its
1734
+ * network fee: it decides nothing, every move walks the same times the same way. For a farm that has passed more of them
1735
+ * than a move's transaction can walk (`farmNeedsWalk`, `walkFarmParts`).
1736
+ */
1737
+ export const walkFarmIx = ({ project, id, farm, most = WALK_PER_TX }) =>
1738
+ lootIx('walk_farm', { most }, farmSide(project, id, farm));
1739
+ /** Whether a farm reads or writes its seats' players' profiles (`Farm::uses_profile`): a gate or a boost on a profile,
1740
+ * or a growth written on one. Its moves then pass the player's profile. */
1741
+ const usesProfile = (farm) => [...(farm.gates ?? []), ...(farm.boosts ?? []).map((b) => b.rule)].some((r) => r?.of === 'Profile') || (farm.growth ?? []).some((g) => g.profile);
1742
+ const profileOf = (project, farm, player) => (usesProfile(farm) ? pda.profile(project, player) : null);
1743
+
1744
+ /** What a move that pays a seat writes: the rewards' mint, when the farm mints at a fixed rate (it mints at payout). */
1745
+ const payWrites = (farm) => (farm.rate?.mints ? ['rewards_mint'] : []);
1249
1746
 
1250
1747
  /** A token seat's accounts; `paid`: rewards may be paid (an unstake), so the player's account of them comes. */
1251
1748
  function tokenSeat({ player, project, id, farm, programs, paid = false }) {
@@ -1259,33 +1756,70 @@ function tokenSeat({ player, project, id, farm, programs, paid = false }) {
1259
1756
  mint,
1260
1757
  player_tokens: ata(player, mint, tokenProgramOf(programs, mint)),
1261
1758
  player_rewards: paid ? ata(player, farm.rewards_mint, tokenProgramOf(programs, farm.rewards_mint)) : null,
1262
- ...farmSinks(project, farm),
1759
+ exit_pool: sinkPool(project, farm.exit_to),
1760
+ profile: profileOf(project, farm, player),
1263
1761
  };
1264
1762
  }
1265
1763
 
1266
1764
  /**
1267
- * `player` stakes `amount` at lock tier `tier`. `farm` is decoded; `extra` carries token hook/memo accounts. A new tier
1268
- * on a seat with a booster, in a farm with growth, needs the booster's growth written first: put a `refreshSeatIx` of
1269
- * the seat before it, in the same transaction.
1765
+ * `player` stakes `amount` at lock tier `tier`. `farm` is decoded; `extra` carries token hook/memo accounts. A seat with
1766
+ * a booster on, or one whose growth the stake changes, needs a `refreshSeatIx` of it first, in the same transaction
1767
+ * (`stakeRefreshFirst`).
1270
1768
  */
1271
1769
  export const stakeTokensIx = ({ player, project, id, farm, amount, tier = 0, programs, shown, extra = [] }) =>
1272
- lootIx('stake_tokens', { amount, tier }, tokenSeat({ player, project, id, farm, programs }), { remaining: [...shownAccounts(project, shown), ...extra], writable: [...farmWrites(farm), ...tollWrites(farm)] });
1770
+ lootIx('stake_tokens', { amount, tier }, tokenSeat({ player, project, id, farm, programs }), { remaining: [...shownAccounts(project, shown), ...tailOf(extra)] });
1273
1771
 
1274
1772
  /**
1275
- * `player` takes `amount` out (0: only collects what is owed, and takes out tokens whose unbonding wait is over);
1276
- * `forfeit` gives up rewards that can't move. Before the tier's lock has run, a tier with an early-exit fee
1277
- * (`exit_bps`) lets it leave, leaving that share of `amount` to the farm's `exit_to`. In a farm that unbonds
1278
- * (`unbond`), what is taken out waits that long first, earning nothing (`withdrawable`).
1773
+ * `player` takes `amount` out (0: only collects what is owed); `forfeit` gives up rewards that can't move. Before the
1774
+ * tier's lock has run, a tier with an early-exit fee (`exit_bps`) lets it leave, leaving that share of `amount` to the
1775
+ * farm's `exit_to`; it keeps the rewards it was credited.
1279
1776
  */
1280
1777
  export const unstakeTokensIx = ({ player, project, id, farm, amount, forfeit = false, programs, extra = [] }) =>
1281
1778
  lootIx('unstake_tokens', { amount, forfeit }, tokenSeat({ player, project, id, farm, programs, paid: !forfeit }), {
1282
- remaining: extra,
1283
- // An exit fee that burns writes the staked mint; a toll that burns, the rewards' mint.
1284
- writable: [...farmWrites(farm), ...tollWrites(farm), ...(farm.exit_to === 'Burn' ? ['mint'] : [])],
1779
+ remaining: tailOf(extra),
1780
+ // An exit fee that burns writes the staked mint; a farm that mints, its rewards' mint when it pays.
1781
+ writable: [...(forfeit ? [] : payWrites(farm)), ...(farm.exit_to === 'Burn' ? ['mint'] : [])],
1782
+ });
1783
+
1784
+ /**
1785
+ * Whether taking `amount` out of a token seat (decoded `seat`) needs a `refreshSeatIx` of it first, in the same
1786
+ * transaction (`RefreshFirst`): the farm grows, and the exit closes the seat or drops it below a growth's least stake, so
1787
+ * what it grew since its last refresh is written before it goes. Never under `forfeit`.
1788
+ */
1789
+ export const refreshFirst = (farm, seat, amount, forfeit = false) =>
1790
+ !forfeit && BigInt(amount) > 0n && (farm.growth ?? []).some((g) => {
1791
+ const least = BigInt(g.at_least ?? 0) > 0n ? BigInt(g.at_least) : 1n;
1792
+ return BigInt(seat.amount) >= least && BigInt(seat.amount) - BigInt(amount) < least;
1285
1793
  });
1286
1794
 
1287
- /** What a token seat may take out now (Unix seconds `now`): its unbonded tokens once their wait is over, else 0. */
1288
- export const withdrawable = (seat, now) => (BigInt(seat.leaving ?? 0) > 0n && BigInt(now) >= BigInt(seat.leaving_at) ? BigInt(seat.leaving) : 0n);
1795
+ /**
1796
+ * Whether a stake of tokens at `tier` in a token seat (decoded `seat`; null for a new one), or with `compound` a
1797
+ * compound of what it is owed, needs a `refreshSeatIx` of it first, in the same transaction (`RefreshFirst`), at `now`
1798
+ * (Unix seconds, the chain's clock): a seat with a booster on (its boost is read again there, so a top-up never stakes
1799
+ * more at a boost its booster no longer gives), or a seat that grows (a booster's growth, or its player's profile's)
1800
+ * when the stake changes how: a new tier, a farm whose growth reads the amount, a seat past its term; a compound when
1801
+ * that growth reads the amount.
1802
+ * @param {any} farm
1803
+ * @param {any} seat
1804
+ * @param {{ tier?: number, now?: bigint | number, compound?: boolean }} [options]
1805
+ */
1806
+ export function stakeRefreshFirst(farm, seat, { tier = seat?.tier, now, compound = false } = {}) {
1807
+ if (!seat) return false;
1808
+ const growth = farm.growth ?? [];
1809
+ const booster = seat.asset !== undefined && seat.asset !== NO_ADDRESS;
1810
+ const grows = BigInt(seat.amount) > 0n && growth.some((g) => g.profile || booster);
1811
+ const amountGrows = growth.some((g) => g.profile || BigInt(g.at_least ?? 0) > 0n);
1812
+ if (compound) return BigInt(seat.amount) > 0n && (booster || (grows && amountGrows));
1813
+ const changes = Number(tier) !== Number(seat.tier) || amountGrows || (now !== undefined && BigInt(seat.earns_until ?? 0) <= BigInt(now));
1814
+ return booster || (grows && changes);
1815
+ }
1816
+
1817
+ /**
1818
+ * Whether a booster put on a token seat of `farm` (decoded) needs the seat refreshed first, in the same transaction
1819
+ * (`stake_nft`'s `RefreshFirst`): in a farm of tokens whose growth writes its players' profiles, the booster's growth
1820
+ * starts from now, so what the seat grew before is written first.
1821
+ */
1822
+ export const boosterRefreshFirst = (farm) => Boolean(farm.farmed?.Tokens) && (farm.growth ?? []).some((g) => g.profile);
1289
1823
 
1290
1824
  /**
1291
1825
  * Whether a farm (decoded) takes a stake at lock tier `tier` at `now` (Unix seconds, the chain's clock), as the program
@@ -1335,6 +1869,7 @@ export function piece({ project, asset, wallet, collection, hasAuthority = true,
1335
1869
  ? { collection: c.collection ?? null, tree_config: pda.treeConfig(c.tree), leaf_delegate: delegate, bubblegum_program: BUBBLEGUM_PROGRAM, compression_program: COMPRESSION_PROGRAM, log_wrapper: NOOP_PROGRAM }
1336
1870
  : {};
1337
1871
  return {
1872
+ kind: 'compressed',
1338
1873
  accounts: { ...NO_PIECE, asset: c.id, sheet: sheetOf(c.id), leaf: pda.leaf(c.id), tree: c.tree, ...bubblegum },
1339
1874
  proof: c.proof.map((node) => meta(typeof node === 'string' ? node : encodeBase58(node), false, false)),
1340
1875
  writable: calls ? ['leaf', 'tree', 'tree_config'] : [],
@@ -1350,6 +1885,7 @@ export function piece({ project, asset, wallet, collection, hasAuthority = true,
1350
1885
  const calls = moves || locks;
1351
1886
  const records = calls && pnft;
1352
1887
  return {
1888
+ kind: 'tokenMetadata',
1353
1889
  accounts: {
1354
1890
  ...NO_PIECE, asset, sheet: sheetOf(asset), metadata: tokenMetadata.metadata(asset), token,
1355
1891
  edition: calls ? tokenMetadata.edition(asset) : null,
@@ -1372,6 +1908,7 @@ export function piece({ project, asset, wallet, collection, hasAuthority = true,
1372
1908
  const inNone = !collection || collection === MPL_CORE_PROGRAM;
1373
1909
  const calls = moves || locks || writes || burns;
1374
1910
  return {
1911
+ kind: 'core',
1375
1912
  accounts: {
1376
1913
  ...NO_PIECE, asset, sheet: hasAuthority ? null : sheetOf(asset),
1377
1914
  collection: calls && !inNone ? collection : null, mpl_core_program: calls ? MPL_CORE_PROGRAM : null,
@@ -1381,6 +1918,41 @@ export function piece({ project, asset, wallet, collection, hasAuthority = true,
1381
1918
  };
1382
1919
  }
1383
1920
 
1921
+ /** The accounts a further NFT of a call of several takes (`Piece::another`), after the first NFT's: only what its kind
1922
+ * needs, the program's own address for one left out, then its proof. The first NFT passes every program any needs. */
1923
+ const ANOTHER = {
1924
+ core: ['asset', 'sheet', 'collection'],
1925
+ tokenMetadata: ['asset', 'sheet', 'metadata', 'edition', 'token', 'token_record', 'project_token', 'project_record', 'rule_set'],
1926
+ compressed: ['asset', 'sheet', 'collection', 'leaf', 'tree', 'tree_config', 'leaf_delegate'],
1927
+ };
1928
+ export function anotherMetas(p) {
1929
+ const writable = new Set([...p.writable, ...PIECE.filter((a) => a.writable).map((a) => a.name)]);
1930
+ return [...ANOTHER[p.kind].map((name) => (p.accounts[name] == null ? meta(LOOT_PROGRAM, false, false) : meta(p.accounts[name], false, writable.has(name)))), ...p.proof];
1931
+ }
1932
+
1933
+ /** The programs every NFT of a call of several needs, on the first NFT's piece (the call's own): Core, Token Metadata's
1934
+ * and Bubblegum's, by what the others are. */
1935
+ function withPrograms(first, others) {
1936
+ const kinds = new Set(others.map((p) => p.kind));
1937
+ const a = { ...first.accounts };
1938
+ if (kinds.has('core')) a.mpl_core_program ??= MPL_CORE_PROGRAM;
1939
+ if (kinds.has('tokenMetadata')) {
1940
+ a.token_metadata_program ??= TOKEN_METADATA_PROGRAM;
1941
+ a.spl_token_program ??= TOKEN_PROGRAM;
1942
+ a.spl_ata_program ??= ASSOCIATED_TOKEN_PROGRAM;
1943
+ a.instructions ??= SYSVAR_INSTRUCTIONS;
1944
+ a.rules_program ??= others.some((p) => p.accounts.rule_set && p.accounts.rule_set !== TOKEN_METADATA_PROGRAM) ? AUTH_RULES_PROGRAM : others.some((p) => p.accounts.token_record) ? TOKEN_METADATA_PROGRAM : null;
1945
+ }
1946
+ if (kinds.has('compressed')) Object.assign(a, { bubblegum_program: a.bubblegum_program ?? BUBBLEGUM_PROGRAM, compression_program: a.compression_program ?? COMPRESSION_PROGRAM, log_wrapper: a.log_wrapper ?? NOOP_PROGRAM });
1947
+ return { ...first, accounts: a };
1948
+ }
1949
+
1950
+ /**
1951
+ * Several NFTs on one call (a hold, a stash, a farm's seats): their groups sorted by key (`keyOf`), the first the
1952
+ * call's own and each further one after it, in ascending order (`OutOfOrder` otherwise). Returns [first, ...rest].
1953
+ */
1954
+ const ascending = (items, keyOf) => [...items].sort((a, b) => compareBytes(addressBytes(keyOf(a)), addressBytes(keyOf(b))));
1955
+
1384
1956
  /** The account an NFT paid goes to: the wallet it is given to, the stash it goes into, or the
1385
1957
  * incinerator for a burn (any account stands in for a Core asset, which Core burns). `then`: 'Burn', { Give: [wallet] }
1386
1958
  * or { Stash: [id] }. */
@@ -1427,7 +1999,7 @@ export function payNftIx({ player, project, asset, to = null, collection, hasAut
1427
1999
  if (to === project) throw new Error('An NFT paid can’t go to the project’s own address: name a wallet, or burn it.');
1428
2000
  const receiver = to ?? INCINERATOR;
1429
2001
  const p = passedOn({ project, owner: player, receiver, burns: receiver === INCINERATOR, asset, collection, hasAuthority, tokenMetadata: tm, compressed });
1430
- return lootIx('pay_nft', { reference: reference(ref) }, { player, project, ...p.accounts, to: receiver }, { remaining: [...p.proof, ...extra], writable: p.writable });
2002
+ return lootIx('pay_nft', { reference: reference(ref) }, { player, project, ...p.accounts, to: receiver }, { remaining: [...p.proof, ...tailOf(extra)], writable: p.writable });
1431
2003
  }
1432
2004
 
1433
2005
  /**
@@ -1491,11 +2063,53 @@ export const TOKEN_ACCOUNT_RENT = 2_039_280n;
1491
2063
  /** A route that only hands the NFT to the project or sends it on (a hold, an auction's lot, a refund): no Sheet, no write. */
1492
2064
  const ONLY_MOVES = { moves: true, locks: false, writes: false, reads: false };
1493
2065
 
2066
+ /** Whether `asset` goes as its hold (`["hold", asset, 0]`, passed in its piece's place): `hold` true for every NFT a
2067
+ * route owes, or a list of the assets to hold. */
2068
+ const holds = (hold, asset) => (Array.isArray(hold) ? hold.includes(asset) : Boolean(hold));
2069
+
1494
2070
  /** A piece that reads no NFT: `at` (an auction's or a seat's own address) stands in for one. */
1495
2071
  const standIn = (at) => ({ accounts: { ...NO_PIECE, asset: at }, proof: [], writable: [] });
1496
2072
 
1497
- /** Anyone closes a compressed NFT's record (`pda.leaf`) that no longer matches its tree; its rent goes to `payer`, who paid it. */
1498
- export const closeLeafIx = ({ asset, tree, payer }) => lootIx('close_leaf', {}, { record: pda.leaf(asset), tree, payer });
2073
+ /**
2074
+ * A compressed NFT's record (`pda.leaf`) closes, its rent back to `payer`, who paid it: at any time with `payer` signing
2075
+ * (`signs`, the default), or by anyone once it no longer matches its tree. A route that finds no record asks for a proof
2076
+ * again (`NotProven`), so a client closes it in the transaction that used it (`withLeafCloses`).
2077
+ */
2078
+ export const closeLeafIx = ({ asset, tree, payer, signs = true }) => lootIx('close_leaf', {}, { record: pda.leaf(asset), tree, payer, project: null, edition: null }, { signer: signs ? ['payer'] : [] });
2079
+
2080
+ const PROVE_LEAF = leaves(instructionDef('prove_leaf').accounts).map((a) => a.name);
2081
+ const CLOSE_LEAF = leaves(instructionDef('close_leaf').accounts).map((a) => a.name);
2082
+ const isLoot = (ix, name) => ix.programId === LOOT_PROGRAM && instructionOf(ix.data)?.name === name;
2083
+
2084
+ /**
2085
+ * `instructions` with a `close_leaf` after them for each compressed NFT's record a `prove_leaf` among them makes, its
2086
+ * prover signing as the record's payer, so the record's rent comes back in the same transaction (one closed already is
2087
+ * left alone).
2088
+ */
2089
+ export function withLeafCloses(instructions) {
2090
+ const at = (ix, names, name) => ix.keys[names.indexOf(name)]?.pubkey;
2091
+ const closed = new Set(instructions.filter((ix) => isLoot(ix, 'close_leaf')).map((ix) => at(ix, CLOSE_LEAF, 'record')));
2092
+ const closes = [];
2093
+ for (const ix of instructions.filter((x) => isLoot(x, 'prove_leaf'))) {
2094
+ const record = at(ix, PROVE_LEAF, 'record');
2095
+ if (closed.has(record)) continue;
2096
+ closed.add(record);
2097
+ closes.push(closeLeafIx({ asset: at(ix, PROVE_LEAF, 'asset'), tree: at(ix, PROVE_LEAF, 'tree'), payer: at(ix, PROVE_LEAF, 'signer') }));
2098
+ }
2099
+ return closes.length ? [...instructions, ...closes] : instructions;
2100
+ }
2101
+
2102
+ /**
2103
+ * Anyone closes the Sheet of an NFT that is burned: `burned` is what shows it (the Core asset's own address, burned to
2104
+ * one byte; a Token Metadata mint holding none; or a compressed NFT's record proving its leaf burned). Its rent goes to
2105
+ * the project's `treasury` wallet. A compressed NFT's (`tree`: the tree it was minted in) closes only on a record proven
2106
+ * empty (its data hash all zeros: a live leaf of the default owner is not burned) that still reads against that tree,
2107
+ * or whose tree is gone: the tree goes in the edition's place, and `burned` defaults to the NFT's record
2108
+ * (`pda.leaf`). A Token Metadata NFT's (`tokenMetadata`: true, or its kind) names its edition, which must be closed
2109
+ * too: its mint holds none, with no mint authority but its edition (`NotBurned` otherwise).
2110
+ */
2111
+ export const closeSheetIx = ({ project, nft, tree = null, burned = tree ? pda.leaf(nft) : nft, treasury, tokenMetadata: tm = null }) =>
2112
+ lootIx('close_leaf', {}, { record: pda.sheet(project, nft), tree: burned, payer: treasury, project, edition: tm ? tokenMetadata.edition(nft) : tree });
1499
2113
 
1500
2114
  /**
1501
2115
  * Anyone proves a compressed NFT's leaf (`compressed` from `compressedFromDas`): put it before the instruction that
@@ -1531,6 +2145,7 @@ function nftSeat({ player, project, id, farm, asset, collection, hasAuthority =
1531
2145
  const p = piece({ project, asset, wallet: player, collection, hasAuthority, tokenMetadata: tm, compressed, ...keeps });
1532
2146
  const key = compressed ? compressed.id : asset;
1533
2147
  return {
2148
+ kind: p.kind,
1534
2149
  accounts: {
1535
2150
  ...farmSide(project, id, farm),
1536
2151
  player,
@@ -1539,67 +2154,101 @@ function nftSeat({ player, project, id, farm, asset, collection, hasAuthority =
1539
2154
  seat: pda.seat(farmAddress, farm.farmed.Tokens ? player : key),
1540
2155
  // Rewards are paid only as an NFT's own seat leaves.
1541
2156
  player_rewards: leaving && !farm.farmed.Tokens ? ata(player, farm.rewards_mint, tokenProgramOf(programs, farm.rewards_mint)) : null,
2157
+ profile: profileOf(project, farm, player),
1542
2158
  },
1543
2159
  proof: p.proof,
1544
2160
  writable: p.writable,
1545
2161
  };
1546
2162
  }
1547
2163
 
2164
+ /** A further seat's group of a call of several (`NftSeat`): the seat, its NFT's accounts (`anotherMetas`), its box and
2165
+ * order leaving a mission that rolls (the program's address for none), then its proof (and an unstake's prize's). */
2166
+ const seatGroup = (seat, p, box = null, order = null) => [meta(seat, false, true), ...anotherMetas({ ...p, proof: [] }), meta(box ?? LOOT_PROGRAM, false, false), meta(order ?? LOOT_PROGRAM, false, Boolean(order)), ...p.proof];
2167
+
1548
2168
  /**
1549
2169
  * `player` stakes an NFT: on a seat of its own in a farm of collectibles, or as the booster of the player's token seat
1550
2170
  * in a farm of tokens. Any kind (`piece`): a Core asset, a Token Metadata NFT's mint with `tokenMetadata`, or
1551
2171
  * `compressed` (after a `proveLeafIx` in the same transaction). `previous`: in a farm that watches, the last staker of
1552
- * an NFT that changed hands, whose seat this takes over. `extra` carries reward token hooks/memos and Core transfer
1553
- * adapter accounts.
2172
+ * an NFT that changed hands, whose seat this takes over (alone). In a farm of collectibles, `more` NFTs (each { asset,
2173
+ * collection, hasAuthority, tokenMetadata, compressed }) go in the same call at the same tier, one fee, by their seats'
2174
+ * addresses. `extra` carries reward token hooks/memos and Core transfer adapter accounts.
1554
2175
  */
1555
- export const stakeNftIx = ({ player, project, id, farm, asset, collection, hasAuthority = true, tokenMetadata: tm, compressed, previous = null, tier = 0, programs, extra = [] }) => {
2176
+ export const stakeNftIx = ({ player, project, id, farm, asset, collection, hasAuthority = true, tokenMetadata: tm, compressed, previous = null, tier = 0, programs, more = [], extra = [] }) => {
1556
2177
  // Its lock's end, when the farm keeps one, is written on it.
1557
2178
  const keeps = farmKeeps(farm, { writes: Boolean(farm.lock_key) });
1558
- const seat = nftSeat({ player, project, id, farm, asset, collection, hasAuthority, tokenMetadata: tm, compressed, previous, programs, keeps });
1559
- return lootIx('stake_nft', { tier }, { ...seat.accounts, toll_pool: farmSinks(project, farm).toll_pool }, { remaining: [...seat.proof, ...extra], writable: [...farmWrites(farm), ...tollWrites(farm), ...seat.writable] });
2179
+ const [first, ...rest] = ascending([{ asset, collection, hasAuthority, tokenMetadata: tm, compressed }, ...more].map((n) => nftSeat({ player, project, id, farm, ...n, previous, programs, keeps })), (x) => x.accounts.seat);
2180
+ const seat = withPrograms({ ...first, kind: first.kind }, rest);
2181
+ const groups = rest.flatMap((x) => seatGroup(x.accounts.seat, x));
2182
+ return lootIx('stake_nft', { tier }, seat.accounts, { remaining: [...seat.proof, ...groups, ...tailOf(extra)], writable: seat.writable });
1560
2183
  };
1561
2184
 
1562
2185
  /**
1563
2186
  * `player` takes an NFT out once its lock has run: it grows on its way out (a farm with growth writes on it, or on its
1564
2187
  * Sheet), and from its own seat takes its tier's prize. `forfeit` gives up the rewards and the prize. Leaving a tier
1565
- * that rolls (`roll`: its box id), the NFT is rolled on that box, in an order at `nonce` (anyone then asks the oracle,
1566
- * `askRollIx`, and settles); a box that can burn it or give it away keeps it in the project's hands until then.
1567
- */
1568
- export const unstakeNftIx = ({ player, project, id, farm, asset, collection, hasAuthority = true, tokenMetadata: tm, compressed, forfeit = false, roll = null, nonce = 0n, tier, projectCollection, poolMints, items, nft, programs, extra = [] }) => {
1569
- // It grows on its way out; a mission that rolls may hand it to the project.
1570
- const keeps = farmKeeps(farm, { writes: Boolean(farm.growth?.length), rolls: roll != null });
1571
- const seat = nftSeat({ player, project, id, farm, asset, collection, hasAuthority, tokenMetadata: tm, compressed, programs, leaving: true, keeps });
1572
- // Leaving a mission that rolls (its tier's `roll`, a box id): the box, and the order the roll is made in.
1573
- const rolls = roll != null ? { loot_box: pda.box(project, roll), order: pda.order(project, player, nonce) } : { loot_box: null, order: null };
1574
- // A mission's prize (its tier's, `tier` the seat's): its accounts after the proof, unread when it is not earned; a
1575
- // roll's order at `nonce` plus 1. A forfeit takes neither the prize nor the rewards (the program reads neither), so it
1576
- // passes neither: the smallest way out.
1577
- const prize = tier != null && !forfeit ? farm.tiers[tier]?.prize : null;
1578
- const prized = prize ? prizeMetas({ project, collection: projectCollection ?? collection, prize, winner: player, nonce: toBigInt(nonce) + 1n, poolMints, items, nft, programs }) : [];
2188
+ * that rolls (`roll`: its box id, `boxes` its decoded box by id), the NFT is rolled on that box, in an order named by
2189
+ * `nonce` (`namedRollNonce`: the NFT's own on a box that holds it; its player or the project's keys then ask the oracle,
2190
+ * `askRollIx`, and settles); a box that can burn it or give it away keeps it in the project's hands until then. `more`
2191
+ * NFTs of the player's in the farm (each { asset, collection, hasAuthority, tokenMetadata, compressed, tier, roll }) leave
2192
+ * in the same call, one fee, one transfer of rewards: by their seats' addresses, the `i`th's roll at `nonce + 2i` and its
2193
+ * prize's at `nonce + 2i + 1` (`seatNonces` says which NFT has which). One still locked refuses the whole call.
2194
+ */
2195
+ export const unstakeNftIx = ({ player, project, id, farm, asset, collection, hasAuthority = true, tokenMetadata: tm, compressed, forfeit = false, roll = null, nonce = 0n, tier, projectCollection, poolMints, items, nft, boxes = {}, programs, more = [], extra = [] }) => {
2196
+ const seats = ascending([{ asset, collection, hasAuthority, tokenMetadata: tm, compressed, tier, roll }, ...more].map((n) => {
2197
+ // It grows on its way out; a mission that rolls may hand it to the project.
2198
+ const keeps = farmKeeps(farm, { writes: Boolean(farm.growth?.length), rolls: n.roll != null });
2199
+ return { n, seat: nftSeat({ player, project, id, farm, ...n, programs, leaving: true, keeps }) };
2200
+ }), (x) => x.seat.accounts.seat);
2201
+ const several = seats.length > 1;
2202
+ // Each seat's mission: the box and the order its roll is made in, at `nonce + 2i`; its prize (its tier's) after its
2203
+ // proof, a roll's order at `nonce + 2i + 1`. A forfeit takes neither the prize nor the rewards (the program reads
2204
+ // neither), so a call of one passes neither: the smallest way out. In a call of several each seat with a prize passes
2205
+ // its accounts, given or not, so the next seat's start where the program looks.
2206
+ const parts = seats.map(({ n, seat }, i) => {
2207
+ const at = BigInt.asUintN(64, toBigInt(nonce) + 2n * BigInt(i));
2208
+ const prize = n.tier != null && (!forfeit || several) ? farm.tiers[n.tier]?.prize : null;
2209
+ const prized = prize ? prizeMetas({ project, collection: projectCollection ?? n.collection, prize, winner: player, nonce: BigInt.asUintN(64, at + 1n), poolMints, items, nft, programs }) : [];
2210
+ const rolls = n.roll != null ? { box: pda.box(project, n.roll), order: pda.order(project, player, namedRollNonce(boxes[n.roll], seat.accounts.asset, at)) } : { box: null, order: null };
2211
+ return { seat, prize, prized, rolls };
2212
+ });
2213
+ const [first, ...rest] = parts;
2214
+ const seat = withPrograms(first.seat, rest.map((x) => x.seat));
1579
2215
  if (forfeit) seat.accounts.player_rewards = null;
1580
- return lootIx('unstake_nft', { forfeit, nonce }, { ...seat.accounts, ...rolls, associated_token_program: prize?.Tokens ? ASSOCIATED_TOKEN_PROGRAM : null, toll_pool: farmSinks(project, farm).toll_pool }, {
1581
- remaining: [...seat.proof, ...prized, ...extra],
1582
- writable: [...farmWrites(farm), ...tollWrites(farm), ...seat.writable],
2216
+ const groups = rest.flatMap((x) => [...seatGroup(x.seat.accounts.seat, x.seat, x.rolls.box, x.rolls.order), ...x.prized]);
2217
+ return lootIx('unstake_nft', { forfeit, nonce }, { ...seat.accounts, loot_box: first.rolls.box, order: first.rolls.order, associated_token_program: parts.some((x) => x.prize?.Tokens) ? ASSOCIATED_TOKEN_PROGRAM : null }, {
2218
+ remaining: [...seat.proof, ...first.prized, ...groups, ...tailOf(extra)],
2219
+ writable: [...(forfeit ? [] : payWrites(farm)), ...seat.writable],
1583
2220
  });
1584
2221
  };
1585
2222
 
2223
+ /** The nonce each NFT's mission roll is named by in an `unstakeNftIx` of several (`assets`), in the order the call
2224
+ * takes them: the `i`th by its seat's address `nonce + 2i` (its order at `namedRollNonce` of it), its prize's roll
2225
+ * `nonce + 2i + 1` (at `prizeRollNonce`). */
2226
+ export const seatNonces = (project, id, farm, player, assets, nonce = 0n) =>
2227
+ Object.fromEntries(ascending(assets, (a) => pda.seat(pda.farm(project, id), farm.farmed.Tokens ? player : a)).map((a, i) => [a, toBigInt(nonce) + 2n * BigInt(i)]));
2228
+
1586
2229
  /**
1587
- * `player` collects what the seat of `by` owes (their own address for tokens, a collectible's asset). `held`: in a farm
2230
+ * `player` collects what the seat of `by` owes (their own address for tokens, a collectible's asset); with `compound`, a
2231
+ * token seat's is staked instead (a seat with a booster on needs a `refreshSeatIx` of it first: `stakeRefreshFirst`). `held`: in a farm
1588
2232
  * that watches, what shows the player still holds the seat's NFT (the Core asset, their token account of a Token
1589
- * Metadata NFT, or a compressed NFT's record, `pda.leaf`, with its `tree`), which such a farm refuses to go without.
2233
+ * Metadata NFT, or a compressed NFT's record, `pda.leaf`, with its `tree`), which such a farm refuses to go without
2234
+ * (`NotProven`). `more` seats of the player's in the farm (each { by, held, tree }) collect in the same call, one fee,
2235
+ * one transfer.
1590
2236
  */
1591
- export const harvestIx = ({ player, project, id, farm, by = player, held = null, tree = null, compound = false, programs, extra = [] }) =>
1592
- lootIx('harvest', { compound }, {
2237
+ export const harvestIx = ({ player, project, id, farm, by = player, held = null, tree = null, compound = false, programs, more = [], extra = [] }) => {
2238
+ const farmAddress = pda.farm(project, id);
2239
+ const [first, ...rest] = ascending([{ by, held, tree }, ...more].map((x) => ({ ...x, seat: pda.seat(farmAddress, x.by) })), (x) => x.seat);
2240
+ const groups = rest.flatMap((x) => [meta(x.seat, false, true), meta(x.held ?? LOOT_PROGRAM, false, false), meta(x.tree ?? LOOT_PROGRAM, false, false)]);
2241
+ return lootIx('harvest', { compound }, {
1593
2242
  ...farmSide(project, id, farm),
1594
2243
  player,
1595
- seat: pda.seat(pda.farm(project, id), by),
2244
+ seat: first.seat,
1596
2245
  player_rewards: ata(player, farm.rewards_mint, tokenProgramOf(programs, farm.rewards_mint)),
1597
- held,
1598
- tree,
2246
+ held: first.held,
2247
+ tree: first.tree,
1599
2248
  // Compounding moves what is owed into the staked vault (a farm of tokens whose rewards are the staked token).
1600
- staked_vault: compound ? pda.staked(pda.farm(project, id)) : null,
1601
- toll_pool: farmSinks(project, farm).toll_pool,
1602
- }, { remaining: extra, writable: [...farmWrites(farm), ...tollWrites(farm)] });
2249
+ staked_vault: compound ? pda.staked(farmAddress) : null,
2250
+ }, { remaining: [...groups, ...tailOf(extra)], writable: payWrites(farm) });
2251
+ };
1603
2252
 
1604
2253
  /**
1605
2254
  * Anyone (`signer`) writes what a seat's NFT grew (a farm with growth), then reads its boosts again. `seat` is the
@@ -1615,7 +2264,7 @@ export const refreshSeatIx = ({ signer, project, id, farm, by, seat, collection,
1615
2264
  const p = seat.asset === NO_ADDRESS
1616
2265
  ? standIn(address)
1617
2266
  : piece({ project, asset: seat.asset, wallet: holder, collection, hasAuthority, tokenMetadata: tm, compressed, ...farmKeeps(farm, { writes: Boolean(farm.growth?.length) }), moves: false, locks: false });
1618
- return lootIx('refresh_seat', {}, { signer, ...farmSide(project, id, farm), seat: address, ...p.accounts }, { remaining: extra, writable: [...farmWrites(farm), ...p.writable] });
2267
+ return lootIx('refresh_seat', {}, { signer, ...farmSide(project, id, farm), seat: address, ...p.accounts, profile: profileOf(project, farm, seat.player) }, { remaining: tailOf(extra), writable: p.writable });
1619
2268
  };
1620
2269
 
1621
2270
  // ---------------------------------------------------------------- recipes and holds
@@ -1624,19 +2273,23 @@ export const refreshSeatIx = ({ signer, project, id, farm, by, seat, collection,
1624
2273
  * `player` crafts recipe `id` (`recipe` decoded, the version shown to them), `times` over. `assets`: the
1625
2274
  * collectibles handed in, in the recipe's order (the burned, the used, and for a `Show` line the NFT shown: its address
1626
2275
  * when it is the project's, `{ asset, collection }` for a Core NFT of another collection, `{ mint, owner }` for a Token
1627
- * Metadata one; for a `Trade` line the NFT traded in, as `piece` takes it: `{ asset, collection, tokenMetadata,
1628
- * compressed }`). A collectible used or renewed that is staked in a farm holding it for the player (escrow) is
1629
- * `{ asset, farm }`, `farm` that farm's id: its seat and farm follow it. A `Staked` line names the farm its collectible
2276
+ * Metadata one, `{ compressed, tree }` for a compressed one; with `farm` (a farm's id: `{ asset, farm }` for the
2277
+ * project's own) when the project locks it in the player's wallet on a seat of that farm, which freezes, and a raise on
2278
+ * it lowers a number: its seat and farm follow it, and the raise may not lower a number that farm reads; for a `Trade` line the NFT traded in, as `piece` takes it: `{ asset, collection, tokenMetadata,
2279
+ * compressed }`). A collectible used that is staked in a farm holding it for the player (escrow) or locking it in their
2280
+ * wallet (freeze) is `{ asset, farm }`, `farm` that farm's id: its seat and farm follow it (a raise on it may not lower a
2281
+ * number its farm reads: `BadRecipe`). A `Staked` line names the farm its collectible
1630
2282
  * is staked in (`Input::Staked { item, farm }`, escrowed or frozen there): its address, or `{ asset, farm }` with that
1631
2283
  * farm, and its seat and that farm follow it. Each item line passes its item's collection
1632
2284
  * (`items[id].collection`, the decoded Items; `collection` when not given), written for a burn: Core writes it as it
1633
2285
  * burns a collectible. A recipe's `raises` write on the kept ones, in their Sheet when the project has no
1634
2286
  * authority over them. The player opens any account a line pays into that is not there. A roll it makes (a
1635
- * `Prize::Roll` or a `RollOn`) passes its box's prize mints (`rollMints`): `boxes` maps the box's id to its decoded
1636
- * LootBox, `rollPools` each pool its prizes name to its mint (null for one never opened; `poolMints` when not given).
2287
+ * `Prize::Roll` or a `RollOn`) passes its box and its order, nothing more; a `RollOn`'s order is where `namedRollNonce`
2288
+ * says, which reads the rolled box (`boxes`: a box's id to its decoded LootBox). Its player or the project's keys then ask for the roll
2289
+ * (`askRollIx`, in the same transaction when it fits).
1637
2290
  * A craft makes several Core calls: raise the compute limit. `extra`: a token's hook accounts.
1638
2291
  */
1639
- export function craftIx({ player, project, collection, id, recipe, assets = [], times = 1, nonce = 0n, holders, programs, treasury, poolMints, items = {}, nfts = {}, boxes = {}, rollPools = poolMints, extra = [] }) {
2292
+ export function craftIx({ player, project, collection, id, recipe, assets = [], times = 1, nonce = 0n, holders, programs, treasury, poolMints, items = {}, nfts = {}, boxes = {}, extra = [] }) {
1640
2293
  const remaining = [];
1641
2294
  const given = [...assets];
1642
2295
  const tokens = (mint) => ata(player, mint, tokenProgramOf(programs, mint));
@@ -1645,8 +2298,8 @@ export function craftIx({ player, project, collection, id, recipe, assets = [],
1645
2298
  if (input.Burn) {
1646
2299
  remaining.push(...itemLine(input.Burn.item, true));
1647
2300
  for (let i = 0; i < input.Burn.count; i++) remaining.push(meta(take(given, 'a collectible to burn'), false, true));
1648
- } else if (input.Use || input.Renew || input.Staked) {
1649
- remaining.push(...itemLine((input.Use ?? input.Renew ?? input.Staked).item));
2301
+ } else if (input.Use || input.Staked) {
2302
+ remaining.push(...itemLine((input.Use ?? input.Staked).item));
1650
2303
  const used = take(given, 'a collectible to use');
1651
2304
  // A staked one is on its seat in the farm its line names.
1652
2305
  if (input.Staked && typeof used !== 'string' && used.farm !== undefined && Number(used.farm) !== input.Staked.farm)
@@ -1670,11 +2323,19 @@ export function craftIx({ player, project, collection, id, recipe, assets = [],
1670
2323
  // with its Sheet, then its collection), a Token Metadata NFT ({ mint, owner }: its metadata, the holder's
1671
2324
  // token account and its Sheet), or a compressed NFT ({ compressed, tree }: its record, proven earlier in the
1672
2325
  // transaction, its tree and its Sheet). What a raise writes on is writable.
2326
+ // One the project locks in the player's wallet on a seat of a farm that freezes (`farm`, that farm's id) has its
2327
+ // seat and farm after its accounts, when a raise on it lowers a number.
2328
+ const place = assets.length - given.length;
1673
2329
  const nft = take(given, 'an NFT to show');
1674
- if (typeof nft === 'string') remaining.push(meta(nft, false, true), meta(input.Show.collection, false, false));
2330
+ if (typeof nft === 'string' || (nft.asset && nft.farm !== undefined && !('collection' in nft))) remaining.push(meta(nft.asset ?? nft, false, true), meta(input.Show.collection, false, false));
1675
2331
  else if (nft.asset) remaining.push(meta(nft.asset, false, true), meta(pda.sheet(project, nft.asset), false, true), meta(nft.collection ?? MPL_CORE_PROGRAM, false, false));
1676
2332
  else if (nft.compressed) remaining.push(...[nft.compressed, pda.leaf(nft.compressed), nft.tree].map((key) => meta(key, false, false)), meta(pda.sheet(project, nft.compressed), false, true));
1677
2333
  else remaining.push(...[nft.mint, tokenMetadata.metadata(nft.mint), ata(nft.owner ?? player, nft.mint, TOKEN_PROGRAM)].map((key) => meta(key, false, false)), meta(pda.sheet(project, nft.mint), false, true));
2334
+ const lowers = (recipe.raises ?? []).some((raise) => Number(raise.place) === place && BigInt(raise.shift.by) < 0n);
2335
+ if (lowers && typeof nft !== 'string' && nft.farm !== undefined && nft.farm !== null) {
2336
+ const farm = pda.farm(project, nft.farm);
2337
+ remaining.push(meta(pda.seat(farm, nft.asset ?? nft.compressed ?? nft.mint), false, false), meta(farm, false, false));
2338
+ }
1678
2339
  }
1679
2340
  }
1680
2341
  const made = {};
@@ -1683,16 +2344,18 @@ export function craftIx({ player, project, collection, id, recipe, assets = [],
1683
2344
  // A prize (`prizeMetas`): collectibles of one item made in order, each at the next serial.
1684
2345
  const prize = output.Prize[0];
1685
2346
  const item = prize.Item?.[0];
1686
- remaining.push(...prizeMetas({ project, collection, prize, winner: player, nonce, poolMints, items, nft: nfts[prize.Nft?.nft], made: made[item] ?? 0, programs, strict: true, boxes, rollPools }));
2347
+ remaining.push(...prizeMetas({ project, collection, prize, winner: player, nonce, poolMints, items, nft: nfts[prize.Nft?.nft], made: made[item] ?? 0, programs }));
1687
2348
  if (item !== undefined) made[item] = (made[item] ?? 0) + 1;
1688
2349
  } else if (output.RollOn) {
1689
- // The box, then the order its roll is made in, at the craft's `nonce`, then the box's prize mints (`rollMints`).
1690
- remaining.push(meta(pda.box(project, output.RollOn.box_id), false, false), meta(pda.order(project, player, nonce), false, true), ...strictRollMints(project, output.RollOn.box_id, boxes, rollPools ?? {}));
2350
+ // The box, then the order its roll is made in (`namedRollNonce` of the craft's `nonce`).
2351
+ const on = assets[output.RollOn.place];
2352
+ const order = pda.order(project, player, namedRollNonce(boxes[output.RollOn.box_id], typeof on === 'string' ? on : on?.asset ?? on?.mint ?? on?.compressed, nonce));
2353
+ remaining.push(meta(pda.box(project, output.RollOn.box_id), false, false), meta(order, false, true));
1691
2354
  }
1692
2355
  }
1693
2356
  const address = pda.recipe(project, id);
1694
- const receipt = limits(recipe.per_wallet) ? pda.receipt(address, player) : null;
1695
- return lootIx('craft', { version: recipe.version, times, nonce }, { player, project, recipe: address, receipt }, { remaining: [...remaining, ...extra] });
2357
+ const counts = limits(recipe.per_wallet) ? pda.counts(project, player) : null;
2358
+ return lootIx('craft', { version: recipe.version, times, nonce }, { player, project, recipe: address, counts }, { remaining: [...remaining, ...tailOf(extra)] });
1696
2359
  }
1697
2360
 
1698
2361
  function take(list, what) {
@@ -1708,9 +2371,16 @@ export const craftNewAssets = (recipe) => recipe.outputs.filter((o) => o.Prize?.
1708
2371
  * asset (`collection` its own, Core's address for none), a Token Metadata NFT (`tokenMetadata`), or `compressed` (after
1709
2372
  * a `proveLeafIx`).
1710
2373
  */
1711
- export function holdIx({ player, project, collection, asset, tokenMetadata: tm, compressed, freeAt, reference: ref }) {
1712
- const p = piece({ project, asset, wallet: player, collection, tokenMetadata: tm, compressed, ...ONLY_MOVES });
1713
- return lootIx('hold', { free_at: toBigInt(freeAt), reference: reference(ref) }, { player, project, ...p.accounts, hold: pda.hold(p.accounts.asset, freeAt) }, { remaining: p.proof, writable: p.writable });
2374
+ export function holdIx({ player, project, collection, asset, tokenMetadata: tm, compressed, freeAt, reference: ref, more = [] }) {
2375
+ // Several NFTs (`more`: each { asset, collection, tokenMetadata, compressed }) go on holds of their own with one end, one
2376
+ // fee: by their holds' addresses, the first the call's own.
2377
+ const [first, ...rest] = ascending([{ asset, collection, tokenMetadata: tm, compressed }, ...more].map((n) => {
2378
+ const p = piece({ project, asset: n.asset, wallet: player, collection: n.collection, tokenMetadata: n.tokenMetadata, compressed: n.compressed, ...ONLY_MOVES });
2379
+ return { p, hold: pda.hold(p.accounts.asset, freeAt) };
2380
+ }), (x) => x.hold);
2381
+ const p = withPrograms(first.p, rest.map((x) => x.p));
2382
+ const groups = rest.flatMap((x) => [meta(x.hold, false, true), ...anotherMetas(x.p)]);
2383
+ return lootIx('hold', { free_at: toBigInt(freeAt), reference: reference(ref) }, { player, project, ...p.accounts, hold: first.hold }, { remaining: [...p.proof, ...groups], writable: p.writable });
1714
2384
  }
1715
2385
 
1716
2386
  // ---------------------------------------------------------------- stashes
@@ -1719,10 +2389,13 @@ export function holdIx({ player, project, collection, asset, tokenMetadata: tm,
1719
2389
  * The project's owner or authority (`signer`) puts an NFT it holds into stash `id`, or (`out`: a wallet) takes one the
1720
2390
  * stash holds out to that wallet. The NFT is any kind, as `piece` takes it: `collection` a Core asset's own,
1721
2391
  * `tokenMetadata`, or `compressed` after a `proveLeafIx` in the same transaction. The signer pays what the stash grows
1722
- * by, and gets back the rent it no longer needs. Taken out with `out` the project's own address, the entry is only dropped
1723
- * from the stash and the NFT stays where it is. `out` is never another account of the program (`ToTheProject`).
2392
+ * by, and gets back the rent it no longer needs. `drop`: the NFT leaves the stash and stays where it is, owed to the
2393
+ * treasury in its hold (`["hold", nft, 0]`, the signer paying its rent until anyone `release`s it to the treasury, one
2394
+ * NFT a call). `out` is never the project's own address nor another account of the program (`ToTheProject`).
1724
2395
  */
1725
- export function stashIx({ signer, project, id, lottery = null, asset, out = null, collection, tokenMetadata: tm, compressed }) {
2396
+ export function stashIx({ signer, project, id, lottery = null, lotteryState = null, asset, out = null, drop = false, weight = 1, collection, tokenMetadata: tm, compressed, more = [] }) {
2397
+ if (drop && more.length) throw new Error('A stash drops one NFT a call.');
2398
+ if (drop) out = pda.hold(compressed?.id ?? asset, 0);
1726
2399
  const to = out ?? signer;
1727
2400
  // A lottery's own stash (`lottery`: the lottery's id): an NFT its place names leaves only as that prize, or once the
1728
2401
  // lottery has ended; taking one out passes the lottery.
@@ -1731,10 +2404,19 @@ export function stashIx({ signer, project, id, lottery = null, asset, out = null
1731
2404
  // Out to a wallet or the project's own address, never another account of the program (a record, a stash, a fee
1732
2405
  // box), where no record names the NFT and nothing could move it again (ToTheProject): the ones this call names are
1733
2406
  // refused before sending, and the program refuses any other (js/play only takes one out to the signer's wallet).
1734
- if (out && out !== project && [stash, own, pda.fees(project), pda.platform()].includes(out))
1735
- throw new Error('An NFT taken out of a stash can’t go to an account of the program: name a wallet, or the project’s own address to only drop it from the stash.');
1736
- const p = piece({ project, asset, wallet: to, collection, tokenMetadata: tm, compressed, ...ONLY_MOVES });
1737
- return lootIx('stash', { of: own ? { Lottery: [lottery] } : { Project: [id] }, out: Boolean(out) }, { signer, project, stash, to, ...p.accounts, lottery: own && out ? own : null }, { remaining: p.proof, writable: p.writable });
2407
+ if (out && !drop && [project, stash, own, pda.fees(project), pda.platform()].includes(out))
2408
+ throw new Error('An NFT taken out of a stash can’t go to the project or an account of the program: name a wallet, or drop it (`drop`: it stays, owed to the treasury).');
2409
+ // Several NFTs (`more`: each { asset, weight, collection, tokenMetadata, compressed }) go in or out in one call, one
2410
+ // fee, in ascending order of their addresses; put in, each weighs its `weight` in the stash's draws.
2411
+ const [first, ...rest] = ascending([{ asset, weight, collection, tokenMetadata: tm, compressed }, ...more].map((n) => ({
2412
+ n, p: piece({ project, asset: n.asset, wallet: drop ? project : to, collection: n.collection, tokenMetadata: n.tokenMetadata, compressed: n.compressed, ...ONLY_MOVES }),
2413
+ })), (x) => x.p.accounts.asset);
2414
+ const p = withPrograms(first.p, rest.map((x) => x.p));
2415
+ const weights = out ? [] : [first, ...rest].map((x) => x.n.weight ?? 1);
2416
+ // A lottery's own stash names the lottery, and the round it waits on (`idleRound`; `lotteryState`: the decoded
2417
+ // Lottery; none for one never written), whose places say what it locks.
2418
+ const round = own && lotteryState ? idleRound(own, lotteryState) : null;
2419
+ return lootIx('stash', { of: own ? { Lottery: [lottery] } : { Project: [id] }, out: Boolean(out), weights }, { signer, project, stash, to, ...p.accounts, lottery: own, round }, { remaining: [...p.proof, ...rest.flatMap((x) => anotherMetas(x.p))], writable: p.writable });
1738
2420
  }
1739
2421
 
1740
2422
  // ---------------------------------------------------------------- contests
@@ -1743,7 +2425,7 @@ export function stashIx({ signer, project, id, lottery = null, asset, out = null
1743
2425
  * `player` backs `side` with `amount`, naming the contest's incarnation (which fixes its terms). `contest` is the
1744
2426
  * decoded Contest. A contest of NFTs (`contest.nft`) takes one NFT instead (`nft`: { asset, collection, tokenMetadata,
1745
2427
  * compressed }, as `piece` takes it), which the contest holds until it is claimed. `shown`/`listProof` pass its gate.
1746
- * `extra`: a hook's accounts.
2428
+ * A new entry's record is the player's to pay (not refunded). `extra`: a hook's accounts.
1747
2429
  */
1748
2430
  export function enterIx({ player, project, id, contest, side, amount, shown, listProof, nft, playerToken, programs, extra = [] }) {
1749
2431
  const address = pda.contest(project, id);
@@ -1756,85 +2438,112 @@ export function enterIx({ player, project, id, contest, side, amount, shown, lis
1756
2438
  player,
1757
2439
  project,
1758
2440
  contest: address,
1759
- entry: pda.entry(address, player),
2441
+ entries: contestEntries(address, contest),
1760
2442
  player_token: playerToken ?? ata(player, contest.mint, tokenProgramOf(programs, contest.mint)),
1761
2443
  mint: contest.mint,
1762
2444
  },
1763
- { remaining: [...gateAccounts(project, { gate: contest.gate, wallet: player, shown, listProof }), ...(staked ? [...pieceMetas(staked), ...staked.proof] : []), ...extra] },
2445
+ { remaining: [...gateAccounts(project, { gate: contest.gate, wallet: player, shown, listProof }), ...(staked ? [...pieceMetas(staked), ...staked.proof] : []), ...tailOf(extra)] },
1764
2446
  );
1765
2447
  }
1766
2448
 
1767
2449
  /**
1768
- * Anyone (`payer`) claims `player`'s entry on `side`, into the player's account of the mint, which `payer` opens when
1769
- * it is not there (the last entry's claim closes the pot and writes the mint). A winning entry of a contest with a
1770
- * prize (`contest.prize`) also gets it (`prizeMetas`: `collection`, `poolMints`, `items`, `nft`; a roll's order at
1771
- * `nonce`, `prizeNonce(entry)` by default), `payer` paying what it opens. For a contest of NFTs, `entry` is the decoded
1772
- * Entry: its own NFT (`own`, its kind as `piece` takes it) goes back to its player, or to the opener for a loser's NFT
1773
- * no winner took; a winner, while losers' NFTs are left, also takes one (`taking`: { player, side } of a losing entry
1774
- * that still holds one, and `kind`, its NFT's kind). While losers' NFTs are left only the winner sends their claim, and
1775
- * picks which (`payer` the player; `WinnerPicks` otherwise). `extra`: a hook's accounts.
2450
+ * Anyone (`payer`) claims `player`'s entry (`entries`: the contest's decoded entries, `decodeEntries`), into the
2451
+ * player's account of the mint, which `payer` opens when it is not there. A winner's first claim also gives the
2452
+ * contest's prize (`prizeMetas`: `collection`, `poolMints`, `items`, `nft`; a roll's order at `nonce`, `prizeNonce` of the
2453
+ * contest and the player by default), unless the player (signing) `forgo`es it. In a contest of NFTs it moves the NFTs of
2454
+ * the entries `nfts` names by their place in the list (the player's own by default, while the contest holds it): an
2455
+ * entry's own back to its player (anyone sends it); a loser's to the winner while it has picks left of its share (only
2456
+ * the winner sends it, and picks which: `WinnerPicks`), or to the opener once no winner has picks left. `kinds` maps an
2457
+ * NFT to its kind, as `piece` takes it. With `hold` (true, or a list of assets), each becomes a hold for the wallet it goes to (an NFT that can't
2458
+ * move now); with `end`, the winner leaves the rest of its share to the opener. The claim that leaves nothing owed
2459
+ * closes the contest (`projectState`: its treasury); a contest that owes nothing (nobody entered, every part claimed,
2460
+ * off by itself with none in) is closed by any claim, the `payer`'s. `extra`: a hook's accounts.
1776
2461
  */
1777
- export function claimIx({ payer, project, id, contest, player, side, entry, own = {}, taking, nonce, collection, poolMints, items, nft, prizeMint, forgo = false, programs, extra = [] }) {
2462
+ export function claimIx({ payer, project, projectState, id, contest, entries, player, nfts, kinds = {}, nonce, collection, poolMints, items, nft, prizeMint, forgo = false, end = false, hold = false, programs, extra = [] }) {
1778
2463
  const address = pda.contest(project, id);
1779
- const entryAddress = pda.entry(address, player);
1780
- nonce = toBigInt(nonce ?? prizeNonce(entryAddress));
1781
- const won = contest.verdict === 'Won' && Number(contest.winner) === Number(side);
1782
- // A winning entry of a contest with a prize: the prize's accounts, after the NFTs'.
1783
- // A split prize is paid from the contest's vault (named accounts below), not given as a prize line.
2464
+ // A contest that owes nothing (nobody entered, or every part claimed) is only closed: by any claim, entry 0, any player.
2465
+ const closing = BigInt(contest.owed) === 0n;
2466
+ player ??= payer;
2467
+ const i = closing ? 0 : entries.findIndex((e) => e.player === player);
2468
+ if (i < 0) throw new Error(`${player} has no entry in this contest.`);
2469
+ const me = closing ? { side: 0, bits: CLAIMED | ENDED, amount: 0n, taken: 0 } : entries[i];
2470
+ nonce = toBigInt(nonce ?? prizeNonce(address, player));
2471
+ const won = contest.verdict === 'Won' && Number(contest.winner) === Number(me.side);
2472
+ const first = won && !(me.bits & CLAIMED);
2473
+ // A winner's first claim of a contest with a prize: the prize's accounts, after the NFTs'. A split prize is paid from
2474
+ // the contest's vault (named accounts below), not given as a prize line.
1784
2475
  const split = Boolean(contest.prize_split);
1785
- const prize = contest.prize != null && won && !split && !forgo ? prizeMetas({ project, collection, prize: contest.prize, winner: player, nonce, poolMints, items, nft, programs }) : [];
2476
+ const prize = contest.prize != null && first && !split && !forgo ? prizeMetas({ project, collection, prize: contest.prize, winner: player, nonce, poolMints, items, nft, programs }) : [];
1786
2477
  const pMint = split ? splitMint(contest.prize, prizeMint ?? poolMints?.[contest.prize?.Tokens?.from.Pool?.[0]]) : null;
1787
- const nfts = [];
1788
- if (contest.nft && entry && entry.asset !== NO_ADDRESS) {
1789
- const lost = contest.verdict === 'Won' && !won;
1790
- const mine = piece({ project, asset: entry.asset, wallet: lost ? contest.opener : player, ...own, ...ONLY_MOVES });
1791
- nfts.push(...pieceMetas(mine), ...mine.proof);
1792
- if (won && taking) {
1793
- const theirs = pda.entry(address, taking.player);
1794
- const p = piece({ project, asset: taking.asset, wallet: player, ...taking.kind, ...ONLY_MOVES });
1795
- nfts.push(meta(theirs, false, true), ...pieceMetas(p), ...p.proof);
2478
+ nfts ??= !closing && contest.nft && Number(me.amount) === 1 ? [i] : [];
2479
+ const moved = [];
2480
+ let taken = Number(me.taken);
2481
+ for (const j of nfts) {
2482
+ const theirs = entries[j];
2483
+ let to = contest.opener;
2484
+ if (contest.verdict !== 'Won' || Number(theirs.side) === Number(contest.winner)) to = theirs.player;
2485
+ else if (won && payer === player && !(me.bits & ENDED) && taken < shareOf(contest, me)) [to, taken] = [player, taken + 1];
2486
+ moved.push(meta(theirs.player, false, true));
2487
+ if (holds(hold, theirs.nft)) moved.push(meta(pda.hold(theirs.nft, 0), false, true));
2488
+ else {
2489
+ const p = piece({ project, asset: theirs.nft, wallet: to, ...kinds[theirs.nft], ...ONLY_MOVES });
2490
+ moved.push(...pieceMetas(p), ...p.proof);
1796
2491
  }
1797
2492
  }
2493
+ // What it leaves owed: at 0 it closes the contest, burning what its pot (and a split prize's vault) still hold.
2494
+ const owed = BigInt(contest.owed);
2495
+ const left = closing ? 0n : !contest.nft ? owed - (contest.verdict === 'Won' ? (first ? BigInt(me.amount) : 0n) : 1n) : owed - BigInt(nfts.length) - (first && contest.prize ? 1n : 0n);
2496
+ // The claim that leaves nothing owed closes the contest, its rent and what is left to the treasury: it names it.
2497
+ if (left <= 0n && !projectState?.treasury) throw new Error('This claim closes the contest: pass projectState, for its treasury.');
1798
2498
  return lootIx(
1799
2499
  'claim',
1800
- { nonce, forgo },
2500
+ { entry: i, nonce, forgo, end, nfts },
1801
2501
  {
1802
2502
  payer,
1803
2503
  project,
1804
2504
  contest: address,
1805
- entry: entryAddress,
2505
+ entries: contestEntries(address, contest),
1806
2506
  player,
1807
2507
  opener: contest.opener,
1808
2508
  to: ata(player, contest.mint, tokenProgramOf(programs, contest.mint)),
1809
2509
  mint: contest.mint,
1810
2510
  prize_mint: pMint,
2511
+ // A split prize from a pool: what its vault holds at the close goes back to that pool (`WrongPool` without it).
2512
+ prize_pool: split && contest.prize?.Tokens?.from?.Pool ? pda.pool(project, contest.prize.Tokens.from.Pool[0]) : null,
1811
2513
  prize_vault: split ? pda.prizeVault(address) : null,
1812
2514
  prize_to: split ? ata(player, pMint, tokenProgramOf(programs, pMint)) : null,
2515
+ treasury: projectState?.treasury ?? null,
1813
2516
  },
1814
- // The last claim closes the pot (and a split prize's vault), writing their mints.
1815
- { writable: Number(contest.entries) === 1 ? ['mint', ...(split ? ['prize_mint'] : [])] : [], remaining: [...nfts, ...prize, ...extra] },
2517
+ { writable: left <= 0n ? ['mint', ...(split ? ['prize_mint'] : [])] : [], remaining: [...moved, ...prize, ...tailOf(extra)] },
1816
2518
  );
1817
2519
  }
1818
2520
 
1819
2521
  // ---------------------------------------------------------------- auctions
1820
2522
 
1821
- /** What a bid signs: sha256(mint | Borsh(AuctionArgs)). */
1822
- export function auctionTerms(mint, args) {
1823
- return sha256(concat(addressBytes(mint), encodeBorsh(IDL, { defined: { name: 'AuctionArgs' } }, args)));
1824
- }
1825
-
1826
- /** An auction's AuctionArgs from its decoded account (what `open_auction` fixed). */
1827
- export const auctionArgsOf = (auction) => ({
1828
- lot: auction.lot,
1829
- reserve: auction.reserve,
1830
- min_raise_bps: auction.min_raise_bps,
1831
- starts_at: auction.starts_at,
1832
- ends_at: auction.ends_at,
1833
- extend: auction.extend,
1834
- shares: auction.shares,
1835
- gate: auction.gate ?? null,
1836
- nft_bids: auction.nft_bids ?? null,
1837
- });
2523
+ /**
2524
+ * What a taker signs for an auction, an offer or a listing, as the program works it out (`rail::terms`): sha256 of the
2525
+ * record's bytes, all but the span `[start, end)` that changes as it is taken. `record` is the account's bytes as read,
2526
+ * or its decoded value, written back to the same bytes (`encodeAccount`).
2527
+ */
2528
+ function recordTerms(name, record, span) {
2529
+ const data = record instanceof Uint8Array ? record : encodeAccount(IDL, name, record);
2530
+ const [start, end] = span(data);
2531
+ return sha256(concat(data.subarray(0, start), data.subarray(end)));
2532
+ }
2533
+
2534
+ /**
2535
+ * What a bid signs: the auction's bytes but the bid, which sits last (`high_bidder`, `high`, `last_bid_at`, `held`:
2536
+ * 60 bytes, and an auction of NFT bids' room for `MAX_BID_NFTS` after them). `auction`: the decoded Auction, or its
2537
+ * account's bytes.
2538
+ */
2539
+ export function auctionTerms(auction) {
2540
+ if (auction instanceof Uint8Array) {
2541
+ const room = decode('Auction', auction).nft_bids ? 32 * MAX_BID_NFTS : 0;
2542
+ return recordTerms('Auction', auction, (data) => [data.length - 60 - room, data.length]);
2543
+ }
2544
+ // Written back with no bid's NFTs, its bytes end at the bid's 60.
2545
+ return recordTerms('Auction', { ...auction, gate: auction.gate ?? null, nft_bids: auction.nft_bids ?? null, held: [] }, (data) => [data.length - 60, data.length]);
2546
+ }
1838
2547
 
1839
2548
  /** When bids close, as the program reckons it: `ends_at`, or `extend` seconds after the last bid when that is later. */
1840
2549
  export const auctionEnds = (auction) => {
@@ -1843,23 +2552,26 @@ export const auctionEnds = (auction) => {
1843
2552
  return late > BigInt(auction.ends_at) ? late : BigInt(auction.ends_at);
1844
2553
  };
1845
2554
 
1846
- /** The least next bid the program takes, or null when no u64 amount can top it. */
2555
+ /** The least next bid the program takes, or null when nothing can top it (no u64 amount; a bid of NFTs past `MAX_BID_NFTS`). */
1847
2556
  export function leastBid(auction) {
1848
2557
  const high = BigInt(auction.high);
1849
2558
  if (high === 0n) return BigInt(auction.reserve) > 0n ? BigInt(auction.reserve) : 1n;
1850
2559
  const raise = (high * BigInt(auction.min_raise_bps) + 9_999n) / 10_000n;
1851
2560
  const next = high + (raise > 0n ? raise : 1n);
1852
- return next <= (1n << 64n) - 1n ? next : null;
2561
+ // A bid of NFTs holds at most `MAX_BID_NFTS`: past it, nobody can top the high bid.
2562
+ return next <= (auction.nft_bids ? BigInt(MAX_BID_NFTS) : (1n << 64n) - 1n) ? next : null;
1853
2563
  }
1854
2564
 
1855
2565
  /**
1856
2566
  * `seller` opens its auction `id` of `args.lot` ({ Item: [id] }, { Asset: [address] } or { Roll: [box id] }), alone,
1857
2567
  * and pays its rent: a player for an NFT of the project's own collection they hold; the project's owner or authority for an
1858
2568
  * NFT of any kind it holds (`piece`: `collection` a Core asset's own, `tokenMetadata`, or `compressed` after a
1859
- * `proveLeafIx`), a new unit of its item, or a roll of one of its boxes (a mystery lot). Its address names the seller, so an
1860
- * id is the seller's own.
2569
+ * `proveLeafIx`), a new unit of its item, or a roll of one of its boxes (a mystery lot: the box must be written, burn
2570
+ * no share of its price, and make an order with the auction's shares within one call's growth: `LotRefused`). Its
2571
+ * address names the seller, so an id is the seller's own.
1861
2572
  */
1862
2573
  export function openAuctionIx({ seller, project, collection, tokenMetadata: tm, compressed, id, mint, args }) {
2574
+ stored(args.shares);
1863
2575
  const auction = pda.auction(project, seller, id);
1864
2576
  const asset = args.lot.Asset?.[0] ?? null;
1865
2577
  // An item lot reads no NFT: the auction's own address stands in.
@@ -1872,14 +2584,19 @@ export function openAuctionIx({ seller, project, collection, tokenMetadata: tm,
1872
2584
  );
1873
2585
  }
1874
2586
 
2587
+ /** The most NFTs one bid of an auction of NFT bids holds (`MAX_BID_NFTS`, not in the IDL): a bid of more fails
2588
+ * (`BidTooLow`), and a high bid of this many can't be topped. Its reserve is 1 to this many (`BadAuction`). */
2589
+ export const MAX_BID_NFTS = 3;
2590
+
1875
2591
  /**
1876
2592
  * `bidder` bids `amount` on auction `id`, signing its terms; the bid it tops goes back to the high bidder's account
1877
2593
  * of the mint, which `bidder` opens when it is not there. `auction` is the decoded Auction. `shown`/`listProof`: what
1878
2594
  * passes the auction's gate. An auction of NFT bids (`auction.nft_bids`) takes `nfts` instead ([{ asset, collection,
1879
2595
  * tokenMetadata }], as `piece` takes each; never a compressed NFT: `CompressedBid`), handed to the project, and gives the high bid's NFTs
1880
- * (`held`, the kinds of `auction.held`, in order) back to its bidder. `extra`: a hook's accounts.
2596
+ * (`held`, the kinds of `auction.held`, in order) back to its bidder; one that can't move now (`hold`: true for all, or a
2597
+ * list of assets) becomes a hold for them instead, `bidder` paying its rent until its release. `extra`: a hook's accounts.
1881
2598
  */
1882
- export function bidIx({ bidder, project, id, auction, amount, shown, listProof, nfts = [], held = [], bidderToken, programs, extra = [] }) {
2599
+ export function bidIx({ bidder, project, id, auction, amount, shown, listProof, nfts = [], held = [], hold = false, bidderToken, programs, extra = [] }) {
1883
2600
  const tokenProgram = tokenProgramOf(programs, auction.mint);
1884
2601
  const outbid = BigInt(auction.high) > 0n;
1885
2602
  const bundle = [];
@@ -1890,13 +2607,14 @@ export function bidIx({ bidder, project, id, auction, amount, shown, listProof,
1890
2607
  bundle.push(...pieceMetas(p), ...p.proof);
1891
2608
  }
1892
2609
  (auction.held ?? []).forEach((asset, i) => {
2610
+ if (holds(hold, asset)) return bundle.push(meta(pda.hold(asset, 0), false, true));
1893
2611
  const p = piece({ project, asset, wallet: auction.high_bidder, ...held[i], ...ONLY_MOVES });
1894
2612
  bundle.push(...pieceMetas(p), ...p.proof);
1895
2613
  });
1896
2614
  }
1897
2615
  return lootIx(
1898
2616
  'bid',
1899
- { amount: auction.nft_bids ? BigInt(nfts.length) : amount, terms: auctionTerms(auction.mint, auctionArgsOf(auction)) },
2617
+ { amount: auction.nft_bids ? BigInt(nfts.length) : amount, terms: auctionTerms(auction) },
1900
2618
  {
1901
2619
  bidder,
1902
2620
  project,
@@ -1908,7 +2626,7 @@ export function bidIx({ bidder, project, id, auction, amount, shown, listProof,
1908
2626
  mint: auction.mint,
1909
2627
  },
1910
2628
  // The vault takes a bid in tokens; NFT bids leave it alone, unlocked.
1911
- { remaining: [...gateAccounts(project, { gate: auction.gate, wallet: bidder, shown, listProof }), ...bundle, ...extra], writable: auction.nft_bids ? [] : ['vault'] },
2629
+ { remaining: [...gateAccounts(project, { gate: auction.gate, wallet: bidder, shown, listProof }), ...bundle, ...tailOf(extra)], writable: auction.nft_bids ? [] : ['vault'] },
1912
2630
  );
1913
2631
  }
1914
2632
 
@@ -1920,28 +2638,43 @@ export function bidIx({ bidder, project, id, auction, amount, shown, listProof,
1920
2638
  * roll), however long after the end it is sent. With no bid, the lot goes home. `signer` opens any account the end pays into that is
1921
2639
  * not there. `item` is the decoded Item of an item lot; for an NFT lot, `collection` its Core collection now (the
1922
2640
  * project's when left out), `tokenMetadata` for a Token Metadata NFT, or `compressed` (after a `proveLeafIx`).
1923
- */
1924
- export function endAuctionIx({ signer, project, projectState, id, auction, item = null, collection, tokenMetadata: tm, compressed, held = [], reference: ref, nonce, holders, programs, extra = [] }) {
2641
+ * `hold` (anyone may; true for all, or a list of assets): an end that wouldn't fit otherwise (a compressed lot's long
2642
+ * proof, a hooked mint, many shares) makes the NFT lot a hold for whoever it goes to (its hold passed as `order`, no
2643
+ * piece nor proof), and a winning NFT of a bid a hold for `proceeds` (its hold's address in its piece's place); anyone
2644
+ * then delivers each with `releaseIx`, and the end's sender gets the rent back then.
2645
+ */
2646
+ export function endAuctionIx({ signer, project, projectState, id, auction, item = null, collection, tokenMetadata: tm, compressed, held = [], hold = false, reference: ref, nonce, holders, programs, extra = [] }) {
2647
+ // The lot went at an earlier end, and its seller's account couldn't take the money then (`proceeds_owed`): this end pays
2648
+ // it and closes the auction, naming no lot, share or winning NFT.
2649
+ const owed = BigInt(auction.proceeds_owed ?? 0) > 0n;
2650
+ const lotHeld = !owed && Boolean(auction.lot.Asset) && holds(hold, auction.lot.Asset[0]);
1925
2651
  const tokenProgram = tokenProgramOf(programs, auction.mint);
1926
2652
  const sold = BigInt(auction.high) > 0n;
1927
- const itemId = auction.lot.Item?.[0];
1928
- const made = sold && item?.kind === 'Collectible' ? pda.itemAsset(pda.item(project, itemId), auction.serial) : null;
1929
- const asset = auction.lot.Asset?.[0] ?? made;
2653
+ const itemId = owed ? undefined : auction.lot.Item?.[0];
2654
+ const made = !owed && sold && item?.kind === 'Collectible' ? pda.itemAsset(pda.item(project, itemId), auction.pin) : null;
2655
+ const asset = owed ? null : auction.lot.Asset?.[0] ?? made;
1930
2656
  const proceeds = auction.lot.Asset ? auction.seller : projectState.treasury;
1931
2657
  const address = pda.auction(project, auction.seller, id);
1932
2658
  const receiver = sold ? auction.high_bidder : auction.seller;
1933
- // A roll lot sold: its box, and the order the winner's roll is made in.
1934
- const roll = sold ? auction.lot.Roll?.[0] : undefined;
2659
+ // A roll lot sold: its box, and the order the winner's roll is made in. The winning bid stays in the vault as that
2660
+ // order's payment, split by the auction's shares when it settles (no shares' accounts here).
2661
+ const roll = sold && !owed ? auction.lot.Roll?.[0] : undefined;
1935
2662
  nonce = toBigInt(nonce ?? prizeNonce(address, BigInt(auction.last_bid_at)));
1936
2663
  // An NFT lot is sent on; a collectible sold is minted into its item's collection, which Core writes.
1937
- const p = auction.lot.Asset
2664
+ // With `hold`, an NFT lot is its address alone (no proof), and becomes a hold for whoever it goes to.
2665
+ const p = owed
2666
+ ? standIn(address)
2667
+ : lotHeld
2668
+ ? standIn(asset)
2669
+ : auction.lot.Asset
1938
2670
  ? piece({ project, asset, wallet: receiver, collection, tokenMetadata: tm, compressed, ...ONLY_MOVES })
1939
2671
  : asset
1940
2672
  ? { ...piece({ project, asset, collection: item.collection, moves: false, locks: false, reads: false }), writable: ['collection'] }
1941
2673
  : standIn(address);
1942
2674
  // A winning bid of NFTs: each to where the rest of a bid in tokens would go (`held`, their kinds in order).
1943
- const winning = sold && auction.nft_bids
2675
+ const winning = !owed && sold && auction.nft_bids
1944
2676
  ? (auction.held ?? []).flatMap((nft, i) => {
2677
+ if (holds(hold, nft)) return [meta(pda.hold(nft, 0), false, true)];
1945
2678
  const q = piece({ project, asset: nft, wallet: proceeds, ...held[i], ...ONLY_MOVES });
1946
2679
  return [...pieceMetas(q), ...q.proof];
1947
2680
  })
@@ -1962,11 +2695,11 @@ export function endAuctionIx({ signer, project, projectState, id, auction, item
1962
2695
  item: itemId !== undefined ? pda.item(project, itemId) : null,
1963
2696
  ...p.accounts,
1964
2697
  loot_box: roll !== undefined ? pda.box(project, roll) : null,
1965
- order: roll !== undefined ? pda.order(project, auction.high_bidder, prizeRollNonce(nonce)) : null,
2698
+ order: roll !== undefined ? pda.order(project, auction.high_bidder, prizeRollNonce(nonce)) : lotHeld ? pda.hold(asset, 0) : null,
1966
2699
  },
1967
2700
  {
1968
- remaining: [...p.proof, ...(sold ? shareAccounts(project, auction.shares, auction.mint, { holders, programs, treasury: projectState.treasury }) : []), ...winning, ...extra],
1969
- writable: [...(sold && sharesBurn(auction.shares) ? ['mint'] : []), ...(sold && !auction.nft_bids ? ['vault'] : []), ...p.writable],
2701
+ remaining: [...p.proof, ...(!owed && sold && roll === undefined ? shareAccounts(project, auction.shares, auction.mint, { holders, programs, treasury: projectState.treasury }) : []), ...winning, ...tailOf(extra)],
2702
+ writable: [...(!owed && sold && sharesBurn(auction.shares) ? ['mint'] : []), ...(sold && !auction.nft_bids ? ['vault'] : []), ...p.writable],
1970
2703
  },
1971
2704
  );
1972
2705
  }
@@ -1977,11 +2710,9 @@ export function endAuctionIx({ signer, project, projectState, id, auction, item
1977
2710
  // attributes, a Token Metadata or compressed NFT with none, so an attribute test fails for an NFT that doesn't carry
1978
2711
  // the key itself (every Token Metadata or compressed one).
1979
2712
 
1980
- /** The hash a taker signs, as the program rebuilds it: sha256(mint | rule | deal | shares), Borsh. `offer` is the decoded Offer. */
1981
- export function offerTerms(offer) {
1982
- const borsh = (name, value) => encodeBorsh(IDL, { defined: { name } }, value);
1983
- return sha256(concat(addressBytes(offer.mint), borsh('Rule', offer.rule), borsh('Deal', offer.deal), encodeBorsh(IDL, { vec: { defined: { name: 'Share' } } }, offer.shares)));
1984
- }
2713
+ /** What a taker signs: the offer's bytes but `left`, its last 2 (what a take changes). `offer`: the decoded Offer, or its
2714
+ * account's bytes. */
2715
+ export const offerTerms = (offer) => recordTerms('Offer', offer, (data) => [data.length - 2, data.length]);
1985
2716
 
1986
2717
  /** What one take of a deal pays out of its offer's money: a bid's price, a loan's amount. */
1987
2718
  export const dealUnit = (deal) => BigInt(deal.Buy?.price ?? deal.Lend.amount);
@@ -1992,12 +2723,13 @@ export const dealUnit = (deal) => BigInt(deal.Buy?.price ?? deal.Lend.amount);
1992
2723
  * lasts } }, left, shares } (shares only on a price). Its address names the maker, so an id is the maker's own.
1993
2724
  */
1994
2725
  export function openOfferIx({ maker, project, id, mint, args, makerToken, programs, extra = [] }) {
2726
+ stored(args.shares);
1995
2727
  const tokenProgram = tokenProgramOf(programs, mint);
1996
2728
  return lootIx(
1997
2729
  'open_offer',
1998
2730
  { id, args: { shares: [], ...args } },
1999
2731
  { maker, project, offer: pda.offer(project, maker, id), mint, maker_token: makerToken ?? ata(maker, mint, tokenProgram), vault: vault(project, mint, tokenProgram) },
2000
- { remaining: extra },
2732
+ { remaining: tailOf(extra) },
2001
2733
  );
2002
2734
  }
2003
2735
 
@@ -2008,7 +2740,7 @@ export function closeOfferIx({ maker, project, id, offer, programs, extra = [] }
2008
2740
  'close_offer',
2009
2741
  {},
2010
2742
  { maker, project, offer: pda.offer(project, maker, id), mint: offer.mint, vault: vault(project, offer.mint, tokenProgram), maker_token: ata(maker, offer.mint, tokenProgram) },
2011
- { remaining: extra },
2743
+ { remaining: tailOf(extra) },
2012
2744
  );
2013
2745
  }
2014
2746
 
@@ -2026,7 +2758,7 @@ export function sellToOfferIx({ seller, project, projectState, id, offer, asset,
2026
2758
  { terms: offerTerms(offer) },
2027
2759
  { seller, project, offer: pda.offer(project, offer.maker, id), maker: offer.maker, ...p.accounts, mint: offer.mint, vault: vault(project, offer.mint, tokenProgram), seller_token: ata(seller, offer.mint, tokenProgram) },
2028
2760
  {
2029
- remaining: [...p.proof, ...shareAccounts(project, offer.shares, offer.mint, { holders, programs, treasury: projectState?.treasury }), ...extra],
2761
+ remaining: [...p.proof, ...shareAccounts(project, offer.shares, offer.mint, { holders, programs, treasury: projectState?.treasury }), ...tailOf(extra)],
2030
2762
  writable: [...(sharesBurn(offer.shares) ? ['mint'] : []), ...p.writable],
2031
2763
  },
2032
2764
  );
@@ -2048,38 +2780,37 @@ export function borrowIx({ borrower, project, id, offer, due, asset, collection,
2048
2780
  borrower, project, offer: pda.offer(project, offer.maker, id), maker: offer.maker, hold: pda.hold(p.accounts.asset, due), ...p.accounts,
2049
2781
  mint: offer.mint, vault: vault(project, offer.mint, tokenProgram), borrower_token: ata(borrower, offer.mint, tokenProgram),
2050
2782
  },
2051
- { remaining: [...p.proof, ...extra], writable: p.writable },
2783
+ { remaining: [...p.proof, ...tailOf(extra)], writable: p.writable },
2052
2784
  );
2053
2785
  }
2054
2786
 
2055
2787
  /**
2056
2788
  * Anyone (`payer`: the borrower, a friend) repays a loan before it is due (`hold` the decoded Hold, with its debt): the
2057
2789
  * debt's `repay` goes to the lender, and the NFT is the borrower's again. Only money moves: anyone then sends the NFT
2058
- * home with `releaseIx` (in the same transaction, or later: a repaid loan's release has no deadline). Free. `extra`: a
2790
+ * home with `releaseIx` (in the same transaction, or later: a repaid loan's release has no deadline). Free. `repay`: the
2791
+ * debt the payer was shown (`hold.debt.repay` as read): any other amount is refused (`TermsChanged`). `extra`: a
2059
2792
  * transfer hook's accounts.
2060
2793
  */
2061
- export function repayIx({ payer, project, hold, payerToken, programs, extra = [] }) {
2794
+ export function repayIx({ payer, project, hold, repay, payerToken, programs, extra = [] }) {
2795
+ if (repay === undefined || repay === null) throw new Error('A repay names the debt the payer was shown (repay: hold.debt.repay as read).');
2062
2796
  const { lender, mint } = hold.debt;
2063
2797
  const tokenProgram = tokenProgramOf(programs, mint);
2064
2798
  return lootIx(
2065
2799
  'repay',
2066
- {},
2800
+ { repay: toBigInt(repay) },
2067
2801
  {
2068
2802
  payer, project, hold: pda.hold(hold.asset, hold.free_at), lender,
2069
2803
  lender_token: ata(lender, mint, tokenProgram), payer_token: payerToken ?? ata(payer, mint, tokenProgram), mint,
2070
2804
  },
2071
- { remaining: extra },
2805
+ { remaining: tailOf(extra) },
2072
2806
  );
2073
2807
  }
2074
2808
 
2075
2809
  // ---------------------------------------------------------------- listings
2076
2810
 
2077
- /** What a taker signs: sha256(maker | asset | mint | Borsh(ListingDeal) | Borsh(shares)). */
2078
- export function listingTerms(listing) {
2079
- const deal = encodeBorsh(IDL, { defined: { name: 'ListingDeal' } }, listing.deal);
2080
- const shares = encodeBorsh(IDL, { vec: { defined: { name: 'Share' } } }, listing.shares ?? []);
2081
- return sha256(concat(addressBytes(listing.maker), addressBytes(listing.asset), addressBytes(listing.mint), deal, shares));
2082
- }
2811
+ /** What a taker signs: the listing's bytes but `user`, `until` and `stopped` (bytes 129 to 170, what a rental changes).
2812
+ * `listing`: the decoded Listing, or its account's bytes. */
2813
+ export const listingTerms = (listing) => recordTerms('Listing', listing, () => [129, 170]);
2083
2814
 
2084
2815
  /**
2085
2816
  * `maker` (any wallet, signing alone) lists an NFT they hold, of any kind (as `piece` takes it), for sale
@@ -2090,7 +2821,7 @@ export function listingTerms(listing) {
2090
2821
  */
2091
2822
  export function openListingIx({ maker, project, asset, mint, deal, shares = [], collection, hasAuthority, tokenMetadata: tm, compressed, extra = [] }) {
2092
2823
  const p = piece({ project, asset, wallet: maker, collection, hasAuthority, tokenMetadata: tm, compressed, ...ONLY_MOVES });
2093
- return lootIx('open_listing', { deal, shares }, { maker, project, listing: pda.listing(project, p.accounts.asset), ...p.accounts, mint }, { remaining: [...p.proof, ...extra], writable: p.writable });
2824
+ return lootIx('open_listing', { deal, shares: stored(shares) }, { maker, project, listing: pda.listing(project, p.accounts.asset), ...p.accounts, mint }, { remaining: [...p.proof, ...tailOf(extra)], writable: p.writable });
2094
2825
  }
2095
2826
 
2096
2827
  /**
@@ -2116,7 +2847,7 @@ export function takeListingIx({ taker, project, projectState, listing, periods =
2116
2847
  ...p.accounts, mint: listing.mint, taker_token: takerToken ?? ata(taker, listing.mint, tokenProgram),
2117
2848
  },
2118
2849
  {
2119
- remaining: [...p.proof, ...shareAccounts(project, listing.shares ?? [], listing.mint, { holders, programs, treasury: projectState?.treasury }), ...extra],
2850
+ remaining: [...p.proof, ...shareAccounts(project, listing.shares ?? [], listing.mint, { holders, programs, treasury: projectState?.treasury }), ...tailOf(extra)],
2120
2851
  writable: [...(sharesBurn(listing.shares ?? []) ? ['mint'] : []), ...p.writable],
2121
2852
  },
2122
2853
  );
@@ -2128,7 +2859,7 @@ export function takeListingIx({ taker, project, projectState, listing, periods =
2128
2859
  */
2129
2860
  export function closeListingIx({ maker, project, listing, collection, hasAuthority, tokenMetadata: tm, compressed, extra = [] }) {
2130
2861
  const p = piece({ project, asset: listing.asset, wallet: maker, collection, hasAuthority, tokenMetadata: tm, compressed, ...ONLY_MOVES });
2131
- return lootIx('close_listing', {}, { maker, project, listing: pda.listing(project, listing.asset), ...p.accounts }, { remaining: [...p.proof, ...extra], writable: p.writable });
2862
+ return lootIx('close_listing', {}, { maker, project, listing: pda.listing(project, listing.asset), ...p.accounts }, { remaining: [...p.proof, ...tailOf(extra)], writable: p.writable });
2132
2863
  }
2133
2864
 
2134
2865
  // ---------------------------------------------------------------- Token Metadata NFTs