@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/server.mjs
CHANGED
|
@@ -13,10 +13,12 @@ import { spawn } from 'node:child_process';
|
|
|
13
13
|
import { equalBytes, fromBase64, toBase64 } from '../chain/bytes.js';
|
|
14
14
|
import { checkCluster, notMainnet, rpcClient } from '../chain/rpc.js';
|
|
15
15
|
import { addressBytes, encodeBase58 } from '../chain/base58.js';
|
|
16
|
+
import { slotHashes, tableAddress } from '../chain/lookup.js';
|
|
17
|
+
import { SYSVAR_SLOT_HASHES } from '../chain/pda.js';
|
|
16
18
|
import { MINT_BYTES } from '../chain/token.js';
|
|
17
19
|
import { compileMessage, readTransaction, wireTransaction } from '../chain/tx.js';
|
|
18
20
|
import { accountKind, decode } from '../client/accounts.js';
|
|
19
|
-
import { LOOT_PROGRAM } from '../client/pda.js';
|
|
21
|
+
import { LOOT_PROGRAM, pda } from '../client/pda.js';
|
|
20
22
|
import { check, describe } from '../economy/check.mjs';
|
|
21
23
|
import { cost } from '../economy/cost.mjs';
|
|
22
24
|
import { LEGAL, legalFlags } from '../economy/legal.mjs';
|
|
@@ -47,25 +49,27 @@ class ApproveError extends Error {
|
|
|
47
49
|
export function devnetChain(url = process.env.XSOLANA_RPC_URL || DEVNET) {
|
|
48
50
|
const rpc = rpcClient(notMainnet(url));
|
|
49
51
|
let cluster = null;
|
|
50
|
-
const checked = () =>
|
|
51
|
-
cluster
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
const checked = () =>
|
|
53
|
+
(cluster ??= checkCluster(rpc).catch(e => {
|
|
54
|
+
cluster = null;
|
|
55
|
+
throw e;
|
|
56
|
+
}));
|
|
54
57
|
return {
|
|
55
58
|
url,
|
|
56
59
|
latestBlockhash: async () => {
|
|
57
60
|
await checked();
|
|
58
61
|
return rpc.latestBlockhash();
|
|
59
62
|
},
|
|
60
|
-
rent: async
|
|
61
|
-
send: async
|
|
63
|
+
rent: async bytes => BigInt(await rpc.call('getMinimumBalanceForRentExemption', [bytes])),
|
|
64
|
+
send: async wire => {
|
|
62
65
|
await checked();
|
|
63
66
|
return rpc.send(wire);
|
|
64
67
|
},
|
|
65
68
|
confirm: (signature, options) => rpc.confirm(signature, options),
|
|
66
|
-
account:
|
|
69
|
+
account: address => rpc.account(address),
|
|
67
70
|
/** A sent transaction's status from the whole history: { err, confirmationStatus }, or null when unseen. */
|
|
68
|
-
signatureStatus: async
|
|
71
|
+
signatureStatus: async signature =>
|
|
72
|
+
(await rpc.call('getSignatureStatuses', [[signature], { searchTransactionHistory: true }])).value[0],
|
|
69
73
|
blockHeight: () => rpc.blockHeight(),
|
|
70
74
|
};
|
|
71
75
|
}
|
|
@@ -77,7 +81,10 @@ export function devnetChain(url = process.env.XSOLANA_RPC_URL || DEVNET) {
|
|
|
77
81
|
*/
|
|
78
82
|
export async function writeWhole(file, text) {
|
|
79
83
|
const target = await realpath(file).catch(() => file);
|
|
80
|
-
const mode = await stat(target).then(
|
|
84
|
+
const mode = await stat(target).then(
|
|
85
|
+
s => s.mode & 0o777,
|
|
86
|
+
() => 0o644,
|
|
87
|
+
);
|
|
81
88
|
const temp = path.join(path.dirname(target), `.${path.basename(target)}.${randomBytes(6).toString('hex')}.tmp`);
|
|
82
89
|
try {
|
|
83
90
|
const handle = await openFile(temp, 'wx', mode);
|
|
@@ -101,7 +108,10 @@ function walletAddress(wallet) {
|
|
|
101
108
|
try {
|
|
102
109
|
addressBytes(text);
|
|
103
110
|
} catch {
|
|
104
|
-
throw new ApproveError(
|
|
111
|
+
throw new ApproveError(
|
|
112
|
+
400,
|
|
113
|
+
`${text ? JSON.stringify(text.slice(0, 60)) : 'No wallet'}: that is not a Solana address. Connect a wallet and try again.`,
|
|
114
|
+
);
|
|
105
115
|
}
|
|
106
116
|
return text;
|
|
107
117
|
}
|
|
@@ -110,18 +120,25 @@ function walletAddress(wallet) {
|
|
|
110
120
|
* One approval session for the spec in `file`. `chain`: { latestBlockhash, rent(bytes), send(wire), confirm(sig) }.
|
|
111
121
|
* `write(file, text)` writes the spec file (`writeWhole`). Exposed for tests; `approve` serves it.
|
|
112
122
|
*/
|
|
113
|
-
export async function createApproval({
|
|
123
|
+
export async function createApproval({
|
|
124
|
+
file,
|
|
125
|
+
chain = devnetChain(),
|
|
126
|
+
now = () => Math.floor(Date.now() / 1000),
|
|
127
|
+
clock = Date.now,
|
|
128
|
+
write = writeWhole,
|
|
129
|
+
}) {
|
|
114
130
|
let spec = JSON.parse(await readFile(file, 'utf8'));
|
|
115
131
|
if (spec.economy) spec = spec.economy;
|
|
116
132
|
const verdict = check(spec, { now: now() });
|
|
117
|
-
if (!verdict.ok)
|
|
133
|
+
if (!verdict.ok)
|
|
134
|
+
throw new ApproveError(400, `${file} does not check:\n${describe({ refusals: verdict.refusals, warnings: [] })}`);
|
|
118
135
|
const keys = new Map();
|
|
119
136
|
let prepared = null;
|
|
120
137
|
let busy = false;
|
|
121
138
|
// One wallet read, prepare or complete at a time: each settles and writes the live section, and a batch built while
|
|
122
139
|
// another batch lands would send that batch's steps again.
|
|
123
140
|
let queue = Promise.resolve();
|
|
124
|
-
const serial =
|
|
141
|
+
const serial = fn => {
|
|
125
142
|
const run = queue.then(fn, fn);
|
|
126
143
|
queue = run.catch(() => {});
|
|
127
144
|
return run;
|
|
@@ -148,13 +165,19 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
|
|
|
148
165
|
await checkMints({ m: model(spec, { now: at }), chain });
|
|
149
166
|
} catch (e) {
|
|
150
167
|
if (!(e instanceof MintMismatch)) throw e;
|
|
151
|
-
throw new ApproveError(
|
|
168
|
+
throw new ApproveError(
|
|
169
|
+
409,
|
|
170
|
+
`${e.message} Nothing was sent: correct ${path.basename(file)}, then run \`xsolana approve\` again.`,
|
|
171
|
+
);
|
|
152
172
|
}
|
|
153
173
|
try {
|
|
154
174
|
return await checkLive({ live: live(), m: model(spec, { now: at }), wallet, chain });
|
|
155
175
|
} catch (e) {
|
|
156
176
|
if (!(e instanceof LiveMismatch)) throw e;
|
|
157
|
-
throw new ApproveError(
|
|
177
|
+
throw new ApproveError(
|
|
178
|
+
409,
|
|
179
|
+
`${e.message} Nothing was sent. ${path.basename(file)}’s live section is not what the chain says: restore the file \`xsolana approve\` wrote, or remove its live section to start a new setup.`,
|
|
180
|
+
);
|
|
158
181
|
}
|
|
159
182
|
}
|
|
160
183
|
|
|
@@ -164,13 +187,30 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
|
|
|
164
187
|
for (const name of p0.freshKeys) if (!keys.has(name)) keys.set(name, freshKeypair());
|
|
165
188
|
// The accounts the owner makes outside loot: a mint, and the project's tree of compressed NFTs.
|
|
166
189
|
const tree = compressedTree(model(spec, { now: at }));
|
|
167
|
-
const rents = new Map(
|
|
168
|
-
|
|
190
|
+
const rents = new Map(
|
|
191
|
+
await Promise.all(
|
|
192
|
+
[MINT_BYTES, ...(tree ? [tree.bytes] : [])].map(async bytes => [
|
|
193
|
+
bytes,
|
|
194
|
+
boundedRent(bytes, await chain.rent(bytes)),
|
|
195
|
+
]),
|
|
196
|
+
),
|
|
197
|
+
);
|
|
198
|
+
const rent = bytes => {
|
|
169
199
|
if (!rents.has(bytes)) throw new Error(`No rent known for ${bytes} bytes.`);
|
|
170
200
|
return rents.get(bytes);
|
|
171
201
|
};
|
|
172
202
|
const addresses = Object.fromEntries([...keys].map(([n, k]) => [n, k.address]));
|
|
173
|
-
|
|
203
|
+
// A recent slot, for the project's standing lookup table (made at go-live at a slot still in the slot hashes when it lands).
|
|
204
|
+
const hashes = slotHashes((await chain.account(SYSVAR_SLOT_HASHES).catch(() => null))?.data);
|
|
205
|
+
const p = prepare(spec, {
|
|
206
|
+
owner,
|
|
207
|
+
keys: addresses,
|
|
208
|
+
live: live() ?? {},
|
|
209
|
+
now: at,
|
|
210
|
+
rent,
|
|
211
|
+
only,
|
|
212
|
+
slot: hashes[0] ?? null,
|
|
213
|
+
});
|
|
174
214
|
if (!p.ok) throw new ApproveError(400, describe({ refusals: p.refusals, warnings: [] }));
|
|
175
215
|
return p;
|
|
176
216
|
}
|
|
@@ -179,7 +219,10 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
|
|
|
179
219
|
* What each step pays to whom and which keys it trusts, read back from its instructions, in full. A change says, for
|
|
180
220
|
* a record that is live already, how each line differs from what is on chain (`readLive`).
|
|
181
221
|
*/
|
|
182
|
-
const names = () =>
|
|
222
|
+
const names = () =>
|
|
223
|
+
namesOf(model(spec, { now: at }), {
|
|
224
|
+
treasury: madeSoFar(live())?.treasury ?? model(spec, { now: at }).treasuryWallet ?? null,
|
|
225
|
+
});
|
|
183
226
|
/**
|
|
184
227
|
* What is live, read from the chain once a wallet connects and the file's live section is checked: for each step,
|
|
185
228
|
* the lines of the records it writes as they are on chain now ([] for a record not there yet), and the treasury.
|
|
@@ -197,7 +240,16 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
|
|
|
197
240
|
if (lines) was.set(step.id, lines);
|
|
198
241
|
}
|
|
199
242
|
}
|
|
200
|
-
|
|
243
|
+
// A grant published before and gone from the chain (closed) that the change makes again with a list: a list leaf
|
|
244
|
+
// names no grant or incarnation, so the same root would pay every listed round again.
|
|
245
|
+
const remade = [];
|
|
246
|
+
const published = new Set((spec.live.published.grants ?? []).map(g => g.key));
|
|
247
|
+
for (const g of model(spec, { now: at }).grants) {
|
|
248
|
+
if (!g.list?.length || !published.has(g.key) || !p.ctx.project) continue;
|
|
249
|
+
if (!(await chain.account(pda.grant(p.ctx.project, g.id)).catch(() => 'unread')))
|
|
250
|
+
remade.push(`“${g.name ?? g.key}”`);
|
|
251
|
+
}
|
|
252
|
+
wasLive = { steps: was, treasury: p.ctx.treasury, remade };
|
|
201
253
|
}
|
|
202
254
|
const detailsOf = (t, i, n) => {
|
|
203
255
|
const lines = stepDetails(t.built[i], n);
|
|
@@ -205,12 +257,27 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
|
|
|
205
257
|
return was ? compareDetails(lines, was) : lines;
|
|
206
258
|
};
|
|
207
259
|
/** The lines a change is held against for its outside-address warning: what was read from the chain, or nothing yet. */
|
|
208
|
-
const wasLines = () =>
|
|
209
|
-
|
|
260
|
+
const wasLines = () =>
|
|
261
|
+
spec.live?.published
|
|
262
|
+
? [
|
|
263
|
+
...(wasLive ? [...wasLive.steps.values()].flat() : []),
|
|
264
|
+
...(wasLive?.treasury ? [`Treasury: ${wasLive.treasury}`] : []),
|
|
265
|
+
]
|
|
266
|
+
: null;
|
|
267
|
+
const remadeLists = () =>
|
|
268
|
+
wasLive?.remade?.length
|
|
269
|
+
? `This change makes ${wasLive.remade.join(', ')} again after it was closed, with the same list: a list names no grant, so every wallet on it could claim every listed round again. Give the grant a fresh list (new rounds) before you approve.`
|
|
270
|
+
: null;
|
|
271
|
+
const unread = () =>
|
|
272
|
+
spec.live?.published && !wasLive
|
|
273
|
+
? 'What is live has not been read from the chain yet: connect your wallet, and each line is marked against what is on chain.'
|
|
274
|
+
: null;
|
|
210
275
|
|
|
211
276
|
// The project's own settings a change names are never published: the page says so.
|
|
212
277
|
const changed = mode === 'change' ? changedParts(spec).settings : [];
|
|
213
|
-
const settings = changed.length
|
|
278
|
+
const settings = changed.length
|
|
279
|
+
? `This change names new project settings (${changed.join(', ')}) that publishing never writes: on chain they stay as they are.`
|
|
280
|
+
: null;
|
|
214
281
|
|
|
215
282
|
const legal = legalFlags(verdict);
|
|
216
283
|
|
|
@@ -218,7 +285,17 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
|
|
|
218
285
|
const said = new Map();
|
|
219
286
|
function view(p, n = names(), extra = {}) {
|
|
220
287
|
const done = new Set(live()?.landed ?? []);
|
|
221
|
-
const steps = p.ok
|
|
288
|
+
const steps = p.ok
|
|
289
|
+
? p.transactions.flatMap(t =>
|
|
290
|
+
t.steps.map((s, i) => ({
|
|
291
|
+
id: s.id,
|
|
292
|
+
says: s.says,
|
|
293
|
+
details: detailsOf(t, i, n),
|
|
294
|
+
signer: t.signer,
|
|
295
|
+
done: done.has(s.id),
|
|
296
|
+
})),
|
|
297
|
+
)
|
|
298
|
+
: [];
|
|
222
299
|
for (const s of steps) said.set(s.id, s);
|
|
223
300
|
return {
|
|
224
301
|
name: spec.name ?? path.basename(file),
|
|
@@ -227,13 +304,25 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
|
|
|
227
304
|
program: LOOT_PROGRAM,
|
|
228
305
|
cluster: 'devnet',
|
|
229
306
|
setupSol: cost(spec, { now: at, live: live(), only }).setup?.totalSol ?? null,
|
|
230
|
-
warnings: [
|
|
307
|
+
warnings: [
|
|
308
|
+
...(extra.refused ? [extra.refused] : []),
|
|
309
|
+
...verdict.warnings.filter(w => !Object.hasOwn(LEGAL, w.code)).map(w => w.message),
|
|
310
|
+
...[
|
|
311
|
+
settings,
|
|
312
|
+
unread(),
|
|
313
|
+
remadeLists(),
|
|
314
|
+
outsideAddresses(
|
|
315
|
+
steps.flatMap(s => s.details),
|
|
316
|
+
{ was: wasLines(), owner: n.owner },
|
|
317
|
+
),
|
|
318
|
+
].filter(Boolean),
|
|
319
|
+
],
|
|
231
320
|
// The legal flags in plain words, shown in a block of their own whatever the agent said: Approve waits until the
|
|
232
321
|
// owner ticks that they read them.
|
|
233
322
|
legal,
|
|
234
323
|
authority: model(spec).authority,
|
|
235
324
|
steps,
|
|
236
|
-
landed: [...done].map(
|
|
325
|
+
landed: [...done].map(id => ({ id, says: said.get(id)?.says ?? id, details: said.get(id)?.details ?? [] })),
|
|
237
326
|
signatures: [...signatures],
|
|
238
327
|
// The project the setup sends to, once a wallet is connected: always that wallet's own (or the one it is the
|
|
239
328
|
// authority of).
|
|
@@ -295,17 +384,26 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
|
|
|
295
384
|
const v = view(p0, namesOf(model(spec, { now: at }), { owner: wallet }), { refused: e.message });
|
|
296
385
|
return { ...v, remaining: p0.ok ? p0.transactions.length : 0, done: false };
|
|
297
386
|
}
|
|
298
|
-
if (!p0.ok || !p0.transactions.length)
|
|
387
|
+
if (!p0.ok || !p0.transactions.length)
|
|
388
|
+
return { ...view(p0, names(), { project: trusted.project }), remaining: 0, done: p0.ok };
|
|
299
389
|
const p = await build(trusted.owner);
|
|
300
390
|
await readLive(p);
|
|
301
|
-
const v = view(p, namesOf(model(spec, { now: at }), { owner: wallet, treasury: p.ctx.treasury }), {
|
|
391
|
+
const v = view(p, namesOf(model(spec, { now: at }), { owner: wallet, treasury: p.ctx.treasury }), {
|
|
392
|
+
project: p.ctx.project,
|
|
393
|
+
});
|
|
302
394
|
return { ...v, remaining: p.transactions.length, done: false };
|
|
303
395
|
}
|
|
304
396
|
|
|
305
397
|
async function prepareFor(wallet) {
|
|
306
398
|
// A second ask while a batch is out for signing (a double click) gets that same batch, not a new one that would
|
|
307
399
|
// make the first signature fail.
|
|
308
|
-
if (
|
|
400
|
+
if (
|
|
401
|
+
prepared &&
|
|
402
|
+
prepared.wallet === wallet &&
|
|
403
|
+
prepared.generation === generation &&
|
|
404
|
+
prepared.expires > clock() + 15_000
|
|
405
|
+
)
|
|
406
|
+
return answer(prepared);
|
|
309
407
|
await settlePending();
|
|
310
408
|
const trusted = await trust(wallet);
|
|
311
409
|
const p = await build(trusted.owner);
|
|
@@ -314,18 +412,24 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
|
|
|
314
412
|
const authority = p.ctx.authority;
|
|
315
413
|
const first = p.transactions[0];
|
|
316
414
|
const signer = first.signer === 'authority' ? authority : p.ctx.owner;
|
|
317
|
-
if (signer !== wallet)
|
|
415
|
+
if (signer !== wallet)
|
|
416
|
+
throw new ApproveError(
|
|
417
|
+
403,
|
|
418
|
+
first.signer === 'authority'
|
|
419
|
+
? `The next step is the authority’s: connect the wallet ${authority} and approve it there.`
|
|
420
|
+
: `The next step is the project owner’s: connect the wallet ${p.ctx.owner} and approve it there.`,
|
|
421
|
+
);
|
|
318
422
|
const batch = [];
|
|
319
423
|
for (const t of p.transactions) {
|
|
320
424
|
if (t.signer !== first.signer || batch.length >= PER_APPROVAL) break;
|
|
321
425
|
batch.push(t);
|
|
322
426
|
}
|
|
323
427
|
const { blockhash, lastValidBlockHeight } = await chain.latestBlockhash();
|
|
324
|
-
const wires = batch.map(
|
|
428
|
+
const wires = batch.map(t => {
|
|
325
429
|
const message = compileMessage({ payer: wallet, blockhash, instructions: t.instructions });
|
|
326
430
|
const { signers } = readTransaction(wireTransaction(message));
|
|
327
|
-
const sigs = signers.map(
|
|
328
|
-
const fresh = [...keys.values()].find(
|
|
431
|
+
const sigs = signers.map(s => {
|
|
432
|
+
const fresh = [...keys.values()].find(k => k.address === s);
|
|
329
433
|
return fresh ? fresh.sign(message) : new Uint8Array(64);
|
|
330
434
|
});
|
|
331
435
|
return wireTransaction(message, sigs);
|
|
@@ -333,13 +437,27 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
|
|
|
333
437
|
// What each transaction pays to whom, read back from the very instructions the wallet signs.
|
|
334
438
|
const n = namesOf(model(spec, { now: at }), { owner: wallet, treasury: p.ctx.treasury });
|
|
335
439
|
// Every line names the project it goes to first: always the wallet's own, or the one it is the authority of.
|
|
336
|
-
const details = batch.map(
|
|
337
|
-
|
|
440
|
+
const details = batch.map(t => [
|
|
441
|
+
`Project: ${p.ctx.project}${p.ctx.owner === wallet ? ' (your own)' : ` (owned by ${p.ctx.owner})`}`,
|
|
442
|
+
...t.steps.flatMap((_, i) => detailsOf(t, i, n)),
|
|
443
|
+
]);
|
|
444
|
+
prepared = {
|
|
445
|
+
id: randomBytes(12).toString('hex'),
|
|
446
|
+
wallet,
|
|
447
|
+
wires,
|
|
448
|
+
batch,
|
|
449
|
+
details,
|
|
450
|
+
ctx: p.ctx,
|
|
451
|
+
lastValidBlockHeight,
|
|
452
|
+
generation,
|
|
453
|
+
expires: clock() + PREPARE_TTL_MS,
|
|
454
|
+
};
|
|
338
455
|
return answer(prepared);
|
|
339
456
|
}
|
|
340
457
|
|
|
341
458
|
async function complete(wallet, id, signed) {
|
|
342
|
-
if (!prepared || !sameText(prepared.id, String(id ?? '')))
|
|
459
|
+
if (!prepared || !sameText(prepared.id, String(id ?? '')))
|
|
460
|
+
throw new ApproveError(409, 'That batch is not the one prepared: ask for the next one.');
|
|
343
461
|
if (prepared.expires < clock()) {
|
|
344
462
|
prepared = null;
|
|
345
463
|
throw new ApproveError(410, 'That batch expired (90 seconds). Ask for it again.');
|
|
@@ -347,7 +465,8 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
|
|
|
347
465
|
if (prepared.wallet !== wallet) throw new ApproveError(403, 'That batch was prepared for another wallet.');
|
|
348
466
|
const batch = prepared;
|
|
349
467
|
prepared = null;
|
|
350
|
-
if (batch.generation !== generation)
|
|
468
|
+
if (batch.generation !== generation)
|
|
469
|
+
throw new ApproveError(409, 'Something landed after that batch was built: ask for the next one.');
|
|
351
470
|
const wires = checkSigned(batch.wires, signed, wallet);
|
|
352
471
|
busy = true;
|
|
353
472
|
try {
|
|
@@ -357,7 +476,12 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
|
|
|
357
476
|
// sent twice (`settlePending`).
|
|
358
477
|
const signature = encodeBase58(readTransaction(wire).signatures[0]);
|
|
359
478
|
const sent = batch.batch[i];
|
|
360
|
-
await remember({
|
|
479
|
+
await remember({
|
|
480
|
+
signature,
|
|
481
|
+
steps: sent.steps.map(s => s.id),
|
|
482
|
+
made: madeBy(sent, batch.ctx),
|
|
483
|
+
lastValidBlockHeight: batch.lastValidBlockHeight,
|
|
484
|
+
});
|
|
361
485
|
try {
|
|
362
486
|
await chain.send(wire);
|
|
363
487
|
} catch (e) {
|
|
@@ -373,7 +497,11 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
|
|
|
373
497
|
const status = chain.signatureStatus ? await chain.signatureStatus(signature).catch(() => null) : null;
|
|
374
498
|
if (!landedStatus(status)) {
|
|
375
499
|
if (status?.err || !chain.signatureStatus) await forget(signature);
|
|
376
|
-
else
|
|
500
|
+
else
|
|
501
|
+
throw new ApproveError(
|
|
502
|
+
503,
|
|
503
|
+
`${e.message} It may still land: it is looked up on chain before anything is sent again, so approving again never sends it twice. Wait a minute, then ask for the next batch.`,
|
|
504
|
+
);
|
|
377
505
|
throw e;
|
|
378
506
|
}
|
|
379
507
|
}
|
|
@@ -388,17 +516,26 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
|
|
|
388
516
|
|
|
389
517
|
/** A prepared batch as the page gets it: the wires to sign, and what each does in words. */
|
|
390
518
|
function answer(b) {
|
|
391
|
-
return {
|
|
519
|
+
return {
|
|
520
|
+
id: b.id,
|
|
521
|
+
project: b.ctx.project,
|
|
522
|
+
transactions: b.wires.map(toBase64),
|
|
523
|
+
says: b.batch.map(t => t.steps.map(s => s.says).join('; ')),
|
|
524
|
+
details: b.details,
|
|
525
|
+
};
|
|
392
526
|
}
|
|
393
527
|
|
|
394
528
|
/** What a transaction's steps make, from the context it was built with: recorded when it lands. */
|
|
395
529
|
function madeBy(transaction, ctx) {
|
|
396
|
-
const ids = new Set(transaction.steps.map(
|
|
397
|
-
const mints = Object.fromEntries(
|
|
530
|
+
const ids = new Set(transaction.steps.map(s => s.id));
|
|
531
|
+
const mints = Object.fromEntries(
|
|
532
|
+
[...ids].filter(id => id.startsWith('token:')).map(id => [id.slice(6), ctx.mints[id.slice(6)]]),
|
|
533
|
+
);
|
|
398
534
|
return {
|
|
399
535
|
...(ids.has('project') ? { project: ctx.project, treasury: ctx.treasury, authority: ctx.authority } : {}),
|
|
400
536
|
...(ids.has('collection') && ctx.collection ? { collection: ctx.collection } : {}),
|
|
401
537
|
...(ids.has('tree') && ctx.tree ? { tree: ctx.tree } : {}),
|
|
538
|
+
...(ids.has('table') && ctx.slot != null ? { tables: [tableAddress(ctx.owner, ctx.slot)[0]] } : {}),
|
|
402
539
|
...(Object.keys(mints).length ? { mints } : {}),
|
|
403
540
|
};
|
|
404
541
|
}
|
|
@@ -409,7 +546,16 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
|
|
|
409
546
|
*/
|
|
410
547
|
async function remember(entry) {
|
|
411
548
|
const was = spec.live ?? {};
|
|
412
|
-
spec = {
|
|
549
|
+
spec = {
|
|
550
|
+
...spec,
|
|
551
|
+
live: {
|
|
552
|
+
...was,
|
|
553
|
+
landed: was.landed ?? [],
|
|
554
|
+
partial: true,
|
|
555
|
+
...(only ? { changing: [...only] } : {}),
|
|
556
|
+
pending: [...(was.pending ?? []), entry].slice(-60),
|
|
557
|
+
},
|
|
558
|
+
};
|
|
413
559
|
generation++;
|
|
414
560
|
await write(file, `${JSON.stringify(spec, null, 2)}\n`);
|
|
415
561
|
}
|
|
@@ -417,7 +563,7 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
|
|
|
417
563
|
/** Takes a transaction out of the pending list: it landed (and is recorded), failed, or can no longer land. */
|
|
418
564
|
async function forget(signature) {
|
|
419
565
|
if (!spec.live?.pending) return;
|
|
420
|
-
const pending = spec.live.pending.filter(
|
|
566
|
+
const pending = spec.live.pending.filter(p => p.signature !== signature);
|
|
421
567
|
const { pending: _, ...rest } = spec.live;
|
|
422
568
|
spec = { ...spec, live: pending.length ? { ...rest, pending } : rest };
|
|
423
569
|
generation++;
|
|
@@ -428,7 +574,9 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
|
|
|
428
574
|
async function projectOwner(project) {
|
|
429
575
|
if (!project || !chain.account) return null;
|
|
430
576
|
const account = await chain.account(project).catch(() => null);
|
|
431
|
-
return account && account.owner === LOOT_PROGRAM && accountKind(account.data) === 'Project'
|
|
577
|
+
return account && account.owner === LOOT_PROGRAM && accountKind(account.data) === 'Project'
|
|
578
|
+
? decode('Project', account.data).owner
|
|
579
|
+
: null;
|
|
432
580
|
}
|
|
433
581
|
|
|
434
582
|
/**
|
|
@@ -445,7 +593,11 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
|
|
|
445
593
|
const status = await chain.signatureStatus(entry.signature);
|
|
446
594
|
if (landedStatus(status)) {
|
|
447
595
|
if (!signatures.includes(entry.signature)) signatures.push(entry.signature);
|
|
448
|
-
await landed(
|
|
596
|
+
await landed(
|
|
597
|
+
{ steps: entry.steps.map(id => ({ id })) },
|
|
598
|
+
{ ...(entry.made ?? {}), owner: await projectOwner(entry.made?.project ?? spec.live?.project) },
|
|
599
|
+
entry.signature,
|
|
600
|
+
);
|
|
449
601
|
continue;
|
|
450
602
|
}
|
|
451
603
|
if (status?.err) {
|
|
@@ -458,7 +610,10 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
|
|
|
458
610
|
await forget(entry.signature);
|
|
459
611
|
continue;
|
|
460
612
|
}
|
|
461
|
-
throw new ApproveError(
|
|
613
|
+
throw new ApproveError(
|
|
614
|
+
409,
|
|
615
|
+
`The transaction ${entry.signature}, sent earlier, has not landed yet and may still: wait a minute and try again, so nothing is sent twice.`,
|
|
616
|
+
);
|
|
462
617
|
}
|
|
463
618
|
}
|
|
464
619
|
|
|
@@ -469,23 +624,27 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
|
|
|
469
624
|
*/
|
|
470
625
|
async function landed(transaction, ctx, signature) {
|
|
471
626
|
const was = spec.live ?? {};
|
|
472
|
-
const ids = transaction.steps.map(
|
|
627
|
+
const ids = transaction.steps.map(s => s.id);
|
|
473
628
|
const made = new Set(ids);
|
|
474
629
|
const mints = { ...(was.mints ?? {}) };
|
|
475
|
-
for (const id of ids)
|
|
630
|
+
for (const id of ids)
|
|
631
|
+
if (id.startsWith('token:') && ctx.mints?.[id.slice(6)]) mints[id.slice(6)] = ctx.mints[id.slice(6)];
|
|
476
632
|
const progress = {
|
|
477
633
|
...was,
|
|
478
634
|
program: LOOT_PROGRAM,
|
|
479
635
|
...(made.has('project') ? { project: ctx.project, treasury: ctx.treasury, authority: ctx.authority } : {}),
|
|
480
636
|
...(made.has('collection') && ctx.collection ? { collection: ctx.collection } : {}),
|
|
481
637
|
...(made.has('tree') && ctx.tree ? { tree: ctx.tree } : {}),
|
|
638
|
+
...(made.has('table') && ctx.slot != null ? { tables: [tableAddress(ctx.owner, ctx.slot)[0]] } : {}),
|
|
482
639
|
mints,
|
|
483
640
|
landed: [...new Set([...(was.landed ?? []), ...ids])],
|
|
484
641
|
partial: true,
|
|
485
642
|
...(only ? { changing: [...only] } : {}),
|
|
486
643
|
// (each signature once: one found again by `settlePending` may have been recorded already)
|
|
487
|
-
transactions: (was.transactions ?? []).some(
|
|
488
|
-
|
|
644
|
+
transactions: (was.transactions ?? []).some(t => t.signature === signature)
|
|
645
|
+
? was.transactions
|
|
646
|
+
: [...(was.transactions ?? []), { signature }].slice(-60),
|
|
647
|
+
pending: (was.pending ?? []).filter(p => p.signature !== signature),
|
|
489
648
|
};
|
|
490
649
|
if (!progress.pending.length) delete progress.pending;
|
|
491
650
|
if (!Object.keys(progress.mints).length) delete progress.mints;
|
|
@@ -496,7 +655,13 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
|
|
|
496
655
|
if (rest.ok && rest.transactions.length === 0) {
|
|
497
656
|
const { live: _, ...bare } = spec;
|
|
498
657
|
// Everything the setup made, whichever session or transaction made it.
|
|
499
|
-
const all = {
|
|
658
|
+
const all = {
|
|
659
|
+
...ctx,
|
|
660
|
+
...Object.fromEntries(
|
|
661
|
+
['project', 'treasury', 'authority', 'collection', 'tree', 'tables'].map(k => [k, progress[k] ?? ctx[k]]),
|
|
662
|
+
),
|
|
663
|
+
mints: { ...(ctx.mints ?? {}), ...(progress.mints ?? {}) },
|
|
664
|
+
};
|
|
500
665
|
const final = recordLanded(bare, { ctx: all, now: at, at: new Date(clock()).toISOString() });
|
|
501
666
|
final.live = { ...final.live, transactions: progress.transactions };
|
|
502
667
|
spec = final;
|
|
@@ -514,12 +679,19 @@ export async function createApproval({ file, chain = devnetChain(), now = () =>
|
|
|
514
679
|
function boundedRent(bytes, lamports) {
|
|
515
680
|
const most = (BigInt(bytes) + ACCOUNT_OVERHEAD) * MOST_RENT_PER_BYTE;
|
|
516
681
|
const given = BigInt(lamports);
|
|
517
|
-
if (given > most)
|
|
682
|
+
if (given > most)
|
|
683
|
+
throw new ApproveError(
|
|
684
|
+
502,
|
|
685
|
+
`The RPC says an account of ${bytes} bytes needs ${given} lamports of rent; no cluster charges more than ${most}. Nothing was sent: use another RPC (XSOLANA_RPC_URL).`,
|
|
686
|
+
);
|
|
518
687
|
return given;
|
|
519
688
|
}
|
|
520
689
|
|
|
521
690
|
/** Whether a signature's status says it landed: confirmed or finalized, and not failed. */
|
|
522
|
-
const landedStatus =
|
|
691
|
+
const landedStatus = status =>
|
|
692
|
+
Boolean(
|
|
693
|
+
status && !status.err && (status.confirmationStatus === 'confirmed' || status.confirmationStatus === 'finalized'),
|
|
694
|
+
);
|
|
523
695
|
|
|
524
696
|
const sameText = (a, b) => {
|
|
525
697
|
const x = Buffer.from(String(a));
|
|
@@ -529,7 +701,8 @@ const sameText = (a, b) => {
|
|
|
529
701
|
|
|
530
702
|
/** The wallet's signed copies: each the exact prepared message, each with the wallet's valid signature. */
|
|
531
703
|
function checkSigned(prepared, signed, wallet) {
|
|
532
|
-
if (!Array.isArray(signed) || signed.length !== prepared.length)
|
|
704
|
+
if (!Array.isArray(signed) || signed.length !== prepared.length)
|
|
705
|
+
throw new ApproveError(400, 'The wallet’s answer does not match the request.');
|
|
533
706
|
return prepared.map((wire, i) => {
|
|
534
707
|
let mine;
|
|
535
708
|
let theirs;
|
|
@@ -539,11 +712,13 @@ function checkSigned(prepared, signed, wallet) {
|
|
|
539
712
|
} catch {
|
|
540
713
|
throw new ApproveError(400, 'The wallet returned something that is not a transaction.');
|
|
541
714
|
}
|
|
542
|
-
if (!equalBytes(mine.message, theirs.message))
|
|
715
|
+
if (!equalBytes(mine.message, theirs.message))
|
|
716
|
+
throw new ApproveError(400, 'The wallet changed the transaction before signing it, so nothing was sent.');
|
|
543
717
|
const slot = mine.signers.indexOf(wallet);
|
|
544
718
|
if (slot < 0) throw new ApproveError(400, 'This transaction was not prepared for that wallet.');
|
|
545
719
|
const signature = theirs.signatures[slot];
|
|
546
|
-
if (!verifySignature(wallet, mine.message, signature))
|
|
720
|
+
if (!verifySignature(wallet, mine.message, signature))
|
|
721
|
+
throw new ApproveError(400, 'The wallet’s signature is missing or does not match your wallet.');
|
|
547
722
|
const signatures = [...mine.signatures];
|
|
548
723
|
signatures[slot] = signature;
|
|
549
724
|
return wireTransaction(mine.message, signatures);
|
|
@@ -574,7 +749,7 @@ export function ownRequest(headers, port) {
|
|
|
574
749
|
const hosts = [`127.0.0.1:${port}`, `localhost:${port}`];
|
|
575
750
|
if (!hosts.includes(String(headers.host ?? '').toLowerCase())) return false;
|
|
576
751
|
if (headers.origin === undefined) return true;
|
|
577
|
-
return hosts.some(
|
|
752
|
+
return hosts.some(h => String(headers.origin).toLowerCase() === `http://${h}`);
|
|
578
753
|
}
|
|
579
754
|
|
|
580
755
|
/** A request's body, as text: refused (413) past `limit` bytes, and the rest of it read and let go. */
|
|
@@ -582,7 +757,7 @@ function readBody(req, limit = BODY_LIMIT) {
|
|
|
582
757
|
return new Promise((resolve, reject) => {
|
|
583
758
|
const chunks = [];
|
|
584
759
|
let size = 0;
|
|
585
|
-
req.on('data',
|
|
760
|
+
req.on('data', chunk => {
|
|
586
761
|
size += chunk.length;
|
|
587
762
|
if (size > limit) return reject(new ApproveError(413, 'That request is too large.'));
|
|
588
763
|
chunks.push(chunk);
|
|
@@ -614,9 +789,18 @@ export async function openBrowser(url, { spawnImpl = spawn, dir = tmpdir() } = {
|
|
|
614
789
|
try {
|
|
615
790
|
await chmod(folder, 0o700);
|
|
616
791
|
const file = path.join(folder, 'open.html');
|
|
617
|
-
const href = url.replace(/[&<>"']/g,
|
|
618
|
-
await writeFile(
|
|
619
|
-
|
|
792
|
+
const href = url.replace(/[&<>"']/g, c => `&#${c.charCodeAt(0)};`);
|
|
793
|
+
await writeFile(
|
|
794
|
+
file,
|
|
795
|
+
`<!doctype html><meta charset="utf-8"><meta name="referrer" content="no-referrer"><meta http-equiv="refresh" content="0;url=${href}"><title>xsolana approve</title><a href="${href}">Open the approval page</a>\n`,
|
|
796
|
+
{ mode: 0o600, flag: 'wx' },
|
|
797
|
+
);
|
|
798
|
+
const [cmd, args] =
|
|
799
|
+
process.platform === 'darwin'
|
|
800
|
+
? ['open', [file]]
|
|
801
|
+
: process.platform === 'win32'
|
|
802
|
+
? ['cmd', ['/c', 'start', '', file]]
|
|
803
|
+
: ['xdg-open', [file]];
|
|
620
804
|
spawnImpl(cmd, args, { stdio: 'ignore', detached: true }).unref();
|
|
621
805
|
} catch {
|
|
622
806
|
/* the link is printed anyway */
|
|
@@ -631,16 +815,33 @@ export async function openBrowser(url, { spawnImpl = spawn, dir = tmpdir() } = {
|
|
|
631
815
|
* after. The default `openLink` keeps even the code off the opener's command line (`openBrowser`); one that answers a
|
|
632
816
|
* function has it called once the code is used.
|
|
633
817
|
*/
|
|
634
|
-
export async function approve({
|
|
818
|
+
export async function approve({
|
|
819
|
+
file = 'xsolana.json',
|
|
820
|
+
port,
|
|
821
|
+
open = true,
|
|
822
|
+
chain = devnetChain(),
|
|
823
|
+
log = (...a) => console.error(...a),
|
|
824
|
+
openLink = openBrowser,
|
|
825
|
+
} = {}) {
|
|
635
826
|
const session = await createApproval({ file: path.resolve(file), chain });
|
|
636
827
|
const token = randomBytes(18).toString('base64url');
|
|
637
828
|
let code = randomBytes(18).toString('base64url');
|
|
638
829
|
// (removes the file the browser was opened with, once: `openLink` answers how)
|
|
639
830
|
let removeOpener = null;
|
|
640
|
-
const forget = () => {
|
|
831
|
+
const forget = () => {
|
|
832
|
+
const remove = removeOpener;
|
|
833
|
+
removeOpener = null;
|
|
834
|
+
remove?.();
|
|
835
|
+
};
|
|
641
836
|
const page = await readFile(path.join(ROOT, 'approve/page.html'), 'utf8');
|
|
642
837
|
const policy = pagePolicy(page);
|
|
643
|
-
const headers = {
|
|
838
|
+
const headers = {
|
|
839
|
+
'cache-control': 'no-store',
|
|
840
|
+
'x-content-type-options': 'nosniff',
|
|
841
|
+
'referrer-policy': 'no-referrer',
|
|
842
|
+
'content-security-policy': policy,
|
|
843
|
+
'x-frame-options': 'DENY',
|
|
844
|
+
};
|
|
644
845
|
const server = createServer(async (req, res) => {
|
|
645
846
|
const url = new URL(req.url, 'http://127.0.0.1');
|
|
646
847
|
const send = (status, body, type = 'application/json') => {
|
|
@@ -649,27 +850,41 @@ export async function approve({ file = 'xsolana.json', port, open = true, chain
|
|
|
649
850
|
};
|
|
650
851
|
try {
|
|
651
852
|
// Only a request to this server by its own name (DNS rebinding: a site whose name now points at 127.0.0.1).
|
|
652
|
-
if (!ownRequest(req.headers, server.address().port))
|
|
853
|
+
if (!ownRequest(req.headers, server.address().port))
|
|
854
|
+
return send(403, 'Open the link `xsolana approve` printed.', 'text/plain');
|
|
653
855
|
if (req.method === 'GET' && url.pathname === '/') {
|
|
654
856
|
const once = url.searchParams.get('c');
|
|
655
857
|
if (once !== null) {
|
|
656
|
-
if (!code || !sameText(once, code))
|
|
858
|
+
if (!code || !sameText(once, code))
|
|
859
|
+
return send(
|
|
860
|
+
403,
|
|
861
|
+
'This link has been opened already: open the link `xsolana approve` printed.',
|
|
862
|
+
'text/plain',
|
|
863
|
+
);
|
|
657
864
|
code = null;
|
|
658
865
|
forget();
|
|
659
866
|
res.writeHead(303, { ...headers, location: `/?t=${token}` });
|
|
660
867
|
return res.end();
|
|
661
868
|
}
|
|
662
|
-
if (!sameText(url.searchParams.get('t') ?? '', token))
|
|
869
|
+
if (!sameText(url.searchParams.get('t') ?? '', token))
|
|
870
|
+
return send(403, 'Open the link `xsolana approve` printed.', 'text/plain');
|
|
663
871
|
return send(200, page.replace('__TOKEN__', token), 'text/html; charset=utf-8');
|
|
664
872
|
}
|
|
665
|
-
if (req.method === 'GET' && BROWSER_FILES.test(url.pathname))
|
|
666
|
-
|
|
873
|
+
if (req.method === 'GET' && BROWSER_FILES.test(url.pathname))
|
|
874
|
+
return send(
|
|
875
|
+
200,
|
|
876
|
+
await readFile(path.join(ROOT, url.pathname.slice(4)), 'utf8'),
|
|
877
|
+
'text/javascript; charset=utf-8',
|
|
878
|
+
);
|
|
879
|
+
if (!sameText(req.headers['x-xsolana-token'] ?? '', token))
|
|
880
|
+
return send(403, { error: 'Not this session’s page.' });
|
|
667
881
|
if (req.method === 'GET' && url.pathname === '/api/state') {
|
|
668
882
|
const wallet = url.searchParams.get('wallet');
|
|
669
883
|
return send(200, wallet ? await session.stateFor(wallet) : session.state());
|
|
670
884
|
}
|
|
671
885
|
const body = req.method === 'POST' ? await readJson(req) : {};
|
|
672
|
-
if (req.method === 'POST' && url.pathname === '/api/prepare')
|
|
886
|
+
if (req.method === 'POST' && url.pathname === '/api/prepare')
|
|
887
|
+
return send(200, await session.prepare(String(body.wallet ?? '')));
|
|
673
888
|
if (req.method === 'POST' && url.pathname === '/api/complete') {
|
|
674
889
|
const state = await session.complete(String(body.wallet ?? ''), body.id, body.transactions);
|
|
675
890
|
if (state.done) {
|
|
@@ -697,7 +912,9 @@ export async function approve({ file = 'xsolana.json', port, open = true, chain
|
|
|
697
912
|
const link = `${origin}/?t=${token}`;
|
|
698
913
|
const opened = `${origin}/?c=${code}`;
|
|
699
914
|
log(`Approve going live in your wallet (devnet): ${link}`);
|
|
700
|
-
log(
|
|
915
|
+
log(
|
|
916
|
+
'Leave this running until the page says it is live. Ctrl+C stops it; running it again goes on from where it stopped.',
|
|
917
|
+
);
|
|
701
918
|
if (open && !process.env.XSOLANA_NO_OPEN) {
|
|
702
919
|
const remove = await openLink(opened);
|
|
703
920
|
if (typeof remove === 'function') {
|