@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.
- package/README.md +95 -52
- package/bin/xsolana.mjs +23 -8
- package/examples/blind-forge.json +2 -4
- package/examples/coin-flip-arena.json +4 -12
- package/examples/duel-arena.json +1 -4
- package/examples/elements-set.json +2 -9
- package/examples/expedition-guild.json +1 -8
- package/examples/fair-presale.json +1 -3
- package/examples/guild-missions.json +3 -10
- package/examples/halving-seasons.json +4 -9
- package/examples/hero-trials.json +15 -6
- package/examples/holder-den.json +38 -0
- package/examples/holders-drop.json +105 -0
- package/examples/ideas.json +788 -788
- package/examples/match-predictions.json +24 -0
- package/examples/merge-2048.json +5 -5
- package/examples/nft-raffle.json +1 -0
- package/examples/pack-league.json +108 -0
- package/examples/paper-hands-farm.json +4 -16
- package/examples/partner-vault.json +20 -7
- package/examples/sacrifice-raffle.json +2 -10
- package/examples/shelter-fair.json +1 -0
- package/examples/stakers-draw.json +4 -15
- package/examples/star-fleet.json +1 -8
- package/examples/starforce.json +6 -6
- package/examples/stride-sneakers.json +164 -0
- package/examples/trade-in-shop.json +2 -3
- package/js/approve/details.mjs +122 -52
- package/js/approve/ed25519.mjs +5 -1
- package/js/approve/live-check.mjs +83 -26
- package/js/approve/page.html +217 -78
- package/js/approve/page.js +43 -15
- package/js/approve/server.mjs +290 -73
- package/js/chain/base58.js +7 -1
- package/js/chain/borsh.js +24 -11
- package/js/chain/bytes.js +11 -8
- package/js/chain/core.js +35 -7
- package/js/chain/curve.js +1 -1
- package/js/chain/lookup.js +124 -36
- package/js/chain/mint.js +6 -2
- package/js/chain/pda.js +2 -2
- package/js/chain/programs.js +6 -6
- package/js/chain/rpc.js +44 -16
- package/js/chain/token.js +13 -2
- package/js/chain/token2022.js +55 -16
- package/js/chain/tx.js +32 -22
- package/js/client/accounts.js +653 -131
- package/js/client/compressed.js +4 -3
- package/js/client/grant.js +267 -83
- package/js/client/idl/loot.js +1 -1
- package/js/client/idl/loot.json +2406 -2176
- package/js/client/index.js +1 -0
- package/js/client/ix.js +3059 -664
- package/js/client/pda.js +39 -33
- package/js/client/policy.js +52 -24
- package/js/client/random.js +70 -0
- package/js/client/sizes.js +414 -16
- package/js/client/traits.js +14 -5
- package/js/connect/connect.mjs +59 -17
- package/js/economy/check.mjs +4153 -725
- package/js/economy/cost.mjs +403 -64
- package/js/economy/examples.mjs +176 -35
- package/js/economy/explain.mjs +466 -111
- package/js/economy/guide.mjs +296 -72
- package/js/economy/legal.mjs +12 -6
- package/js/economy/live.mjs +83 -19
- package/js/economy/model.mjs +462 -124
- package/js/economy/plan.mjs +1306 -219
- package/js/economy/schema.json +390 -484
- package/js/economy/secrets.mjs +17 -7
- package/js/economy/simulate.mjs +883 -223
- package/js/economy/txsize.mjs +15 -3
- package/js/economy/validate.mjs +61 -24
- package/js/mcp/server.mjs +42 -14
- package/js/mcp/tools.mjs +307 -88
- package/js/play/index.js +2864 -619
- package/js/play/keys.js +193 -0
- package/js/results/index.js +223 -30
- package/js/results/server.js +230 -0
- package/js/results/wire.js +64 -16
- package/package.json +6 -4
- package/release.json +5 -5
- package/skills/xsolana-economy/SKILL.md +39 -21
- package/skills/xsolana-economy/references/after-launch.md +58 -38
- package/skills/xsolana-economy/references/apps.md +97 -50
- package/skills/xsolana-economy/references/sentence-to-spec.md +148 -121
- package/skills/xsolana-economy/references/telling-the-person.md +61 -17
- package/types/chain/core.d.ts +4 -2
- package/types/chain/lookup.d.ts +5 -2
- package/types/chain/rpc.d.ts +3 -1
- package/types/client/accounts.d.ts +217 -56
- package/types/client/grant.d.ts +72 -27
- package/types/client/idl/loot.d.ts +602 -549
- package/types/client/index.d.ts +1 -0
- package/types/client/ix.d.ts +3884 -247
- package/types/client/pda.d.ts +22 -16
- package/types/client/policy.d.ts +39 -14
- package/types/client/random.d.ts +26 -0
- package/types/client/sizes.d.ts +93 -5
- package/types/economy/check.d.mts +5 -10
- package/types/economy/cost.d.mts +2 -1
- package/types/economy/guide.d.mts +5 -0
- package/types/economy/live.d.mts +16 -1
- package/types/economy/model.d.mts +151 -38
- package/types/economy/plan.d.mts +237 -194
- package/types/economy/simulate.d.mts +21 -13
- package/types/mcp/tools.d.mts +21 -13
- package/types/play/index.d.ts +453 -53
- package/types/play/keys.d.ts +51 -0
- package/types/results/index.d.ts +7 -6
- package/types/results/server.d.ts +126 -0
- package/types/results/wire.d.ts +1 -1
package/js/approve/details.mjs
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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(
|
|
41
|
-
stashes: new Map(m.stashes.map(
|
|
42
|
-
grants: new Map(m.grants.map(
|
|
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 = {
|
|
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])
|
|
79
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
99
|
-
|
|
100
|
-
const
|
|
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 =
|
|
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')
|
|
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
|
|
121
|
-
|
|
122
|
-
|
|
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 =
|
|
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(
|
|
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 === '
|
|
135
|
-
|
|
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(
|
|
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(
|
|
143
|
-
lines.push(
|
|
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))
|
|
147
|
-
|
|
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
|
|
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) =>
|
|
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(
|
|
172
|
-
const stash =
|
|
173
|
-
const item =
|
|
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
|
|
176
|
-
|
|
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')
|
|
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 =
|
|
189
|
-
const when =
|
|
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.
|
|
200
|
-
|
|
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
|
|
206
|
-
return [
|
|
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))
|
|
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(
|
|
286
|
+
...list.map(e => {
|
|
220
287
|
const entry = typeof e === 'string' ? { wallet: e } : e;
|
|
221
|
-
const gets =
|
|
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 =
|
|
303
|
+
const topic = line => line.split(':')[0];
|
|
234
304
|
const left = [...was];
|
|
235
|
-
const out = now.map(
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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).`
|
package/js/approve/ed25519.mjs
CHANGED
|
@@ -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({
|
|
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')
|
|
34
|
-
|
|
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')
|
|
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)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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)
|
|
55
|
-
|
|
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)
|
|
60
|
-
|
|
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)
|
|
72
|
-
|
|
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 =
|
|
78
|
-
|
|
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(
|
|
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)
|
|
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(
|
|
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)
|
|
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')
|
|
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)
|
|
113
|
-
|
|
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(
|
|
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 =
|
|
140
|
-
|
|
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) =>
|
|
200
|
+
const scale = (said, real) =>
|
|
201
|
+
said > real ? `${10n ** BigInt(said - real)} times` : `1/${10n ** BigInt(real - said)} of`;
|