@xgamesfun/solana 0.26.3 → 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 +9 -9
- package/js/approve/live-check.mjs +106 -1
- package/js/approve/page.html +1 -1
- package/js/approve/page.js +19 -5
- package/js/approve/server.mjs +192 -33
- package/js/chain/metadata.js +92 -0
- package/js/economy/check.mjs +10 -2
- package/js/economy/cost.mjs +6 -5
- package/js/economy/explain.mjs +3 -2
- package/js/economy/guide.mjs +1 -1
- 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 +30 -11
- package/package.json +1 -1
- package/release.json +1 -1
- package/skills/xsolana-economy/SKILL.md +4 -4
- package/skills/xsolana-economy/references/telling-the-person.md +4 -2
- package/types/chain/metadata.d.ts +45 -0
- package/types/economy/cost.d.mts +11 -3
- 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/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/economy/check.mjs
CHANGED
|
@@ -58,6 +58,7 @@ import {
|
|
|
58
58
|
utf8Bytes,
|
|
59
59
|
wholeCount,
|
|
60
60
|
weights,
|
|
61
|
+
tokenLabels,
|
|
61
62
|
} from './model.mjs';
|
|
62
63
|
// plan.mjs imports check() in turn: each module only calls the other's functions, never at load time.
|
|
63
64
|
import { boxArgs, boxOrderBytes, placeholderContext, recipeArgs, setupSteps, stepAloneBytes } from './plan.mjs';
|
|
@@ -1017,8 +1018,15 @@ export function check(
|
|
|
1017
1018
|
// A token the setup makes is named on chain as it is made (its Token Metadata record), and the record takes so
|
|
1018
1019
|
// many bytes of each, not characters: a longer one would be refused on chain, in the middle of going live. One
|
|
1019
1020
|
// with no name or ticker at all would be written blank, and show as an unknown token for the record's price. A
|
|
1020
|
-
// token that is live
|
|
1021
|
-
|
|
1021
|
+
// token that is live keeps its words as labels, held to nothing, until they are changed: a change is written to
|
|
1022
|
+
// its record, so the new words are held to what a record takes.
|
|
1023
|
+
const liveAs = economy.live?.mints?.[t.key]
|
|
1024
|
+
? (economy.live.published?.tokens ?? []).find(x => x.key === t.key)
|
|
1025
|
+
: null;
|
|
1026
|
+
const written =
|
|
1027
|
+
!economy.live?.mints?.[t.key] ||
|
|
1028
|
+
(liveAs && JSON.stringify(tokenLabels(liveAs)) !== JSON.stringify(tokenLabels(t)));
|
|
1029
|
+
if (source === 'game' && written)
|
|
1022
1030
|
for (const [field, what] of [
|
|
1023
1031
|
['name', 'name'],
|
|
1024
1032
|
['symbol', 'ticker'],
|
package/js/economy/cost.mjs
CHANGED
|
@@ -39,9 +39,10 @@ import { inAll, round, simulate } from './simulate.mjs';
|
|
|
39
39
|
const NO_OPTIONS = Object.freeze(/** @type {Record<string, any>} */ ({}));
|
|
40
40
|
|
|
41
41
|
const sol = lamports => round(Number(lamports) / Number(LAMPORTS_PER_SOL));
|
|
42
|
-
/** Setup steps that are no loot instruction, and so charge no fee: a token minted,
|
|
43
|
-
* grant's funds. Every other step is the owner or the authority calling loot, which
|
|
44
|
-
|
|
42
|
+
/** Setup steps that are no loot instruction, and so charge no fee: a token minted, a token's name written again, the
|
|
43
|
+
* collection made, SOL put in a grant's funds. Every other step is the owner or the authority calling loot, which
|
|
44
|
+
* charges the project fee. */
|
|
45
|
+
const NO_FEE_STEP = /^(token:|name:|collection$|fund:|rent:)/;
|
|
45
46
|
/** Setup cost from a plan (plan.mjs `plan`). */
|
|
46
47
|
export function setupCost(plan, economy) {
|
|
47
48
|
const m = model(economy);
|
|
@@ -473,8 +474,8 @@ export function actionCosts(economy, { now } = NO_OPTIONS) {
|
|
|
473
474
|
|
|
474
475
|
/** Setup and play costs for a spec, or its refusals. */
|
|
475
476
|
/** `only` and `live`: a change to a live economy, priced as only what it publishes (live.mjs `changedParts`). */
|
|
476
|
-
export function cost(economy, { now = Math.floor(Date.now() / 1000), only = null, live = null } = {}) {
|
|
477
|
-
const plan = compile(economy, { now, only, live });
|
|
477
|
+
export function cost(economy, { now = Math.floor(Date.now() / 1000), only = null, live = null, renames = null } = {}) {
|
|
478
|
+
const plan = compile(economy, { now, only, live, renames });
|
|
478
479
|
if (!plan.ok) return plan;
|
|
479
480
|
return {
|
|
480
481
|
ok: true,
|
package/js/economy/explain.mjs
CHANGED
|
@@ -205,14 +205,15 @@ export function explainEconomy(economy, { now = Math.floor(Date.now() / 1000) }
|
|
|
205
205
|
if (economy.prompt) lines.push(`Asked for: “${economy.prompt}”`);
|
|
206
206
|
|
|
207
207
|
// A token the setup makes is named on chain as it is made (plan.mjs): what wallets will show, and that a picture
|
|
208
|
-
// needs its link before going live. One that is live already keeps what it has
|
|
208
|
+
// needs its link before going live. One that is live already keeps what it has until the spec changes its name,
|
|
209
|
+
// which is written at the next approve (live.mjs changedParts, approve/live-check.mjs readRenames).
|
|
209
210
|
section(
|
|
210
211
|
'The tokens going live makes',
|
|
211
212
|
[...m.tokens.values()]
|
|
212
213
|
.filter(t => t.source === 'game' && !economy.live?.mints?.[t.key])
|
|
213
214
|
.map(
|
|
214
215
|
t =>
|
|
215
|
-
`${t.symbol}${t.name !== t.symbol ? ` (“${t.name}”)` : ''}: ${t.fixedSupply ? `a fixed supply of ${num(t.supply)}, all of it to your wallet, and no more ever` : 'minted by the project as its rules say'}. It is named on chain as it is made, so wallets show ${t.uri ? 'its name, its ticker and the picture its link gives' : 'its name and ticker and no picture: for one, give it a `uri` (a link to a JSON file that names its `image`) before going live'}. Only your wallet can change the name, ticker and link later
|
|
216
|
+
`${t.symbol}${t.name !== t.symbol ? ` (“${t.name}”)` : ''}: ${t.fixedSupply ? `a fixed supply of ${num(t.supply)}, all of it to your wallet, and no more ever` : 'minted by the project as its rules say'}. It is named on chain as it is made, so wallets show ${t.uri ? 'its name, its ticker and the picture its link gives' : 'its name and ticker and no picture: for one, give it a `uri` (a link to a JSON file that names its `image`) before going live'}. Only your wallet can change the name, ticker and link later: change them here and approve again.`,
|
|
216
217
|
),
|
|
217
218
|
);
|
|
218
219
|
|
package/js/economy/guide.mjs
CHANGED
|
@@ -10,7 +10,7 @@ const sol = lamports => Number(lamports) / Number(LAMPORTS_PER_SOL);
|
|
|
10
10
|
export const PARTS = {
|
|
11
11
|
tokens: {
|
|
12
12
|
def: 'token',
|
|
13
|
-
says: 'The currencies: SOL, any existing mint by address (classic or Token-2022, coins from a token launchpad or an AMM included), or a token the game makes when it goes live: one the game mints as its rules say (points, tickets, shards, an emission), or one with a fixed `supply` minted once to the owner with minting turned off for good (a community token you keep). A token the game makes is named on chain as it is made (`name`, `symbol`, and `uri`: a link to a JSON file naming its picture), which is what wallets show; only the owner’s wallet can change that later,
|
|
13
|
+
says: 'The currencies: SOL, any existing mint by address (classic or Token-2022, coins from a token launchpad or an AMM included), or a token the game makes when it goes live: one the game mints as its rules say (points, tickets, shards, an emission), or one with a fixed `supply` minted once to the owner with minting turned off for good (a community token you keep). A token the game makes is named on chain as it is made (`name`, `symbol`, and `uri`: a link to a JSON file naming its picture), which is what wallets show; only the owner’s wallet can change that later, by changing them in the spec and approving again. loot does not put a token on a market: for trading, launch it on a token launchpad or an AMM, then name the mint. `xsolana token <mint>` (xsolana_token) reads any existing mint into its entry, with its decimals, program and `powers` (what its own keys can do), so none of it is typed by hand.',
|
|
14
14
|
acts: 'nobody: a token is a fact',
|
|
15
15
|
},
|
|
16
16
|
currency: {
|
package/js/economy/live.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { encodeBase58 } from '../chain/base58.js';
|
|
4
4
|
import { randomBytes } from '../chain/bytes.js';
|
|
5
5
|
import { LOOT_PROGRAM } from '../client/pda.js';
|
|
6
|
-
import { compressedTree, listOf, model } from './model.mjs';
|
|
6
|
+
import { compressedTree, listOf, model, tokenLabels } from './model.mjs';
|
|
7
7
|
import { auctionSeller, plan, setupContext } from './plan.mjs';
|
|
8
8
|
|
|
9
9
|
/** A key for each fresh account the plan creates. Their secret halves are made by whoever signs (the page). */
|
|
@@ -17,12 +17,12 @@ export function freshAddresses(names, random = randomBytes) {
|
|
|
17
17
|
*/
|
|
18
18
|
export function prepare(
|
|
19
19
|
economy,
|
|
20
|
-
{ owner, keys, live = null, now = Math.floor(Date.now() / 1000), rent, only = null, slot = null },
|
|
20
|
+
{ owner, keys, live = null, now = Math.floor(Date.now() / 1000), rent, only = null, slot = null, renames = null },
|
|
21
21
|
) {
|
|
22
22
|
const m = model(economy, { now });
|
|
23
23
|
// `slot`: a recent slot (in the slot hashes now), which the project's standing lookup table is made at.
|
|
24
24
|
const ctx = { ...setupContext(m, { owner, keys, live: live ?? {}, rent }), slot };
|
|
25
|
-
const p = plan(economy, { now, ctx, live, only });
|
|
25
|
+
const p = plan(economy, { now, ctx, live, only, renames });
|
|
26
26
|
if (!p.ok) return p;
|
|
27
27
|
// Going live always makes the project's standing lookup table (the plan's "table" step). Without a recent slot it
|
|
28
28
|
// can't be made: refused when a part's widest transaction needs it (`wideParts`), so no player is left with a
|
|
@@ -133,10 +133,11 @@ const FIXED = new Set(['farm', 'pool', 'auction']);
|
|
|
133
133
|
*/
|
|
134
134
|
export function changedParts(economy) {
|
|
135
135
|
const published = economy.live?.published;
|
|
136
|
-
if (!published) return { only: null, fixed: [], removed: [], settings: [] };
|
|
136
|
+
if (!published) return { only: null, fixed: [], removed: [], settings: [], renames: [] };
|
|
137
137
|
const only = new Set();
|
|
138
138
|
const fixed = [];
|
|
139
139
|
const removed = [];
|
|
140
|
+
const renames = [];
|
|
140
141
|
// A grant's `trust` is the spec's own note about its server, never sent on chain: changing it publishes nothing.
|
|
141
142
|
const strip = ({ id, index, shopBox, seller, trust, ...rest }) => JSON.stringify(rest);
|
|
142
143
|
for (const [list, kind] of Object.entries(PART_LISTS)) {
|
|
@@ -155,8 +156,20 @@ export function changedParts(economy) {
|
|
|
155
156
|
}
|
|
156
157
|
for (const key of before.keys()) removed.push(`${kind}:${key}`);
|
|
157
158
|
}
|
|
158
|
-
|
|
159
|
-
|
|
159
|
+
// A token is made once. One whose name, ticker or link the spec now gives otherwise than it was published with
|
|
160
|
+
// (`renames`; by what they come to, not how they are spelled: model.mjs tokenLabels) has its name record written
|
|
161
|
+
// again, when it has one its owner can still change: that is read from the chain as the change is approved
|
|
162
|
+
// (approve/live-check.mjs readRenames), never taken from this file.
|
|
163
|
+
for (const t of economy.tokens ?? []) {
|
|
164
|
+
const was = (published.tokens ?? []).find(x => x.key === t.key);
|
|
165
|
+
if (!was) only.add(`token:${t.key}`);
|
|
166
|
+
else if (
|
|
167
|
+
['project', 'game'].includes(t.source) &&
|
|
168
|
+
economy.live.mints?.[t.key] &&
|
|
169
|
+
JSON.stringify(tokenLabels(was)) !== JSON.stringify(tokenLabels(t))
|
|
170
|
+
)
|
|
171
|
+
renames.push(t.key);
|
|
172
|
+
}
|
|
160
173
|
// The first compressed prize opens the project's tree.
|
|
161
174
|
if (!economy.live.tree && compressedTree(model(economy))) only.add('tree');
|
|
162
175
|
// A traits root set again replaces the last.
|
|
@@ -168,5 +181,12 @@ export function changedParts(economy) {
|
|
|
168
181
|
if ((published.treasury ?? null) !== (economy.treasury ?? null)) settings.push('its treasury');
|
|
169
182
|
if (JSON.stringify(published.collections ?? []) !== JSON.stringify(economy.collections ?? []))
|
|
170
183
|
settings.push('the collections it takes');
|
|
171
|
-
return { only, fixed, removed, settings };
|
|
184
|
+
return { only, fixed, removed, settings, renames };
|
|
172
185
|
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* In words, for a tool that does not read the chain: the spec changes what these tokens are called, and what becomes
|
|
189
|
+
* of that is decided when the change is approved.
|
|
190
|
+
*/
|
|
191
|
+
export const renamesNote = renames =>
|
|
192
|
+
`The spec changes what ${renames.join(', ')} ${renames.length === 1 ? 'is' : 'are'} called. A token’s name on chain is its name record: approving reads it, and writes the new name, ticker and link there when the token has a record its owner can still change (and nothing when the record says them already). A token that went live before setups named tokens has no record and can’t be given one, and a record handed on, made unchangeable or given a royalty outside these tools is left as it is: the approval page says so, sends nothing for that token, and the change stays listed here until the spec gives its old name back.`;
|
package/js/economy/model.mjs
CHANGED
|
@@ -392,6 +392,16 @@ export const spendOf = (then, m) => {
|
|
|
392
392
|
return { Give: [then.giveTo] };
|
|
393
393
|
};
|
|
394
394
|
|
|
395
|
+
/**
|
|
396
|
+
* What a token the setup makes is called on chain, from its entry in a spec as written: [name, ticker, link], with
|
|
397
|
+
* the names it gets when the spec gives none (`tokenOf`: its key in capitals for a ticker, its ticker or key for a
|
|
398
|
+
* name). Two entries with the same labels write the same record, however they are spelled.
|
|
399
|
+
*/
|
|
400
|
+
export function tokenLabels(t) {
|
|
401
|
+
const symbol = t.symbol ?? String(t.key).toUpperCase().slice(0, 10);
|
|
402
|
+
return [t.name ?? t.symbol ?? t.key, symbol, t.uri ?? ''];
|
|
403
|
+
}
|
|
404
|
+
|
|
395
405
|
/** What a token is on chain: its mint (or "@mint:<key>" until it exists), program, decimals, and who can mint it. */
|
|
396
406
|
function tokenOf(given) {
|
|
397
407
|
// "project" is the spec's word for a token the project mints; "game" is the older spelling of it.
|
|
@@ -408,10 +418,11 @@ function tokenOf(given) {
|
|
|
408
418
|
gameMints: false,
|
|
409
419
|
};
|
|
410
420
|
const decimals = t.decimals ?? (t.source === 'game' ? 0 : undefined);
|
|
421
|
+
const [name, symbol] = tokenLabels(t);
|
|
411
422
|
return {
|
|
412
423
|
...t,
|
|
413
|
-
symbol
|
|
414
|
-
name
|
|
424
|
+
symbol,
|
|
425
|
+
name,
|
|
415
426
|
decimals,
|
|
416
427
|
program: t.program ?? 'spl-token',
|
|
417
428
|
mint: t.mint ?? `@mint:${t.key}`,
|
package/js/economy/plan.mjs
CHANGED
|
@@ -21,7 +21,13 @@ import {
|
|
|
21
21
|
TOKEN_PROGRAM,
|
|
22
22
|
} from '../chain/pda.js';
|
|
23
23
|
import { createTableIx, extendTableIx } from '../chain/lookup.js';
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
createFungibleMetadataIx,
|
|
26
|
+
METADATA_BYTES,
|
|
27
|
+
METADATA_FEE,
|
|
28
|
+
METADATA_LIMITS,
|
|
29
|
+
updateFungibleMetadataIx,
|
|
30
|
+
} from '../chain/metadata.js';
|
|
25
31
|
import { MEMO_PROGRAM } from '../chain/token2022.js';
|
|
26
32
|
import {
|
|
27
33
|
computeUnitLimit,
|
|
@@ -78,6 +84,7 @@ import {
|
|
|
78
84
|
weights,
|
|
79
85
|
wholeCount,
|
|
80
86
|
windowOf,
|
|
87
|
+
tokenLabels,
|
|
81
88
|
} from './model.mjs';
|
|
82
89
|
import { BUDGET } from './txsize.mjs';
|
|
83
90
|
|
|
@@ -723,7 +730,7 @@ const placeholderKey = name => encodeBase58(sha256(utf8(`loot-plan:${name}`)));
|
|
|
723
730
|
* seats there as it is released, its clock stopped while nobody is in. A seed that lands
|
|
724
731
|
* before the first stake or after it is released the same way from that stake on, so seeding first would change nothing.
|
|
725
732
|
*/
|
|
726
|
-
export function setupSteps(economy, { now = 0, only = null, live: given = null } = {}) {
|
|
733
|
+
export function setupSteps(economy, { now = 0, only = null, live: given = null, renames = null } = {}) {
|
|
727
734
|
const m = model(economy, { now });
|
|
728
735
|
const live = madeSoFar(given);
|
|
729
736
|
const steps = [];
|
|
@@ -731,19 +738,51 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
|
|
|
731
738
|
const landed = new Set(live?.landed ?? []);
|
|
732
739
|
const add = step => want(step.part) && !landed.has(step.id) && steps.push({ creates: [], signers: [owner], ...step });
|
|
733
740
|
const project = ctx => ctx.project;
|
|
734
|
-
|
|
741
|
+
// The project's own first steps (its collection, the project, the accounts it pays into, its lookup table) belong to
|
|
742
|
+
// a new setup, and to one cut short before the file was recorded as live (`partial`, with nothing `published`): gone
|
|
743
|
+
// on with, whichever of them has not landed is still made. Before, a setup cut short after the project's step never
|
|
744
|
+
// made the rest.
|
|
745
|
+
const fresh = !live?.project || Boolean(given?.partial && !given?.published);
|
|
735
746
|
|
|
736
747
|
// Tokens the project mints: a classic mint with the owner as its authority, handed to the project right after.
|
|
737
748
|
// Each is named on chain in the same step (its Token Metadata record: the name, the ticker and the link wallets
|
|
738
749
|
// show), while the owner is still its mint authority: Token Metadata takes a record from nobody else, so a mint whose
|
|
739
750
|
// minting was handed on or turned off without one can never have one. The owner stays the record's update authority.
|
|
740
751
|
for (const t of m.tokens.values()) {
|
|
752
|
+
// A live token whose name record is to be written again (`renames`: its key; the approval reads the chain for
|
|
753
|
+
// which, live-check.mjs readRenames): the new name, ticker and link, signed by the owner, the record's update
|
|
754
|
+
// authority. Not a part a change scopes (`only`), and never held back as landed: the chain says when it is done.
|
|
755
|
+
const labels = [t.name, t.symbol, t.uri ?? ''];
|
|
756
|
+
const writable =
|
|
757
|
+
labels.every(
|
|
758
|
+
(v, i) => utf8(String(v)).length <= [METADATA_LIMITS.name, METADATA_LIMITS.symbol, METADATA_LIMITS.uri][i],
|
|
759
|
+
) &&
|
|
760
|
+
String(t.name).trim() !== '' &&
|
|
761
|
+
String(t.symbol).trim() !== '';
|
|
762
|
+
if (t.source === 'game' && live?.mints?.[t.key] && renames && new Set(renames).has(t.key) && writable)
|
|
763
|
+
steps.push({
|
|
764
|
+
creates: [],
|
|
765
|
+
signers: [owner],
|
|
766
|
+
id: `name:${t.key}`,
|
|
767
|
+
part: `name:${t.key}`,
|
|
768
|
+
// (named by the ticker it was published with: the new one is what it is changed to)
|
|
769
|
+
says: `Change what the token ${tokenLabels((given?.published?.tokens ?? []).find(x => x.key === t.key) ?? t)[1]} (mint ${live.mints[t.key]}) is called on chain to “${t.name}”, ${t.symbol}, ${t.uri ? `with the link ${t.uri}` : 'no picture link'}: its name record is written again, and stays only your wallet’s to change`,
|
|
770
|
+
build: ctx => [
|
|
771
|
+
updateFungibleMetadataIx({
|
|
772
|
+
mint: ctx.mints[t.key],
|
|
773
|
+
authority: ctx.owner,
|
|
774
|
+
name: t.name,
|
|
775
|
+
symbol: t.symbol,
|
|
776
|
+
uri: t.uri ?? '',
|
|
777
|
+
}),
|
|
778
|
+
],
|
|
779
|
+
});
|
|
741
780
|
if (t.source !== 'game' || live?.mints?.[t.key] || (t.mint && !t.mint.startsWith('@'))) continue;
|
|
742
781
|
const key = `new:mint/${t.key}`;
|
|
743
|
-
const named = `write its name on chain (“${t.name}”, ${t.symbol}, ${t.uri ? `with the link ${t.uri}` : 'no picture link'}: only your wallet can change it later,
|
|
782
|
+
const named = `write its name on chain (“${t.name}”, ${t.symbol}, ${t.uri ? `with the link ${t.uri}` : 'no picture link'}: only your wallet can change it later, and Token Metadata keeps ${Number(METADATA_FEE) / 1e9} SOL for it)`;
|
|
744
783
|
const record = { what: `the name of ${t.symbol} on chain`, bytes: METADATA_BYTES, fee: Number(METADATA_FEE) };
|
|
745
784
|
// The checker refuses a name, ticker or link too long for a record before any plan is made, except on a token
|
|
746
|
-
// that is live already
|
|
785
|
+
// that is live already and whose words were not changed. Such a token is made here only when a live economy is
|
|
747
786
|
// planned as if it were new (explain's cost of the whole setup, which is never sent): its record is left out.
|
|
748
787
|
const fits = ['name', 'symbol', 'uri'].every(f => utf8(String(t[f] ?? '')).length <= METADATA_LIMITS[f]);
|
|
749
788
|
const name = ctx =>
|
|
@@ -926,26 +965,39 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
|
|
|
926
965
|
// the first of its addresses, then extended a group a step.
|
|
927
966
|
if (fresh) {
|
|
928
967
|
const named = standingAddresses(m);
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
:
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
968
|
+
// A table whose making landed with no address kept (a file an earlier release left, which recorded a transaction
|
|
969
|
+
// found landed without what it made) can't be added to: its address is of a slot nobody recorded. It stays as it
|
|
970
|
+
// is, and nothing more is planned for it.
|
|
971
|
+
const lost = landed.has('table') && !given?.tables?.[0];
|
|
972
|
+
groupsOf(named, TABLE_GROUP).forEach(
|
|
973
|
+
(group, i) =>
|
|
974
|
+
(i && lost) ||
|
|
975
|
+
add({
|
|
976
|
+
id: i ? `table:${i}` : 'table',
|
|
977
|
+
part: 'project',
|
|
978
|
+
says: i
|
|
979
|
+
? `Add ${group.length} more addresses to the project’s lookup table`
|
|
980
|
+
: `Make the project’s lookup table of the ${named.length} addresses its transactions name most, so a large one takes one approval`,
|
|
981
|
+
creates: i
|
|
982
|
+
? [
|
|
983
|
+
{
|
|
984
|
+
what: `room for ${group.length} addresses in the lookup table`,
|
|
985
|
+
bytes: 32 * group.length,
|
|
986
|
+
grows: true,
|
|
987
|
+
},
|
|
988
|
+
]
|
|
989
|
+
: [{ what: 'the project’s lookup table', bytes: 56 + 32 * group.length }],
|
|
990
|
+
build: ctx => {
|
|
991
|
+
const addresses = group.map(at => at(ctx)).filter(Boolean);
|
|
992
|
+
// A table made already (its first step landed in an earlier batch, or before the setup was cut short) is the
|
|
993
|
+
// one the rest is added to: its address is of the slot it was made at, never of this batch's.
|
|
994
|
+
if (i && ctx.tables?.[0]) return [extendTableIx({ table: ctx.tables[0], authority: ctx.owner, addresses })];
|
|
995
|
+
// With no recent slot known (a plan built offline), there is no table to make.
|
|
996
|
+
if (ctx.slot == null) return [];
|
|
997
|
+
const { address: table, instruction } = createTableIx({ authority: ctx.owner, slot: ctx.slot });
|
|
998
|
+
return [...(i ? [] : [instruction]), extendTableIx({ table, authority: ctx.owner, addresses })];
|
|
999
|
+
},
|
|
1000
|
+
}),
|
|
949
1001
|
);
|
|
950
1002
|
}
|
|
951
1003
|
|
|
@@ -1809,6 +1861,8 @@ export function setupContext(m, { owner: ownerKey, keys, live: given = {}, rent
|
|
|
1809
1861
|
project: own,
|
|
1810
1862
|
treasury: live.treasury ?? m.treasuryWallet ?? ownerKey,
|
|
1811
1863
|
mints,
|
|
1864
|
+
// the project's lookup table, once its first step landed
|
|
1865
|
+
tables: live.tables ?? null,
|
|
1812
1866
|
keys,
|
|
1813
1867
|
rent,
|
|
1814
1868
|
};
|
|
@@ -1876,11 +1930,14 @@ export function pack(steps, ctx) {
|
|
|
1876
1930
|
* The plan for a spec: refusals, or every step packed into transactions. With `ctx` the transactions carry the
|
|
1877
1931
|
* real instructions; without it they are built on stand-in addresses (for sizing, cost and showing).
|
|
1878
1932
|
*/
|
|
1879
|
-
export function plan(
|
|
1933
|
+
export function plan(
|
|
1934
|
+
economy,
|
|
1935
|
+
{ now = Math.floor(Date.now() / 1000), ctx = null, only = null, live = null, renames = null } = {},
|
|
1936
|
+
) {
|
|
1880
1937
|
// With a real context the project is known: a box's write is checked with its own address searches counted.
|
|
1881
1938
|
const checked = check(economy, { now, ...(ctx?.project ? { project: ctx.project } : {}) });
|
|
1882
1939
|
if (!checked.ok) return { ok: false, refusals: checked.refusals, warnings: checked.warnings };
|
|
1883
|
-
const { steps, model: m } = setupSteps(economy, { now, only, live });
|
|
1940
|
+
const { steps, model: m } = setupSteps(economy, { now, only, live, renames });
|
|
1884
1941
|
if (ctx)
|
|
1885
1942
|
ctx.tokenPrograms ??= Object.fromEntries(
|
|
1886
1943
|
[...m.tokens.values()]
|
package/js/economy/schema.json
CHANGED
|
@@ -363,7 +363,7 @@
|
|
|
363
363
|
"type": "object",
|
|
364
364
|
"required": ["key", "source"],
|
|
365
365
|
"additionalProperties": false,
|
|
366
|
-
"description": "A currency. `sol`: SOL (players pay in wrapped SOL). `existing`: any mint by address, launched anywhere, classic or Token-2022, pump.fun included (xgames.fun takes no share of its fees). `project` (`game` is its older spelling): a token made when the economy goes live: the project mints it as its rules say (points, tickets, shards), or, with `supply`, a fixed supply minted once to the owner (a community token you keep). A token made this way is named on chain as it is made (its `name`, `symbol` and `uri`), which is what wallets show. Only the owner's wallet can change that later
|
|
366
|
+
"description": "A currency. `sol`: SOL (players pay in wrapped SOL). `existing`: any mint by address, launched anywhere, classic or Token-2022, pump.fun included (xgames.fun takes no share of its fees). `project` (`game` is its older spelling): a token made when the economy goes live: the project mints it as its rules say (points, tickets, shards), or, with `supply`, a fixed supply minted once to the owner (a community token you keep). A token made this way is named on chain as it is made (its `name`, `symbol` and `uri`), which is what wallets show. Only the owner's wallet can change that later: a change to them in a spec that is live is written to the token's name record at the next approve, when it has one its owner can still change (a token that went live before setups named tokens has none).",
|
|
367
367
|
"properties": {
|
|
368
368
|
"key": {
|
|
369
369
|
"$ref": "#/$defs/key"
|
|
@@ -2037,6 +2037,12 @@
|
|
|
2037
2037
|
"tree": {
|
|
2038
2038
|
"$ref": "#/$defs/address"
|
|
2039
2039
|
},
|
|
2040
|
+
"tables": {
|
|
2041
|
+
"type": "array",
|
|
2042
|
+
"items": {
|
|
2043
|
+
"$ref": "#/$defs/address"
|
|
2044
|
+
}
|
|
2045
|
+
},
|
|
2040
2046
|
"mints": {
|
|
2041
2047
|
"type": "object",
|
|
2042
2048
|
"additionalProperties": {
|
package/js/mcp/tools.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import { LOOT_PROGRAM, pda } from '../client/pda.js';
|
|
|
16
16
|
import { check, describe } from '../economy/check.mjs';
|
|
17
17
|
import { legalFlags } from '../economy/legal.mjs';
|
|
18
18
|
import { cost, explainCost } from '../economy/cost.mjs';
|
|
19
|
-
import { changedParts } from '../economy/live.mjs';
|
|
19
|
+
import { changedParts, renamesNote } from '../economy/live.mjs';
|
|
20
20
|
import { example, examples, ideas, search, partsUsed } from '../economy/examples.mjs';
|
|
21
21
|
import { explainEconomy } from '../economy/explain.mjs';
|
|
22
22
|
import { describeReview, review } from '../economy/review.mjs';
|
|
@@ -247,10 +247,11 @@ function flagsToTell(economy, now) {
|
|
|
247
247
|
* what can't change or was taken out.
|
|
248
248
|
*/
|
|
249
249
|
function liveChange(economy) {
|
|
250
|
-
if (!economy.live?.project || !economy.live?.published) return { scope: {}, notes: '', settings: [] };
|
|
251
|
-
const { only, fixed, removed, settings } = changedParts(economy);
|
|
250
|
+
if (!economy.live?.project || !economy.live?.published) return { scope: {}, notes: '', settings: [], changes: 0 };
|
|
251
|
+
const { only, fixed, removed, settings, renames } = changedParts(economy);
|
|
252
252
|
const notes = [
|
|
253
253
|
settings.length ? `\n${settingsNote(settings, economy.live)}` : '',
|
|
254
|
+
renames.length ? `\n${renamesNote(renames)}` : '',
|
|
254
255
|
fixed.length
|
|
255
256
|
? `\nThese can't change once live, and stay as they were: ${fixed.join(', ')} (a farm, an auction or a pool's token).`
|
|
256
257
|
: '',
|
|
@@ -258,7 +259,8 @@ function liveChange(economy) {
|
|
|
258
259
|
? `\nTaken out of the spec but still on chain: ${removed.join(', ')}. To stop one, keep it and set "active": false.`
|
|
259
260
|
: '',
|
|
260
261
|
].join('');
|
|
261
|
-
|
|
262
|
+
// (`changes`: whether anything is to be published at all: a part, or a token's name)
|
|
263
|
+
return { scope: { only, live: economy.live, renames }, notes, settings, changes: only.size + renames.length };
|
|
262
264
|
}
|
|
263
265
|
|
|
264
266
|
/** In words: the spec changes the project's own settings, which publishing never writes. */
|
|
@@ -428,11 +430,20 @@ export const tools = [
|
|
|
428
430
|
const c = cost(economy, { now: nowOf(ctx), ...change.scope });
|
|
429
431
|
if (!c.ok) return refused(c, from);
|
|
430
432
|
const head = change.scope.live
|
|
431
|
-
? change.
|
|
432
|
-
? `This economy is live: this prices only the change (${[...change.scope.only].join(', ')}).\n`
|
|
433
|
+
? change.changes
|
|
434
|
+
? `This economy is live: this prices only the change (${[...change.scope.only, ...change.scope.renames.map(key => `the name of ${key}`)].join(', ')}).\n`
|
|
433
435
|
: 'This economy is live and nothing has changed since it went live: publishing costs nothing.\n'
|
|
434
436
|
: '';
|
|
435
|
-
|
|
437
|
+
// (a name's price is a transaction's, if it is sent at all: only approving reads the token's record)
|
|
438
|
+
const names = change.scope.renames?.length ? `\n${renamesNote(change.scope.renames)}` : '';
|
|
439
|
+
return {
|
|
440
|
+
ok: true,
|
|
441
|
+
from,
|
|
442
|
+
setup: c.setup,
|
|
443
|
+
actions: c.actions,
|
|
444
|
+
fees: c.fees,
|
|
445
|
+
text: head + explainCost(c) + names + footer,
|
|
446
|
+
};
|
|
436
447
|
},
|
|
437
448
|
},
|
|
438
449
|
{
|
|
@@ -559,7 +570,7 @@ export const tools = [
|
|
|
559
570
|
async handler(input, ctx) {
|
|
560
571
|
const { economy, from, file } = await load(input, ctx);
|
|
561
572
|
const change = liveChange(economy);
|
|
562
|
-
if (change.scope.live && !change.
|
|
573
|
+
if (change.scope.live && !change.changes && change.settings.length)
|
|
563
574
|
return {
|
|
564
575
|
ok: false,
|
|
565
576
|
transactions: 0,
|
|
@@ -567,7 +578,7 @@ export const tools = [
|
|
|
567
578
|
steps: [],
|
|
568
579
|
text: `${from} is live, and its parts are as they are on chain. ${settingsNote(change.settings, economy.live)}${footer}`,
|
|
569
580
|
};
|
|
570
|
-
if (change.scope.live && !change.
|
|
581
|
+
if (change.scope.live && !change.changes)
|
|
571
582
|
return {
|
|
572
583
|
ok: true,
|
|
573
584
|
transactions: 0,
|
|
@@ -578,7 +589,15 @@ export const tools = [
|
|
|
578
589
|
const p = plan(economy, { now: nowOf(ctx), ...change.scope });
|
|
579
590
|
if (!p.ok) return refused(p, from);
|
|
580
591
|
const c = cost(economy, { now: nowOf(ctx), ...change.scope });
|
|
581
|
-
|
|
592
|
+
// A token's new name is listed as the file has it: whether it is sent is the chain's to say, as approving reads it.
|
|
593
|
+
const steps = p.transactions.flatMap(t =>
|
|
594
|
+
t.steps.map(s =>
|
|
595
|
+
s.id.startsWith('name:')
|
|
596
|
+
? `${s.says} (sent only if the token has a name record its owner can still change, read when approving)`
|
|
597
|
+
: s.says,
|
|
598
|
+
),
|
|
599
|
+
);
|
|
600
|
+
const renaming = Boolean(change.scope.renames?.length);
|
|
582
601
|
// the file as the approve command names it, relative to the project folder (projectFile's), and plain (plainName)
|
|
583
602
|
const target = file ? path.relative(realPath(path.resolve(cwdOf(ctx))), file) || DEFAULT_FILE : DEFAULT_FILE;
|
|
584
603
|
if (plainName(target) === null)
|
|
@@ -592,7 +611,7 @@ export const tools = [
|
|
|
592
611
|
const command = approveCommand(target);
|
|
593
612
|
const text = [
|
|
594
613
|
...flagsToTell(economy, nowOf(ctx)),
|
|
595
|
-
`${change.scope.live ? 'Publishing this change to the live economy' : 'Going live'}: ${plural(p.transactions.length, 'transaction')}, ${plural(p.approvals, 'wallet approval')}, about ${c.setup.totalSol} SOL on devnet:`,
|
|
614
|
+
`${change.scope.live ? 'Publishing this change to the live economy' : 'Going live'}: ${renaming ? 'at most ' : ''}${plural(p.transactions.length, 'transaction')}, ${plural(p.approvals, 'wallet approval')}, about ${c.setup.totalSol} SOL on devnet:`,
|
|
596
615
|
...steps.map((s, i) => `${i + 1}. ${s}`),
|
|
597
616
|
'',
|
|
598
617
|
`To put it live, the person runs this in a terminal in the project folder (not you): ${command}`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xgamesfun/solana",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.4",
|
|
4
4
|
"mcpName": "io.github.XGamesFun/solana-kit",
|
|
5
5
|
"description": "Loot boxes, gacha, shops, lotteries, auctions, NFT staking, crafting and rewards on Solana without writing a smart contract: one shared program, its JS client, a CLI, and an MCP server and skill so Claude Code, Codex or any AI agent can design, check, simulate and launch an economy.",
|
|
6
6
|
"license": "Apache-2.0",
|
package/release.json
CHANGED
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
"sbfBytes": 1783800,
|
|
5
5
|
"sbfSha256": "af318ccfa4d2901076b85fcd1b4467b03253adf0de16e73a9dfc1ef8077b2c14",
|
|
6
6
|
"idlSha256": "0e88fc826a585cf17917cbc654e33b68232771769fe9aae1d5899a257940ff47",
|
|
7
|
-
"clientSha256": "
|
|
7
|
+
"clientSha256": "fcb5710a80efbff36c80d0249104c8d7c65b9d84cf70c12b8567e3a7d0af4a2b"
|
|
8
8
|
}
|
|
@@ -9,9 +9,9 @@ An economy is one JSON spec (`xsolana.json` in the project) on **loot**, a Solan
|
|
|
9
9
|
writes or deploys a contract: the spec is checked, simulated and priced by the `xsolana_*` tools, and the project's
|
|
10
10
|
owner puts it live by approving in their own wallet. It runs on **devnet** until the program is audited.
|
|
11
11
|
|
|
12
|
-
The tools are the `xsolana_*` MCP tools. Without MCP, the same from a terminal: `npx -y @xgamesfun/solana@0.26.
|
|
13
|
-
`npx -y @xgamesfun/solana@0.26.
|
|
14
|
-
or `npx -y @xgamesfun/solana@0.26.
|
|
12
|
+
The tools are the `xsolana_*` MCP tools. Without MCP, the same from a terminal: `npx -y @xgamesfun/solana@0.26.4 <guide|examples|check|simulate|cost|explain|review|publish|status> [xsolana.json]`,
|
|
13
|
+
`npx -y @xgamesfun/solana@0.26.4 token <mint>` for a coin made elsewhere,
|
|
14
|
+
or `npx -y @xgamesfun/solana@0.26.4 tool <name> '<json>'` for any tool (always with the exact version).
|
|
15
15
|
|
|
16
16
|
## Rules (always)
|
|
17
17
|
|
|
@@ -123,7 +123,7 @@ or `npx -y @xgamesfun/solana@0.26.3 tool <name> '<json>'` for any tool (always w
|
|
|
123
123
|
level would cost. Run it again after every change that touches a route that pays.
|
|
124
124
|
9. **When they say "put it live"**: `xsolana_publish` lists every step and its cost and gives the command. Tell them to
|
|
125
125
|
run it themselves in the project folder, exactly as `xsolana_publish` gives it (it names the exact version:
|
|
126
|
-
`npx -y @xgamesfun/solana@0.26.
|
|
126
|
+
`npx -y @xgamesfun/solana@0.26.4 approve xsolana.json`; never drop the version). It opens a page on their computer where
|
|
127
127
|
they connect a devnet wallet and approve; what lands is written back into `xsolana.json`. You publish nothing.
|
|
128
128
|
10. **After it is live**: `xsolana_status` shows each part on chain; read it before acting on what the person says
|
|
129
129
|
happened. A later change is the same loop: save, publish, and the approval sends only what changed. What only the
|