@xgamesfun/solana 0.26.2 → 0.26.4
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 +11 -8
- package/bin/xsolana.mjs +8 -1
- package/examples/blind-forge.json +2 -1
- package/examples/community-coin.json +100 -0
- package/js/approve/live-check.mjs +217 -3
- package/js/approve/page.html +1 -1
- package/js/approve/page.js +41 -11
- package/js/approve/server.mjs +236 -41
- package/js/chain/metadata.js +92 -0
- package/js/chain/mint.js +55 -0
- package/js/chain/token2022.js +89 -0
- package/js/economy/check.mjs +43 -5
- package/js/economy/cost.mjs +6 -5
- package/js/economy/explain.mjs +4 -3
- package/js/economy/guide.mjs +2 -2
- package/js/economy/legal.mjs +2 -0
- package/js/economy/live.mjs +27 -7
- package/js/economy/model.mjs +13 -2
- package/js/economy/plan.mjs +84 -27
- package/js/economy/schema.json +7 -1
- package/js/mcp/tools.mjs +174 -11
- package/package.json +1 -1
- package/release.json +1 -1
- package/skills/xsolana-economy/SKILL.md +27 -5
- package/skills/xsolana-economy/references/sentence-to-spec.md +1 -1
- package/skills/xsolana-economy/references/telling-the-person.md +7 -3
- package/types/chain/metadata.d.ts +45 -0
- package/types/chain/mint.d.ts +29 -0
- package/types/chain/token2022.d.ts +38 -0
- package/types/economy/cost.d.mts +11 -3
- package/types/economy/legal.d.mts +1 -0
- package/types/economy/live.d.mts +29 -5
- package/types/economy/model.d.mts +6 -0
- package/types/economy/plan.d.mts +22 -5
- package/types/mcp/server.d.mts +53 -1
- package/types/mcp/tools.d.mts +111 -4
package/js/approve/server.mjs
CHANGED
|
@@ -27,7 +27,7 @@ import { ACCOUNT_OVERHEAD, compressedTree, model, MOST_RENT_PER_BYTE } from '../
|
|
|
27
27
|
import { madeSoFar, plan, PER_APPROVAL } from '../economy/plan.mjs';
|
|
28
28
|
import { compareDetails, liveStepDetails, namesOf, outsideAddresses, stepDetails } from './details.mjs';
|
|
29
29
|
import { freshKeypair, verifySignature } from './ed25519.mjs';
|
|
30
|
-
import { checkLive, checkMints, LiveMismatch, MintMismatch } from './live-check.mjs';
|
|
30
|
+
import { checkLive, checkMints, LiveMismatch, MintMismatch, readRenames } from './live-check.mjs';
|
|
31
31
|
|
|
32
32
|
const PREPARE_TTL_MS = 90_000;
|
|
33
33
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
@@ -71,6 +71,8 @@ export function devnetChain(url = process.env.XSOLANA_RPC_URL || DEVNET) {
|
|
|
71
71
|
signatureStatus: async signature =>
|
|
72
72
|
(await rpc.call('getSignatureStatuses', [[signature], { searchTransactionHistory: true }])).value[0],
|
|
73
73
|
blockHeight: () => rpc.blockHeight(),
|
|
74
|
+
/** The chain's current epoch (a mint's transfer fee changes on one). */
|
|
75
|
+
epoch: async () => Number((await rpc.call('getEpochInfo')).epoch),
|
|
74
76
|
};
|
|
75
77
|
}
|
|
76
78
|
|
|
@@ -148,11 +150,60 @@ export async function createApproval({
|
|
|
148
150
|
const signatures = [];
|
|
149
151
|
const at = now();
|
|
150
152
|
const mode = !spec.live ? 'new' : spec.live.partial ? 'resume' : 'change';
|
|
151
|
-
/**
|
|
152
|
-
|
|
153
|
+
/**
|
|
154
|
+
* The parts this session publishes: all of them for a new economy, what changed for a live one. A change that was
|
|
155
|
+
* cut short (`partial`, with what was `published` before it) goes on as what the file says changed now, with what
|
|
156
|
+
* it was changing when it stopped: a part added to the spec since is published too, never recorded as live unsent,
|
|
157
|
+
* and a change that had no part to publish (a token's name alone) is still a change.
|
|
158
|
+
*/
|
|
159
|
+
let only =
|
|
160
|
+
mode === 'change'
|
|
161
|
+
? changedParts(spec).only
|
|
162
|
+
: spec.live?.published
|
|
163
|
+
? new Set([...(spec.live.changing ?? []), ...changedParts(spec).only])
|
|
164
|
+
: spec.live?.changing
|
|
165
|
+
? new Set(spec.live.changing)
|
|
166
|
+
: null;
|
|
167
|
+
if (mode === 'resume' && spec.live.published) spec = { ...spec, live: { ...spec.live, changing: [...only] } };
|
|
168
|
+
/**
|
|
169
|
+
* The live tokens whose name record this session writes again (plan.mjs `renames`), and in words the names the spec
|
|
170
|
+
* changes that can't be written (`held`: their keys). Until a wallet connects: what the file itself says changed
|
|
171
|
+
* since it was published. Once one has: what the chain says (`readRenames`), which is all that is ever sent.
|
|
172
|
+
*/
|
|
173
|
+
let renames = new Set(spec.live?.published ? changedParts(spec).renames : []);
|
|
174
|
+
let renameNotes = [];
|
|
175
|
+
let held = new Set();
|
|
176
|
+
/** The tokens whose record already says the spec's new name, though the file's published spec says the old one. */
|
|
177
|
+
let settled = new Set();
|
|
178
|
+
/**
|
|
179
|
+
* The names this session has written itself, with this spec's words: never offered again, even to a connection
|
|
180
|
+
* that reads a slot behind. (A name an earlier session sent is not among them: the spec may have changed since, so
|
|
181
|
+
* the chain alone says whether it is the one wanted.)
|
|
182
|
+
*/
|
|
183
|
+
const renamed = new Set();
|
|
184
|
+
/** The transactions this session sent itself. */
|
|
185
|
+
const sentHere = new Set();
|
|
186
|
+
/** What a plan of this session is scoped by. */
|
|
187
|
+
const scope = () => ({ only, renames });
|
|
153
188
|
|
|
154
189
|
/** What is live so far: the spec's own live section (a resumed or changed economy) and what landed this session. */
|
|
155
190
|
const live = () => spec.live ?? null;
|
|
191
|
+
/**
|
|
192
|
+
* What the chain says the keys of the mints this spec names can do, where the spec doesn't say it (`checkMints`,
|
|
193
|
+
* read when a wallet connects): shown with the legal flags, so the owner ticks having read them before approving.
|
|
194
|
+
*/
|
|
195
|
+
let mintPowers = [];
|
|
196
|
+
/**
|
|
197
|
+
* A name for those lines as they stand (null: none). The page hands it back with the owner's tick: a batch is given
|
|
198
|
+
* out only to a page that showed these very lines, so nothing read after the tick, or never shown, is skipped.
|
|
199
|
+
*/
|
|
200
|
+
const readKey = () =>
|
|
201
|
+
mintPowers.length
|
|
202
|
+
? createHash('sha256')
|
|
203
|
+
.update(JSON.stringify(mintPowers.map(t => t.says)))
|
|
204
|
+
.digest('hex')
|
|
205
|
+
.slice(0, 24)
|
|
206
|
+
: null;
|
|
156
207
|
|
|
157
208
|
/**
|
|
158
209
|
* The owner a setup for `wallet` is built for, and its project, once every mint the spec names is held against the
|
|
@@ -162,7 +213,10 @@ export async function createApproval({
|
|
|
162
213
|
*/
|
|
163
214
|
async function trust(wallet) {
|
|
164
215
|
try {
|
|
165
|
-
|
|
216
|
+
const m = model(spec, { now: at });
|
|
217
|
+
// the project a mint the spec calls the project's must name: the live one, else this wallet's own to be
|
|
218
|
+
const project = live()?.project ?? pda.project(wallet, m.projectNumber);
|
|
219
|
+
mintPowers = (await checkMints({ m, chain, project })).powers;
|
|
166
220
|
} catch (e) {
|
|
167
221
|
if (!(e instanceof MintMismatch)) throw e;
|
|
168
222
|
throw new ApproveError(
|
|
@@ -171,7 +225,21 @@ export async function createApproval({
|
|
|
171
225
|
);
|
|
172
226
|
}
|
|
173
227
|
try {
|
|
174
|
-
|
|
228
|
+
const m = model(spec, { now: at });
|
|
229
|
+
const trusted = await checkLive({ live: live(), m, wallet, chain });
|
|
230
|
+
// which names are written again is the chain's to say, read for the project's owner
|
|
231
|
+
const read = await readRenames({
|
|
232
|
+
live: live(),
|
|
233
|
+
m,
|
|
234
|
+
chain,
|
|
235
|
+
owner: trusted.owner,
|
|
236
|
+
published: spec.live?.published ?? null,
|
|
237
|
+
});
|
|
238
|
+
renames = new Set(read.renames.filter(key => !renamed.has(key)));
|
|
239
|
+
renameNotes = read.notes;
|
|
240
|
+
held = new Set(read.held);
|
|
241
|
+
settled = new Set(read.settled);
|
|
242
|
+
return trusted;
|
|
175
243
|
} catch (e) {
|
|
176
244
|
if (!(e instanceof LiveMismatch)) throw e;
|
|
177
245
|
throw new ApproveError(
|
|
@@ -182,7 +250,7 @@ export async function createApproval({
|
|
|
182
250
|
}
|
|
183
251
|
|
|
184
252
|
async function build(owner) {
|
|
185
|
-
const p0 = plan(spec, { now: at, live: live(),
|
|
253
|
+
const p0 = plan(spec, { now: at, live: live(), ...scope() });
|
|
186
254
|
if (!p0.ok) throw new ApproveError(400, describe({ refusals: p0.refusals, warnings: [] }));
|
|
187
255
|
for (const name of p0.freshKeys) if (!keys.has(name)) keys.set(name, freshKeypair());
|
|
188
256
|
// The accounts the owner makes outside loot: a mint, and the project's tree of compressed NFTs.
|
|
@@ -208,7 +276,7 @@ export async function createApproval({
|
|
|
208
276
|
live: live() ?? {},
|
|
209
277
|
now: at,
|
|
210
278
|
rent,
|
|
211
|
-
|
|
279
|
+
...scope(),
|
|
212
280
|
slot: hashes[0] ?? null,
|
|
213
281
|
});
|
|
214
282
|
if (!p.ok) throw new ApproveError(400, describe({ refusals: p.refusals, warnings: [] }));
|
|
@@ -284,8 +352,11 @@ export async function createApproval({
|
|
|
284
352
|
|
|
285
353
|
/** What each step does, in words, kept after it lands (the plan no longer lists it then). */
|
|
286
354
|
const said = new Map();
|
|
355
|
+
/** The same for the steps still to do: a step's words become `said` when this session's own transaction lands it. */
|
|
356
|
+
const planned = new Map();
|
|
287
357
|
function view(p, n = names(), extra = {}) {
|
|
288
|
-
|
|
358
|
+
// A step the plan lists is still to do, whatever landed under its id before (a token's name written, then changed
|
|
359
|
+
// again before the change was gone on with): it is never shown as done, nor among what landed.
|
|
289
360
|
const steps = p.ok
|
|
290
361
|
? p.transactions.flatMap(t =>
|
|
291
362
|
t.steps.map((s, i) => ({
|
|
@@ -293,23 +364,27 @@ export async function createApproval({
|
|
|
293
364
|
says: s.says,
|
|
294
365
|
details: detailsOf(t, i, n),
|
|
295
366
|
signer: t.signer,
|
|
296
|
-
done:
|
|
367
|
+
done: false,
|
|
297
368
|
})),
|
|
298
369
|
)
|
|
299
370
|
: [];
|
|
300
|
-
|
|
371
|
+
const todo = new Set(steps.map(s => s.id));
|
|
372
|
+
const done = new Set((live()?.landed ?? []).filter(id => !todo.has(id)));
|
|
373
|
+
// (what a landed step did is kept in its own words: a later step of the same id never rewrites them)
|
|
374
|
+
for (const s of steps) planned.set(s.id, s);
|
|
301
375
|
return {
|
|
302
376
|
name: spec.name ?? path.basename(file),
|
|
303
377
|
file: path.basename(file),
|
|
304
378
|
mode,
|
|
305
379
|
program: LOOT_PROGRAM,
|
|
306
380
|
cluster: 'devnet',
|
|
307
|
-
setupSol: cost(spec, { now: at, live: live(),
|
|
381
|
+
setupSol: cost(spec, { now: at, live: live(), ...scope() }).setup?.totalSol ?? null,
|
|
308
382
|
warnings: [
|
|
309
383
|
...(extra.refused ? [extra.refused] : []),
|
|
310
384
|
...verdict.warnings.filter(w => !Object.hasOwn(LEGAL, w.code)).map(w => w.message),
|
|
311
385
|
...[
|
|
312
386
|
settings,
|
|
387
|
+
...renameNotes,
|
|
313
388
|
unread(),
|
|
314
389
|
remadeLists(),
|
|
315
390
|
outsideAddresses(
|
|
@@ -319,8 +394,12 @@ export async function createApproval({
|
|
|
319
394
|
].filter(Boolean),
|
|
320
395
|
],
|
|
321
396
|
// The legal flags in plain words, shown in a block of their own whatever the agent said: Approve waits until the
|
|
322
|
-
// owner ticks that they read them.
|
|
323
|
-
|
|
397
|
+
// owner ticks that they read them. With them, what the chain says a named token's mint can do that the spec
|
|
398
|
+
// doesn't say.
|
|
399
|
+
legal: [...legal, ...mintPowers.map(t => ({ code: 'W-MINT-POWERS', says: t.says }))],
|
|
400
|
+
...(readKey() ? { read: readKey() } : {}),
|
|
401
|
+
// A token's name the spec changes and the chain can't take: said whatever else is on the page, done or not.
|
|
402
|
+
unwritten: [...renameNotes],
|
|
324
403
|
authority: model(spec).authority,
|
|
325
404
|
steps,
|
|
326
405
|
landed: [...done].map(id => ({ id, says: said.get(id)?.says ?? id, details: said.get(id)?.details ?? [] })),
|
|
@@ -349,9 +428,14 @@ export async function createApproval({
|
|
|
349
428
|
},
|
|
350
429
|
/** The steps still to go and what is done, built on stand-in addresses until a wallet connects. */
|
|
351
430
|
state() {
|
|
352
|
-
const p = plan(spec, { now: at, live: live(),
|
|
431
|
+
const p = plan(spec, { now: at, live: live(), ...scope() });
|
|
353
432
|
const v = view(p);
|
|
354
|
-
|
|
433
|
+
// (a file still marked as under way is settled, and recorded as live, only by a connected wallet's read)
|
|
434
|
+
return {
|
|
435
|
+
...v,
|
|
436
|
+
remaining: p.ok ? p.transactions.length : 0,
|
|
437
|
+
done: p.ok && p.transactions.length === 0 && !spec.live?.partial,
|
|
438
|
+
};
|
|
355
439
|
},
|
|
356
440
|
/**
|
|
357
441
|
* The same, for the connected `wallet`: built exactly as its batches will be (its own project, the treasury on
|
|
@@ -361,10 +445,13 @@ export async function createApproval({
|
|
|
361
445
|
stateFor(wallet) {
|
|
362
446
|
return serial(() => stateFor(walletAddress(wallet)));
|
|
363
447
|
},
|
|
364
|
-
/**
|
|
365
|
-
|
|
448
|
+
/**
|
|
449
|
+
* The next batch for `wallet` to sign: [base64 wire], with what each does. `read`: the state's `read`, as the page
|
|
450
|
+
* showed it when the owner ticked the box (what the chain says of the tokens this economy names).
|
|
451
|
+
*/
|
|
452
|
+
prepare(wallet, read = null) {
|
|
366
453
|
if (busy) return Promise.reject(new ApproveError(409, 'A batch is being sent: wait for it.'));
|
|
367
|
-
return serial(() => prepareFor(walletAddress(wallet)));
|
|
454
|
+
return serial(() => prepareFor(walletAddress(wallet), read));
|
|
368
455
|
},
|
|
369
456
|
/** The wallet's answer: checked against what was prepared, then sent and confirmed one by one. */
|
|
370
457
|
complete(wallet, id, signed) {
|
|
@@ -375,11 +462,15 @@ export async function createApproval({
|
|
|
375
462
|
|
|
376
463
|
async function stateFor(wallet) {
|
|
377
464
|
let trusted;
|
|
378
|
-
let p0 = plan(spec, { now: at, live: live(),
|
|
465
|
+
let p0 = plan(spec, { now: at, live: live(), ...scope() });
|
|
379
466
|
try {
|
|
380
467
|
await settlePending();
|
|
381
|
-
|
|
468
|
+
// (what a transaction found landed leaves to do: shown as it is under a refusal too)
|
|
469
|
+
p0 = plan(spec, { now: at, live: live(), ...scope() });
|
|
382
470
|
trusted = await trust(wallet);
|
|
471
|
+
await finish({ owner: trusted.owner });
|
|
472
|
+
await settle();
|
|
473
|
+
p0 = plan(spec, { now: at, live: live(), ...scope() });
|
|
383
474
|
} catch (e) {
|
|
384
475
|
if (!(e instanceof ApproveError)) throw e;
|
|
385
476
|
const v = view(p0, namesOf(model(spec, { now: at }), { now: at, owner: wallet }), { refused: e.message });
|
|
@@ -388,6 +479,14 @@ export async function createApproval({
|
|
|
388
479
|
if (!p0.ok || !p0.transactions.length)
|
|
389
480
|
return { ...view(p0, names(), { project: trusted.project }), remaining: 0, done: p0.ok };
|
|
390
481
|
const p = await build(trusted.owner);
|
|
482
|
+
if (!p.transactions.length) {
|
|
483
|
+
// Steps are planned and none builds: the lookup table's, which can't be made without a recent slot (the chain's
|
|
484
|
+
// slot hashes did not read). Nothing needs the table, or the build would have refused, so the setup is as live
|
|
485
|
+
// as it can be made and is recorded so.
|
|
486
|
+
await finish(p.ctx, { nothingBuilds: true });
|
|
487
|
+
const left = plan(spec, { now: at, live: live(), ...scope() });
|
|
488
|
+
return { ...view(left, names(), { project: trusted.project }), remaining: 0, done: !spec.live?.partial };
|
|
489
|
+
}
|
|
391
490
|
await readLive(p);
|
|
392
491
|
const v = view(p, namesOf(model(spec, { now: at }), { now: at, owner: wallet, treasury: p.ctx.treasury }), {
|
|
393
492
|
project: p.ctx.project,
|
|
@@ -395,7 +494,7 @@ export async function createApproval({
|
|
|
395
494
|
return { ...v, remaining: p.transactions.length, done: false };
|
|
396
495
|
}
|
|
397
496
|
|
|
398
|
-
async function prepareFor(wallet) {
|
|
497
|
+
async function prepareFor(wallet, read = null) {
|
|
399
498
|
// A second ask while a batch is out for signing (a double click) gets that same batch, not a new one that would
|
|
400
499
|
// make the first signature fail.
|
|
401
500
|
if (
|
|
@@ -407,8 +506,21 @@ export async function createApproval({
|
|
|
407
506
|
return answer(prepared);
|
|
408
507
|
await settlePending();
|
|
409
508
|
const trusted = await trust(wallet);
|
|
509
|
+
await finish({ owner: trusted.owner });
|
|
510
|
+
await settle();
|
|
511
|
+
// What the chain says a named token's mint can do, where the spec doesn't say it, is read with the wallet's own
|
|
512
|
+
// state: nothing is handed out to sign until the page has shown those very lines and the owner ticked after.
|
|
513
|
+
if (readKey() && read !== readKey())
|
|
514
|
+
throw new ApproveError(
|
|
515
|
+
409,
|
|
516
|
+
'What the chain says of this economy’s tokens was not on the page when the box was ticked: read the lines above, tick the box again, then approve.',
|
|
517
|
+
);
|
|
410
518
|
const p = await build(trusted.owner);
|
|
411
|
-
if (!p.transactions.length)
|
|
519
|
+
if (!p.transactions.length) {
|
|
520
|
+
// (planned steps none of which builds: see stateFor)
|
|
521
|
+
await finish(p.ctx, { nothingBuilds: true });
|
|
522
|
+
throw new ApproveError(409, 'Everything has landed already.');
|
|
523
|
+
}
|
|
412
524
|
await readLive(p);
|
|
413
525
|
const authority = p.ctx.authority;
|
|
414
526
|
const first = p.transactions[0];
|
|
@@ -477,6 +589,7 @@ export async function createApproval({
|
|
|
477
589
|
// sent twice (`settlePending`).
|
|
478
590
|
const signature = encodeBase58(readTransaction(wire).signatures[0]);
|
|
479
591
|
const sent = batch.batch[i];
|
|
592
|
+
sentHere.add(signature);
|
|
480
593
|
await remember({
|
|
481
594
|
signature,
|
|
482
595
|
steps: sent.steps.map(s => s.id),
|
|
@@ -507,7 +620,7 @@ export async function createApproval({
|
|
|
507
620
|
}
|
|
508
621
|
}
|
|
509
622
|
signatures.push(signature);
|
|
510
|
-
await landed(sent, batch.ctx, signature);
|
|
623
|
+
await landed(sent, batch.ctx, signature, { more: i < wires.length - 1 });
|
|
511
624
|
}
|
|
512
625
|
} finally {
|
|
513
626
|
busy = false;
|
|
@@ -567,6 +680,12 @@ export async function createApproval({
|
|
|
567
680
|
const pending = spec.live.pending.filter(p => p.signature !== signature);
|
|
568
681
|
const { pending: _, ...rest } = spec.live;
|
|
569
682
|
spec = { ...spec, live: pending.length ? { ...rest, pending } : rest };
|
|
683
|
+
// A change of which nothing was sent that could land is no change under way: the file reads as live again, and
|
|
684
|
+
// the next approve works out the change afresh.
|
|
685
|
+
if (!pending.length && rest.partial && rest.published && !rest.landed?.length) {
|
|
686
|
+
const { partial: _p, landed: _l, changing: _c, ...asItWas } = rest;
|
|
687
|
+
spec = { ...spec, live: asItWas };
|
|
688
|
+
}
|
|
570
689
|
generation++;
|
|
571
690
|
await write(file, `${JSON.stringify(spec, null, 2)}\n`);
|
|
572
691
|
}
|
|
@@ -594,10 +713,13 @@ export async function createApproval({
|
|
|
594
713
|
const status = await chain.signatureStatus(entry.signature);
|
|
595
714
|
if (landedStatus(status)) {
|
|
596
715
|
if (!signatures.includes(entry.signature)) signatures.push(entry.signature);
|
|
716
|
+
// (never recorded as the whole setup here: whether a token's name is still to write is read from the chain
|
|
717
|
+
// first, in `trust`, and `finish` records it after)
|
|
597
718
|
await landed(
|
|
598
719
|
{ steps: entry.steps.map(id => ({ id })) },
|
|
599
720
|
{ ...(entry.made ?? {}), owner: await projectOwner(entry.made?.project ?? spec.live?.project) },
|
|
600
721
|
entry.signature,
|
|
722
|
+
{ more: true },
|
|
601
723
|
);
|
|
602
724
|
continue;
|
|
603
725
|
}
|
|
@@ -621,15 +743,28 @@ export async function createApproval({
|
|
|
621
743
|
/**
|
|
622
744
|
* One transaction landed: the spec's live section learns its steps, and the file is written at once. It names only
|
|
623
745
|
* what those steps made (the project once `project` landed, a mint once its `token:` step did, …): an account whose
|
|
624
|
-
* own transaction has not landed is made again when the setup goes on.
|
|
746
|
+
* own transaction has not landed is made again when the setup goes on. `more`: more of the same batch is still to
|
|
747
|
+
* send, or the chain has not been read yet for what is left (a transaction found landed from an earlier session):
|
|
748
|
+
* the file is not recorded as live on this landing.
|
|
625
749
|
*/
|
|
626
|
-
async function landed(transaction, ctx, signature) {
|
|
750
|
+
async function landed(transaction, ctx, signature, { more = false } = {}) {
|
|
627
751
|
const was = spec.live ?? {};
|
|
628
752
|
const ids = transaction.steps.map(s => s.id);
|
|
629
753
|
const made = new Set(ids);
|
|
630
754
|
const mints = { ...(was.mints ?? {}) };
|
|
631
755
|
for (const id of ids)
|
|
632
756
|
if (id.startsWith('token:') && ctx.mints?.[id.slice(6)]) mints[id.slice(6)] = ctx.mints[id.slice(6)];
|
|
757
|
+
// What this session sent itself did what its steps were shown to do (one found landed from an earlier session
|
|
758
|
+
// was built from the file as it was then, which may have been edited since: only the chain says what it did).
|
|
759
|
+
if (sentHere.has(signature))
|
|
760
|
+
for (const id of ids) {
|
|
761
|
+
if (planned.has(id)) said.set(id, planned.get(id));
|
|
762
|
+
// a name written with this spec's words is done
|
|
763
|
+
if (id.startsWith('name:')) {
|
|
764
|
+
renames.delete(id.slice(5));
|
|
765
|
+
renamed.add(id.slice(5));
|
|
766
|
+
}
|
|
767
|
+
}
|
|
633
768
|
const progress = {
|
|
634
769
|
...was,
|
|
635
770
|
program: LOOT_PROGRAM,
|
|
@@ -637,6 +772,8 @@ export async function createApproval({
|
|
|
637
772
|
...(made.has('collection') && ctx.collection ? { collection: ctx.collection } : {}),
|
|
638
773
|
...(made.has('tree') && ctx.tree ? { tree: ctx.tree } : {}),
|
|
639
774
|
...(made.has('table') && ctx.slot != null ? { tables: [tableAddress(ctx.owner, ctx.slot)[0]] } : {}),
|
|
775
|
+
// (a transaction found landed from an earlier session names the table it made itself)
|
|
776
|
+
...(made.has('table') && ctx.slot == null && ctx.tables?.length ? { tables: ctx.tables } : {}),
|
|
640
777
|
mints,
|
|
641
778
|
landed: [...new Set([...(was.landed ?? []), ...ids])],
|
|
642
779
|
partial: true,
|
|
@@ -652,22 +789,77 @@ export async function createApproval({
|
|
|
652
789
|
if (!progress.collection) delete progress.collection;
|
|
653
790
|
spec = { ...spec, live: progress };
|
|
654
791
|
generation++;
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
792
|
+
if (!more) record(ctx);
|
|
793
|
+
await write(file, `${JSON.stringify(spec, null, 2)}\n`);
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
/**
|
|
797
|
+
* Records the setup or the change as live, once nothing of it is left: no transaction still looked for, and no step
|
|
798
|
+
* in a plan made with what the chain says of the tokens' names (`trust`, read for this session's wallet before this
|
|
799
|
+
* is ever called). `published` becomes the spec as it is, but for a token's name the chain could not take (`held`):
|
|
800
|
+
* that token keeps the name, ticker and link it was published with, so the file never says of a name what the chain
|
|
801
|
+
* does not, and the next approve says it again. Returns whether it did.
|
|
802
|
+
*/
|
|
803
|
+
function record(ctx = {}, { nothingBuilds = false } = {}) {
|
|
804
|
+
const progress = spec.live;
|
|
805
|
+
if (!progress?.partial || progress.pending?.length) return false;
|
|
806
|
+
// `nothingBuilds`: the plan built for this wallet has no transaction, whatever a plan on stand-in addresses lists
|
|
807
|
+
const rest = plan(spec, { now: at, live: progress, ...scope() });
|
|
808
|
+
if (!rest.ok || (rest.transactions.length && !nothingBuilds)) return false;
|
|
809
|
+
const { live: _, ...bare } = spec;
|
|
810
|
+
// Everything the setup made, whichever session or transaction made it.
|
|
811
|
+
const all = {
|
|
812
|
+
...ctx,
|
|
813
|
+
...Object.fromEntries(
|
|
814
|
+
['project', 'treasury', 'authority', 'collection', 'tree', 'tables'].map(k => [k, progress[k] ?? ctx[k]]),
|
|
815
|
+
),
|
|
816
|
+
mints: { ...(ctx.mints ?? {}), ...(progress.mints ?? {}) },
|
|
817
|
+
};
|
|
818
|
+
const final = recordLanded(bare, { ctx: all, now: at, at: new Date(clock()).toISOString() });
|
|
819
|
+
const before = progress.published?.tokens ?? null;
|
|
820
|
+
if (before && held.size)
|
|
821
|
+
final.live.published.tokens = final.live.published.tokens.map(t => {
|
|
822
|
+
const was = held.has(t.key) ? before.find(x => x.key === t.key) : null;
|
|
823
|
+
if (!was) return t;
|
|
824
|
+
const { name: _n, symbol: _s, uri: _u, ...kept } = t;
|
|
825
|
+
return {
|
|
826
|
+
...kept,
|
|
827
|
+
...Object.fromEntries(['name', 'symbol', 'uri'].filter(f => was[f] !== undefined).map(f => [f, was[f]])),
|
|
828
|
+
};
|
|
829
|
+
});
|
|
830
|
+
final.live = { ...final.live, transactions: progress.transactions ?? [] };
|
|
831
|
+
spec = final;
|
|
832
|
+
only = changedParts(spec).only;
|
|
833
|
+
return true;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/** `record`, with the file written when it did: after a wallet's read found nothing left of a setup under way. */
|
|
837
|
+
async function finish(ctx, options) {
|
|
838
|
+
if (!record(ctx, options)) return;
|
|
839
|
+
generation++;
|
|
840
|
+
await write(file, `${JSON.stringify(spec, null, 2)}\n`);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* A name the chain already carries is the published one from here on (`settled`, from this wallet's read): only
|
|
845
|
+
* that token's name, ticker and link move in the file's published spec, so no tool keeps announcing a change that
|
|
846
|
+
* is done. A setup or change under way records it when it is recorded as live.
|
|
847
|
+
*/
|
|
848
|
+
async function settle() {
|
|
849
|
+
const now = spec.live;
|
|
850
|
+
if (!now?.published || now.partial || !settled.size) return;
|
|
851
|
+
const tokens = (now.published.tokens ?? []).map(t => {
|
|
852
|
+
const said = settled.has(t.key) ? (spec.tokens ?? []).find(x => x.key === t.key) : null;
|
|
853
|
+
if (!said) return t;
|
|
854
|
+
const { name: _n, symbol: _s, uri: _u, ...kept } = t;
|
|
855
|
+
return {
|
|
856
|
+
...kept,
|
|
857
|
+
...Object.fromEntries(['name', 'symbol', 'uri'].filter(f => said[f] !== undefined).map(f => [f, said[f]])),
|
|
665
858
|
};
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
}
|
|
859
|
+
});
|
|
860
|
+
spec = { ...spec, live: { ...now, published: { ...now.published, tokens } } };
|
|
861
|
+
settled = new Set();
|
|
862
|
+
generation++;
|
|
671
863
|
await write(file, `${JSON.stringify(spec, null, 2)}\n`);
|
|
672
864
|
}
|
|
673
865
|
}
|
|
@@ -885,7 +1077,10 @@ export async function approve({
|
|
|
885
1077
|
}
|
|
886
1078
|
const body = req.method === 'POST' ? await readJson(req) : {};
|
|
887
1079
|
if (req.method === 'POST' && url.pathname === '/api/prepare')
|
|
888
|
-
return send(
|
|
1080
|
+
return send(
|
|
1081
|
+
200,
|
|
1082
|
+
await session.prepare(String(body.wallet ?? ''), typeof body.read === 'string' ? body.read : null),
|
|
1083
|
+
);
|
|
889
1084
|
if (req.method === 'POST' && url.pathname === '/api/complete') {
|
|
890
1085
|
const state = await session.complete(String(body.wallet ?? ''), body.id, body.transactions);
|
|
891
1086
|
if (state.done) {
|
package/js/chain/metadata.js
CHANGED
|
@@ -72,5 +72,97 @@ export function createFungibleMetadataIx({ mint, authority, payer = authority, n
|
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
const UPDATE_V2 = 15;
|
|
76
|
+
/**
|
|
77
|
+
* Token Metadata's UpdateMetadataAccountV2 for a fungible token's record: a new name, ticker and link ('' for none),
|
|
78
|
+
* all three written as given. `authority` is the record's update authority (for a token a setup made: the project's
|
|
79
|
+
* owner) and signs; it stays the update authority, and the record stays changeable. The record's words are written
|
|
80
|
+
* whole, with no royalty, creators, collection or uses: as a record a setup made has them. One that was given any of
|
|
81
|
+
* those outside these tools would lose them (or refuse the write), so the approval sends this only to a record that
|
|
82
|
+
* has none (`parseRecord`'s `plain`; approve/live-check.mjs readRenames). The mint is untouched, and it costs only the
|
|
83
|
+
* transaction: the record keeps its size.
|
|
84
|
+
*/
|
|
85
|
+
export function updateFungibleMetadataIx({ mint, authority, name, symbol, uri = '' }) {
|
|
86
|
+
for (const [field, value] of Object.entries({ name, symbol, uri })) {
|
|
87
|
+
if (typeof value !== 'string') throw new TypeError(`A token's ${field} is text`);
|
|
88
|
+
if (utf8(value).length > METADATA_LIMITS[field])
|
|
89
|
+
throw new RangeError(`A token's ${field} is at most ${METADATA_LIMITS[field]} bytes`);
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
programId: METAPLEX_METADATA_PROGRAM,
|
|
93
|
+
keys: [meta(metadataAddress(mint), false, true), meta(authority, true, false)],
|
|
94
|
+
data: concat(
|
|
95
|
+
Uint8Array.of(UPDATE_V2),
|
|
96
|
+
Uint8Array.of(1), // new data
|
|
97
|
+
text(name),
|
|
98
|
+
text(symbol),
|
|
99
|
+
text(uri),
|
|
100
|
+
uintLE(0, 2), // no resale royalty
|
|
101
|
+
NONE, // no creators
|
|
102
|
+
NONE, // no collection
|
|
103
|
+
NONE, // no uses
|
|
104
|
+
NONE, // the update authority: as it is
|
|
105
|
+
NONE, // primary sale: as it is
|
|
106
|
+
NONE, // changeable: as it is
|
|
107
|
+
),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* A Token Metadata record as the chain holds it, or null when the data is not one (its first byte says a metadata
|
|
113
|
+
* record; the strings are padded with NULs to their full length):
|
|
114
|
+
* { updateAuthority, mint, name, symbol, uri, royalty, creators, mutable, collection, uses, plain }.
|
|
115
|
+
* `royalty`: its resale royalty, in hundredths of a percent. `creators`: how many it lists. `mutable`: its update
|
|
116
|
+
* authority can still change it. `collection`, `uses`: whether it names one. `plain`: none of royalty, creators,
|
|
117
|
+
* collection or uses, as a setup's own record is: writing its words again changes nothing else of it.
|
|
118
|
+
*/
|
|
119
|
+
export function parseRecord(data) {
|
|
120
|
+
try {
|
|
121
|
+
if (!data || data.length < 1 + 32 + 32 + 12 || data[0] !== 4) return null;
|
|
122
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
123
|
+
let at = 65;
|
|
124
|
+
const words = [];
|
|
125
|
+
for (let i = 0; i < 3; i++) {
|
|
126
|
+
const length = view.getUint32(at, true);
|
|
127
|
+
if (at + 4 + length > data.length) return null;
|
|
128
|
+
words.push(new TextDecoder().decode(data.subarray(at + 4, at + 4 + length)).replaceAll('\0', ''));
|
|
129
|
+
at += 4 + length;
|
|
130
|
+
}
|
|
131
|
+
if (at + 3 > data.length) return null;
|
|
132
|
+
const royalty = view.getUint16(at, true);
|
|
133
|
+
at += 2;
|
|
134
|
+
let creators = 0;
|
|
135
|
+
if (data[at++] === 1) {
|
|
136
|
+
creators = view.getUint32(at, true);
|
|
137
|
+
at += 4 + 34 * creators;
|
|
138
|
+
}
|
|
139
|
+
if (at + 2 > data.length) return null;
|
|
140
|
+
const mutable = data[at + 1] === 1;
|
|
141
|
+
at += 2; // sold once, and whether it can be changed
|
|
142
|
+
// What follows is each there or not (a byte), with its value when it is: the edition's nonce, the token's standard,
|
|
143
|
+
// then the collection (verified, and its address) and the uses (a method and two counts). A record cut short
|
|
144
|
+
// before them names neither.
|
|
145
|
+
for (let i = 0; i < 2 && at < data.length; i++) at += data[at] === 1 ? 2 : 1;
|
|
146
|
+
const collection = at < data.length && data[at] === 1;
|
|
147
|
+
at += collection ? 34 : 1;
|
|
148
|
+
const uses = at < data.length && data[at] === 1;
|
|
149
|
+
return {
|
|
150
|
+
updateAuthority: encodeBase58(data.subarray(1, 33)),
|
|
151
|
+
mint: encodeBase58(data.subarray(33, 65)),
|
|
152
|
+
name: words[0],
|
|
153
|
+
symbol: words[1],
|
|
154
|
+
uri: words[2],
|
|
155
|
+
royalty,
|
|
156
|
+
creators,
|
|
157
|
+
mutable,
|
|
158
|
+
collection,
|
|
159
|
+
uses,
|
|
160
|
+
plain: royalty === 0 && creators === 0 && !collection && !uses,
|
|
161
|
+
};
|
|
162
|
+
} catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
75
167
|
/** The update authority a Token Metadata record names (who can change its name, ticker and link). */
|
|
76
168
|
export const metadataUpdateAuthority = data => (data?.length >= 33 ? encodeBase58(data.subarray(1, 33)) : null);
|
package/js/chain/mint.js
CHANGED
|
@@ -5,6 +5,7 @@ import { encodeBase58 } from './base58.js';
|
|
|
5
5
|
import { fromUtf8, readUintLE } from './bytes.js';
|
|
6
6
|
import { NATIVE_MINT, TOKEN_2022_PROGRAM, TOKEN_PROGRAM } from './pda.js';
|
|
7
7
|
import { MINT_BYTES, TOKEN_ACCOUNT_BYTES } from './token.js';
|
|
8
|
+
import { powersOf } from './token2022.js';
|
|
8
9
|
|
|
9
10
|
/** Where Token-2022 writes what an account with extensions is (1 a mint, 2 a token account): right after a token
|
|
10
11
|
* account's classic bytes. */
|
|
@@ -98,3 +99,57 @@ export function parseMint(address, owner, data, metadataData = null) {
|
|
|
98
99
|
uri: meta?.uri || null,
|
|
99
100
|
};
|
|
100
101
|
}
|
|
102
|
+
|
|
103
|
+
/** A ticker as a spec's key: lower case, the letters a key takes, a letter first; null when nothing is left. */
|
|
104
|
+
function keyOf(symbol) {
|
|
105
|
+
const key = String(symbol ?? '')
|
|
106
|
+
.toLowerCase()
|
|
107
|
+
.replace(/[^a-z0-9-]/g, '')
|
|
108
|
+
.replace(/^[^a-z]+/, '')
|
|
109
|
+
.slice(0, 32);
|
|
110
|
+
// "sol" is SOL's own key in every spec: a coin that calls itself SOL is not it
|
|
111
|
+
return key === 'sol' ? 'sol-coin' : key || null;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* A text cut to what a spec takes: at most `most` UTF-16 units (what its schema counts), never half a character, with
|
|
115
|
+
* no control characters (a name is a label on one line).
|
|
116
|
+
*/
|
|
117
|
+
function clip(text, most) {
|
|
118
|
+
let out = '';
|
|
119
|
+
for (const c of String(text)) {
|
|
120
|
+
const code = c.codePointAt(0);
|
|
121
|
+
// a control character or a line break is a space: a name is a label on one line
|
|
122
|
+
const plain = code < 0x20 || (code >= 0x7f && code <= 0x9f) || code === 0x2028 || code === 0x2029 ? ' ' : c;
|
|
123
|
+
if (out.length + plain.length > most) break;
|
|
124
|
+
out += plain;
|
|
125
|
+
}
|
|
126
|
+
return out.replace(/ +/g, ' ').trim();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* A token made elsewhere as an economy spec names it: its `tokens` entry (`source: "existing"`), every field read from
|
|
131
|
+
* the chain so nothing is typed in by hand: the mint, its decimals and token program, its name and ticker when it has
|
|
132
|
+
* a name record, and `powers` (token2022.js `powersOf`: what the mint's own keys can do). `account`: the mint's
|
|
133
|
+
* ({ owner, data }); `record`: its Token Metadata record's data, when there is one. `key`: what the spec will call it
|
|
134
|
+
* (default: its ticker in the letters a key takes, else "token"). `project`: the loot project, whose own tokens read
|
|
135
|
+
* `mintAuthority: "project"`; `epoch`: the chain's current epoch, for a transfer fee since changed (`powersOf`). The
|
|
136
|
+
* name and ticker are cut to what a spec takes, on one line. Wrapped SOL is SOL (`{ key: "sol", source: "sol" }`).
|
|
137
|
+
* Null when `account` is no mint.
|
|
138
|
+
*/
|
|
139
|
+
export function tokenEntry(address, account, record = null, { key = null, project = null, epoch = null } = {}) {
|
|
140
|
+
const mint = account ? parseMint(address, account.owner, account.data, record) : null;
|
|
141
|
+
if (!mint) return null;
|
|
142
|
+
if (mint.native) return { key: key ?? 'sol', source: 'sol' };
|
|
143
|
+
const name = mint.name ? clip(mint.name, 32) : '',
|
|
144
|
+
symbol = mint.symbol ? clip(mint.symbol, 10) : '';
|
|
145
|
+
return {
|
|
146
|
+
key: key ?? keyOf(symbol) ?? 'token',
|
|
147
|
+
source: 'existing',
|
|
148
|
+
...(name ? { name } : {}),
|
|
149
|
+
...(symbol ? { symbol } : {}),
|
|
150
|
+
decimals: mint.decimals,
|
|
151
|
+
program: mint.program,
|
|
152
|
+
mint: mint.mint,
|
|
153
|
+
powers: powersOf(account, { project, epoch }),
|
|
154
|
+
};
|
|
155
|
+
}
|