@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
@@ -4,7 +4,7 @@
4
4
  // claim it. Addresses are written in full, so an owner sees a payee that an edit re-pointed before they sign.
5
5
  import { decodeBorsh } from '../chain/borsh.js';
6
6
  import { equalBytes, toHex } from '../chain/bytes.js';
7
- import { accountKind, decode, IDL, keyLeaf, merkleRoot } from '../client/index.js';
7
+ import { accountKind, decode, IDL, instructionOf, keyLeaf, merkleRoot } from '../client/index.js';
8
8
  import { LOOT_PROGRAM, pda } from '../client/pda.js';
9
9
  import { fromUnits } from '../economy/model.mjs';
10
10
  import { grantArgs } from '../economy/plan.mjs';
@@ -13,12 +13,12 @@ const NOBODY = '11111111111111111111111111111111';
13
13
  const ZERO = new Uint8Array(32);
14
14
 
15
15
  /** An instruction's accounts by name, nested groups flattened, in the order the program takes them. */
16
- const leafNames = (list) => list.flatMap((a) => (a.accounts ? leafNames(a.accounts) : [a.name]));
16
+ const leafNames = list => list.flatMap(a => (a.accounts ? leafNames(a.accounts) : [a.name]));
17
17
 
18
18
  /** A loot instruction read back: { name, args, accounts: { name: address } }, or null for another program's. */
19
19
  function readLootInstruction(ix) {
20
20
  if (ix.programId !== LOOT_PROGRAM) return null;
21
- const def = IDL.instructions.find((i) => i.discriminator.length <= ix.data.length && i.discriminator.every((b, j) => ix.data[j] === b));
21
+ const def = instructionOf(ix.data);
22
22
  if (!def) return null;
23
23
  const args = {};
24
24
  let at = def.discriminator.length;
@@ -37,9 +37,9 @@ export function namesOf(m, { owner = null, treasury = null } = {}) {
37
37
  m,
38
38
  owner,
39
39
  treasury,
40
- pools: new Map(m.pools.map((p) => [p.index, p.key])),
41
- stashes: new Map(m.stashes.map((s) => [s.id, s.name ?? s.key])),
42
- grants: new Map(m.grants.map((g) => [g.id, g])),
40
+ pools: new Map(m.pools.map(p => [p.index, p.key])),
41
+ stashes: new Map(m.stashes.map(s => [s.id, s.name ?? s.key])),
42
+ grants: new Map(m.grants.map(g => [g.id, g])),
43
43
  };
44
44
  }
45
45
 
@@ -57,7 +57,14 @@ export function stepDetails(instructions, names) {
57
57
  }
58
58
 
59
59
  /** The account each record an `upsert` writes is kept in on chain. */
60
- const RECORD_ACCOUNT = { Item: 'Item', Box: 'LootBox', Schedule: 'Schedule', Lottery: 'Lottery', Recipe: 'Recipe', Grant: 'Grant' };
60
+ const RECORD_ACCOUNT = {
61
+ Item: 'Item',
62
+ Box: 'LootBox',
63
+ Schedule: 'Schedule',
64
+ Lottery: 'Lottery',
65
+ Recipe: 'Recipe',
66
+ Grant: 'Grant',
67
+ };
61
68
 
62
69
  /**
63
70
  * What is live for one step, read from the chain (`chain.account`): the lines of the records its `upsert`s write, as
@@ -68,15 +75,19 @@ export async function liveStepDetails(instructions, names, chain) {
68
75
  const reads = [];
69
76
  for (const ix of instructions) {
70
77
  const read = readLootInstruction(ix);
71
- const [kind] = Object.keys(read?.name === 'upsert' ? read.args.def ?? {} : {});
78
+ const [kind] = Object.keys(read?.name === 'upsert' ? (read.args.def ?? {}) : {});
72
79
  if (!RECORD_ACCOUNT[kind]) continue;
73
80
  const account = await chain.account(read.accounts.target);
74
81
  if (!account) {
75
82
  reads.push(null);
76
83
  continue;
77
84
  }
78
- if (account.owner !== LOOT_PROGRAM || accountKind(account.data) !== RECORD_ACCOUNT[kind]) throw new Error(`${read.accounts.target} is not the ${kind.toLowerCase()} it should be.`);
79
- reads.push({ ...read, args: { def: { [kind]: { id: read.args.def[kind].id, args: decode(RECORD_ACCOUNT[kind], account.data) } } } });
85
+ if (account.owner !== LOOT_PROGRAM || accountKind(account.data) !== RECORD_ACCOUNT[kind])
86
+ throw new Error(`${read.accounts.target} is not the ${kind.toLowerCase()} it should be.`);
87
+ reads.push({
88
+ ...read,
89
+ args: { def: { [kind]: { id: read.args.def[kind].id, args: decode(RECORD_ACCOUNT[kind], account.data) } } },
90
+ });
80
91
  }
81
92
  return reads.length ? readDetails(reads.filter(Boolean), names) : null;
82
93
  }
@@ -86,26 +97,39 @@ function readDetails(reads, names) {
86
97
  const lines = [];
87
98
  for (const read of reads) {
88
99
  if (read?.unreadable) {
89
- lines.push(`This step could not be read back (${read.unreadable}): check what it pays in your wallet before approving`);
100
+ lines.push(
101
+ `This step could not be read back (${read.unreadable}): check what it pays in your wallet before approving`,
102
+ );
90
103
  continue;
91
104
  }
92
105
  if (!read) continue;
93
106
  const owner = read.accounts.owner ?? read.accounts.seller;
94
107
  const project = read.accounts.project;
95
- const pots = new Map(names.m.lotteries.map((l) => [pda.pot(pda.lottery(project, l.id)), l.name ?? l.key]));
108
+ const pots = new Map(
109
+ names.m.lotteries
110
+ .filter(l => Number.isInteger(l.pot))
111
+ .map(l => [pda.lotteryPot(pda.lottery(project, l.id), l.pot), l.name ?? l.key]),
112
+ );
113
+ const vaults = new Map(names.m.farms.map(f => [pda.rewards(pda.farm(project, f.id)), f.name ?? f.key]));
96
114
  // Before a wallet connects, the plan stands a made-up address in for it (one no spec may name): that one is not
97
115
  // shown. Once one is connected, only that wallet is called yours.
98
- const yours = (a) => (names.owner == null ? 'your wallet' : a === names.owner ? `your wallet (${a})` : `the project’s owner, ${a}`);
99
- const address = (a) => (a === owner ? yours(a) : a);
100
- const wallet = (a) => (a === owner ? yours(a) : `the wallet ${a}`);
116
+ const yours = a =>
117
+ names.owner == null ? 'your wallet' : a === names.owner ? `your wallet (${a})` : `the project’s owner, ${a}`;
118
+ const address = a => (a === owner ? yours(a) : a);
119
+ const wallet = a => (a === owner ? yours(a) : `the wallet ${a}`);
101
120
  const treasury = () => `the treasury (${names.treasury ? address(names.treasury) : 'your wallet'})`;
102
- const where = (to) => {
121
+ const where = to => {
103
122
  if (to === 'Treasury') return treasury();
104
123
  const [kind] = Object.keys(to);
105
124
  const v = to[kind];
106
125
  if (kind === 'Pool') return `the pool “${names.pools.get(v[0]) ?? v[0]}”`;
107
126
  if (kind === 'Wallet') return wallet(v[0]);
108
- if (kind === 'Account') return pots.has(v[0]) ? `the pot of “${pots.get(v[0])}”` : `the token account ${v[0]}`;
127
+ if (kind === 'Account')
128
+ return pots.has(v[0])
129
+ ? `the pot of “${pots.get(v[0])}”`
130
+ : vaults.has(v[0])
131
+ ? `the rewards of the farm “${vaults.get(v[0])}”`
132
+ : `the token account ${v[0]}`;
109
133
  if (kind === 'Holder') return `whoever holds the NFT ${v[0]}`;
110
134
  if (kind === 'Chosen') return 'a wallet the buyer chooses';
111
135
  return JSON.stringify(to);
@@ -117,34 +141,63 @@ function readDetails(reads, names) {
117
141
  }
118
142
  // Where what a split leaves goes: a lottery's pot, an auction's seller (or the treasury for a new item), else the treasury.
119
143
  const def = read.args.def ?? {};
120
- const restTo = def.Lottery ? 'its pot' : read.name === 'open_auction' ? (read.args.args?.lot?.Asset ? `the seller, ${address(read.accounts.seller)}` : treasury()) : treasury();
121
- const to = (v) => (v === 'Burn' ? 'burned' : `to ${where(v)}`);
122
- const isTo = (v) => v === 'Burn' || v === 'Treasury' || (v && typeof v === 'object' && ['Pool', 'Account', 'Holder', 'Wallet', 'Chosen'].some((k) => k in v) && Object.keys(v).length === 1);
144
+ const restTo = def.Lottery
145
+ ? def.Lottery.args?.terms?.pays_from?.Pot
146
+ ? 'its pot'
147
+ : 'the round’s places'
148
+ : read.name === 'open_auction'
149
+ ? read.args.args?.lot?.Asset
150
+ ? `the seller, ${address(read.accounts.seller)}`
151
+ : treasury()
152
+ : treasury();
153
+ const to = v => (v === 'Burn' ? 'burned' : `to ${where(v)}`);
154
+ const isTo = v =>
155
+ v === 'Burn' ||
156
+ v === 'Treasury' ||
157
+ (v &&
158
+ typeof v === 'object' &&
159
+ ['Pool', 'Account', 'Holder', 'Wallet', 'Chosen'].some(k => k in v) &&
160
+ Object.keys(v).length === 1);
123
161
  const grantId = read.args.def?.Grant?.id;
124
162
  if (read.args.def?.Grant) lines.push(...grantTerms(read.args.def.Grant.args, names));
125
- const walk = (value) => {
163
+ const walk = value => {
126
164
  if (Array.isArray(value)) return value.forEach(walk);
127
165
  if (!value || typeof value !== 'object' || value instanceof Uint8Array) return;
128
166
  for (const [k, v] of Object.entries(value)) {
129
167
  if (k === 'shares' && Array.isArray(v)) {
130
168
  const rest = 10_000 - v.reduce((n, s) => n + Number(s.bps), 0);
131
- const parts = v.map((s) => `${Number(s.bps) / 100}% ${to(s.to)}`);
169
+ const parts = v.map(s => `${Number(s.bps) / 100}% ${to(s.to)}`);
132
170
  if (rest > 0) parts.push(v.length ? `the rest (${rest / 100}%) to ${restTo}` : `all of it to ${restTo}`);
133
171
  lines.push(`Pays: ${parts.join(', ')}`);
134
- } else if (k === 'to' && isTo(v)) lines.push(v === 'Burn' ? 'Burns the tokens it takes' : `Sends tokens to ${where(v)}`);
135
- else if (k === 'Give' && Array.isArray(v) && typeof v[0] === 'string') lines.push(`Gives the NFT to ${address(v[0])}`);
172
+ } else if (k === 'pays_from' && v && typeof v === 'object') {
173
+ // Where a lottery's places are paid from: a pot kept from round to round, or each round's own money.
174
+ lines.push(
175
+ v.Pot
176
+ ? `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
+ : `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
+ );
179
+ } else if (k === 'to' && isTo(v))
180
+ lines.push(v === 'Burn' ? 'Burns the tokens it takes' : `Sends tokens to ${where(v)}`);
181
+ else if (k === 'Give' && Array.isArray(v) && typeof v[0] === 'string')
182
+ lines.push(`Gives the NFT to ${address(v[0])}`);
136
183
  else if (k === 'vouchers' && Array.isArray(v)) {
137
- const keys = v.filter((a) => a !== NOBODY);
184
+ const keys = v.filter(a => a !== NOBODY);
138
185
  if (keys.length) lines.push(`Voucher keys (each decides who gets it): ${keys.join(', ')}`);
139
186
  } else if (k === 'key_root' && v instanceof Uint8Array) {
140
187
  if (!equalBytes(v, ZERO)) {
141
188
  const listed = names.grants.get(grantId)?.keys ?? [];
142
- const ours = listed.length && equalBytes(merkleRoot(listed.map((key) => keyLeaf(key))), v);
143
- lines.push(ours ? `Voucher keys from its list (each decides who gets it): ${listed.join(', ')}` : `Voucher keys: a list with the root ${toHex(v)}, which is not the spec’s list`);
189
+ const ours = listed.length && equalBytes(merkleRoot(listed.map(key => keyLeaf(key))), v);
190
+ lines.push(
191
+ ours
192
+ ? `Voucher keys from its list (each decides who gets it): ${listed.join(', ')}`
193
+ : `Voucher keys: a list with the root ${toHex(v)}, which is not the spec’s list`,
194
+ );
144
195
  }
145
196
  } else if (k === 'list_root' && v instanceof Uint8Array) {
146
- if (!equalBytes(v, ZERO)) lines.push(...listLines(v, names.grants.get(grantId), read.args.def.Grant.args.gives, names));
147
- } else if (k === 'open' && v === true) lines.push('Open: any wallet may claim it, with no voucher and no list, within its limits');
197
+ if (!equalBytes(v, ZERO))
198
+ lines.push(...listLines(v, names.grants.get(grantId), read.args.def.Grant.args.gives, names));
199
+ } else if (k === 'open' && v === true)
200
+ lines.push('Open: any wallet may claim it, with no voucher and no list, within its limits');
148
201
  else walk(v);
149
202
  }
150
203
  };
@@ -157,36 +210,44 @@ function readDetails(reads, names) {
157
210
  function prizeToken(gives, m) {
158
211
  const from = gives?.Tokens?.from;
159
212
  if (!from) return null;
160
- const key = from.Pool ? (m.pools.find((p) => p.index === from.Pool[0])?.currency ?? m.currencyKey) : [...m.tokens.values()].find((t) => t.mint === (from.Mint ?? from.Vest)?.[0])?.key;
213
+ const key = from.Pool
214
+ ? (m.pools.find(p => p.index === from.Pool[0])?.currency ?? m.currencyKey)
215
+ : [...m.tokens.values()].find(t => t.mint === (from.Mint ?? from.Vest)?.[0])?.key;
161
216
  return m.tokens.get(key) ?? null;
162
217
  }
163
218
 
164
219
  /** Base units of `token` in its own words ("100 COIN"), or as base units when the token is not known. */
165
- const tokens = (units, token) => (token ? `${fromUnits(BigInt(units), token.decimals ?? 0)} ${token.symbol ?? token.key}` : `${units} base units`);
220
+ const tokens = (units, token) =>
221
+ token ? `${fromUnits(BigInt(units), token.decimals ?? 0)} ${token.symbol ?? token.key}` : `${units} base units`;
166
222
 
167
223
  /** A prize in words, from its bytes. Named by the spec's parts; an NFT or a tree is not a payee, so its address is left out. */
168
224
  function prizeWords(gives, m) {
169
225
  const [kind] = Object.keys(gives ?? {});
170
226
  const v = gives?.[kind];
171
- const named = (list, id, field = 'id') => list.find((x) => x[field] === id);
172
- const stash = (id) => `the stash “${named(m.stashes, id)?.name ?? named(m.stashes, id)?.key ?? id}”`;
173
- const item = (id) => `the item “${named(m.items, id)?.name ?? named(m.items, id)?.key ?? id}”`;
227
+ const named = (list, id, field = 'id') => list.find(x => x[field] === id);
228
+ const stash = id => `the stash “${named(m.stashes, id)?.name ?? named(m.stashes, id)?.key ?? id}”`;
229
+ const item = id => `the item “${named(m.items, id)?.name ?? named(m.items, id)?.key ?? id}”`;
174
230
  if (kind === 'Tokens') {
175
- const from = v.from.Pool ? `from the pool “${m.pools.find((p) => p.index === v.from.Pool[0])?.key ?? v.from.Pool[0]}”` : v.from.Mint ? `minted from ${v.from.Mint[0]}` : `from the vault ${v.from.Vest[0]}`;
176
- const amount = v.amount.Fixed ? tokens(v.amount.Fixed[0], prizeToken(gives, m)) : `${Number(v.amount.Bps[0]) / 100}% of what it holds`;
231
+ const from = v.from.Pool
232
+ ? `from the pool “${m.pools.find(p => p.index === v.from.Pool[0])?.key ?? v.from.Pool[0]}”`
233
+ : v.from.Mint
234
+ ? `minted from ${v.from.Mint[0]}`
235
+ : `from the vault ${v.from.Vest[0]}`;
236
+ const amount = tokens(v.amount, prizeToken(gives, m));
177
237
  return `${amount} ${from}`;
178
238
  }
179
239
  if (kind === 'Item') return item(v[0]);
180
240
  if (kind === 'Nft') return `an NFT of ${stash(v.stash)}`;
181
241
  if (kind === 'Stash') return `an NFT of ${stash(v[0])} at random`;
182
- if (kind === 'Roll') return `a roll of the box “${m.allBoxes.find((b) => b.id === v[0])?.name ?? m.allBoxes.find((b) => b.id === v[0])?.key ?? v[0]}”`;
242
+ if (kind === 'Roll')
243
+ return `a roll of the box “${m.allBoxes.find(b => b.id === v[0])?.name ?? m.allBoxes.find(b => b.id === v[0])?.key ?? v[0]}”`;
183
244
  if (kind === 'Compressed') return `a compressed NFT of ${item(v.item)}`;
184
245
  if (kind === 'Held') return 'an NFT the lottery holds';
185
246
  return 'nothing';
186
247
  }
187
248
 
188
- const times = (n) => `${n} claim${Number(n) === 1 ? '' : 's'}`;
189
- const when = (seconds) => new Date(Number(seconds) * 1000).toISOString().replace('.000Z', 'Z');
249
+ const times = n => `${n} claim${Number(n) === 1 ? '' : 's'}`;
250
+ const when = seconds => new Date(Number(seconds) * 1000).toISOString().replace('.000Z', 'Z');
190
251
 
191
252
  /** A grant's prize and its limits: what each claim takes from the project, and how many claims there can be. */
192
253
  function grantTerms(args, names) {
@@ -196,14 +257,19 @@ function grantTerms(args, names) {
196
257
  l.wallet?.per_window ? `each wallet ${times(l.wallet.per_window)} every ${l.wallet.window} seconds` : null,
197
258
  l.per_person ? `each person ${times(l.per_person)}` : null,
198
259
  l.per_key ? `each voucher key ${times(l.per_key)}` : null,
199
- l.per_day ? `${times(l.per_day)} a day` : null,
200
- l.total ? `${times(l.total)} in all` : null,
260
+ l.all?.per_window
261
+ ? `${times(l.all.per_window)} ${Number(l.all.window) === 86_400 ? 'a day' : `every ${l.all.window} seconds`}`
262
+ : null,
263
+ l.all?.total ? `${times(l.all.total)} in all` : null,
201
264
  BigInt(l.window?.opens_at ?? 0) ? `opens ${when(l.window.opens_at)}` : null,
202
265
  BigInt(l.window?.closes_at ?? 0) ? `closes ${when(l.window.closes_at)}` : null,
203
266
  ].filter(Boolean);
204
267
  // A voucher or a list entry may name an amount of its own, up to the grant's fixed amount (0 is the prize as it is).
205
- const upTo = !args.open && args.gives?.Tokens?.amount?.Fixed ? 'up to ' : '';
206
- return [`Each claim gets: ${upTo}${prizeWords(args.gives, names.m)}`, `Limits: ${limits.length ? limits.join(', ') : 'none'}`];
268
+ const upTo = !args.open && args.gives?.Tokens ? 'up to ' : '';
269
+ return [
270
+ `Each claim gets: ${upTo}${prizeWords(args.gives, names.m)}`,
271
+ `Limits: ${limits.length ? limits.join(', ') : 'none'}`,
272
+ ];
207
273
  }
208
274
 
209
275
  /**
@@ -212,13 +278,17 @@ function grantTerms(args, names) {
212
278
  */
213
279
  function listLines(root, g, gives, names) {
214
280
  const list = g?.list ?? [];
215
- if (!list.length || !equalBytes(grantArgs(g, names.m).list_root, root)) return [`Its list: a list with the root ${toHex(root)}, which is not the spec’s list`];
281
+ if (!list.length || !equalBytes(grantArgs(g, names.m).list_root, root))
282
+ return [`Its list: a list with the root ${toHex(root)}, which is not the spec’s list`];
216
283
  const token = prizeToken(gives, names.m);
217
284
  return [
218
285
  `Only wallets on its list may claim it (${list.length} wallet${list.length === 1 ? '' : 's'}, root ${toHex(root)})`,
219
- ...list.map((e) => {
286
+ ...list.map(e => {
220
287
  const entry = typeof e === 'string' ? { wallet: e } : e;
221
- const gets = entry.amount !== undefined ? `gets ${entry.amount} ${token?.symbol ?? token?.key ?? 'tokens'}` : 'gets the prize above';
288
+ const gets =
289
+ entry.amount !== undefined
290
+ ? `gets ${entry.amount} ${token?.symbol ?? token?.key ?? 'tokens'}`
291
+ : 'gets the prize above';
222
292
  return `On its list, ${entry.wallet}: ${gets}${entry.round ? `, in round ${entry.round}` : ''}`;
223
293
  }),
224
294
  ];
@@ -230,23 +300,23 @@ function listLines(root, g, gives, names) {
230
300
  * lacks is listed as no longer so.
231
301
  */
232
302
  export function compareDetails(now, was) {
233
- const topic = (line) => line.split(':')[0];
303
+ const topic = line => line.split(':')[0];
234
304
  const left = [...was];
235
- const out = now.map((line) => {
305
+ const out = now.map(line => {
236
306
  const same = left.indexOf(line);
237
307
  if (same >= 0) return left.splice(same, 1)[0];
238
- const i = left.findIndex((old) => topic(old) === topic(line));
308
+ const i = left.findIndex(old => topic(old) === topic(line));
239
309
  if (i < 0) return `${line} (new)`;
240
310
  const [old] = left.splice(i, 1);
241
311
  return `${line} (changed: it was ${old.slice(topic(old).length + 1).trim() || old})`;
242
312
  });
243
- return [...out, ...left.map((old) => `No longer: ${old}`)];
313
+ return [...out, ...left.map(old => `No longer: ${old}`)];
244
314
  }
245
315
 
246
316
  const ADDRESS = /\b[1-9A-HJ-NP-Za-km-z]{32,44}\b/g;
247
317
 
248
318
  /** Every address the lines name, in the order they name them. */
249
- const addressesIn = (lines) => [...new Set(lines.flatMap((l) => l.match(ADDRESS) ?? []))];
319
+ const addressesIn = lines => [...new Set(lines.flatMap(l => l.match(ADDRESS) ?? []))];
250
320
 
251
321
  /**
252
322
  * A warning for the page when the approval pays or trusts an address: on a change, those the live economy did not
@@ -254,7 +324,7 @@ const addressesIn = (lines) => [...new Set(lines.flatMap((l) => l.match(ADDRESS)
254
324
  */
255
325
  export function outsideAddresses(lines, { was = null, owner = null } = {}) {
256
326
  const known = new Set([...(was ? addressesIn(was) : []), ...(owner ? [owner] : [])]);
257
- const outside = addressesIn(lines).filter((a) => !known.has(a));
327
+ const outside = addressesIn(lines).filter(a => !known.has(a));
258
328
  if (!outside.length) return null;
259
329
  return was
260
330
  ? `This change pays or trusts ${outside.length === 1 ? 'an address' : 'addresses'} the live economy does not: ${outside.join(', ')}. Approve only if you meant ${outside.length === 1 ? 'it' : 'each one'} (the lines below say what each gets).`
@@ -28,7 +28,11 @@ export function freshKeypair() {
28
28
  export function verifySignature(address, message, signature) {
29
29
  if (!signature || signature.length !== 64) return false;
30
30
  try {
31
- const key = createPublicKey({ key: Buffer.concat([SPKI_ED25519, Buffer.from(addressBytes(address))]), format: 'der', type: 'spki' });
31
+ const key = createPublicKey({
32
+ key: Buffer.concat([SPKI_ED25519, Buffer.from(addressBytes(address))]),
33
+ format: 'der',
34
+ type: 'spki',
35
+ });
32
36
  return verify(null, Buffer.from(message), key, Buffer.from(signature));
33
37
  } catch {
34
38
  return false;
@@ -30,19 +30,33 @@ export async function checkLive({ live, m, wallet, chain }) {
30
30
  return t?.source === 'game' && (!t.mint || t.mint.startsWith('@'));
31
31
  });
32
32
  if (!made.project && !setupMints.length && !made.collection && !made.tree) return { owner: wallet, project: own };
33
- if (typeof chain.account !== 'function') throw new Error('The file says part of this economy is live already, and this connection cannot read the chain to check it: nothing was sent.');
34
- const read = (address) => chain.account(address);
33
+ if (typeof chain.account !== 'function')
34
+ throw new Error(
35
+ 'The file says part of this economy is live already, and this connection cannot read the chain to check it: nothing was sent.',
36
+ );
37
+ const read = address => chain.account(address);
35
38
 
36
39
  let owner = wallet;
37
40
  let project = own;
38
41
  if (made.project) {
39
42
  const account = await read(made.project);
40
- if (!account || account.owner !== LOOT_PROGRAM || accountKind(account.data) !== 'Project') throw new LiveMismatch(`The file names the project ${made.project}, and there is no loot project at that address.`);
43
+ if (!account || account.owner !== LOOT_PROGRAM || accountKind(account.data) !== 'Project')
44
+ throw new LiveMismatch(
45
+ `The file names the project ${made.project}, and there is no loot project at that address.`,
46
+ );
41
47
  const state = decode('Project', account.data);
42
- if (pda.project(state.owner, m.projectNumber) !== made.project) throw new LiveMismatch(`The file names the project ${made.project}, which is not project number ${m.projectNumber} of its owner ${state.owner}.`);
43
- if (state.owner !== wallet && state.authority !== wallet) throw new LiveMismatch(`The file names the project ${made.project}, which belongs to the wallet ${state.owner}, not to the connected wallet ${wallet}. Your own project number ${m.projectNumber} is at ${own}.`);
44
- if (made.treasury && made.treasury !== state.treasury) throw new LiveMismatch(`The file says the treasury is ${made.treasury}; on chain it is ${state.treasury}.`);
45
- if (made.authority && made.authority !== state.authority) throw new LiveMismatch(`The file says the authority is ${made.authority}; on chain it is ${state.authority}.`);
48
+ if (pda.project(state.owner, m.projectNumber) !== made.project)
49
+ throw new LiveMismatch(
50
+ `The file names the project ${made.project}, which is not project number ${m.projectNumber} of its owner ${state.owner}.`,
51
+ );
52
+ if (state.owner !== wallet && state.authority !== wallet)
53
+ throw new LiveMismatch(
54
+ `The file names the project ${made.project}, which belongs to the wallet ${state.owner}, not to the connected wallet ${wallet}. Your own project number ${m.projectNumber} is at ${own}.`,
55
+ );
56
+ if (made.treasury && made.treasury !== state.treasury)
57
+ throw new LiveMismatch(`The file says the treasury is ${made.treasury}; on chain it is ${state.treasury}.`);
58
+ if (made.authority && made.authority !== state.authority)
59
+ throw new LiveMismatch(`The file says the authority is ${made.authority}; on chain it is ${state.authority}.`);
46
60
  owner = state.owner;
47
61
  project = made.project;
48
62
  }
@@ -51,13 +65,25 @@ export async function checkLive({ live, m, wallet, chain }) {
51
65
  const t = m.tokens.get(key);
52
66
  const account = await read(address);
53
67
  const mint = account ? parseMint(address, account.owner, account.data) : null;
54
- if (!mint) throw new LiveMismatch(`The file says the setup made the token ${t.symbol ?? key} at ${address}, and there is no mint there.`);
55
- if (mint.decimals !== (t.decimals ?? 0)) throw new LiveMismatch(`The mint ${address} the file names for ${t.symbol ?? key} has ${mint.decimals} decimals, not ${t.decimals ?? 0}.`);
68
+ if (!mint)
69
+ throw new LiveMismatch(
70
+ `The file says the setup made the token ${t.symbol ?? key} at ${address}, and there is no mint there.`,
71
+ );
72
+ if (mint.decimals !== (t.decimals ?? 0))
73
+ throw new LiveMismatch(
74
+ `The mint ${address} the file names for ${t.symbol ?? key} has ${mint.decimals} decimals, not ${t.decimals ?? 0}.`,
75
+ );
56
76
  // The setup hands a token's minting to the project, or turns it off for good (a fixed supply); it never keeps a
57
77
  // freeze authority.
58
78
  const minter = t.fixedSupply ? null : project;
59
- if (mint.mintAuthority !== minter) throw new LiveMismatch(`The mint ${address} the file names for ${t.symbol ?? key} is minted by ${mint.mintAuthority ?? 'nobody'}, not ${minter ?? 'nobody'} as the setup leaves it.`);
60
- if (mint.freezeAuthority) throw new LiveMismatch(`The mint ${address} the file names for ${t.symbol ?? key} can be frozen by ${mint.freezeAuthority}; the setup makes none that can.`);
79
+ if (mint.mintAuthority !== minter)
80
+ throw new LiveMismatch(
81
+ `The mint ${address} the file names for ${t.symbol ?? key} is minted by ${mint.mintAuthority ?? 'nobody'}, not ${minter ?? 'nobody'} as the setup leaves it.`,
82
+ );
83
+ if (mint.freezeAuthority)
84
+ throw new LiveMismatch(
85
+ `The mint ${address} the file names for ${t.symbol ?? key} can be frozen by ${mint.freezeAuthority}; the setup makes none that can.`,
86
+ );
61
87
  }
62
88
 
63
89
  if (made.collection) {
@@ -68,14 +94,24 @@ export async function checkLive({ live, m, wallet, chain }) {
68
94
  } catch {
69
95
  collection = null;
70
96
  }
71
- if (!collection) throw new LiveMismatch(`The file names the collection ${made.collection}, and there is no Core collection there.`);
72
- if (collection.updateAuthority !== owner) throw new LiveMismatch(`The collection ${made.collection} the file names belongs to ${collection.updateAuthority}, not to the project's owner ${owner}.`);
97
+ if (!collection)
98
+ throw new LiveMismatch(
99
+ `The file names the collection ${made.collection}, and there is no Core collection there.`,
100
+ );
101
+ if (collection.updateAuthority !== owner)
102
+ throw new LiveMismatch(
103
+ `The collection ${made.collection} the file names belongs to ${collection.updateAuthority}, not to the project's owner ${owner}.`,
104
+ );
73
105
  }
74
106
 
75
107
  if (made.tree) {
76
108
  const config = await read(pda.treeConfig(made.tree));
77
- const creator = config?.owner === BUBBLEGUM_PROGRAM && config.data.length >= 40 ? encodeBase58(config.data.subarray(8, 40)) : null;
78
- if (creator !== project) throw new LiveMismatch(`The file names the tree ${made.tree}, which is not a tree of the project ${project}.`);
109
+ const creator =
110
+ config?.owner === BUBBLEGUM_PROGRAM && config.data.length >= 40
111
+ ? encodeBase58(config.data.subarray(8, 40))
112
+ : null;
113
+ if (creator !== project)
114
+ throw new LiveMismatch(`The file names the tree ${made.tree}, which is not a tree of the project ${project}.`);
79
115
  }
80
116
  return { owner, project };
81
117
  }
@@ -91,28 +127,45 @@ export class MintMismatch extends Error {}
91
127
  * decimals, and is not read. Throws MintMismatch.
92
128
  */
93
129
  export async function checkMints({ m, chain }) {
94
- const named = [...m.tokens.values()].filter((t) => t.mint && !t.mint.startsWith('@'));
130
+ const named = [...m.tokens.values()].filter(t => t.mint && !t.mint.startsWith('@'));
95
131
  // The routes that read a mint's default account state once, as they open, and refuse one that can start frozen.
96
132
  const once = new Map();
97
133
  const reads = (key, what) => key && !once.has(key) && once.set(key, what);
98
- for (const l of m.lotteries) reads(l.currency ?? m.currencyKey, `the lottery ${l.name ?? l.key}`), reads(l.ticketCurrency, `the lottery ${l.name ?? l.key}`);
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}`));
99
137
  for (const c of m.contests) reads(c.currency ?? m.currencyKey, `the contest ${c.name ?? c.key}`);
100
138
  for (const a of m.auctions) reads(a.currency ?? m.currencyKey, `the auction ${a.name ?? a.key}`);
101
- for (const s of m.schedules.filter((x) => x.sealed)) reads(s.currency ?? m.currencyKey, `the sealed schedule ${s.name ?? s.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}`);
102
141
  for (const t of named) {
103
142
  const what = `the token ${t.symbol ?? t.key} (“${t.key}”)`;
104
143
  if (t.wsol) {
105
- if (t.decimals !== 9) throw new MintMismatch(`The spec gives ${what} ${t.decimals} decimals; its mint ${t.mint} is wrapped SOL, which has 9. Write "decimals": 9.`);
144
+ if (t.decimals !== 9)
145
+ throw new MintMismatch(
146
+ `The spec gives ${what} ${t.decimals} decimals; its mint ${t.mint} is wrapped SOL, which has 9. Write "decimals": 9.`,
147
+ );
106
148
  continue;
107
149
  }
108
- if (typeof chain.account !== 'function') throw new MintMismatch(`This connection cannot read the chain, so the decimals of ${what} at ${t.mint} cannot be checked.`);
150
+ if (typeof chain.account !== 'function')
151
+ throw new MintMismatch(
152
+ `This connection cannot read the chain, so the decimals of ${what} at ${t.mint} cannot be checked.`,
153
+ );
109
154
  const account = await chain.account(t.mint);
110
155
  const mint = account ? parseMint(t.mint, account.owner, account.data) : null;
111
156
  if (!mint) throw new MintMismatch(`The spec names ${what} at ${t.mint}, and there is no token mint there.`);
112
- if (mint.decimals !== t.decimals) throw new MintMismatch(`The spec gives ${what} ${t.decimals ?? 'no'} decimals; its mint ${t.mint} has ${mint.decimals}, so every amount in it would be ${scale(t.decimals ?? 0, mint.decimals)} what the page shows. Write "decimals": ${mint.decimals}.`);
113
- if (mint.program !== t.program) throw new MintMismatch(`The spec says ${what} is a ${t.program} token; its mint ${t.mint} is ${mint.program}. Write "program": "${mint.program}".`);
157
+ if (mint.decimals !== t.decimals)
158
+ throw new MintMismatch(
159
+ `The spec gives ${what} ${t.decimals ?? 'no'} decimals; its mint ${t.mint} has ${mint.decimals}, so every amount in it would be ${scale(t.decimals ?? 0, mint.decimals)} what the page shows. Write "decimals": ${mint.decimals}.`,
160
+ );
161
+ if (mint.program !== t.program)
162
+ throw new MintMismatch(
163
+ `The spec says ${what} is a ${t.program} token; its mint ${t.mint} is ${mint.program}. Write "program": "${mint.program}".`,
164
+ );
114
165
  if (once.has(t.key) && mayStartFrozen(account))
115
- throw new MintMismatch(`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.`);
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
+ );
116
169
  }
117
170
  await checkTreasury({ m, chain });
118
171
  }
@@ -136,9 +189,13 @@ export async function checkTreasury({ m, chain }) {
136
189
  if (!treasury || typeof chain.account !== 'function') return;
137
190
  const account = await chain.account(treasury);
138
191
  if (!account || (account.owner === SYSTEM_PROGRAM && !account.executable)) return;
139
- const what = HOLDS_A_VAULT[account.owner] ?? (account.executable ? 'a program' : `an account of the program ${account.owner}`);
140
- throw new MintMismatch(`The spec’s treasury ${treasury} is ${what}: nobody can sign for what it would be paid, so every split’s rest would be stuck there. Name the wallet that signs (for a Squads multisig, its vault address; for a Realms DAO, its treasury).`);
192
+ const what =
193
+ HOLDS_A_VAULT[account.owner] ?? (account.executable ? 'a program' : `an account of the program ${account.owner}`);
194
+ throw new MintMismatch(
195
+ `The spec’s treasury ${treasury} is ${what}: nobody can sign for what it would be paid, so every split’s rest would be stuck there. Name the wallet that signs (for a Squads multisig, its vault address; for a Realms DAO, its treasury).`,
196
+ );
141
197
  }
142
198
 
143
199
  /** How much an amount written with `said` decimals is, on a mint of `real` decimals, against what it says. */
144
- const scale = (said, real) => (said > real ? `${10n ** BigInt(said - real)} times` : `1/${10n ** BigInt(real - said)} of`);
200
+ const scale = (said, real) =>
201
+ said > real ? `${10n ** BigInt(said - real)} times` : `1/${10n ** BigInt(real - said)} of`;