@xgamesfun/solana 0.20.1 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +95 -52
  2. package/bin/xsolana.mjs +23 -8
  3. package/examples/blind-forge.json +2 -4
  4. package/examples/coin-flip-arena.json +4 -12
  5. package/examples/duel-arena.json +1 -4
  6. package/examples/elements-set.json +2 -9
  7. package/examples/expedition-guild.json +1 -8
  8. package/examples/fair-presale.json +1 -3
  9. package/examples/guild-missions.json +3 -10
  10. package/examples/halving-seasons.json +4 -9
  11. package/examples/hero-trials.json +15 -6
  12. package/examples/holder-den.json +38 -0
  13. package/examples/holders-drop.json +105 -0
  14. package/examples/ideas.json +788 -788
  15. package/examples/match-predictions.json +24 -0
  16. package/examples/merge-2048.json +5 -5
  17. package/examples/nft-raffle.json +1 -0
  18. package/examples/pack-league.json +108 -0
  19. package/examples/paper-hands-farm.json +4 -16
  20. package/examples/partner-vault.json +20 -7
  21. package/examples/sacrifice-raffle.json +2 -10
  22. package/examples/shelter-fair.json +1 -0
  23. package/examples/stakers-draw.json +4 -15
  24. package/examples/star-fleet.json +1 -8
  25. package/examples/starforce.json +6 -6
  26. package/examples/stride-sneakers.json +164 -0
  27. package/examples/trade-in-shop.json +2 -3
  28. package/js/approve/details.mjs +122 -52
  29. package/js/approve/ed25519.mjs +5 -1
  30. package/js/approve/live-check.mjs +83 -26
  31. package/js/approve/page.html +217 -78
  32. package/js/approve/page.js +43 -15
  33. package/js/approve/server.mjs +290 -73
  34. package/js/chain/base58.js +7 -1
  35. package/js/chain/borsh.js +24 -11
  36. package/js/chain/bytes.js +11 -8
  37. package/js/chain/core.js +35 -7
  38. package/js/chain/curve.js +1 -1
  39. package/js/chain/lookup.js +124 -36
  40. package/js/chain/mint.js +6 -2
  41. package/js/chain/pda.js +2 -2
  42. package/js/chain/programs.js +6 -6
  43. package/js/chain/rpc.js +44 -16
  44. package/js/chain/token.js +13 -2
  45. package/js/chain/token2022.js +55 -16
  46. package/js/chain/tx.js +32 -22
  47. package/js/client/accounts.js +653 -131
  48. package/js/client/compressed.js +4 -3
  49. package/js/client/grant.js +267 -83
  50. package/js/client/idl/loot.js +1 -1
  51. package/js/client/idl/loot.json +2406 -2176
  52. package/js/client/index.js +1 -0
  53. package/js/client/ix.js +3059 -664
  54. package/js/client/pda.js +39 -33
  55. package/js/client/policy.js +52 -24
  56. package/js/client/random.js +70 -0
  57. package/js/client/sizes.js +414 -16
  58. package/js/client/traits.js +14 -5
  59. package/js/connect/connect.mjs +59 -17
  60. package/js/economy/check.mjs +4153 -725
  61. package/js/economy/cost.mjs +403 -64
  62. package/js/economy/examples.mjs +176 -35
  63. package/js/economy/explain.mjs +466 -111
  64. package/js/economy/guide.mjs +296 -72
  65. package/js/economy/legal.mjs +12 -6
  66. package/js/economy/live.mjs +83 -19
  67. package/js/economy/model.mjs +462 -124
  68. package/js/economy/plan.mjs +1306 -219
  69. package/js/economy/schema.json +390 -484
  70. package/js/economy/secrets.mjs +17 -7
  71. package/js/economy/simulate.mjs +883 -223
  72. package/js/economy/txsize.mjs +15 -3
  73. package/js/economy/validate.mjs +61 -24
  74. package/js/mcp/server.mjs +42 -14
  75. package/js/mcp/tools.mjs +307 -88
  76. package/js/play/index.js +2864 -619
  77. package/js/play/keys.js +193 -0
  78. package/js/results/index.js +223 -30
  79. package/js/results/server.js +230 -0
  80. package/js/results/wire.js +64 -16
  81. package/package.json +6 -4
  82. package/release.json +5 -5
  83. package/skills/xsolana-economy/SKILL.md +39 -21
  84. package/skills/xsolana-economy/references/after-launch.md +58 -38
  85. package/skills/xsolana-economy/references/apps.md +97 -50
  86. package/skills/xsolana-economy/references/sentence-to-spec.md +148 -121
  87. package/skills/xsolana-economy/references/telling-the-person.md +61 -17
  88. package/types/chain/core.d.ts +4 -2
  89. package/types/chain/lookup.d.ts +5 -2
  90. package/types/chain/rpc.d.ts +3 -1
  91. package/types/client/accounts.d.ts +217 -56
  92. package/types/client/grant.d.ts +72 -27
  93. package/types/client/idl/loot.d.ts +602 -549
  94. package/types/client/index.d.ts +1 -0
  95. package/types/client/ix.d.ts +3884 -247
  96. package/types/client/pda.d.ts +22 -16
  97. package/types/client/policy.d.ts +39 -14
  98. package/types/client/random.d.ts +26 -0
  99. package/types/client/sizes.d.ts +93 -5
  100. package/types/economy/check.d.mts +5 -10
  101. package/types/economy/cost.d.mts +2 -1
  102. package/types/economy/guide.d.mts +5 -0
  103. package/types/economy/live.d.mts +16 -1
  104. package/types/economy/model.d.mts +151 -38
  105. package/types/economy/plan.d.mts +237 -194
  106. package/types/economy/simulate.d.mts +21 -13
  107. package/types/mcp/tools.d.mts +21 -13
  108. package/types/play/index.d.ts +453 -53
  109. package/types/play/keys.d.ts +51 -0
  110. package/types/results/index.d.ts +7 -6
  111. package/types/results/server.d.ts +126 -0
  112. package/types/results/wire.d.ts +1 -1
@@ -1,17 +1,21 @@
1
- // Reading the loot program's accounts and receipts. Every account starts with a one-byte tag (1 to 23); the IDL
2
- // describes most of them, and the ones it doesn't (a receipt, a sheet, a stash, …) are read here by hand. Values come back in the
1
+ // Reading the loot program's accounts and receipts. Every account starts with a one-byte tag (1 to 29); the IDL
2
+ // describes most of them, and the ones it doesn't (a list of records, a lottery round, a sheet, a stash, …) are read
3
+ // here by hand. Values come back in the
3
4
  // IDL's shapes (chain/borsh.js).
4
- import { encodeBase58, NO_ADDRESS } from '../chain/base58.js';
5
- import { decodeAccount, decodeEvents } from '../chain/borsh.js';
6
- import { compareBytes, fromHex, readUintLE, toHex } from '../chain/bytes.js';
5
+ import { addressBytes, encodeBase58, NO_ADDRESS } from '../chain/base58.js';
6
+ import { decodeAccount, decodeBorsh, decodeEvents } from '../chain/borsh.js';
7
+ import { compareBytes, concat, readUintLE, toHex, uintLE } from '../chain/bytes.js';
7
8
  import { sha256 } from '../chain/sha256.js';
8
9
  import { SYSTEM_PROGRAM } from '../chain/pda.js';
9
- import { LOOT_PROGRAM } from './pda.js';
10
+ import { LOOT_PROGRAM, pda } from './pda.js';
10
11
  import IDL from './idl/loot.js';
11
- import { MAX_RECEIPT_ROUNDS, RECEIPT_BASE_BYTES, RECEIPT_ROUND_BYTES } from './sizes.js';
12
- export { MAX_RECEIPT_ROUNDS } from './sizes.js';
13
12
 
14
- /** Account tags: each account's first byte, as the program writes it. */
13
+ /** The bytes of one time in a farm's record of ends (`decodeEnds`): rent its writer pays, not refunded when a seat leaves. */
14
+ export const END_BYTES = 44;
15
+ /** The bytes of a farm's record of ends with no time in it, its owner's rent at `open_farm`. */
16
+ export const ENDS_HEAD_BYTES = 7;
17
+
18
+ /** Account tags: each account's first byte, as the program writes it; every list of records (`decodeList`) is 26. */
15
19
  export const TAGS = Object.freeze({
16
20
  Platform: 1,
17
21
  Project: 2,
@@ -23,63 +27,274 @@ export const TAGS = Object.freeze({
23
27
  Recipe: 8,
24
28
  Hold: 9,
25
29
  Contest: 10,
26
- Entry: 11,
27
30
  Sheet: 12,
28
31
  Traits: 13,
29
- Grant: 14,
30
- Receipt: 15,
31
- LotteryContribution: 16,
32
+ LotteryRound: 16,
32
33
  Farm: 17,
33
34
  Seat: 18,
34
35
  Proven: 19,
35
36
  Auction: 20,
36
- Stash: 21,
37
37
  Offer: 22,
38
38
  Listing: 23,
39
+ Profile: 24,
40
+ Grant: 27,
41
+ // A table, and a stash: a table of NFTs (21 was a stash's own before 0.22).
42
+ Table: 25,
43
+ // A list of records: a wallet's counts, a grant's claims or rounds, a contest's entries.
44
+ List: 26,
45
+ // A farm's record of when its seats stop earning (`decodeEnds`).
46
+ Ends: 29,
39
47
  });
40
48
 
41
49
  const BY_TAG = Object.fromEntries(Object.entries(TAGS).map(([name, tag]) => [tag, name]));
42
50
 
43
51
  /** The kind of a loot account from its first byte, or null. */
44
- export const accountKind = (data) => (data?.length ? BY_TAG[data[0]] ?? null : null);
52
+ export const accountKind = data => (data?.length ? (BY_TAG[data[0]] ?? null) : null);
53
+
54
+ /**
55
+ * The records the program writes by hand, as IDL types: a lottery round's header and what it keeps (`LotteryRound`,
56
+ * tag 16), and a contest's entry. The client reads them with the same Borsh codec as any account.
57
+ */
58
+ const RECORDS = [
59
+ {
60
+ name: 'Round',
61
+ type: { kind: 'enum', variants: ['Open', 'Drawing', 'Drawn', 'Cancelled'].map(name => ({ name })) },
62
+ },
63
+ {
64
+ name: 'TokenPrize',
65
+ type: {
66
+ kind: 'struct',
67
+ fields: [
68
+ { name: 'run', type: 'u16' },
69
+ { name: 'mint', type: 'pubkey' },
70
+ { name: 'each', type: 'u64' },
71
+ { name: 'sent', type: 'u64' },
72
+ { name: 'arrived', type: 'u64' },
73
+ { name: 'minted', type: 'bool' },
74
+ ],
75
+ },
76
+ },
77
+ {
78
+ name: 'Opened',
79
+ type: {
80
+ kind: 'struct',
81
+ fields: [
82
+ { name: 'mint', type: 'pubkey' },
83
+ { name: 'by', type: 'pubkey' },
84
+ ],
85
+ },
86
+ },
87
+ {
88
+ name: 'LotteryRound',
89
+ type: {
90
+ kind: 'struct',
91
+ fields: [
92
+ ['project', 'pubkey'],
93
+ ['lottery', 'pubkey'],
94
+ ['id', 'u32'],
95
+ ['incarnation', 'u64'],
96
+ ['round', 'u32'],
97
+ ['bump', 'u8'],
98
+ ['opener', 'pubkey'],
99
+ ['lent', 'u64'],
100
+ ['head', 'u32'],
101
+ ['status', { defined: { name: 'Round' } }],
102
+ // The terms, as the lottery's revision had them at the round's first purchase; whether its entries keep what
103
+ // arrived (a payment mint with a transfer fee, or a share the buyer chooses, paid as they buy).
104
+ ['revision', 'u32'],
105
+ ['terms', { defined: { name: 'LotteryTerms' } }],
106
+ ['fee', 'bool'],
107
+ // The totals, split at each purchase: what each share paid in common takes, the entries that own a part of
108
+ // their own, and the parts owed now.
109
+ ['sold', 'u64'],
110
+ ['to_pot', 'u64'],
111
+ ['common', { vec: 'u64' }],
112
+ ['owns', 'u32'],
113
+ ['owed', 'u32'],
114
+ // The draw, and the money the ask fixed: the pot's (what the round owes the pot back: `pot_owed`).
115
+ ['asked_first', 'u64'],
116
+ ['asked_slot', 'u64'],
117
+ ['first_asked_at', 'i64'],
118
+ ['asked_at', 'i64'],
119
+ ['answer', { array: ['u8', 32] }],
120
+ ['first', 'u64'],
121
+ ['pot_total', 'u64'],
122
+ ['pot_sent', 'u64'],
123
+ ['pot_arrived', 'u64'],
124
+ ['pot_owed', 'u64'],
125
+ ['paid', 'bytes'],
126
+ ['prizes', { vec: { defined: { name: 'TokenPrize' } } }],
127
+ ['accounts', { vec: { defined: { name: 'Opened' } } }],
128
+ // The NFT of the lottery's own stash each place that holds or draws one got, in place order, and how many.
129
+ ['nfts', { vec: 'pubkey' }],
130
+ ['assigned', 'u32'],
131
+ ].map(([name, type]) => ({ name, type })),
132
+ },
133
+ },
134
+ {
135
+ name: 'ContestEntry',
136
+ type: {
137
+ kind: 'struct',
138
+ fields: [
139
+ ['player', 'pubkey'],
140
+ ['side', 'u16'],
141
+ ['amount', 'u64'],
142
+ ['bits', 'u8'],
143
+ ['nft', 'pubkey'],
144
+ ['place', 'u32'],
145
+ ['taken', 'u32'],
146
+ ].map(([name, type]) => ({ name, type })),
147
+ },
148
+ },
149
+ ];
150
+ const IDLX = {
151
+ ...IDL,
152
+ types: [...IDL.types, ...RECORDS],
153
+ accounts: [...IDL.accounts, { name: 'LotteryRound', discriminator: [TAGS.LotteryRound] }],
154
+ };
45
155
 
46
- /** A loot account of the named kind (one the IDL describes). Throws when the data is not one. */
47
- export const decode = (name, data) => decodeAccount(IDL, name, data);
156
+ /** A loot account of the named kind (one the IDL describes, or a `LotteryRound`). Throws when the data is not one. */
157
+ export const decode = (name, data) => decodeAccount(IDLX, name, data);
48
158
 
49
159
  /** Any loot account: { kind, value }, or null for data that is no loot account. */
50
160
  export function decodeAny(data) {
51
161
  const kind = accountKind(data);
52
162
  if (!kind) return null;
53
- if (kind === 'Receipt') return { kind, value: decodeReceipt(data) };
163
+ if (kind === 'Profile') return { kind, value: decodeProfile(data) };
164
+ if (kind === 'List') return { kind, value: decodeList(data) };
165
+ if (kind === 'Ends') return { kind, value: decodeEnds(data) };
166
+ if (kind === 'LotteryRound') return { kind, value: decodeRound(data) };
54
167
  if (kind === 'Sheet') return { kind, value: decodeSheet(data) };
168
+ if (kind === 'Table')
169
+ return data[37] === PAYLOAD.Nft ? { kind: 'Stash', value: decodeStash(data) } : { kind, value: decodeTable(data) };
55
170
  if (kind === 'Proven') return { kind, value: decodeProven(data) };
56
171
  if (kind === 'Traits') return { kind, value: decodeTraits(data) };
57
- if (kind === 'Stash') return { kind, value: decodeStash(data) };
58
172
  return { kind, value: decode(kind, data) };
59
173
  }
60
174
 
175
+ // ---------------------------------------------------------------- lists of records (M1)
176
+
61
177
  /**
62
- * A counter (a Receipt): a wallet's on a box, a lottery, a recipe or a grant, a person's or a key's on
63
- * a grant. { count (lifetime where tracked), window (the window `inWindow` counts), inWindow, rounds (the first 8
64
- * bytes of sha256 of each round a grant paid it, by voucher or list, in order), claimed(round) }. A round it holds
65
- * was paid once and can never be paid again.
178
+ * A list of records (tag 26, `list.rs`): { width, records } in their order: a wallet's counts
179
+ * (`["counts", project, wallet]`) and a grant's claims are limit records (28 bytes, `readLimit`), a grant's rounds 16
180
+ * bytes, a contest's entries 43 or 83 (`decodeEntries`).
66
181
  */
67
- export function decodeReceipt(data) {
68
- if (!(data instanceof Uint8Array) || data[0] !== TAGS.Receipt || data.length < RECEIPT_BASE_BYTES) throw new Error('Not a Receipt account');
69
- const n = Number(readUintLE(data, RECEIPT_BASE_BYTES - 4, 4));
70
- if (n > MAX_RECEIPT_ROUNDS) throw new Error('Receipt history exceeds the supported capacity');
71
- if (data.length !== RECEIPT_BASE_BYTES + RECEIPT_ROUND_BYTES * n) throw new Error('Receipt length does not match its rounds');
72
- const rounds = Array.from({ length: n }, (_, i) => data.slice(RECEIPT_BASE_BYTES + RECEIPT_ROUND_BYTES * i, RECEIPT_BASE_BYTES + RECEIPT_ROUND_BYTES * (i + 1)));
73
- if (rounds.some((round, i) => i > 0 && compareBytes(rounds[i - 1], round) >= 0)) throw new Error('Receipt rounds must be unique and sorted');
74
- const ids = new Set(rounds.map((r) => toHex(r)));
75
- const claimed = (round) => ids.has(toHex(sha256(typeof round === 'string' ? fromHex(round) : Uint8Array.from(round)).subarray(0, 8)));
76
- return {
77
- count: Number(readUintLE(data, 1, 4)),
78
- window: Number(readUintLE(data, 5, 4)),
79
- inWindow: Number(readUintLE(data, 9, 4)),
80
- rounds,
81
- claimed,
82
- };
182
+ export function decodeList(data) {
183
+ if (!(data instanceof Uint8Array) || data[0] !== TAGS.List || data.length < 7) throw new Error('Not a list account');
184
+ const width = Number(readUintLE(data, 1, 2));
185
+ const count = Number(readUintLE(data, 3, 4));
186
+ if (!width || data.length !== 7 + width * count) throw new Error('List account does not match its records');
187
+ return { width, records: Array.from({ length: count }, (_, i) => data.subarray(7 + width * i, 7 + width * (i + 1))) };
188
+ }
189
+
190
+ /**
191
+ * A farm's record of when its seats stop earning (`["ends", farm]`, tag 29; `Farm.keeps_ends`): its times in order, each
192
+ * `{ at, rewardPerWeight, weight, seats }`: the time (Unix seconds), `reward_per_weight` there once the farm has walked past
193
+ * it, the weight that stops earning then, and the seats that still need the record.
194
+ */
195
+ export function decodeEnds(data) {
196
+ if (!(data instanceof Uint8Array) || data[0] !== TAGS.Ends || data.length < 7)
197
+ throw new Error('Not a record of ends');
198
+ const width = Number(readUintLE(data, 1, 2));
199
+ const count = Number(readUintLE(data, 3, 4));
200
+ if (width !== END_BYTES || data.length !== 7 + width * count)
201
+ throw new Error('The record of ends does not match its records');
202
+ return Array.from({ length: count }, (_, i) => {
203
+ const at = 7 + width * i;
204
+ let time = 0n;
205
+ for (let b = 0; b < 8; b++) time = (time << 8n) | BigInt(data[at + b]);
206
+ return {
207
+ at: time,
208
+ rewardPerWeight: readUintLE(data, at + 8, 16),
209
+ weight: readUintLE(data, at + 24, 16),
210
+ seats: Number(readUintLE(data, at + 40, 4)),
211
+ };
212
+ });
213
+ }
214
+
215
+ /** The first 16 bytes of the sha256 of `parts` (addresses, bytes or u64s): a record's key in a list. */
216
+ export const recordKey16 = (...parts) =>
217
+ sha256(
218
+ concat(...parts.map(p => (typeof p === 'string' ? addressBytes(p) : p instanceof Uint8Array ? p : uintLE(p, 8)))),
219
+ ).subarray(0, 16);
220
+
221
+ /** The key of a wallet's count for a box, a recipe (incarnation 0) or a lottery (its incarnation), in its counts. */
222
+ export const countKey = (record, incarnation = 0) => recordKey16(record, BigInt(incarnation));
223
+
224
+ /** What the limit record at `key` counts in a decoded list (`decodeList`, or null for none): { count, window, inWindow }. */
225
+ export function readLimit(list, key) {
226
+ const record = list?.records.find(r => compareBytes(r.subarray(0, 16), key) === 0);
227
+ const n = i => (record ? Number(readUintLE(record, 16 + 4 * i, 4)) : 0);
228
+ return { count: n(0), window: n(1), inWindow: n(2) };
229
+ }
230
+
231
+ /** A fetched list account, or null for none (or an empty System account someone sent SOL to, which the program opens
232
+ * as the list). Any other owner or layout is refused. */
233
+ export function readListAccount(account) {
234
+ if (account == null) return null;
235
+ if (
236
+ account.owner === SYSTEM_PROGRAM &&
237
+ account.data instanceof Uint8Array &&
238
+ account.data.length === 0 &&
239
+ !account.executable
240
+ )
241
+ return null;
242
+ if (account.owner !== LOOT_PROGRAM || account.executable) throw new Error('The list has an invalid owner.');
243
+ return decodeList(account.data);
244
+ }
245
+
246
+ /**
247
+ * A contest's entries (`["entries", contest, incarnation, 0]`), in the list's order (by player): [{ player, side,
248
+ * amount (0 once paid or refunded; 1 while a contest of NFTs holds its NFT), bits (`CLAIMED`, `ENDED`), nft, place (how
249
+ * many entered its side before it), taken (the losers' NFTs a winner took) }]. A claim names an entry by its index here.
250
+ */
251
+ export function decodeEntries(data) {
252
+ const { width, records } = decodeList(data);
253
+ return records.map(r => {
254
+ const full = new Uint8Array(83);
255
+ full.set(r.subarray(0, Math.min(width, 83)));
256
+ return decodeBorsh(IDLX, { defined: { name: 'ContestEntry' } }, full)[0];
257
+ });
258
+ }
259
+
260
+ /** A winner's first claim came (its prize given or forgone); a winner ended its picks early. */
261
+ export const CLAIMED = 1;
262
+ export const ENDED = 2;
263
+
264
+ /** How many losers' NFTs a winner of a contest of NFTs takes: the share fixed at the decision, one more for an early
265
+ * place on its side. */
266
+ export const shareOf = (contest, entry) =>
267
+ Number(contest.share) + (Number(entry.place) < Number(contest.extras) ? 1 : 0);
268
+
269
+ /**
270
+ * What a winning entry that put in `amount` takes with its first claim of a decided contest of tokens: its part of what
271
+ * the decision wrote (`pot_won`, the pot once the shares were taken; `prize_won`, a split prize's vault), by what it put
272
+ * in against its side's total. The winners claimed before it took `won × claimed ÷ total`, so the parts add up exactly
273
+ * whatever the order: `{ pot, prize }` (the prize part is 0 when the claim forgoes it). Tokens sent to the pot after the
274
+ * decision are nobody's.
275
+ */
276
+ export function contestWinnings(contest, amount) {
277
+ const total = BigInt(contest.sides[contest.winner] ?? 0) || 1n;
278
+ const claimed = total - BigInt(contest.unclaimed);
279
+ const part = won => (BigInt(won) * (claimed + BigInt(amount))) / total - (BigInt(won) * claimed) / total;
280
+ return { pot: part(contest.pot_won), prize: part(contest.prize_won) };
281
+ }
282
+
283
+ /** What a count allows `n` more of at `now` under `limit` ({ total, per_window, window }, 0 is no limit; a lottery's
284
+ * window is its round plus one, given as `window`). */
285
+ export function countAllows(
286
+ count,
287
+ limit,
288
+ now,
289
+ n = 1,
290
+ window = Number(limit.window) ? Math.floor(Number(now) / Number(limit.window)) + 1 : 0,
291
+ ) {
292
+ const total = (count?.count ?? 0) + n;
293
+ const inWindow = (count && count.window === window ? count.inWindow : 0) + n;
294
+ return (
295
+ (!Number(limit.total) || total <= Number(limit.total)) &&
296
+ (!Number(limit.per_window) || inWindow <= Number(limit.per_window))
297
+ );
83
298
  }
84
299
 
85
300
  /**
@@ -88,12 +303,29 @@ export function decodeReceipt(data) {
88
303
  */
89
304
  export function decodeSheet(data) {
90
305
  if (data == null || data.length === 0) return null;
91
- if (!(data instanceof Uint8Array) || data[0] !== TAGS.Sheet || data.length < 69) throw new Error('Not a Sheet account');
306
+ if (!(data instanceof Uint8Array) || data[0] !== TAGS.Sheet || data.length < 69)
307
+ throw new Error('Not a Sheet account');
308
+ const { attributes, head } = attributesAt(data, 'Sheet');
309
+ return { project: head(1), nft: head(33), attributes };
310
+ }
311
+
312
+ /** A player's profile (`["profile", project, wallet]`): { project, wallet, attributes }, what the project keeps about
313
+ * the wallet (its XP, level, rank). Its layout is a Sheet's, under its own tag. */
314
+ export function decodeProfile(data) {
315
+ if (data == null || data.length === 0) return null;
316
+ if (!(data instanceof Uint8Array) || data[0] !== TAGS.Profile || data.length < 69)
317
+ throw new Error('Not a Profile account');
318
+ const { attributes, head } = attributesAt(data, 'Profile');
319
+ return { project: head(1), wallet: head(33), attributes };
320
+ }
321
+
322
+ /** The attributes after a Sheet's or a profile's two addresses, read with every length checked against the data. */
323
+ function attributesAt(data, name) {
92
324
  let at = 65;
93
325
  // Every length is checked against the data: a count or a text longer than what is there is a truncated account.
94
326
  const text = () => {
95
327
  const n = Number(readUintLE(data, at, 4));
96
- if (at + 4 + n > data.length) throw new Error('Sheet account is truncated');
328
+ if (at + 4 + n > data.length) throw new Error(`${name} account is truncated`);
97
329
  const value = new TextDecoder().decode(data.subarray(at + 4, at + 4 + n));
98
330
  at += 4 + n;
99
331
  return value;
@@ -101,19 +333,68 @@ export function decodeSheet(data) {
101
333
  const count = Number(readUintLE(data, at, 4));
102
334
  at += 4;
103
335
  // Each attribute takes 8 bytes at least (two empty texts).
104
- if (count * 8 > data.length - at) throw new Error('Sheet account is truncated');
336
+ if (count * 8 > data.length - at) throw new Error(`${name} account is truncated`);
105
337
  const attributes = Array.from({ length: count }, () => ({ key: text(), value: text() }));
106
- return { project: encodeBase58(data.subarray(1, 33)), nft: encodeBase58(data.subarray(33, 65)), attributes };
338
+ return { attributes, end: at, head: from => encodeBase58(data.subarray(from, from + 32)) };
107
339
  }
108
340
 
109
- /** A stash (`["stash", project, id]`): { project, id, nfts }, the NFTs the project holds to give away, in no order. */
341
+ /** What a table's rows carry (its payload byte). */
342
+ export const PAYLOAD = Object.freeze({ Number: 0, Label: 1, Nft: 2 });
343
+ const TABLE_HEAD = 51;
344
+
345
+ /** Where row `r` of a table of `len`-byte rows is: groups of 64 blocks of 64 rows, each led by its weight in stock. */
346
+ export function tableRowAt(len, r) {
347
+ const block = 8 + 64 * len;
348
+ const group = 8 + 64 * block;
349
+ return TABLE_HEAD + Math.floor(r / 4096) * group + 8 + (Math.floor(r / 64) % 64) * block + 8 + (r % 64) * len;
350
+ }
351
+
352
+ /**
353
+ * A table (`["table", project, id]`, `table.rs`): { project, id, payload ('Number' | 'Label'), sealed, count, total
354
+ * (the weight in stock), rows: [{ weight, left, label }] }. A row's `left` is `NO_LIMIT` (4294967295) for a stock that
355
+ * never runs out.
356
+ */
357
+ export function decodeTable(data) {
358
+ if (data == null || data.length === 0) return null;
359
+ if (!(data instanceof Uint8Array) || data[0] !== TAGS.Table || data.length < TABLE_HEAD)
360
+ throw new Error('Not a Table account');
361
+ const payload = Object.keys(PAYLOAD).find(k => PAYLOAD[k] === data[37]);
362
+ if (!payload || payload === 'Nft') throw new Error('Not a Table account');
363
+ const count = Number(readUintLE(data, 39, 4));
364
+ const head = {
365
+ project: encodeBase58(data.subarray(1, 33)),
366
+ id: Number(readUintLE(data, 33, 4)),
367
+ payload,
368
+ sealed: data[38] === 1,
369
+ count,
370
+ total: readUintLE(data, 43, 8),
371
+ };
372
+ const len = payload === 'Label' ? 40 : 8;
373
+ if (count && tableRowAt(len, count - 1) + len > data.length) throw new Error('Table account is truncated');
374
+ const rows = Array.from({ length: count }, (_, r) => {
375
+ const at = tableRowAt(len, r);
376
+ const label =
377
+ payload === 'Label' ? new TextDecoder().decode(data.subarray(at + 8, at + 40)).replace(/\0+$/, '') : '';
378
+ return { weight: Number(readUintLE(data, at, 4)), left: Number(readUintLE(data, at + 4, 4)), label };
379
+ });
380
+ return { ...head, rows };
381
+ }
382
+
383
+ /** A stash (`["stash", project, id]`, or a lottery's own): { project, id, nfts, weights }, the NFTs the project holds to
384
+ * give away and the weight each is drawn by, as a table of NFTs keeps them (a row: its weight, then the NFT). */
110
385
  export function decodeStash(data) {
111
386
  if (data == null || data.length === 0) return null;
112
- if (!(data instanceof Uint8Array) || data[0] !== TAGS.Stash || data.length < 41) throw new Error('Not a Stash account');
113
- const count = Number(readUintLE(data, 37, 4));
114
- if (41 + 32 * count > data.length) throw new Error('Stash account is truncated');
115
- const nfts = Array.from({ length: count }, (_, i) => encodeBase58(data.subarray(41 + 32 * i, 73 + 32 * i)));
116
- return { project: encodeBase58(data.subarray(1, 33)), id: Number(readUintLE(data, 33, 4)), nfts };
387
+ if (!(data instanceof Uint8Array) || data[0] !== TAGS.Table || data[37] !== PAYLOAD.Nft || data.length < TABLE_HEAD)
388
+ throw new Error('Not a Stash account');
389
+ const count = Number(readUintLE(data, 39, 4));
390
+ if (count && tableRowAt(36, count - 1) + 36 > data.length) throw new Error('Stash account is truncated');
391
+ const rows = Array.from({ length: count }, (_, r) => tableRowAt(36, r));
392
+ return {
393
+ project: encodeBase58(data.subarray(1, 33)),
394
+ id: Number(readUintLE(data, 33, 4)),
395
+ nfts: rows.map(at => encodeBase58(data.subarray(at + 4, at + 36))),
396
+ weights: rows.map(at => Number(readUintLE(data, at, 4))),
397
+ };
117
398
  }
118
399
 
119
400
  /**
@@ -124,9 +405,10 @@ export function decodeStash(data) {
124
405
  export function decodeProven(data) {
125
406
  // Its shortest: the tag, five addresses, the root, data hash and creator hash, no asset data hash, the flags and
126
407
  // nonce, and its payer.
127
- if (!(data instanceof Uint8Array) || data[0] !== TAGS.Proven || data.length < 1 + 32 * 8 + 1 + 1 + 8 + 32) throw new Error('Not a Proven account');
128
- const key = (at) => encodeBase58(data.subarray(at, at + 32));
129
- const none = (k) => (k === NO_ADDRESS ? null : k);
408
+ if (!(data instanceof Uint8Array) || data[0] !== TAGS.Proven || data.length < 1 + 32 * 8 + 1 + 1 + 8 + 32)
409
+ throw new Error('Not a Proven account');
410
+ const key = at => encodeBase58(data.subarray(at, at + 32));
411
+ const none = k => (k === NO_ADDRESS ? null : k);
130
412
  const root = 1 + 32 * 5;
131
413
  let at = root + 32 * 3; // the five addresses, then the root, data hash and creator hash
132
414
  if (data[at] !== 0 && data[at] !== 1) throw new Error('Proven account has an unknown asset data hash flag');
@@ -135,15 +417,27 @@ export function decodeProven(data) {
135
417
  if (at + 1 + 8 + 32 > data.length) throw new Error('Proven account is truncated');
136
418
  const flags = data[at];
137
419
  return {
138
- asset: key(1), tree: key(33), owner: none(key(65)), delegate: none(key(97)), collection: none(key(129)),
139
- flags, nonce: readUintLE(data, at + 1, 8), root: toHex(data.subarray(root, root + 32)), payer: key(at + 9),
420
+ asset: key(1),
421
+ tree: key(33),
422
+ owner: none(key(65)),
423
+ delegate: none(key(97)),
424
+ collection: none(key(129)),
425
+ flags,
426
+ nonce: readUintLE(data, at + 1, 8),
427
+ root: toHex(data.subarray(root, root + 32)),
428
+ payer: key(at + 9),
140
429
  };
141
430
  }
142
431
 
143
432
  /** The traits a project vouches for (`["traits", project, key]`): { project, key, root (hex) }. */
144
433
  export function decodeTraits(data) {
145
- if (!(data instanceof Uint8Array) || data[0] !== TAGS.Traits || data.length < 97) throw new Error('Not a Traits account');
146
- return { project: encodeBase58(data.subarray(1, 33)), key: encodeBase58(data.subarray(33, 65)), root: toHex(data.subarray(65, 97)) };
434
+ if (!(data instanceof Uint8Array) || data[0] !== TAGS.Traits || data.length < 97)
435
+ throw new Error('Not a Traits account');
436
+ return {
437
+ project: encodeBase58(data.subarray(1, 33)),
438
+ key: encodeBase58(data.subarray(33, 65)),
439
+ root: toHex(data.subarray(65, 97)),
440
+ };
147
441
  }
148
442
 
149
443
  /**
@@ -155,11 +449,11 @@ export function decodeTokenMetadata(data) {
155
449
  if (!(data instanceof Uint8Array) || data[0] !== 4) return null;
156
450
  let at = 33;
157
451
  const key = () => encodeBase58(data.subarray(at, (at += 32)));
158
- const skip = (n) => (at += n);
452
+ const skip = n => (at += n);
159
453
  const str = () => skip(4 + Number(readUintLE(data, at, 4)));
160
454
  const some = () => at < data.length && data[at++] === 1;
161
455
  const mint = key();
162
- str(), str(), str(), skip(2);
456
+ (str(), str(), str(), skip(2));
163
457
  if (some()) skip(34 * Number(readUintLE(data, at, 4)) + 4);
164
458
  skip(2);
165
459
  if (some()) skip(1); // edition nonce
@@ -180,92 +474,302 @@ export function decodeTokenMetadata(data) {
180
474
  return { mint, collection, programmable, ruleSet };
181
475
  }
182
476
 
183
- /** A fetched receipt, or null for a missing/prefunded empty System account. Other owners/layouts are refused. */
184
- export function readReceiptAccount(account) {
185
- if (account == null) return null;
186
- if (account.owner === SYSTEM_PROGRAM && account.data instanceof Uint8Array && account.data.length === 0 && !account.executable) return null;
187
- if (account.owner !== LOOT_PROGRAM || account.executable) throw new Error('The receipt has an invalid owner.');
188
- return decodeReceipt(account.data);
189
- }
477
+ // ---------------------------------------------------------------- a lottery's rounds (M3)
478
+ //
479
+ // A round is a record of its own (`pda.round`), opened by its first purchase: a header with a copy of the terms, the
480
+ // totals, what it still owes and the draw, then one entry a purchase. The ask (`draw_lottery`) fixes its money and moves
481
+ // the lottery on; after the answer, `pull` pays one part at a time, in any order, and the pull that leaves nothing owed
482
+ // closes it.
483
+
484
+ /** An entry's word: its tickets' end in the low 48 bits, its own part paid in the top bit. */
485
+ const END_BITS = (1n << 48n) - 1n;
486
+ export const OWN_PAID = 1n << 63n;
487
+
488
+ /** The mint a lottery's tickets are paid in (`terms`: a Lottery's or a round's): its ticket mint, burned, or `mint`. */
489
+ export const paymentMint = terms => (ticketsBurn(terms) ? terms.ticket_mint : terms.mint);
490
+ /** Whether a lottery's tickets are burned: they are paid in a ticket mint of its own. */
491
+ export const ticketsBurn = terms => Boolean(terms.ticket_mint) && terms.ticket_mint !== NO_ADDRESS;
492
+
493
+ /** The most pots one lottery keeps (`MAX_POTS`): a pot's id is below it. */
494
+ export const MAX_POTS = Number(IDL.constants.find(c => c.name === 'MAX_POTS').value);
495
+ /** The pot a lottery's places are paid from (`terms.pays_from`: `{ Pot: [id] }`), or null when each round pays from its
496
+ * own ticket money (`{ Round: { rest } }`). */
497
+ export const potIdOf = terms => (terms?.pays_from?.Pot ? Number(terms.pays_from.Pot[0]) : null);
498
+ /** Where what a round's places leave goes (`Part::Common` past the shares): its pot, or the `rest` a round paid from
499
+ * its own money names (`lotteryAddress`: the lottery's). */
500
+ export const leftoverTo = (lotteryAddress, terms) =>
501
+ potIdOf(terms) != null ? { Account: [pda.lotteryPot(lotteryAddress, potIdOf(terms))] } : terms.pays_from.Round.rest;
502
+ /** The ids of the pots a lottery opened (`Lottery::pots`, one bit an id), lowest first: a close empties each. */
503
+ export const lotteryPots = lottery =>
504
+ Array.from({ length: MAX_POTS }, (_, id) => id).filter(id => (Number(lottery.pots ?? 0) >> id) & 1);
190
505
 
191
- /** What a counter allows `n` more of at `now` under `limit` ({ total, per_window, window }, 0 is no limit). */
192
- export function receiptAllows(receipt, limit, now, n = 1) {
193
- const window = Number(limit.window) ? Math.floor(Number(now) / Number(limit.window)) + 1 : 0;
194
- const count = (receipt?.count ?? 0) + n;
195
- const inWindow = (receipt && receipt.window === window ? receipt.inWindow : 0) + n;
196
- return (!Number(limit.total) || count <= Number(limit.total)) && (!Number(limit.per_window) || inWindow <= Number(limit.per_window));
506
+ /** A share the buyer chooses (a referrer, a collectible's holder): paid out of the price as they buy, from their own account. */
507
+ export const chosenAtPurchase = share => Boolean(share.to?.Chosen);
508
+
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);
511
+
512
+ /**
513
+ * 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) }.
517
+ */
518
+ export function decodeRound(data) {
519
+ const r = decode('LotteryRound', data);
520
+ const at = Number(r.head);
521
+ const width = Number(readUintLE(data, at, 2));
522
+ 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)
524
+ throw new Error('LotteryRound entries do not match its header');
525
+ let start = 0n;
526
+ const entries = Array.from({ length: count }, (_, i) => {
527
+ const e = data.subarray(at + 6 + width * i, at + 6 + width * (i + 1));
528
+ const word = readUintLE(e, 32, 8);
529
+ const end = word & END_BITS;
530
+ let k = 40;
531
+ 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);
533
+ const entry = {
534
+ buyer: encodeBase58(e.subarray(0, 32)),
535
+ start,
536
+ end,
537
+ paid: word & ~END_BITS,
538
+ amount,
539
+ nft: r.terms.nft_ticket ? encodeBase58(take(32)) : NO_ADDRESS,
540
+ };
541
+ start = end;
542
+ return entry;
543
+ });
544
+ return { ...r, entries };
197
545
  }
198
546
 
199
- // ---------------------------------------------------------------- a lottery's purchases
547
+ /** How many places a lottery's (or a round's) `places` are, every run counted (`Place.times`). */
548
+ export const placeCount = places => places.reduce((n, p) => n + Number(p.times ?? 1), 0);
549
+
550
+ /** The places of a round that have a winner: every place, but no more than tickets. */
551
+ export const roundWinners = r => Math.min(placeCount(r.terms.places), Number(r.sold));
200
552
 
201
- /** The contribution among `contributions` ([{ address, value }], decoded LotteryContributions) that holds `ticket`. */
202
- export const contributionOf = (contributions, ticket) =>
203
- contributions.find(({ value }) => BigInt(value.first_ticket) <= BigInt(ticket) && BigInt(ticket) < BigInt(value.end)) ?? null;
553
+ /** The ticket that wins `place` of a drawn round: even steps around the list from first place's ticket. */
554
+ 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);
558
+ return ahead < toEnd ? BigInt(r.first) + ahead : ahead - toEnd;
559
+ }
204
560
 
205
- /** Whether a lottery's place wins something: a share of the pot, a roll, or both (as the program counts a place set). */
206
- export const placeSet = (place) => Number(place.bps) > 0 || place.prize != null;
561
+ /** The run of places that holds `place`, and its first place. */
562
+ export function runOf(r, place) {
563
+ let first = 0;
564
+ for (let run = 0; run < r.terms.places.length; run++) {
565
+ if (place < first + Number(r.terms.places[run].times)) return [run, first];
566
+ first += Number(r.terms.places[run].times);
567
+ }
568
+ return [r.terms.places.length, first];
569
+ }
207
570
 
208
- /** The places of a lottery that have a winner this round: as many as are set, but no more than tickets. */
209
- export function lotteryWinners(lottery) {
210
- const set = lottery.places.findIndex((p) => !placeSet(p));
211
- return Math.min(set === -1 ? lottery.places.length : set, Number(lottery.sold));
571
+ /** The places of run `run` with a winner: [first, end). */
572
+ export function runPlaces(r, run) {
573
+ const first = placeCount(r.terms.places.slice(0, run));
574
+ const w = roundWinners(r);
575
+ return [Math.min(first, w), Math.min(first + Number(r.terms.places[run].times), w)];
212
576
  }
213
577
 
214
- /** The ticket that wins `place` of a drawn lottery: even steps around the list from first place's ticket. */
215
- export function ticketOfPlace(lottery, place) {
216
- const sold = BigInt(lottery.sold);
217
- const winners = BigInt(lotteryWinners(lottery) || 1);
218
- const ahead = BigInt(place) * (sold / winners);
219
- const toEnd = sold - BigInt(lottery.first_ticket);
220
- return ahead < toEnd ? BigInt(lottery.first_ticket) + ahead : ahead - toEnd;
578
+ /** The places of run `run` whose ticket is in `start..end` (an entry's), in order. */
579
+ export function placesIn(r, run, start, end) {
580
+ const [from, to] = runPlaces(r, run);
581
+ const out = [];
582
+ 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;
586
+ const len = BigInt(end) - BigInt(start);
587
+ for (const [a, b] of [
588
+ [d, d + len < sold ? d + len : sold],
589
+ [0n, d + len > sold ? d + len - sold : 0n],
590
+ ]) {
591
+ let p = Number((a + step - 1n) / step);
592
+ for (p = Math.max(p, from); p < to && BigInt(p) * step < b; p++) out.push(p);
593
+ }
594
+ return out.sort((x, y) => x - y);
221
595
  }
222
596
 
223
- /** The places of a drawn lottery whose winning ticket is in `first..end` (a contribution's), in order. */
224
- export function placesWon(lottery, first, end) {
225
- const won = [];
226
- for (let place = 0; place < lotteryWinners(lottery); place++) {
227
- const ticket = ticketOfPlace(lottery, place);
228
- if (BigInt(first) <= ticket && ticket < BigInt(end)) won.push(place);
597
+ /** What one place of a pass-through amount takes: `clamp(sent − before, 0, amount) × arrived / sent` (none of nothing sent). */
598
+ const through = (sent, arrived, before, amount) => {
599
+ if (sent === 0n) return 0n;
600
+ const part = sent > before ? (sent - before < amount ? sent - before : amount) : 0n;
601
+ return (part * arrived) / sent;
602
+ };
603
+
604
+ /**
605
+ * What place `place` (counted from the first place of `runs`) takes of a pass-through amount, as the program splits it:
606
+ * runs of `[a, n]` (n places taking `a` each), `sent` what the places are owed and `arrived` what the round holds for
607
+ * them; the last place with a part takes what the others leave.
608
+ */
609
+ export function splitPart(runs, sent, arrived, place) {
610
+ [sent, arrived] = [BigInt(sent), BigInt(arrived)];
611
+ let [c, at, last, paid, before] = [0n, 0, null, 0n, 0n];
612
+ for (const [a, n] of runs) {
613
+ const m = a === 0n ? 0 : Math.min(Number((sent > c ? sent - c + a - 1n : 0n) / a), n);
614
+ if (m > 0) {
615
+ const full = through(sent, arrived, 0n, a);
616
+ [last, before] = [at + m - 1, paid + BigInt(m - 1) * full];
617
+ paid = before + through(sent, arrived, c + BigInt(m - 1) * a, a);
618
+ }
619
+ [c, at] = [c + a * BigInt(n), at + n];
229
620
  }
230
- return won;
621
+ if (last === place) return arrived > before ? arrived - before : 0n;
622
+ [c, at] = [0n, 0];
623
+ for (const [a, n] of runs) {
624
+ if (place < at + n) return through(sent, arrived, c + BigInt(place - at) * a, a);
625
+ [c, at] = [c + a * BigInt(n), at + n];
626
+ }
627
+ return 0n;
231
628
  }
232
629
 
233
- /** The places of a drawn round that have a winner and a prize, in order: what it gives before its money moves. */
234
- export const prizePlaces = (lottery) => [...Array(lotteryWinners(lottery)).keys()].filter((place) => lottery.places[place].prize != null);
630
+ /** A share in basis points of an amount, rounded down. */
631
+ const bpsOf = (amount, bps) => (BigInt(amount) * BigInt(bps)) / 10_000n;
235
632
 
236
- /** The places a contribution won whose prize is not given yet (`Lottery.given`): its next step gives them. */
237
- export function lotteryOwed(lottery, contribution) {
238
- if (lottery.status !== 'Drawn') return [];
239
- const given = Number(lottery.given ?? 0);
240
- const prizes = new Set(prizePlaces(lottery));
241
- return placesWon(lottery, contribution.first_ticket, contribution.end).filter((place) => prizes.has(place) && !(given & (1 << place)));
633
+ /** Whether place `place`'s part is paid: its pot part's bit (every place's first), or with `prize` its prize's (after
634
+ * every place's pot bit). */
635
+ export const placePaid = (r, place, prize) => {
636
+ const at = place + (prize ? placeCount(r.terms.places) : 0);
637
+ return Boolean((r.paid[at >> 3] ?? 0) & (1 << (at & 7)));
638
+ };
639
+
640
+ /** Entry `i`'s places in run `run` not paid yet of its pot part or (`prize`) its token prize, lowest first, each with
641
+ * what it takes: [{ place, amount }]. A pull pays all of them. */
642
+ function unpaidPlaces(r, i, run, prize) {
643
+ const e = r.entries[i];
644
+ const k = prize ? r.prizes.findIndex(p => Number(p.run) === run) : -1;
645
+ if (prize && k < 0) return [];
646
+ const runs =
647
+ k >= 0
648
+ ? [[BigInt(r.prizes[k].each), runPlaces(r, run)[1] - runPlaces(r, run)[0]]]
649
+ : r.terms.places.map((p, j) => [bpsOf(r.pot_total, p.bps), runPlaces(r, j)[1] - runPlaces(r, j)[0]]);
650
+ const [sent, arrived, from] =
651
+ k >= 0 ? [r.prizes[k].sent, r.prizes[k].arrived, runPlaces(r, run)[0]] : [r.pot_sent, r.pot_arrived, 0];
652
+ return placesIn(r, run, e.start, e.end)
653
+ .filter(p => !placePaid(r, p, prize))
654
+ .map(place => ({ place, amount: splitPart(runs, sent, arrived, place - from) }));
242
655
  }
243
656
 
244
- /** Whether every place's prize of a drawn round is given, so its money may move (`PrizesFirst`). */
245
- export const prizesGiven = (lottery) => prizePlaces(lottery).every((place) => Number(lottery.given ?? 0) & (1 << place));
657
+ /** What entry `i`'s places in run `run` take and are not paid yet, of its pot part or (`prize`) its token prize. */
658
+ export const placesOwed = (r, i, run, prize) => unpaidPlaces(r, i, run, prize).reduce((sum, p) => sum + p.amount, 0n);
659
+
660
+ /** How many of the round's owed parts a pull of `{ Pot: [run] }` or a token `{ Prize: [run] }` for entry `i` settles:
661
+ * its places there that take more than nothing (one pull pays them all). */
662
+ export const placesPulled = (r, i, run, prize) => unpaidPlaces(r, i, run, prize).filter(p => p.amount > 0n).length;
663
+
664
+ /** Whether run `run` of a round's places gives tokens (its prize is fixed at the ask, a `TokenPrize`). */
665
+ export const tokenRun = (r, run) => Boolean(r.terms.places[run]?.prize?.Tokens);
246
666
 
247
- /** The lowest place of a drawn round whose prize is not given yet, or null: prizes go in place order (`PlacesInOrder`). */
248
- export const nextPrizePlace = (lottery) => prizePlaces(lottery).find((place) => !(Number(lottery.given ?? 0) & (1 << place))) ?? null;
667
+ /** A place whose prize is an NFT of the lottery's own stash (`Held`, `Raffled`): assigned by the pulls, in place order. */
668
+ export const ownStash = place =>
669
+ Boolean(place.prize?.Held) || place.prize === 'Raffled' || place.prize?.Raffled !== undefined;
670
+
671
+ /** Entry `i`'s places in run `run` whose prize (not tokens) is not given yet, lowest first. */
672
+ export const prizePlacesOwed = (r, i, run) => {
673
+ const e = r.entries[i];
674
+ return placesIn(r, run, e.start, e.end).filter(p => !placePaid(r, p, true));
675
+ };
676
+
677
+ /** Where in `r.nfts` the NFT of `place` is (a place of a run that holds or draws one of the lottery's own stash). */
678
+ export function nftSlot(r, place) {
679
+ let [k, first] = [0, 0];
680
+ for (const p of r.terms.places) {
681
+ if (place < first + Number(p.times)) return k + place - first;
682
+ if (ownStash(p)) k += Number(p.times);
683
+ first += Number(p.times);
684
+ }
685
+ return k;
686
+ }
687
+
688
+ /** How many places of a round win an NFT of the lottery's own stash (every such place with a winner). */
689
+ export function stashPlacesWon(r) {
690
+ const w = roundWinners(r);
691
+ let [first, won] = [0, 0];
692
+ for (const p of r.terms.places) {
693
+ if (ownStash(p)) won += Math.max(0, Math.min(first + Number(p.times), w) - first);
694
+ first += Number(p.times);
695
+ }
696
+ return won;
697
+ }
698
+
699
+ /** Whether entry `i`'s places in run `run` (an NFT of the lottery's own stash) wait for their NFTs to be taken out
700
+ * (`LotteryRound::assign`): a pull with the stash alone does it, and `r.nfts` then names each. */
701
+ export function awaitsNfts(r, i, run) {
702
+ if (!ownStash(r.terms.places[run])) return false;
703
+ const places = prizePlacesOwed(r, i, run);
704
+ return places.some(p => nftSlot(r, p) >= Number(r.assigned));
705
+ }
249
706
 
250
707
  /**
251
- * What a contribution's next `finish_lottery_contribution` does, or null for nothing yet:
252
- * 'prizes' give the prize of the next place it won, one place a call (a drawn round gives every prize before any money moves);
253
- * 'refund' its tickets back to its buyer (a cancelled round, or a lottery that returns them);
254
- * 'finish' its money through the shares into the pot, its tickets burned, or its NFT ticket spent (only once every
255
- * prize of the round is given);
256
- * 'pay' its places' shares of the pot, once every contribution of the round is finished.
708
+ * The parts entry `i` of a round is owed now, in the order a keeper pulls them (`pullIx`'s `part`): its own ('Own': a
709
+ * refund, a no-loss ticket's price, its NFT ticket back or where its price sends it), and with `places`, its places'
710
+ * parts of the pot ({ Pot: [run] }) and prizes ({ Prize: [run] }). Parts owed in common (a share, the pot's, a pool's
711
+ * prize back) are no entry's: `commonOwed`.
257
712
  */
258
- export function lotteryStep(lottery, contribution) {
259
- // Its prize step comes when the lowest place still owed across the round is one it won; until then it waits.
260
- // In a lottery that returns its tickets, a winner's ticket goes back first, then its prizes.
261
- const owed = contribution.done || !lottery.returns ? lotteryOwed(lottery, contribution) : [];
262
- if (owed.length) return owed[0] === nextPrizePlace(lottery) ? 'prizes' : null;
263
- if (contribution.done) return Number(lottery.pending) === 0 ? 'pay' : null;
264
- if (lottery.status === 'Cancelled' || (lottery.status === 'Drawn' && lottery.returns)) return 'refund';
265
- if (lottery.status === 'Drawn') return prizesGiven(lottery) ? 'finish' : null;
266
- return null;
713
+ export function partsOwed(r, i, { places = true } = {}) {
714
+ const e = r.entries[i];
715
+ const parts = [];
716
+ const drawn = r.status === 'Drawn';
717
+ const back = r.status === 'Cancelled' || (r.terms.returns && drawn);
718
+ const nft = e.nft !== NO_ADDRESS;
719
+ if ((e.amount > 0n || nft) && (back || (nft && drawn)) && !(e.paid & OWN_PAID)) parts.push('Own');
720
+ if (!places || !drawn) return parts;
721
+ for (let run = 0; run < r.terms.places.length; run++) {
722
+ const prize = r.terms.places[run].prize;
723
+ if (placesOwed(r, i, run, false) > 0n) parts.push({ Pot: [run] });
724
+ if (tokenRun(r, run) ? placesOwed(r, i, run, true) > 0n : prize != null && prizePlacesOwed(r, i, run).length > 0)
725
+ parts.push({ Prize: [run] });
726
+ }
727
+ return parts;
267
728
  }
268
729
 
730
+ /**
731
+ * The parts of a round owed in common, of no entry (`{ Common: [i] }`): each share paid in common once drawn (one that
732
+ * burns is burned at the close), then what its places leave, owed to its pot or to `rest` (`i` = the shares' count), then after a
733
+ * cancel each pool's token prize back to its pool (past it).
734
+ */
735
+ export function commonOwed(r) {
736
+ const n = r.terms.shares.length;
737
+ const out = [];
738
+ if (r.status === 'Drawn')
739
+ r.terms.shares.forEach((s, i) => s.to !== 'Burn' && BigInt(r.common[i] ?? 0) > 0n && out.push({ Common: [i] }));
740
+ // What the places leave, to the pot or to `rest`: a burned rest stays for the close to burn, and is no part owed.
741
+ if (
742
+ (r.status === 'Drawn' || r.status === 'Cancelled') &&
743
+ BigInt(r.pot_owed) > 0n &&
744
+ leftoverTo(r.lottery, r.terms) !== 'Burn'
745
+ )
746
+ out.push({ Common: [n] });
747
+ if (r.status === 'Cancelled')
748
+ r.prizes.forEach((p, k) => !p.minted && BigInt(p.arrived) > 0n && out.push({ Common: [n + 1 + k] }));
749
+ return out;
750
+ }
751
+
752
+ /** The entry of a round holding `ticket`. */
753
+ export const entryOf = (r, ticket) => r.entries.findIndex(e => e.start <= BigInt(ticket) && BigInt(ticket) < e.end);
754
+
755
+ /** Whether a round is done and owes nothing: the next pull closes it, given its close's accounts. */
756
+ export const roundFinished = r => (r.status === 'Drawn' || r.status === 'Cancelled') && Number(r.owed) === 0;
757
+
758
+ /** Whether a lottery's place wins something: a part of what the round pays from, a prize, or both. */
759
+ export const placeSet = place => Number(place.bps) > 0 || place.prize != null;
760
+
761
+ /** The address of a lottery's current round's record (there or not). */
762
+ export const roundAddress = (lotteryAddress, lottery) => pda.round(lotteryAddress, lottery.incarnation, lottery.round);
763
+
764
+ /** Whether a lottery (decoded) drew once and has ended. */
765
+ export const lotteryEnded = lottery => Number(lottery.every) === 0 && Number(lottery.round) > 0;
766
+
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). */
770
+ export const idleRound = (lotteryAddress, lottery) =>
771
+ pda.round(lotteryAddress, lottery.incarnation, lotteryEnded(lottery) ? 0 : lottery.round);
772
+
269
773
  /**
270
774
  * Who rents a listed NFT at `now` (Unix seconds), from its decoded Listing (or null for none): the renter's wallet while
271
775
  * a rental runs, else null (a sale listing, or a rental that is over).
@@ -275,13 +779,19 @@ export function renterOf(listing, now) {
275
779
  return BigInt(now) < BigInt(listing.until) && listing.user !== SYSTEM_PROGRAM ? listing.user : null;
276
780
  }
277
781
 
782
+ /** Whether a schedule pays every run due when it is sent late (`schedule::catches_up`): only a sealed fixed amount. */
783
+ export const scheduleCatchesUp = schedule => Boolean(schedule.action?.from?.Vest && schedule.action?.amount?.Fixed);
784
+
278
785
  /**
279
- * How many runs of a schedule are due at `now` (Unix seconds, the chain's clock, never the computer's): each its own
280
- * `run_schedule`, several in one transaction to catch up; one too many fails the whole transaction. A sealed schedule
281
- * past its last run is due once whenever its vault holds something (`vaultBalance`), which goes where it always went.
786
+ * How many runs of a schedule are due at `now` (Unix seconds, the chain's clock, never the computer's). One `run_schedule`
787
+ * makes them all. A sealed schedule's fixed amount pays each of them (`scheduleCatchesUp`; up to its `upto`, the runs made
788
+ * after it: `schedule.runs` plus these, and a long backlog takes a few calls). Any other run pays once, for the latest
789
+ * run due, and those before it lapse. A sealed schedule past its last run is due once whenever its vault holds something
790
+ * (`vaultBalance`), which goes where it always went.
282
791
  */
283
792
  export function runsDue(schedule, now, vaultBalance = 0n) {
284
- const runs = Number(schedule.runs), max = Number(schedule.max_runs);
793
+ const runs = Number(schedule.runs),
794
+ max = Number(schedule.max_runs);
285
795
  if (max > 0 && runs >= max) return schedule.action?.from?.Vest && BigInt(vaultBalance) > 0n ? 1 : 0;
286
796
  if (!schedule.active || BigInt(now) < BigInt(schedule.next_at)) return 0;
287
797
  const due = Number((BigInt(now) - BigInt(schedule.next_at)) / BigInt(schedule.every)) + 1;
@@ -299,20 +809,27 @@ export function runsDue(schedule, now, vaultBalance = 0n) {
299
809
  */
300
810
  export function receipts(tx, err) {
301
811
  if (Array.isArray(tx)) {
302
- if (err === undefined) throw new Error("loot.receipts(logs, err) needs the transaction's meta.err (null when it succeeded): a failed transaction keeps its logs");
812
+ if (err === undefined)
813
+ throw new Error(
814
+ "loot.receipts(logs, err) needs the transaction's meta.err (null when it succeeded): a failed transaction keeps its logs",
815
+ );
303
816
  return err === null ? decodeEvents(IDL, tx) : [];
304
817
  }
305
818
  const meta = tx?.meta;
306
- if (!meta || meta.err === undefined) throw new Error('loot.receipts(tx) needs the transaction getTransaction returns, with its meta.err and meta.logMessages');
819
+ if (!meta || meta.err === undefined)
820
+ throw new Error(
821
+ 'loot.receipts(tx) needs the transaction getTransaction returns, with its meta.err and meta.logMessages',
822
+ );
307
823
  return meta.err === null ? decodeEvents(IDL, meta.logMessages ?? []) : [];
308
824
  }
309
825
 
310
826
  /** The program's errors by code: { code: { name, msg } }. */
311
- export const ERRORS = Object.freeze(Object.fromEntries(IDL.errors.map((e) => [e.code, { name: e.name, msg: e.msg }])));
827
+ export const ERRORS = Object.freeze(Object.fromEntries(IDL.errors.map(e => [e.code, { name: e.name, msg: e.msg }])));
312
828
 
313
829
  /**
314
830
  * The loot error in a failed transaction's logs ("custom program error: 0x…", or Anchor's "Error Code: …" line), or
315
- * null. Only loot's own: another program the transaction calls (Bubblegum, the VRF program, …) has codes of its own
831
+ * null: { code, name, msg }, `msg` the IDL's sentence (the program's own log names the error only: its text is
832
+ * compiled out). Only loot's own: another program the transaction calls (Bubblegum, the VRF program, …) has codes of its own
316
833
  * that may be the same numbers, so an error is loot's only when loot is the program that logged it or failed with it.
317
834
  * The first program that fails is where the error came from; the programs that called it fail after it with the same
318
835
  * code.
@@ -339,7 +856,12 @@ export function lootError(logs) {
339
856
  // A line that names no program (a log line, or an error's own text) is the running program's, when it is known.
340
857
  if (running.length && running.at(-1) !== LOOT_PROGRAM) continue;
341
858
  const named = /Error Code: (\w+)\. Error Number: (\d+)\. Error Message: (.+?)\.?$/.exec(line);
342
- if (named) return { code: Number(named[2]), name: named[1], msg: named[3] };
859
+ if (named)
860
+ return {
861
+ code: Number(named[2]),
862
+ name: named[1],
863
+ msg: ERRORS[Number(named[2])]?.name === named[1] ? ERRORS[Number(named[2])].msg : named[3],
864
+ };
343
865
  const m = /custom program error: 0x([0-9a-f]+)/i.exec(line);
344
866
  if (m && ERRORS[parseInt(m[1], 16)]) return { code: parseInt(m[1], 16), ...ERRORS[parseInt(m[1], 16)] };
345
867
  }