@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
@@ -11,7 +11,7 @@ const HEADER = 56;
11
11
  * the tree account's bytes.
12
12
  */
13
13
  export function proofLength(treeData) {
14
- const u32 = (at) => treeData[at] | (treeData[at + 1] << 8) | (treeData[at + 2] << 16) | (treeData[at + 3] * 2 ** 24);
14
+ const u32 = at => treeData[at] | (treeData[at + 1] << 8) | (treeData[at + 2] << 16) | (treeData[at + 3] * 2 ** 24);
15
15
  const [buffer, depth] = [u32(2), u32(6)];
16
16
  const canopyAt = HEADER + 24 + buffer * (32 * depth + 40) + 32 * depth + 40;
17
17
  const canopy = Math.max(0, treeData.length - canopyAt);
@@ -19,7 +19,8 @@ export function proofLength(treeData) {
19
19
  return Math.max(0, depth - Math.round(cached));
20
20
  }
21
21
 
22
- const bytes = (value) => (value == null ? null : typeof value === 'string' ? decodeBase58(value) : Uint8Array.from(value));
22
+ const bytes = value =>
23
+ value == null ? null : typeof value === 'string' ? decodeBase58(value) : Uint8Array.from(value);
23
24
 
24
25
  /**
25
26
  * A compressed NFT from an indexer's answers: `asset` (DAS `getAsset`), `proof` (DAS `getAssetProof`) and the tree
@@ -35,7 +36,7 @@ export function compressedFromDas({ asset, proof, treeData }) {
35
36
  nonce: BigInt(c.leaf_id),
36
37
  owner: asset.ownership.owner,
37
38
  delegate: asset.ownership.delegate ?? asset.ownership.owner,
38
- collection: asset.grouping?.find((g) => g.group_key === 'collection')?.group_value ?? null,
39
+ collection: asset.grouping?.find(g => g.group_key === 'collection')?.group_value ?? null,
39
40
  dataHash: bytes(c.data_hash),
40
41
  creatorHash: bytes(c.creator_hash),
41
42
  assetDataHash: bytes(c.asset_data_hash ?? null),
@@ -1,11 +1,12 @@
1
1
  // Grants: the bytes every voucher signer signs and `claim_grant` rebuilds, the approved list's Merkle tree, the
2
2
  // Ed25519 precompile instruction that carries a voucher's signature, and the claim itself. Browser-safe.
3
3
  //
4
- // voucherBytes "loot-grant-v4" | program (32) | grant (32) | incarnation (u64 LE) | wallet (32) | person_hash (32) | round (16) | expires (i64 LE) | amount (u64 LE)
4
+ // voucherBytes "loot-grant-v5" | sha256(program (32) | grant (32) | incarnation (u64 LE) | wallet (32) | person_hash (32) |
5
+ // expires (i64 LE) | count (u32 LE) | (round (16) | amount (u64 LE)) × count): a wallet's wins of several rounds
5
6
  // listLeaf sha256("loot-list-v3" | wallet (32) | person_hash (32) | round (16) | amount (u64 LE)): a wallet's win of one round
6
7
  //
7
- // `amount`: the tokens this claim gives, up to the grant's fixed amount (a rank's reward, a share); 0 for the grant's
8
- // prize as it is.
8
+ // `amount`: the tokens a round gives, up to the grant's fixed amount (a rank's reward, a share); 0 for the grant's
9
+ // prize as it is. A round pays a wallet once per grant incarnation, whoever signs it again, whenever.
9
10
  // keyLeaf sha256("loot-key-v1" | voucher key (32)): a key under a grant's key root (a promo code's)
10
11
  // merkleRoot a node = sha256(min(a, b) | max(a, b)) (byte-wise); an odd node at a level goes up unchanged
11
12
  // merkleProof the siblings from the leaf up (a promoted level adds none)
@@ -15,22 +16,26 @@
15
16
  // the tests (js/test/client.test.mjs).
16
17
  import { addressBytes } from '../chain/base58.js';
17
18
  import { compareBytes, concat, fromHex, intLE, readUintLE, toBigInt, uintLE, utf8 } from '../chain/bytes.js';
19
+ import { meta } from '../chain/programs.js';
18
20
  import { sha256 } from '../chain/sha256.js';
19
- import { ASSOCIATED_TOKEN_PROGRAM, gateAccounts, lootIx, prizeMetas, prizeNonce } from './ix.js';
21
+ import { recordKey16 } from './accounts.js';
22
+ import { ASSOCIATED_TOKEN_PROGRAM, gateAccounts, lootIx, prizeMetas, prizeNonce, tailOf } from './ix.js';
20
23
  import { LOOT_PROGRAM, pda } from './pda.js';
21
24
  import IDL from './idl/loot.js';
22
25
 
23
- export const VOUCHER_DOMAIN = 'loot-grant-v4';
26
+ export const VOUCHER_DOMAIN = 'loot-grant-v5';
24
27
  export const LIST_DOMAIN = 'loot-list-v3';
25
28
  export const KEY_DOMAIN = 'loot-key-v1';
26
- /** 13 + 32 × 4 + 8 + 16 + 8. */
27
- export const VOUCHER_BYTES = 181;
29
+ /** What a voucher key signs: the prefix (13 bytes), then the sha256 of the voucher's body. */
30
+ export const VOUCHER_BYTES = 45;
31
+ /** A grant claim's compute limit: its rounds' records and prizes can pass the 200,000 a transaction has by default. */
32
+ export const CLAIM_COMPUTE_UNITS = 400_000;
28
33
  export const ZERO_HASH = new Uint8Array(32);
29
34
  export const ED25519_PROGRAM = 'Ed25519SigVerify111111111111111111111111111';
30
- /** The seconds of the day a grant's daily limit counts claims in (the IDL's `DAY_SECONDS`). */
31
- const DAY_SECONDS = Number(IDL.constants.find((c) => c.name === 'DAY_SECONDS').value);
32
- /** The day the program counts `per_day` in. */
33
- export const dayOf = (unixSeconds) => Math.floor(Number(unixSeconds) / DAY_SECONDS);
35
+ /** The seconds of a UTC day (the IDL's `DAY_SECONDS`): a limit's window of a day (`PerWallet.window`). */
36
+ const DAY_SECONDS = Number(IDL.constants.find(c => c.name === 'DAY_SECONDS').value);
37
+ /** The day a window of `DAY_SECONDS` counts in, from the unix epoch. */
38
+ export const dayOf = unixSeconds => Math.floor(Number(unixSeconds) / DAY_SECONDS);
34
39
 
35
40
  /** 32 bytes from a base58 address, 32 bytes or 64 hex characters. */
36
41
  function bytes32(value, what) {
@@ -43,41 +48,88 @@ function bytes32(value, what) {
43
48
  throw new TypeError(`${what} must be an address or 32 bytes`);
44
49
  }
45
50
 
46
- const personBytes = (value) => (value === null || value === undefined || value === '' ? ZERO_HASH : bytes32(value, 'person_hash'));
51
+ const personBytes = value =>
52
+ value === null || value === undefined || value === '' ? ZERO_HASH : bytes32(value, 'person_hash');
47
53
 
48
54
  function roundBytes(value) {
49
- const out = value instanceof Uint8Array ? value : typeof value === 'string' && /^[0-9a-f]{32}$/i.test(value) ? fromHex(value) : null;
55
+ const out =
56
+ value instanceof Uint8Array
57
+ ? value
58
+ : typeof value === 'string' && /^[0-9a-f]{32}$/i.test(value)
59
+ ? fromHex(value)
60
+ : null;
50
61
  if (!out || out.length !== 16) throw new RangeError('round must be 16 bytes (32 hex characters)');
51
62
  return out;
52
63
  }
53
64
 
54
65
  /** An i64 little-endian (two's complement), as the program reads `expires`. */
55
- export const i64LE = (value) => intLE(value, 8);
66
+ export const i64LE = value => intLE(value, 8);
67
+
68
+ /** A claim's rounds: [{ id (16 bytes or 32 hex characters), amount }], or one `round` and its `amount`. */
69
+ const roundsOf = (rounds, round, amount) =>
70
+ (rounds ?? [{ id: round, amount }]).map(r => ({
71
+ id: roundBytes(r.id ?? r.round ?? new Uint8Array(16)),
72
+ amount: toBigInt(r.amount ?? 0),
73
+ }));
74
+
75
+ /** A voucher's body, which its key signs the sha256 of (`voucherBytes`). */
76
+ export function voucherBody({
77
+ program = LOOT_PROGRAM,
78
+ grant,
79
+ incarnation,
80
+ wallet,
81
+ personHash,
82
+ expires,
83
+ rounds,
84
+ round,
85
+ amount = 0n,
86
+ }) {
87
+ const list = roundsOf(rounds, round, amount);
88
+ return concat(
89
+ bytes32(program, 'program'),
90
+ bytes32(grant, 'grant'),
91
+ uintLE(incarnation, 8),
92
+ bytes32(wallet, 'wallet'),
93
+ personBytes(personHash),
94
+ i64LE(expires),
95
+ uintLE(list.length, 4),
96
+ ...list.flatMap(r => [r.id, uintLE(r.amount, 8)]),
97
+ );
98
+ }
56
99
 
57
100
  /**
58
- * The 181 bytes a voucher key signs and `claim_grant` rebuilds from its own accounts and arguments. `round` names the
59
- * win (the project round, draw or giveaway it was won in): the program pays a wallet each round once per grant incarnation.
101
+ * The 45 bytes a voucher key signs and `claim_grant` rebuilds from its own accounts and arguments: the prefix, then the
102
+ * sha256 of the voucher's body (`voucherBody`), so a voucher of many rounds is as short to check as one of one. `rounds`
103
+ * ([{ id, amount }], or one `round` and its `amount`) names the wins (the project round, draw or giveaway each was won
104
+ * in): the program pays a wallet each round once per grant incarnation.
60
105
  */
61
- export function voucherBytes({ program = LOOT_PROGRAM, grant, incarnation, wallet, personHash, round, expires, amount = 0n }) {
62
- const out = concat(utf8(VOUCHER_DOMAIN), bytes32(program, 'program'), bytes32(grant, 'grant'), uintLE(incarnation, 8), bytes32(wallet, 'wallet'), personBytes(personHash), roundBytes(round), i64LE(expires), uintLE(amount, 8));
63
- if (out.length !== VOUCHER_BYTES) throw new Error(`a voucher is ${VOUCHER_BYTES} bytes, not ${out.length}`);
64
- return out;
65
- }
106
+ export const voucherBytes = fields => concat(utf8(VOUCHER_DOMAIN), sha256(voucherBody(fields)));
66
107
 
67
- /** The fields back from voucher bytes (addresses as bytes). */
68
- export function readVoucher(bytes) {
69
- if (!(bytes instanceof Uint8Array) || bytes.length !== VOUCHER_BYTES) throw new RangeError(`a voucher is ${VOUCHER_BYTES} bytes`);
70
- const n = VOUCHER_DOMAIN.length;
71
- if (new TextDecoder().decode(bytes.subarray(0, n)) !== VOUCHER_DOMAIN) throw new Error(`not a ${VOUCHER_DOMAIN} voucher`);
72
- const at = (i, len) => bytes.slice(i, i + len);
73
- let expires = 0n;
74
- for (let i = 7; i >= 0; i--) expires = (expires << 8n) | BigInt(bytes[n + 152 + i]);
108
+ /** The fields back from a voucher's body (`voucherBody`; addresses as bytes, rounds as [{ id, amount }]). */
109
+ export function readVoucher(body) {
110
+ if (!(body instanceof Uint8Array) || body.length < 148) throw new RangeError('a voucher body is at least 148 bytes');
111
+ const at = (i, len) => body.slice(i, i + len);
112
+ let expires = readUintLE(body, 136, 8);
75
113
  if (expires >= 1n << 63n) expires -= 1n << 64n;
76
- return { program: at(n, 32), grant: at(n + 32, 32), incarnation: readUintLE(bytes, n + 64, 8), wallet: at(n + 72, 32), personHash: at(n + 104, 32), round: at(n + 136, 16), expires, amount: readUintLE(bytes, n + 160, 8) };
114
+ const count = Number(readUintLE(body, 144, 4));
115
+ if (body.length !== 148 + 24 * count) throw new RangeError('a voucher body holds 24 bytes a round');
116
+ const rounds = Array.from({ length: count }, (_, i) => ({
117
+ id: at(148 + 24 * i, 16),
118
+ amount: readUintLE(body, 164 + 24 * i, 8),
119
+ }));
120
+ return {
121
+ program: at(0, 32),
122
+ grant: at(32, 32),
123
+ incarnation: readUintLE(body, 64, 8),
124
+ wallet: at(72, 32),
125
+ personHash: at(104, 32),
126
+ expires,
127
+ rounds,
128
+ };
77
129
  }
78
130
 
79
131
  /** A voucher key's leaf under a grant's `key_root`. */
80
- export const keyLeaf = (key) => sha256(concat(utf8(KEY_DOMAIN), bytes32(key, 'key')));
132
+ export const keyLeaf = key => sha256(concat(utf8(KEY_DOMAIN), bytes32(key, 'key')));
81
133
 
82
134
  /** A list entry's round as the leaf names it: a number (a spec's `round`) in the first 8 of 16 bytes; 0 is zeros. */
83
135
  export const listRound = (n = 0) => {
@@ -91,31 +143,41 @@ export const listRound = (n = 0) => {
91
143
  * paid once, so a list can name the winners of many rounds, and a wallet's earlier wins stay claimable when a new
92
144
  * list adds later ones.
93
145
  */
94
- export const listLeaf = (wallet, personHash, round, amount = 0n) => sha256(concat(utf8(LIST_DOMAIN), bytes32(wallet, 'wallet'), personBytes(personHash), roundBytes(round ?? new Uint8Array(16)), uintLE(amount, 8)));
146
+ export const listLeaf = (wallet, personHash, round, amount = 0n) =>
147
+ sha256(
148
+ concat(
149
+ utf8(LIST_DOMAIN),
150
+ bytes32(wallet, 'wallet'),
151
+ personBytes(personHash),
152
+ roundBytes(round ?? new Uint8Array(16)),
153
+ uintLE(amount, 8),
154
+ ),
155
+ );
95
156
 
96
157
  /** The prefix of a wallet's leaf in a list gate (`gateListLeaf`). */
97
158
  export const GATE_LIST_DOMAIN = 'loot-list-v1';
98
159
  /** A wallet's leaf in a list gate (`Gate::List`): sha256("loot-list-v1" | wallet). Its tree is a list's
99
160
  * (`merkleRoot`, `merkleProof`), and its proof's nodes go where an NFT shown would (`gateAccounts`'s `listProof`). */
100
- export const gateListLeaf = (wallet) => sha256(concat(utf8(GATE_LIST_DOMAIN), bytes32(wallet, 'wallet')));
161
+ export const gateListLeaf = wallet => sha256(concat(utf8(GATE_LIST_DOMAIN), bytes32(wallet, 'wallet')));
101
162
 
102
163
  /** The parent of two nodes: sha256 of the smaller then the larger. */
103
164
  export const parentOf = (a, b) => (compareBytes(a, b) <= 0 ? sha256(concat(a, b)) : sha256(concat(b, a)));
104
165
 
105
166
  function levelsOf(leaves) {
106
167
  if (!leaves.length) throw new RangeError('a list needs at least one wallet');
107
- const levels = [leaves.map((l) => bytes32(l, 'leaf'))];
168
+ const levels = [leaves.map(l => bytes32(l, 'leaf'))];
108
169
  while (levels.at(-1).length > 1) {
109
170
  const level = levels.at(-1);
110
171
  const next = [];
111
- for (let i = 0; i < level.length; i += 2) next.push(i + 1 < level.length ? parentOf(level[i], level[i + 1]) : level[i]);
172
+ for (let i = 0; i < level.length; i += 2)
173
+ next.push(i + 1 < level.length ? parentOf(level[i], level[i + 1]) : level[i]);
112
174
  levels.push(next);
113
175
  }
114
176
  return levels;
115
177
  }
116
178
 
117
179
  /** The root of the leaves, in the order given. Whoever builds the list keeps that order. */
118
- export const merkleRoot = (leaves) => levelsOf(leaves).at(-1)[0];
180
+ export const merkleRoot = leaves => levelsOf(leaves).at(-1)[0];
119
181
 
120
182
  /** The siblings of leaf `index`, from the bottom up. */
121
183
  export function merkleProof(leaves, index) {
@@ -138,7 +200,7 @@ export function merkleProof(leaves, index) {
138
200
  */
139
201
  export function gateList(wallets) {
140
202
  if (!wallets.length) throw new RangeError('a list needs at least one wallet');
141
- const leaves = wallets.map((w) => gateListLeaf(w));
203
+ const leaves = wallets.map(w => gateListLeaf(w));
142
204
  let size = 1;
143
205
  while (size < leaves.length) size *= 2;
144
206
  // Filled with a leaf no wallet has (all zeros): a wallet's leaf is a hash, so nobody can prove a filler.
@@ -146,7 +208,7 @@ export function gateList(wallets) {
146
208
  return {
147
209
  root: merkleRoot(leaves),
148
210
  depth: Math.log2(size),
149
- proofOf: (wallet) => {
211
+ proofOf: wallet => {
150
212
  const at = wallets.indexOf(wallet);
151
213
  if (at < 0) throw new RangeError(`${wallet} is not on the list`);
152
214
  return merkleProof(leaves, at);
@@ -172,69 +234,191 @@ export function ed25519Ix({ publicKey, signature, message }) {
172
234
  const keyAt = 16;
173
235
  const signatureAt = keyAt + 32;
174
236
  const messageAt = signatureAt + 64;
175
- const u16 = (n) => uintLE(n, 2);
237
+ const u16 = n => uintLE(n, 2);
176
238
  const THIS = 0xffff;
177
- const data = concat(Uint8Array.of(1, 0), u16(signatureAt), u16(THIS), u16(keyAt), u16(THIS), u16(messageAt), u16(message.length), u16(THIS), key, signature, message);
239
+ const data = concat(
240
+ Uint8Array.of(1, 0),
241
+ u16(signatureAt),
242
+ u16(THIS),
243
+ u16(keyAt),
244
+ u16(THIS),
245
+ u16(messageAt),
246
+ u16(message.length),
247
+ u16(THIS),
248
+ key,
249
+ signature,
250
+ message,
251
+ );
178
252
  return { programId: ED25519_PROGRAM, keys: [], data };
179
253
  }
180
254
 
255
+ /** The kinds of a grant's limit records (`limitKey`). */
256
+ const KINDS = Object.freeze({ wallet: 'wallet', person: 'person', badge: 'badge', code: 'code' });
257
+
258
+ /** The key of a grant's limit record of `kind` ('wallet', 'person', 'badge', 'code') for `subject` (an address or 32
259
+ * bytes): sha256(kind | subject)[..16]. */
260
+ export const limitKey = (kind, subject) => recordKey16(utf8(KINDS[kind] ?? kind), bytes32(subject, 'subject'));
261
+
262
+ /** The key of a wallet's record of a round a grant paid it: sha256(round | wallet)[..16]. */
263
+ export const roundKey = (round, wallet) => recordKey16(roundBytes(round), bytes32(wallet, 'wallet'));
264
+
265
+ /** The shard a record's key falls in, of a grant's `2^bits`: the top `bits` bits of its first byte. */
266
+ export const shardOf = (key, bits) => (Number(bits) ? key[0] >> (8 - Number(bits)) : 0);
267
+
268
+ /** The rounds shard a wallet's paid rounds are kept in: its own limit record's. */
269
+ export const walletShard = (grant, wallet) => shardOf(limitKey('wallet', wallet), grant.shard_bits);
270
+
271
+ /** Whether a grant paid `wallet` for `round`, from the wallet's decoded rounds shard (`decodeList`, null for none). */
272
+ export const roundPaid = (list, wallet, round) =>
273
+ Boolean(list?.records.some(r => compareBytes(r, roundKey(round, wallet)) === 0));
274
+
275
+ /**
276
+ * The fewest shard bits (0 to 8) that keep each of a grant's lists under ~100,000 records over its life: `wallets` ×
277
+ * the rounds each may claim (`rounds`). A 10,000-wallet airdrop takes 0 (one shard).
278
+ */
279
+ export function shardBitsFor(wallets, rounds = 1) {
280
+ const records = Number(wallets) * Math.max(1, Number(rounds));
281
+ let bits = 0;
282
+ while (bits < 8 && records / 2 ** bits > 100_000) bits += 1;
283
+ return bits;
284
+ }
285
+
181
286
  /**
182
287
  * The claim of grant `id` for `wallet`, sent and signed by `signer` (anyone: the play host, the owner's server, the
183
- * player). The grant's funds pay its rent through the signer; what they can't cover, the signer pays. The signer pays
184
- * the player fee with its own SOL. `grant` is the decoded Grant; `count` the wallet's receipt count before this claim (0 for a
185
- * first claim); `poolMint` the mint of a Payout's pool, read from the pool.
186
- * A voucher claim goes right after `ed25519Ix` in the same transaction, `voucherKey` the key that signed it (its
187
- * counter is passed when the grant limits keys; a key under the key root carries its `proof`); a list claim carries
188
- * its `proof`; an open grant (no keys, no key root, no list) needs neither. What passes its gate (`shown`, or
189
- * `listProof` for a list gate) comes first. Its prize (`grant.gives`, `prizeMetas`)
190
- * follows: `poolMint` for tokens from a pool, `item` (the decoded Item) for a collectible or a compressed NFT of one, `nft` for an NFT of a stash;
191
- * a roll's order at `nonce` (`prizeNonce(grant, round, count)` by default), then the rolled box's prize mints
192
- * (`rollMints`: `boxes` maps the box's id to its decoded LootBox, null for one never written; `rollPools` each pool its
193
- * prizes name to its mint, null for one never opened). The grant's funds pay what it opens.
288
+ * player). The grant's funds pay its records through the signer; what they can't cover, the signer pays. The signer pays
289
+ * the player fee, and a roll's order, with its own SOL. `grant` is the decoded Grant.
290
+ *
291
+ * A voucher claim goes right after `ed25519Ix` in the same transaction, `voucherKey` the key that signed it (a key under
292
+ * the key root carries its proof as `proofs[0]`); a list claim carries a proof for each round (`proofs`); an open grant
293
+ * (no keys, no key root, no list) names no round and pays one prize (`count`: the wallet's claims before it, for its
294
+ * roll's nonce). `rounds` ([{ id, amount }], or one `round` and its `amount`) are the wins it names: each paid once, so
295
+ * one already paid is skipped (`paidRounds`: the wallet's decoded rounds shard, `decodeList`, to leave out its prize's
296
+ * accounts). What passes its gate (`shown`, or `listProof` for a list gate) comes first, then each shard of the grant's
297
+ * claims it counts in, then each round's prize (`grant.gives`, `prizeMetas`): `poolMint` for tokens from a pool, `item`
298
+ * (the decoded Item) for a collectible or a compressed NFT of one, `nft` for an NFT of a stash; round `i`'s roll at
299
+ * `prizeRollNonce(nonce + i)` (`nonce`: `prizeNonce` of the grant, the wallet and its rounds by default), its box and
300
+ * order only (`askRollIx` then asks for it, in the same transaction when it fits). Send it with a compute limit of
301
+ * `CLAIM_COMPUTE_UNITS`.
194
302
  */
195
- export function claimGrantIx({ project, id, grant, wallet, signer, count = 0, round, expires, personHash, amount = 0n, proof = [], poolMint, item, nft, boxes = {}, rollPools = {}, shown, listProof, programs, voucherKey, nonce, extra = [] }) {
303
+ export function claimGrantIx({
304
+ project,
305
+ id,
306
+ grant,
307
+ wallet,
308
+ signer,
309
+ count = 0,
310
+ rounds,
311
+ round,
312
+ expires,
313
+ personHash,
314
+ amount = 0n,
315
+ proofs,
316
+ proof = [],
317
+ paidRounds = null,
318
+ poolMint,
319
+ item,
320
+ nft,
321
+ shown,
322
+ listProof,
323
+ programs,
324
+ voucherKey,
325
+ nonce,
326
+ extra = [],
327
+ }) {
196
328
  if (!signer) throw new Error('A claim needs its signer: whoever sends it');
197
329
  // Every claim is its winning wallet's own: the wallet signs it, whatever proves it (a voucher, a list, an open
198
330
  // grant), so nobody claims for a player (WalletMustSign). `signer` pays the fees and what the claim opens: the
199
331
  // wallet itself, or anyone who pays for it.
200
332
  const address = pda.grant(project, id);
201
333
  const person = personBytes(personHash);
202
- const hasPerson = person.some((b) => b !== 0);
203
- // A person a voucher names is counted when the grant limits persons; an open grant gated by an NFT
204
- // counts the NFT shown, at a badge's address (a grant never does both; both go in `person_receipt`).
205
- const badge = typeof shown === 'string' ? shown : shown?.asset ?? shown?.mint ?? shown?.compressed;
206
- const counted = hasPerson && Number(grant.limits?.per_person ?? 0) > 0
207
- ? pda.person(address, grant.incarnation, person)
208
- : grant.open && grant.gate?.Holds && badge ? pda.badge(address, grant.incarnation, badge) : null;
209
- const accounts = {
210
- project,
211
- grant: address,
212
- signer,
213
- wallet,
214
- wallet_receipt: pda.grantReceipt(address, grant.incarnation, wallet),
215
- person_receipt: counted,
216
- code_receipt: voucherKey && Number(grant.limits?.per_key ?? 0) ? pda.code(address, grant.incarnation, voucherKey) : null,
217
- token_program: undefined,
218
- token_2022_program: undefined,
219
- associated_token_program: ASSOCIATED_TOKEN_PROGRAM,
220
- };
221
- const roundAt = roundBytes(round ?? new Uint8Array(16));
222
- nonce = toBigInt(nonce ?? prizeNonce(address, roundAt, toBigInt(count)));
223
- // The prize's accounts, after an NFT the wallet shows: `poolMint` a pool's mint, `item` the decoded Item of a
224
- // collectible (its next serial), `nft` an NFT's kind as `piece` takes it.
334
+ const listed = Uint8Array.from(grant.list_root ?? []).some(b => b !== 0);
335
+ // A claim by voucher or list names its rounds; an open one names none.
336
+ const named = Boolean(voucherKey) || listed || rounds != null || round != null;
337
+ const list = named ? roundsOf(rounds, round ?? new Uint8Array(16), amount) : [];
338
+ // The limit records it counts in: the wallet's, a person's a voucher names, the NFT an open claim of an open grant
339
+ // gated by one shows (a badge), a voucher key's; each shard of the grant's claims once, by shard number.
340
+ const bits = Number(grant.shard_bits ?? 0);
341
+ const keys = [limitKey('wallet', wallet)];
342
+ if (person.some(b => b !== 0) && Number(grant.limits?.per_person ?? 0) > 0) keys.push(limitKey('person', person));
343
+ const badge = typeof shown === 'string' ? shown : (shown?.asset ?? shown?.mint ?? shown?.compressed);
344
+ if (!named && grant.open && grant.gate?.Holds && badge) keys.push(limitKey('badge', badge));
345
+ if (voucherKey && Number(grant.limits?.per_key ?? 0) > 0) keys.push(limitKey('code', voucherKey));
346
+ const shards = [...new Set(keys.map(k => shardOf(k, bits)))]
347
+ .sort((x, y) => x - y)
348
+ .map(shard => meta(pda.claims(address, grant.incarnation, shard), false, true));
349
+ nonce = toBigInt(
350
+ nonce ??
351
+ (named ? prizeNonce(address, wallet, ...list.map(r => r.id)) : prizeNonce(address, wallet, toBigInt(count))),
352
+ );
353
+ // Each round not paid yet gives the grant's prize, or the amount of its tokens the round names, in order; collectibles
354
+ // of one item each at its next serial.
225
355
  const gives = grant.gives;
226
356
  const pool = gives.Tokens?.from.Pool?.[0];
227
357
  const itemId = gives.Item?.[0] ?? gives.Compressed?.item;
228
- if (pool !== undefined && !poolMint) throw new Error('A payout claim needs its pool’s mint (poolMint), read from the pool');
229
- const prize = prizeMetas({
230
- project, prize: gives, winner: wallet, nonce, programs,
231
- poolMints: pool !== undefined ? { [pool]: poolMint } : {}, items: itemId !== undefined ? { [itemId]: item } : {}, nft,
232
- strict: true, boxes, rollPools,
358
+ if (pool !== undefined && !poolMint)
359
+ throw new Error('A payout claim needs its pool’s mint (poolMint), read from the pool');
360
+ const seen = new Set();
361
+ const owed = (named ? list.map((r, i) => ({ ...r, i })) : [{ amount: 0n, i: 0 }]).filter(r => {
362
+ if (!named) return true;
363
+ const hex = Array.from(r.id, b => b.toString(16).padStart(2, '0')).join('');
364
+ if (seen.has(hex) || roundPaid(paidRounds, wallet, r.id)) return false;
365
+ seen.add(hex);
366
+ return true;
233
367
  });
368
+ const prizes = owed.flatMap((r, made) =>
369
+ prizeMetas({
370
+ project,
371
+ prize: r.amount > 0n && gives.Tokens ? { Tokens: { ...gives.Tokens, amount: r.amount } } : gives,
372
+ winner: wallet,
373
+ nonce: BigInt.asUintN(64, nonce + BigInt(r.i)),
374
+ programs,
375
+ poolMints: pool !== undefined ? { [pool]: poolMint } : {},
376
+ items: itemId !== undefined ? { [itemId]: item } : {},
377
+ nft,
378
+ made,
379
+ }),
380
+ );
381
+ proofs ??= listed && !voucherKey ? [proof] : proof.length ? [proof] : [];
234
382
  return lootIx(
235
383
  'claim_grant',
236
- { round: roundAt, expires: toBigInt(expires ?? 0), person_hash: person, amount: toBigInt(amount), proof: proof.map((p) => bytes32(p, 'proof')), nonce },
237
- accounts,
238
- { remaining: [...gateAccounts(project, { gate: grant.gate, wallet, shown, listProof }), ...prize, ...extra], signer: ['wallet'] },
384
+ {
385
+ rounds: list,
386
+ expires: toBigInt(expires ?? 0),
387
+ person_hash: person,
388
+ proofs: proofs.map(p => p.map(x => bytes32(x, 'proof'))),
389
+ nonce,
390
+ },
391
+ {
392
+ project,
393
+ grant: address,
394
+ signer,
395
+ wallet,
396
+ rounds: named ? pda.rounds(address, grant.incarnation, shardOf(keys[0], bits)) : null,
397
+ token_program: undefined,
398
+ token_2022_program: undefined,
399
+ associated_token_program: ASSOCIATED_TOKEN_PROGRAM,
400
+ },
401
+ {
402
+ remaining: [
403
+ ...gateAccounts(project, { gate: grant.gate, wallet, shown, listProof }),
404
+ ...shards,
405
+ ...prizes,
406
+ ...tailOf(extra),
407
+ ],
408
+ signer: ['wallet'],
409
+ },
239
410
  );
240
411
  }
412
+
413
+ /**
414
+ * Anyone closes shard `shard` of a grant's rounds (`rounds`) or claims once the grant is closed or made again
415
+ * (`incarnation` the one the shard is of): its rent goes to the project's `owner`.
416
+ */
417
+ export const closeShardIx = ({ project, id, incarnation, rounds = false, shard, owner }) => {
418
+ const grant = pda.grant(project, id);
419
+ return lootIx(
420
+ 'close_shard',
421
+ { id, incarnation: toBigInt(incarnation), rounds, shard },
422
+ { project, grant, shard: (rounds ? pda.rounds : pda.claims)(grant, incarnation, shard), owner },
423
+ );
424
+ };