@xgamesfun/solana 0.24.0 → 0.25.1

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 (99) hide show
  1. package/DISCLAIMER.md +1 -1
  2. package/NOTICE +4 -4
  3. package/README.md +100 -52
  4. package/bin/xsolana.mjs +32 -11
  5. package/examples/blind-forge.json +2 -4
  6. package/examples/coin-flip-arena.json +2 -5
  7. package/examples/duel-arena.json +1 -4
  8. package/examples/fair-presale.json +1 -3
  9. package/examples/hero-trials.json +14 -2
  10. package/examples/ideas.json +788 -788
  11. package/examples/nft-raffle.json +1 -0
  12. package/examples/partner-vault.json +19 -3
  13. package/examples/sacrifice-raffle.json +2 -10
  14. package/examples/shelter-fair.json +1 -0
  15. package/examples/stakers-draw.json +4 -7
  16. package/examples/trade-in-shop.json +2 -3
  17. package/js/approve/details.mjs +117 -48
  18. package/js/approve/ed25519.mjs +5 -1
  19. package/js/approve/live-check.mjs +83 -26
  20. package/js/approve/page.html +217 -78
  21. package/js/approve/page.js +43 -15
  22. package/js/approve/server.mjs +276 -74
  23. package/js/chain/base58.js +7 -1
  24. package/js/chain/borsh.js +15 -11
  25. package/js/chain/bytes.js +11 -8
  26. package/js/chain/core.js +21 -5
  27. package/js/chain/curve.js +1 -1
  28. package/js/chain/lookup.js +118 -36
  29. package/js/chain/mint.js +6 -2
  30. package/js/chain/pda.js +2 -2
  31. package/js/chain/programs.js +6 -6
  32. package/js/chain/rpc.js +40 -14
  33. package/js/chain/token.js +13 -2
  34. package/js/chain/token2022.js +55 -16
  35. package/js/chain/tx.js +32 -22
  36. package/js/client/accounts.js +270 -80
  37. package/js/client/compressed.js +4 -3
  38. package/js/client/grant.js +165 -40
  39. package/js/client/idl/loot.js +1 -1
  40. package/js/client/idl/loot.json +104 -16
  41. package/js/client/ix.js +2121 -457
  42. package/js/client/pda.js +20 -18
  43. package/js/client/policy.js +12 -5
  44. package/js/client/random.js +2 -2
  45. package/js/client/sizes.js +220 -65
  46. package/js/client/traits.js +14 -5
  47. package/js/connect/connect.mjs +59 -17
  48. package/js/economy/check.mjs +3884 -690
  49. package/js/economy/cost.mjs +381 -57
  50. package/js/economy/examples.mjs +176 -35
  51. package/js/economy/explain.mjs +468 -110
  52. package/js/economy/guide.mjs +282 -59
  53. package/js/economy/legal.mjs +12 -6
  54. package/js/economy/live.mjs +62 -20
  55. package/js/economy/model.mjs +320 -97
  56. package/js/economy/plan.mjs +1022 -203
  57. package/js/economy/review.mjs +760 -0
  58. package/js/economy/schema.json +134 -372
  59. package/js/economy/secrets.mjs +17 -7
  60. package/js/economy/simulate.mjs +771 -178
  61. package/js/economy/trust.mjs +15 -0
  62. package/js/economy/txsize.mjs +15 -3
  63. package/js/economy/validate.mjs +61 -24
  64. package/js/index.js +1 -0
  65. package/js/mcp/server.mjs +43 -15
  66. package/js/mcp/tools.mjs +318 -80
  67. package/js/play/index.js +1960 -461
  68. package/js/play/keys.js +35 -9
  69. package/js/results/index.js +225 -32
  70. package/js/results/server.js +230 -0
  71. package/js/results/wire.js +64 -16
  72. package/package.json +6 -4
  73. package/release.json +5 -5
  74. package/skills/xsolana-economy/SKILL.md +52 -22
  75. package/skills/xsolana-economy/references/after-launch.md +32 -21
  76. package/skills/xsolana-economy/references/apps.md +47 -36
  77. package/skills/xsolana-economy/references/sentence-to-spec.md +149 -129
  78. package/skills/xsolana-economy/references/telling-the-person.md +15 -6
  79. package/skills/xsolana-economy/references/trust-and-cheating.md +126 -0
  80. package/types/chain/core.d.ts +1 -1
  81. package/types/chain/lookup.d.ts +1 -1
  82. package/types/client/accounts.d.ts +24 -3
  83. package/types/client/grant.d.ts +2 -2
  84. package/types/client/idl/loot.d.ts +22 -0
  85. package/types/client/ix.d.ts +54 -45
  86. package/types/client/pda.d.ts +3 -1
  87. package/types/economy/cost.d.mts +1 -1
  88. package/types/economy/explain.d.mts +3 -0
  89. package/types/economy/guide.d.mts +5 -0
  90. package/types/economy/model.d.mts +48 -3
  91. package/types/economy/plan.d.mts +44 -2
  92. package/types/economy/review.d.mts +45 -0
  93. package/types/economy/trust.d.mts +8 -0
  94. package/types/index.d.ts +1 -0
  95. package/types/mcp/tools.d.mts +62 -0
  96. package/types/play/index.d.ts +14 -25
  97. package/types/results/index.d.ts +8 -7
  98. package/types/results/server.d.ts +126 -0
  99. package/types/results/wire.d.ts +1 -1
@@ -15,6 +15,7 @@
15
15
  {
16
16
  "key": "art",
17
17
  "name": "The artwork",
18
+ "rest": "treasury",
18
19
  "price": 0.1,
19
20
  "raffle": {
20
21
  "nft": "ArtNFT1111111111111111111111111111111111111",
@@ -18,7 +18,11 @@
18
18
  "key": "vault",
19
19
  "name": "Partner vault",
20
20
  "collection": "5q8LtcGwYhYRq5M8dQ79wkeLvo8N993a7aoE6DLDBXaw",
21
- "nfts": ["4vd1uA3jK28MwhufZnbtQpyJkVMXEVpKHzDRPUH9DT3v", "E53iEjsuDnz1jP6MJbnhecvTBRWCs5MsmjusfhYNDjD4", "B53EzSitDoQ4usvMN4nKhQRUVb7BhNh9PZPwgyQobpsV"],
21
+ "nfts": [
22
+ "4vd1uA3jK28MwhufZnbtQpyJkVMXEVpKHzDRPUH9DT3v",
23
+ "E53iEjsuDnz1jP6MJbnhecvTBRWCs5MsmjusfhYNDjD4",
24
+ "B53EzSitDoQ4usvMN4nKhQRUVb7BhNh9PZPwgyQobpsV"
25
+ ],
22
26
  "about": "Given by the paid crate and the mystery lot."
23
27
  },
24
28
  {
@@ -31,7 +35,12 @@
31
35
  {
32
36
  "key": "rare",
33
37
  "name": "The rare one",
34
- "nfts": [{ "asset": "BQqnw2mh6xEAhAxC8WrA2uqPg9ShsT13AqriUa9x4ab4", "collection": "5q8LtcGwYhYRq5M8dQ79wkeLvo8N993a7aoE6DLDBXaw" }],
38
+ "nfts": [
39
+ {
40
+ "asset": "BQqnw2mh6xEAhAxC8WrA2uqPg9ShsT13AqriUa9x4ab4",
41
+ "collection": "5q8LtcGwYhYRq5M8dQ79wkeLvo8N993a7aoE6DLDBXaw"
42
+ }
43
+ ],
35
44
  "about": "The raffle's first prize, named by it and given when its place is paid: kept apart so no roll can take it."
36
45
  }
37
46
  ],
@@ -80,8 +89,15 @@
80
89
  {
81
90
  "key": "raffle",
82
91
  "name": "Rare NFT raffle",
92
+ "rest": "treasury",
83
93
  "price": 0.01,
84
- "places": [{ "percent": 0, "prize": { "gives": "nft", "stash": "rare", "nft": "BQqnw2mh6xEAhAxC8WrA2uqPg9ShsT13AqriUa9x4ab4" } }, 90],
94
+ "places": [
95
+ {
96
+ "percent": 0,
97
+ "prize": { "gives": "nft", "stash": "rare", "nft": "BQqnw2mh6xEAhAxC8WrA2uqPg9ShsT13AqriUa9x4ab4" }
98
+ },
99
+ 90
100
+ ],
85
101
  "firstDraw": "publish+7d"
86
102
  }
87
103
  ],
@@ -90,18 +90,10 @@
90
90
  {
91
91
  "key": "weekly",
92
92
  "name": "Grand and minors",
93
+ "pot": 0,
93
94
  "price": 1,
94
95
  "ticketCurrency": "ticket",
95
- "places": [
96
- 50,
97
- 10,
98
- 10,
99
- 10,
100
- 5,
101
- 5,
102
- 5,
103
- 5
104
- ],
96
+ "places": [50, 10, 10, 10, 5, 5, 5, 5],
105
97
  "every": "7d"
106
98
  }
107
99
  ],
@@ -79,6 +79,7 @@
79
79
  {
80
80
  "key": "raffle",
81
81
  "name": "Shelter Raffle",
82
+ "pot": 0,
82
83
  "price": 0.002,
83
84
  "seed": 0.05,
84
85
  "split": [
@@ -66,13 +66,11 @@
66
66
  {
67
67
  "key": "draw",
68
68
  "name": "The stakers' draw",
69
+ "pot": 0,
69
70
  "price": 0.01,
70
71
  "noLoss": true,
71
72
  "seed": 0.1,
72
- "places": [
73
- 60,
74
- 40
75
- ],
73
+ "places": [60, 40],
76
74
  "every": "7d",
77
75
  "perWallet": 20,
78
76
  "gate": {
@@ -86,6 +84,7 @@
86
84
  "grants": [
87
85
  {
88
86
  "key": "race",
87
+ "trust": "decides",
89
88
  "name": "Weekly race rewards",
90
89
  "prize": {
91
90
  "gives": "tokens",
@@ -93,9 +92,7 @@
93
92
  "currency": "gem",
94
93
  "from": "mint"
95
94
  },
96
- "vouchers": [
97
- "AKnL4NNf3DGWZJS6cPknBuEGnVsV4A4m5tgebLHaRSZ9"
98
- ],
95
+ "vouchers": ["AKnL4NNf3DGWZJS6cPknBuEGnVsV4A4m5tgebLHaRSZ9"],
99
96
  "limits": {
100
97
  "perWallet": 52,
101
98
  "perDay": 10
@@ -142,6 +142,7 @@
142
142
  {
143
143
  "key": "shield-raffle",
144
144
  "name": "Shield raffle",
145
+ "pot": 0,
145
146
  "ticketNft": {
146
147
  "rule": {
147
148
  "item": "shield"
@@ -149,9 +150,7 @@
149
150
  "then": "burn"
150
151
  },
151
152
  "seed": 0.1,
152
- "places": [
153
- 100
154
- ],
153
+ "places": [100],
155
154
  "every": "7d"
156
155
  }
157
156
  ],
@@ -13,7 +13,7 @@ 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) {
@@ -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,27 +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]));
96
- const vaults = new Map(names.m.farms.map((f) => [pda.rewards(pda.farm(project, f.id)), f.name ?? f.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]));
97
114
  // Before a wallet connects, the plan stands a made-up address in for it (one no spec may name): that one is not
98
115
  // shown. Once one is connected, only that wallet is called yours.
99
- const yours = (a) => (names.owner == null ? 'your wallet' : a === names.owner ? `your wallet (${a})` : `the project’s owner, ${a}`);
100
- const address = (a) => (a === owner ? yours(a) : a);
101
- 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}`);
102
120
  const treasury = () => `the treasury (${names.treasury ? address(names.treasury) : 'your wallet'})`;
103
- const where = (to) => {
121
+ const where = to => {
104
122
  if (to === 'Treasury') return treasury();
105
123
  const [kind] = Object.keys(to);
106
124
  const v = to[kind];
107
125
  if (kind === 'Pool') return `the pool “${names.pools.get(v[0]) ?? v[0]}”`;
108
126
  if (kind === 'Wallet') return wallet(v[0]);
109
- if (kind === 'Account') return pots.has(v[0]) ? `the pot of “${pots.get(v[0])}”` : vaults.has(v[0]) ? `the rewards of the farm “${vaults.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]}`;
110
133
  if (kind === 'Holder') return `whoever holds the NFT ${v[0]}`;
111
134
  if (kind === 'Chosen') return 'a wallet the buyer chooses';
112
135
  return JSON.stringify(to);
@@ -118,34 +141,63 @@ function readDetails(reads, names) {
118
141
  }
119
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.
120
143
  const def = read.args.def ?? {};
121
- const restTo = def.Lottery ? 'its pot' : read.name === 'open_auction' ? (read.args.args?.lot?.Asset ? `the seller, ${address(read.accounts.seller)}` : treasury()) : treasury();
122
- const to = (v) => (v === 'Burn' ? 'burned' : `to ${where(v)}`);
123
- 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);
124
161
  const grantId = read.args.def?.Grant?.id;
125
162
  if (read.args.def?.Grant) lines.push(...grantTerms(read.args.def.Grant.args, names));
126
- const walk = (value) => {
163
+ const walk = value => {
127
164
  if (Array.isArray(value)) return value.forEach(walk);
128
165
  if (!value || typeof value !== 'object' || value instanceof Uint8Array) return;
129
166
  for (const [k, v] of Object.entries(value)) {
130
167
  if (k === 'shares' && Array.isArray(v)) {
131
168
  const rest = 10_000 - v.reduce((n, s) => n + Number(s.bps), 0);
132
- 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)}`);
133
170
  if (rest > 0) parts.push(v.length ? `the rest (${rest / 100}%) to ${restTo}` : `all of it to ${restTo}`);
134
171
  lines.push(`Pays: ${parts.join(', ')}`);
135
- } else if (k === 'to' && isTo(v)) lines.push(v === 'Burn' ? 'Burns the tokens it takes' : `Sends tokens to ${where(v)}`);
136
- 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])}`);
137
183
  else if (k === 'vouchers' && Array.isArray(v)) {
138
- const keys = v.filter((a) => a !== NOBODY);
184
+ const keys = v.filter(a => a !== NOBODY);
139
185
  if (keys.length) lines.push(`Voucher keys (each decides who gets it): ${keys.join(', ')}`);
140
186
  } else if (k === 'key_root' && v instanceof Uint8Array) {
141
187
  if (!equalBytes(v, ZERO)) {
142
188
  const listed = names.grants.get(grantId)?.keys ?? [];
143
- const ours = listed.length && equalBytes(merkleRoot(listed.map((key) => keyLeaf(key))), v);
144
- 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
+ );
145
195
  }
146
196
  } else if (k === 'list_root' && v instanceof Uint8Array) {
147
- if (!equalBytes(v, ZERO)) lines.push(...listLines(v, names.grants.get(grantId), read.args.def.Grant.args.gives, names));
148
- } 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');
149
201
  else walk(v);
150
202
  }
151
203
  };
@@ -158,36 +210,44 @@ function readDetails(reads, names) {
158
210
  function prizeToken(gives, m) {
159
211
  const from = gives?.Tokens?.from;
160
212
  if (!from) return null;
161
- 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;
162
216
  return m.tokens.get(key) ?? null;
163
217
  }
164
218
 
165
219
  /** Base units of `token` in its own words ("100 COIN"), or as base units when the token is not known. */
166
- 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`;
167
222
 
168
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. */
169
224
  function prizeWords(gives, m) {
170
225
  const [kind] = Object.keys(gives ?? {});
171
226
  const v = gives?.[kind];
172
- const named = (list, id, field = 'id') => list.find((x) => x[field] === id);
173
- const stash = (id) => `the stash “${named(m.stashes, id)?.name ?? named(m.stashes, id)?.key ?? id}”`;
174
- 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}”`;
175
230
  if (kind === 'Tokens') {
176
- 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]}`;
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]}`;
177
236
  const amount = tokens(v.amount, prizeToken(gives, m));
178
237
  return `${amount} ${from}`;
179
238
  }
180
239
  if (kind === 'Item') return item(v[0]);
181
240
  if (kind === 'Nft') return `an NFT of ${stash(v.stash)}`;
182
241
  if (kind === 'Stash') return `an NFT of ${stash(v[0])} at random`;
183
- 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]}”`;
184
244
  if (kind === 'Compressed') return `a compressed NFT of ${item(v.item)}`;
185
245
  if (kind === 'Held') return 'an NFT the lottery holds';
186
246
  return 'nothing';
187
247
  }
188
248
 
189
- const times = (n) => `${n} claim${Number(n) === 1 ? '' : 's'}`;
190
- 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');
191
251
 
192
252
  /** A grant's prize and its limits: what each claim takes from the project, and how many claims there can be. */
193
253
  function grantTerms(args, names) {
@@ -197,14 +257,19 @@ function grantTerms(args, names) {
197
257
  l.wallet?.per_window ? `each wallet ${times(l.wallet.per_window)} every ${l.wallet.window} seconds` : null,
198
258
  l.per_person ? `each person ${times(l.per_person)}` : null,
199
259
  l.per_key ? `each voucher key ${times(l.per_key)}` : null,
200
- l.all?.per_window ? `${times(l.all.per_window)} ${Number(l.all.window) === 86_400 ? 'a day' : `every ${l.all.window} seconds`}` : 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,
201
263
  l.all?.total ? `${times(l.all.total)} in all` : null,
202
264
  BigInt(l.window?.opens_at ?? 0) ? `opens ${when(l.window.opens_at)}` : null,
203
265
  BigInt(l.window?.closes_at ?? 0) ? `closes ${when(l.window.closes_at)}` : null,
204
266
  ].filter(Boolean);
205
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).
206
268
  const upTo = !args.open && args.gives?.Tokens ? 'up to ' : '';
207
- return [`Each claim gets: ${upTo}${prizeWords(args.gives, names.m)}`, `Limits: ${limits.length ? limits.join(', ') : 'none'}`];
269
+ return [
270
+ `Each claim gets: ${upTo}${prizeWords(args.gives, names.m)}`,
271
+ `Limits: ${limits.length ? limits.join(', ') : 'none'}`,
272
+ ];
208
273
  }
209
274
 
210
275
  /**
@@ -213,13 +278,17 @@ function grantTerms(args, names) {
213
278
  */
214
279
  function listLines(root, g, gives, names) {
215
280
  const list = g?.list ?? [];
216
- 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`];
217
283
  const token = prizeToken(gives, names.m);
218
284
  return [
219
285
  `Only wallets on its list may claim it (${list.length} wallet${list.length === 1 ? '' : 's'}, root ${toHex(root)})`,
220
- ...list.map((e) => {
286
+ ...list.map(e => {
221
287
  const entry = typeof e === 'string' ? { wallet: e } : e;
222
- 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';
223
292
  return `On its list, ${entry.wallet}: ${gets}${entry.round ? `, in round ${entry.round}` : ''}`;
224
293
  }),
225
294
  ];
@@ -231,23 +300,23 @@ function listLines(root, g, gives, names) {
231
300
  * lacks is listed as no longer so.
232
301
  */
233
302
  export function compareDetails(now, was) {
234
- const topic = (line) => line.split(':')[0];
303
+ const topic = line => line.split(':')[0];
235
304
  const left = [...was];
236
- const out = now.map((line) => {
305
+ const out = now.map(line => {
237
306
  const same = left.indexOf(line);
238
307
  if (same >= 0) return left.splice(same, 1)[0];
239
- const i = left.findIndex((old) => topic(old) === topic(line));
308
+ const i = left.findIndex(old => topic(old) === topic(line));
240
309
  if (i < 0) return `${line} (new)`;
241
310
  const [old] = left.splice(i, 1);
242
311
  return `${line} (changed: it was ${old.slice(topic(old).length + 1).trim() || old})`;
243
312
  });
244
- return [...out, ...left.map((old) => `No longer: ${old}`)];
313
+ return [...out, ...left.map(old => `No longer: ${old}`)];
245
314
  }
246
315
 
247
316
  const ADDRESS = /\b[1-9A-HJ-NP-Za-km-z]{32,44}\b/g;
248
317
 
249
318
  /** Every address the lines name, in the order they name them. */
250
- const addressesIn = (lines) => [...new Set(lines.flatMap((l) => l.match(ADDRESS) ?? []))];
319
+ const addressesIn = lines => [...new Set(lines.flatMap(l => l.match(ADDRESS) ?? []))];
251
320
 
252
321
  /**
253
322
  * A warning for the page when the approval pays or trusts an address: on a change, those the live economy did not
@@ -255,7 +324,7 @@ const addressesIn = (lines) => [...new Set(lines.flatMap((l) => l.match(ADDRESS)
255
324
  */
256
325
  export function outsideAddresses(lines, { was = null, owner = null } = {}) {
257
326
  const known = new Set([...(was ? addressesIn(was) : []), ...(owner ? [owner] : [])]);
258
- const outside = addressesIn(lines).filter((a) => !known.has(a));
327
+ const outside = addressesIn(lines).filter(a => !known.has(a));
259
328
  if (!outside.length) return null;
260
329
  return was
261
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`;