@xgamesfun/solana 0.25.4 → 0.26.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 (41) hide show
  1. package/README.md +8 -8
  2. package/js/approve/details.mjs +52 -2
  3. package/js/approve/live-check.mjs +0 -15
  4. package/js/approve/server.mjs +5 -4
  5. package/js/chain/token2022.js +3 -2
  6. package/js/client/accounts.js +67 -25
  7. package/js/client/grant.js +7 -2
  8. package/js/client/idl/loot.js +1 -1
  9. package/js/client/idl/loot.json +714 -182
  10. package/js/client/ix.js +617 -113
  11. package/js/client/sizes.js +35 -3
  12. package/js/economy/check.mjs +655 -242
  13. package/js/economy/cost.mjs +2 -4
  14. package/js/economy/explain.mjs +16 -2
  15. package/js/economy/guide.mjs +12 -12
  16. package/js/economy/model.mjs +133 -14
  17. package/js/economy/plan.mjs +36 -11
  18. package/js/economy/review.mjs +50 -8
  19. package/js/economy/schema.json +73 -29
  20. package/js/economy/simulate.mjs +116 -13
  21. package/js/mcp/tools.mjs +2 -2
  22. package/js/play/index.js +847 -296
  23. package/package.json +1 -1
  24. package/release.json +5 -5
  25. package/skills/xsolana-economy/SKILL.md +12 -4
  26. package/skills/xsolana-economy/references/after-launch.md +1 -1
  27. package/skills/xsolana-economy/references/apps.md +11 -2
  28. package/skills/xsolana-economy/references/sentence-to-spec.md +3 -3
  29. package/skills/xsolana-economy/references/telling-the-person.md +21 -4
  30. package/skills/xsolana-economy/references/trust-and-cheating.md +15 -5
  31. package/types/chain/token2022.d.ts +3 -2
  32. package/types/client/accounts.d.ts +34 -11
  33. package/types/client/idl/loot.d.ts +25 -4
  34. package/types/client/ix.d.ts +217 -45
  35. package/types/economy/check.d.mts +5 -0
  36. package/types/economy/model.d.mts +75 -12
  37. package/types/economy/plan.d.mts +22 -1
  38. package/types/economy/simulate.d.mts +12 -1
  39. package/types/mcp/server.d.mts +12 -1
  40. package/types/mcp/tools.d.mts +12 -1
  41. package/types/play/index.d.ts +210 -144
package/README.md CHANGED
@@ -64,7 +64,7 @@ codex plugin add xsolana@xgamesfun
64
64
 
65
65
  **claude.ai, ChatGPT or Claude Desktop:** add `https://xgames.fun/mcp` as a custom connector. Nothing to install.
66
66
 
67
- **Cursor, Cline, opencode or any MCP client:** run `npx -y --package=@xgamesfun/solana@0.25.4 xsolana mcp`.
67
+ **Cursor, Cline, opencode or any MCP client:** run `npx -y --package=@xgamesfun/solana@0.26.0 xsolana mcp`.
68
68
 
69
69
  **Any agent that reads skills** (Copilot, Gemini CLI, Windsurf…): `npx skills add XGamesFun/solana-kit`.
70
70
 
@@ -75,19 +75,19 @@ are in [docs/harnesses.md](docs/harnesses.md).
75
75
 
76
76
  ```bash
77
77
  # find a worked example close to your idea
78
- npx -y @xgamesfun/solana@0.25.4 examples shop with loot boxes
78
+ npx -y @xgamesfun/solana@0.26.0 examples shop with loot boxes
79
79
 
80
80
  # does it work? where does the money go? what does it cost?
81
- npx -y @xgamesfun/solana@0.25.4 check xsolana.json
82
- npx -y @xgamesfun/solana@0.25.4 simulate xsolana.json
83
- npx -y @xgamesfun/solana@0.25.4 cost xsolana.json
81
+ npx -y @xgamesfun/solana@0.26.0 check xsolana.json
82
+ npx -y @xgamesfun/solana@0.26.0 simulate xsolana.json
83
+ npx -y @xgamesfun/solana@0.26.0 cost xsolana.json
84
84
 
85
85
  # the whole economy in plain words, and what a cheater could get
86
- npx -y @xgamesfun/solana@0.25.4 explain xsolana.json
87
- npx -y @xgamesfun/solana@0.25.4 review xsolana.json
86
+ npx -y @xgamesfun/solana@0.26.0 explain xsolana.json
87
+ npx -y @xgamesfun/solana@0.26.0 review xsolana.json
88
88
 
89
89
  # go live, approved in your own wallet
90
- npx -y @xgamesfun/solana@0.25.4 approve xsolana.json
90
+ npx -y @xgamesfun/solana@0.26.0 approve xsolana.json
91
91
  ```
92
92
 
93
93
  Each command names an exact version, so you always run the release you meant to. `xsolana help` lists them all.
@@ -6,10 +6,31 @@ import { decodeBorsh } from '../chain/borsh.js';
6
6
  import { equalBytes, toHex } from '../chain/bytes.js';
7
7
  import { accountKind, decode, IDL, instructionOf, keyLeaf, merkleRoot } from '../client/index.js';
8
8
  import { LOOT_PROGRAM, pda } from '../client/pda.js';
9
+ import { dateWords, FAR_SECONDS } from '../economy/check.mjs';
9
10
  import { fromUnits } from '../economy/model.mjs';
10
11
  import { grantArgs } from '../economy/plan.mjs';
11
12
 
12
13
  const NOBODY = '11111111111111111111111111111111';
14
+ /** What places of tokens from a short pool get, by their `when_short`. */
15
+ const SHORT = {
16
+ InOrder: 'whole prizes, first place first, while the pool covers one',
17
+ InProportion: 'every place the same share of its prize',
18
+ Nothing: 'no place gets anything, and the pool keeps it',
19
+ };
20
+ /** What a far time is, by the field that holds it. */
21
+ const FAR = {
22
+ draws_at: 'The first draw',
23
+ ends_at: 'The auction ends',
24
+ result_by: 'The result is due',
25
+ next_at: 'The first run',
26
+ };
27
+ /** A number of seconds in words: "1 day", "36 hours", "90 seconds". */
28
+ const duration = v => {
29
+ const n = Number(v);
30
+ if (n % 86_400 === 0) return `${n / 86_400} day${n === 86_400 ? '' : 's'}`;
31
+ if (n % 3_600 === 0) return `${n / 3_600} hours`;
32
+ return `${n} seconds`;
33
+ };
13
34
  const ZERO = new Uint8Array(32);
14
35
 
15
36
  /** An instruction's accounts by name, nested groups flattened, in the order the program takes them. */
@@ -32,11 +53,12 @@ function readLootInstruction(ix) {
32
53
  * connected; `treasury`: the treasury's address when it is known (else "your wallet" stands for it, as the setup
33
54
  * makes it).
34
55
  */
35
- export function namesOf(m, { owner = null, treasury = null } = {}) {
56
+ export function namesOf(m, { owner = null, treasury = null, now = null } = {}) {
36
57
  return {
37
58
  m,
38
59
  owner,
39
60
  treasury,
61
+ now,
40
62
  pools: new Map(m.pools.map(p => [p.index, p.key])),
41
63
  stashes: new Map(m.stashes.map(s => [s.id, s.name ?? s.key])),
42
64
  grants: new Map(m.grants.map(g => [g.id, g])),
@@ -151,6 +173,8 @@ function readDetails(reads, names) {
151
173
  : treasury()
152
174
  : treasury();
153
175
  const to = v => (v === 'Burn' ? 'burned' : `to ${where(v)}`);
176
+ // A time more than three years after now: the program takes it, and the owner sees its date before signing.
177
+ const far = v => Number(v) - (names.now ?? Math.floor(Date.now() / 1000)) > FAR_SECONDS;
154
178
  const isTo = v =>
155
179
  v === 'Burn' ||
156
180
  v === 'Treasury' ||
@@ -176,7 +200,33 @@ function readDetails(reads, names) {
176
200
  ? `Places paid from: its pot ${v.Pot[0]}, kept from round to round (what anyone sends it, yield and what earlier rounds left all count)`
177
201
  : `Places paid from: each round’s own ticket money alone; what they leave is ${v.Round.rest === 'Burn' ? 'burned' : `sent to ${where(v.Round.rest)}`}`,
178
202
  );
179
- } else if (k === 'to' && isTo(v))
203
+ } else if (k === 'when_out' && v === 'GiveNothing')
204
+ lines.push(
205
+ 'When an outcome is out: a draw on it gives nothing, and a paid draw is not refunded (the box keeps selling)',
206
+ );
207
+ else if (k === 'closes_before' && BigInt(v ?? 0) > 0n)
208
+ lines.push(`Sales close ${duration(v)} before each draw`);
209
+ else if (k === 'time_weighted' && v === true)
210
+ lines.push(
211
+ 'Tickets weighed by time: each purchase counts its tickets times the seconds left until sales close, so a ticket bought early has a better chance',
212
+ );
213
+ else if (k === 'places' && Array.isArray(v)) {
214
+ // What places of tokens from a pool get when it is short, read once when the draw is asked.
215
+ const short = [...new Set(v.filter(p => p?.prize?.Tokens?.from?.Pool).map(p => p.when_short))];
216
+ if (short.length)
217
+ lines.push(
218
+ `If a pool its places are paid from holds less than they win: ${short.map(x => SHORT[x] ?? x).join('; ')}`,
219
+ );
220
+ walk(v);
221
+ } else if (['draws_at', 'ends_at', 'result_by', 'next_at'].includes(k) && far(v))
222
+ lines.push(
223
+ `${FAR[k]} on ${dateWords(Number(v))}, ${Math.floor((Number(v) - (names.now ?? Math.floor(Date.now() / 1000))) / 31_536_000)} years from now: what waits on it is locked until then. Check the unit before approving`,
224
+ );
225
+ else if (k === 'extend' && read.name === 'open_auction' && Number(v) > FAR_SECONDS)
226
+ lines.push(
227
+ `A late bid moves the end by ${duration(v)}: one bid holds the lot and itself that long. Check the unit before approving`,
228
+ );
229
+ else if (k === 'to' && isTo(v))
180
230
  lines.push(v === 'Burn' ? 'Burns the tokens it takes' : `Sends tokens to ${where(v)}`);
181
231
  else if (k === 'Give' && Array.isArray(v) && typeof v[0] === 'string')
182
232
  lines.push(`Gives the NFT to ${address(v[0])}`);
@@ -6,7 +6,6 @@
6
6
  import { encodeBase58 } from '../chain/base58.js';
7
7
  import { parseCoreCollection, MPL_CORE_PROGRAM } from '../chain/core.js';
8
8
  import { parseMint } from '../chain/mint.js';
9
- import { mayStartFrozen } from '../chain/token2022.js';
10
9
  import { accountKind, decode } from '../client/accounts.js';
11
10
  import { BUBBLEGUM_PROGRAM, LOOT_PROGRAM, pda } from '../client/pda.js';
12
11
  import { SYSTEM_PROGRAM } from '../chain/pda.js';
@@ -128,16 +127,6 @@ export class MintMismatch extends Error {}
128
127
  */
129
128
  export async function checkMints({ m, chain }) {
130
129
  const named = [...m.tokens.values()].filter(t => t.mint && !t.mint.startsWith('@'));
131
- // The routes that read a mint's default account state once, as they open, and refuse one that can start frozen.
132
- const once = new Map();
133
- const reads = (key, what) => key && !once.has(key) && once.set(key, what);
134
- for (const l of m.lotteries)
135
- (reads(l.currency ?? m.currencyKey, `the lottery ${l.name ?? l.key}`),
136
- reads(l.ticketCurrency, `the lottery ${l.name ?? l.key}`));
137
- for (const c of m.contests) reads(c.currency ?? m.currencyKey, `the contest ${c.name ?? c.key}`);
138
- for (const a of m.auctions) reads(a.currency ?? m.currencyKey, `the auction ${a.name ?? a.key}`);
139
- for (const s of m.schedules.filter(x => x.sealed))
140
- reads(s.currency ?? m.currencyKey, `the sealed schedule ${s.name ?? s.key}`);
141
130
  for (const t of named) {
142
131
  const what = `the token ${t.symbol ?? t.key} (“${t.key}”)`;
143
132
  if (t.wsol) {
@@ -162,10 +151,6 @@ export async function checkMints({ m, chain }) {
162
151
  throw new MintMismatch(
163
152
  `The spec says ${what} is a ${t.program} token; its mint ${t.mint} is ${mint.program}. Write "program": "${mint.program}".`,
164
153
  );
165
- if (once.has(t.key) && mayStartFrozen(account))
166
- throw new MintMismatch(
167
- `The mint ${t.mint} of ${what} has a default account state that starts new accounts frozen, or that its freeze authority can switch to frozen, so ${once.get(t.key)} would be refused as it opens (the program refuses such a mint wherever a refund or a payout goes into an account opened for it). Use another token there.`,
168
- );
169
154
  }
170
155
  await checkTreasury({ m, chain });
171
156
  }
@@ -221,6 +221,7 @@ export async function createApproval({
221
221
  */
222
222
  const names = () =>
223
223
  namesOf(model(spec, { now: at }), {
224
+ now: at,
224
225
  treasury: madeSoFar(live())?.treasury ?? model(spec, { now: at }).treasuryWallet ?? null,
225
226
  });
226
227
  /**
@@ -232,7 +233,7 @@ export async function createApproval({
232
233
  async function readLive(p) {
233
234
  if (!spec.live?.published || typeof chain.account !== 'function') return;
234
235
  // The names a published list or key list goes by: the published spec's, each held against the root on chain.
235
- const n = namesOf(model(spec.live.published, { now: at }), { treasury: p.ctx.treasury });
236
+ const n = namesOf(model(spec.live.published, { now: at }), { now: at, treasury: p.ctx.treasury });
236
237
  const was = new Map();
237
238
  for (const t of p.transactions) {
238
239
  for (const [i, step] of t.steps.entries()) {
@@ -381,14 +382,14 @@ export async function createApproval({
381
382
  trusted = await trust(wallet);
382
383
  } catch (e) {
383
384
  if (!(e instanceof ApproveError)) throw e;
384
- const v = view(p0, namesOf(model(spec, { now: at }), { owner: wallet }), { refused: e.message });
385
+ const v = view(p0, namesOf(model(spec, { now: at }), { now: at, owner: wallet }), { refused: e.message });
385
386
  return { ...v, remaining: p0.ok ? p0.transactions.length : 0, done: false };
386
387
  }
387
388
  if (!p0.ok || !p0.transactions.length)
388
389
  return { ...view(p0, names(), { project: trusted.project }), remaining: 0, done: p0.ok };
389
390
  const p = await build(trusted.owner);
390
391
  await readLive(p);
391
- const v = view(p, namesOf(model(spec, { now: at }), { owner: wallet, treasury: p.ctx.treasury }), {
392
+ const v = view(p, namesOf(model(spec, { now: at }), { now: at, owner: wallet, treasury: p.ctx.treasury }), {
392
393
  project: p.ctx.project,
393
394
  });
394
395
  return { ...v, remaining: p.transactions.length, done: false };
@@ -435,7 +436,7 @@ export async function createApproval({
435
436
  return wireTransaction(message, sigs);
436
437
  });
437
438
  // What each transaction pays to whom, read back from the very instructions the wallet signs.
438
- const n = namesOf(model(spec, { now: at }), { owner: wallet, treasury: p.ctx.treasury });
439
+ const n = namesOf(model(spec, { now: at }), { now: at, owner: wallet, treasury: p.ctx.treasury });
439
440
  // Every line names the project it goes to first: always the wallet's own, or the one it is the authority of.
440
441
  const details = batch.map(t => [
441
442
  `Project: ${p.ctx.project}${p.ctx.owner === wallet ? ' (your own)' : ` (owned by ${p.ctx.owner})`}`,
@@ -60,8 +60,9 @@ export function hookProgramOf(mintData) {
60
60
  /**
61
61
  * Whether a mint's new accounts start frozen, or can be made to (as the program judges it): a Token-2022
62
62
  * mint carrying `DefaultAccountState` set to frozen, or set to anything while it has a freeze authority, which can switch
63
- * it to frozen whenever it likes. An auction, a contest, a lottery, an offer and a sealed schedule refuse such a mint:
64
- * each reads the setting once, as it opens, and a refund or payout opened into a frozen account would never land.
63
+ * it to frozen whenever it likes. The program takes such a mint everywhere: it is the project's choice. A refund or a
64
+ * payout opened into an account that starts frozen does not land until its issuer thaws it, so the tools warn the
65
+ * project, and a bidder or a borrower before they sign.
65
66
  */
66
67
  export function mayStartFrozen({ owner, data }) {
67
68
  if (
@@ -107,6 +107,9 @@ const RECORDS = [
107
107
  // The totals, split at each purchase: what each share paid in common takes, the entries that own a part of
108
108
  // their own, and the parts owed now.
109
109
  ['sold', 'u64'],
110
+ // In a weighted round (`time_weighted`), the weight sold: each purchase's tickets times the seconds from it
111
+ // to the close of sales. Null: each ticket weighs one, and the draw is over `sold`.
112
+ ['weight', { option: 'u128' }],
110
113
  ['to_pot', 'u64'],
111
114
  ['common', { vec: 'u64' }],
112
115
  ['owns', 'u32'],
@@ -117,7 +120,8 @@ const RECORDS = [
117
120
  ['first_asked_at', 'i64'],
118
121
  ['asked_at', 'i64'],
119
122
  ['answer', { array: ['u8', 32] }],
120
- ['first', 'u64'],
123
+ // First place's point: below the draw's total (`roundTotal`), so in a weighted round a point of its weight.
124
+ ['first', 'u128'],
121
125
  ['pot_total', 'u64'],
122
126
  ['pot_sent', 'u64'],
123
127
  ['pot_arrived', 'u64'],
@@ -506,55 +510,94 @@ export const lotteryPots = lottery =>
506
510
  /** A share the buyer chooses (a referrer, a collectible's holder): paid out of the price as they buy, from their own account. */
507
511
  export const chosenAtPurchase = share => Boolean(share.to?.Chosen);
508
512
 
509
- /** The bytes of one entry of a round: buyer, word, what arrived (`fee`: a fee mint, or a chosen share), the NFT ticket. */
510
- export const entryWidth = (fee, nft) => 40 + (fee ? 8 : 0) + (nft ? 32 : 0);
513
+ /** The bytes of one entry of a round: buyer, word, what arrived (`fee`: a fee mint, a chosen share, or a weighted
514
+ * round of paid tickets), where its weight ends (`weighted`: a round that weighs its tickets by time), the NFT ticket. */
515
+ export const entryWidth = (fee, nft, weighted = false) => 40 + (fee ? 8 : 0) + (weighted ? 16 : 0) + (nft ? 32 : 0);
516
+
517
+ /** What a round's draw picks a point below: the weight sold in a weighted round, the tickets sold otherwise. */
518
+ export const roundTotal = r => (r.weight != null ? BigInt(r.weight) : BigInt(r.sold));
511
519
 
512
520
  /**
513
521
  * A lottery round (`["round", lottery, incarnation, round]`): its header (`status` 'Open' | 'Drawing' | 'Drawn' |
514
- * 'Cancelled', the terms it copied, `sold`, `owed`, the draw and the money the ask fixed, `nfts` the NFT of the
515
- * lottery's own stash each such place got), and `entries`, one a purchase: { buyer, start, end (its tickets), paid (its
516
- * own part's bit), amount (what reached the round), nft (its NFT ticket) }.
522
+ * 'Cancelled', the terms it copied, `sold`, `weight` (null in a round that doesn't weigh its tickets), `owed`, the draw
523
+ * and the money the ask fixed, `nfts` the NFT of the lottery's own stash each such place got), and `entries`, one a
524
+ * purchase: { buyer, start, end (its slice of the draw: its tickets, or in a weighted round its weight), tickets (how
525
+ * many it bought), paid (its own part's bit), amount (what reached the round), nft (its NFT ticket) }.
517
526
  */
518
527
  export function decodeRound(data) {
519
528
  const r = decode('LotteryRound', data);
520
529
  const at = Number(r.head);
521
530
  const width = Number(readUintLE(data, at, 2));
522
531
  const count = Number(readUintLE(data, at + 2, 4));
523
- if (width !== entryWidth(r.fee, r.terms.nft_ticket != null) || data.length !== at + 6 + width * count)
532
+ const weighted = r.weight != null;
533
+ if (width !== entryWidth(r.fee, r.terms.nft_ticket != null, weighted) || data.length !== at + 6 + width * count)
524
534
  throw new Error('LotteryRound entries do not match its header');
525
535
  let start = 0n;
536
+ let sold = 0n;
526
537
  const entries = Array.from({ length: count }, (_, i) => {
527
538
  const e = data.subarray(at + 6 + width * i, at + 6 + width * (i + 1));
528
539
  const word = readUintLE(e, 32, 8);
529
- const end = word & END_BITS;
540
+ // Its tickets end at the word's low 48 bits; its slice of the draw ends there too, or at its weight's end.
541
+ const last = word & END_BITS;
542
+ const tickets = last - sold;
530
543
  let k = 40;
531
544
  const take = n => e.subarray((k += n) - n, k);
532
- const amount = r.fee ? readUintLE(take(8), 0, 8) : (end - start) * BigInt(r.terms.price);
545
+ const amount = r.fee ? readUintLE(take(8), 0, 8) : tickets * BigInt(r.terms.price);
546
+ const end = weighted ? readUintLE(take(16), 0, 16) : last;
533
547
  const entry = {
534
548
  buyer: encodeBase58(e.subarray(0, 32)),
535
549
  start,
536
550
  end,
551
+ tickets,
537
552
  paid: word & ~END_BITS,
538
553
  amount,
539
554
  nft: r.terms.nft_ticket ? encodeBase58(take(32)) : NO_ADDRESS,
540
555
  };
541
556
  start = end;
557
+ sold = last;
542
558
  return entry;
543
559
  });
544
560
  return { ...r, entries };
545
561
  }
546
562
 
563
+ /** When a lottery's sales close (`terms`: a Lottery's or a round's): `closes_before` seconds before its draw. */
564
+ export const salesCloseAt = terms => BigInt(terms.draws_at) - BigInt(terms.closes_before ?? 0);
565
+
566
+ /**
567
+ * What a purchase of `count` tickets at `now` (Unix seconds, the chain's clock) weighs in a round's draw: one a ticket,
568
+ * or in a lottery that weighs tickets by time (`terms.time_weighted`) each ticket the seconds from the purchase to the
569
+ * close of sales (`salesCloseAt`). 0 once sales have closed: nothing more is sold. The same tickets bought later weigh less.
570
+ */
571
+ export function ticketWeight(terms, count, now) {
572
+ const left = salesCloseAt(terms) - BigInt(now);
573
+ if (left <= 0n) return 0n;
574
+ return BigInt(count) * (terms.time_weighted ? left : 1n);
575
+ }
576
+
577
+ /**
578
+ * A buyer's chance before they buy: { weight, total, share } of a purchase of `count` tickets at `now` into `round` (its
579
+ * decoded record, null when nobody bought into it yet; `terms` the lottery's then): what it would weigh, the round's
580
+ * weight with it, and its share of that (a number from 0 to 1). Each place is drawn over the round's whole weight, so a
581
+ * share is the purchase's chance at one place as things stand: later purchases make it smaller.
582
+ */
583
+ export function purchaseOdds(round, terms, count, now) {
584
+ const weight = ticketWeight(round?.terms ?? terms, count, now);
585
+ const total = (round ? roundTotal(round) : 0n) + weight;
586
+ return { weight, total, share: total > 0n ? Number((weight * 1_000_000n) / total) / 1_000_000 : 0 };
587
+ }
588
+
547
589
  /** How many places a lottery's (or a round's) `places` are, every run counted (`Place.times`). */
548
590
  export const placeCount = places => places.reduce((n, p) => n + Number(p.times ?? 1), 0);
549
591
 
550
592
  /** The places of a round that have a winner: every place, but no more than tickets. */
551
593
  export const roundWinners = r => Math.min(placeCount(r.terms.places), Number(r.sold));
552
594
 
553
- /** The ticket that wins `place` of a drawn round: even steps around the list from first place's ticket. */
595
+ /** The point that wins `place` of a drawn round: even steps of the draw's total around the list from first place's
596
+ * (a ticket's number, or in a weighted round a point of its weight). */
554
597
  export function ticketOfPlace(r, place) {
555
- const sold = BigInt(r.sold);
556
- const ahead = BigInt(place) * (sold / BigInt(roundWinners(r) || 1));
557
- const toEnd = sold - BigInt(r.first);
598
+ const total = roundTotal(r);
599
+ const ahead = BigInt(place) * (total / BigInt(roundWinners(r) || 1));
600
+ const toEnd = total - BigInt(r.first);
558
601
  return ahead < toEnd ? BigInt(r.first) + ahead : ahead - toEnd;
559
602
  }
560
603
 
@@ -575,18 +618,18 @@ export function runPlaces(r, run) {
575
618
  return [Math.min(first, w), Math.min(first + Number(r.terms.places[run].times), w)];
576
619
  }
577
620
 
578
- /** The places of run `run` whose ticket is in `start..end` (an entry's), in order. */
621
+ /** The places of run `run` whose point is in `start..end` (an entry's slice of the draw), in order. */
579
622
  export function placesIn(r, run, start, end) {
580
623
  const [from, to] = runPlaces(r, run);
581
624
  const out = [];
582
625
  if (from >= to || BigInt(start) >= BigInt(end)) return out;
583
- const sold = BigInt(r.sold);
584
- const step = sold / BigInt(roundWinners(r));
585
- const d = (BigInt(start) + sold - BigInt(r.first)) % sold;
626
+ const total = roundTotal(r);
627
+ const step = total / BigInt(roundWinners(r));
628
+ const d = (BigInt(start) + total - BigInt(r.first)) % total;
586
629
  const len = BigInt(end) - BigInt(start);
587
630
  for (const [a, b] of [
588
- [d, d + len < sold ? d + len : sold],
589
- [0n, d + len > sold ? d + len - sold : 0n],
631
+ [d, d + len < total ? d + len : total],
632
+ [0n, d + len > total ? d + len - total : 0n],
590
633
  ]) {
591
634
  let p = Number((a + step - 1n) / step);
592
635
  for (p = Math.max(p, from); p < to && BigInt(p) * step < b; p++) out.push(p);
@@ -761,14 +804,13 @@ export const placeSet = place => Number(place.bps) > 0 || place.prize != null;
761
804
  /** The address of a lottery's current round's record (there or not). */
762
805
  export const roundAddress = (lotteryAddress, lottery) => pda.round(lotteryAddress, lottery.incarnation, lottery.round);
763
806
 
764
- /** Whether a lottery (decoded) drew once and has ended. */
765
- export const lotteryEnded = lottery => Number(lottery.every) === 0 && Number(lottery.round) > 0;
807
+ /** Whether a lottery (decoded) drew its last round and has ended: it draws once (`every` 0) and is past round `last`. */
808
+ export const lotteryEnded = lottery => Number(lottery.every) === 0 && Number(lottery.round) > Number(lottery.last ?? 0);
766
809
 
767
- /** The round a lottery's close, its write again and its own stash's moves name (`lottery::check_idle`): its current
768
- * round's, or once a lottery that draws once has ended, round 0's (which keeps its stash's NFTs until its places took
769
- * them out). */
810
+ /** The round a lottery's close, its write again and its own stash's moves name (`Lottery::waits_on`): its current
811
+ * round's, or once it has ended, its last round's (which keeps its stash's NFTs until its places took them out). */
770
812
  export const idleRound = (lotteryAddress, lottery) =>
771
- pda.round(lotteryAddress, lottery.incarnation, lotteryEnded(lottery) ? 0 : lottery.round);
813
+ pda.round(lotteryAddress, lottery.incarnation, lotteryEnded(lottery) ? lottery.last : lottery.round);
772
814
 
773
815
  /**
774
816
  * Who rents a listed NFT at `now` (Unix seconds), from its decoded Listing (or null for none): the renter's wallet while
@@ -19,7 +19,7 @@ import { compareBytes, concat, fromHex, intLE, readUintLE, toBigInt, uintLE, utf
19
19
  import { meta } from '../chain/programs.js';
20
20
  import { sha256 } from '../chain/sha256.js';
21
21
  import { recordKey16 } from './accounts.js';
22
- import { ASSOCIATED_TOKEN_PROGRAM, gateAccounts, lootIx, prizeMetas, prizeNonce, tailOf } from './ix.js';
22
+ import { ASSOCIATED_TOKEN_PROGRAM, gateAccounts, lootIx, prizeMetas, prizeNonce, shownFor, tailOf } from './ix.js';
23
23
  import { LOOT_PROGRAM, pda } from './pda.js';
24
24
  import IDL from './idl/loot.js';
25
25
 
@@ -400,7 +400,12 @@ export function claimGrantIx({
400
400
  },
401
401
  {
402
402
  remaining: [
403
- ...gateAccounts(project, { gate: grant.gate, wallet, shown, listProof }),
403
+ ...gateAccounts(project, {
404
+ gate: grant.gate,
405
+ wallet,
406
+ shown: shownFor(project, grant.gate, wallet, shown),
407
+ listProof,
408
+ }),
404
409
  ...shards,
405
410
  ...prizes,
406
411
  ...tailOf(extra),