@xgamesfun/solana 0.26.1 → 0.26.2
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 +8 -8
- package/js/chain/metadata.js +76 -0
- package/js/economy/check.mjs +28 -0
- package/js/economy/cost.mjs +17 -3
- package/js/economy/explain.mjs +14 -0
- package/js/economy/guide.mjs +1 -1
- package/js/economy/plan.mjs +30 -4
- package/js/economy/schema.json +4 -3
- package/package.json +1 -1
- package/release.json +1 -1
- package/skills/xsolana-economy/SKILL.md +3 -3
- package/skills/xsolana-economy/references/telling-the-person.md +7 -0
- package/types/chain/metadata.d.ts +40 -0
- package/types/economy/cost.d.mts +2 -0
- package/types/economy/plan.d.mts +14 -14
package/README.md
CHANGED
|
@@ -64,7 +64,7 @@ codex plugin add xsolana@xgamesfun
|
|
|
64
64
|
|
|
65
65
|
**claude.ai, ChatGPT or Claude Desktop:** add `https://xgames.fun/mcp` as a custom connector. Nothing to install.
|
|
66
66
|
|
|
67
|
-
**Cursor, Cline, opencode or any MCP client:** run `npx -y --package=@xgamesfun/solana@0.26.
|
|
67
|
+
**Cursor, Cline, opencode or any MCP client:** run `npx -y --package=@xgamesfun/solana@0.26.2 xsolana mcp`.
|
|
68
68
|
|
|
69
69
|
**Any agent that reads skills** (Copilot, Gemini CLI, Windsurf…): `npx skills add XGamesFun/solana-kit`.
|
|
70
70
|
|
|
@@ -75,19 +75,19 @@ are in [docs/harnesses.md](docs/harnesses.md).
|
|
|
75
75
|
|
|
76
76
|
```bash
|
|
77
77
|
# find a worked example close to your idea
|
|
78
|
-
npx -y @xgamesfun/solana@0.26.
|
|
78
|
+
npx -y @xgamesfun/solana@0.26.2 examples shop with loot boxes
|
|
79
79
|
|
|
80
80
|
# does it work? where does the money go? what does it cost?
|
|
81
|
-
npx -y @xgamesfun/solana@0.26.
|
|
82
|
-
npx -y @xgamesfun/solana@0.26.
|
|
83
|
-
npx -y @xgamesfun/solana@0.26.
|
|
81
|
+
npx -y @xgamesfun/solana@0.26.2 check xsolana.json
|
|
82
|
+
npx -y @xgamesfun/solana@0.26.2 simulate xsolana.json
|
|
83
|
+
npx -y @xgamesfun/solana@0.26.2 cost xsolana.json
|
|
84
84
|
|
|
85
85
|
# the whole economy in plain words, and what a cheater could get
|
|
86
|
-
npx -y @xgamesfun/solana@0.26.
|
|
87
|
-
npx -y @xgamesfun/solana@0.26.
|
|
86
|
+
npx -y @xgamesfun/solana@0.26.2 explain xsolana.json
|
|
87
|
+
npx -y @xgamesfun/solana@0.26.2 review xsolana.json
|
|
88
88
|
|
|
89
89
|
# go live, approved in your own wallet
|
|
90
|
-
npx -y @xgamesfun/solana@0.26.
|
|
90
|
+
npx -y @xgamesfun/solana@0.26.2 approve xsolana.json
|
|
91
91
|
```
|
|
92
92
|
|
|
93
93
|
Each command names an exact version, so you always run the release you meant to. `xsolana help` lists them all.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// A token's name on chain: its Token Metadata record (Metaplex), where wallets and explorers read a classic SPL
|
|
2
|
+
// token's name, ticker and picture link. Only what a setup needs: the record of a fungible token whose mint is made
|
|
3
|
+
// already. Token Metadata takes it only from the mint's authority, so it is made while that authority can still sign:
|
|
4
|
+
// once minting is handed on or turned off, no record can ever be made for the mint.
|
|
5
|
+
import { addressBytes, encodeBase58 } from './base58.js';
|
|
6
|
+
import { concat, uintLE, utf8 } from './bytes.js';
|
|
7
|
+
import { METAPLEX_METADATA_PROGRAM, pdaOf, SYSTEM_PROGRAM, SYSVAR_INSTRUCTIONS, TOKEN_PROGRAM } from './pda.js';
|
|
8
|
+
import { meta } from './programs.js';
|
|
9
|
+
|
|
10
|
+
/** A record's size: Token Metadata pads every one to it, whatever its name, ticker and link. */
|
|
11
|
+
export const METADATA_BYTES = 607;
|
|
12
|
+
/** What Token Metadata keeps in each record it makes, on top of the record's rent: its own fee. */
|
|
13
|
+
export const METADATA_FEE = 10_000_000n;
|
|
14
|
+
/** The longest name, ticker and link a record takes, in UTF-8 bytes. */
|
|
15
|
+
export const METADATA_LIMITS = Object.freeze({ name: 32, symbol: 10, uri: 200 });
|
|
16
|
+
|
|
17
|
+
const CREATE = 42;
|
|
18
|
+
const CREATE_V1 = 0;
|
|
19
|
+
const FUNGIBLE = 2;
|
|
20
|
+
const NONE = Uint8Array.of(0);
|
|
21
|
+
const text = s => concat(uintLE(utf8(s).length, 4), utf8(s));
|
|
22
|
+
|
|
23
|
+
/** The address of a mint's Token Metadata record. */
|
|
24
|
+
export const metadataAddress = mint =>
|
|
25
|
+
pdaOf([utf8('metadata'), addressBytes(METAPLEX_METADATA_PROGRAM), addressBytes(mint)], METAPLEX_METADATA_PROGRAM);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Token Metadata's CreateV1 for a fungible token whose mint exists: its name, its ticker and a link to a JSON file with
|
|
29
|
+
* its picture ('' for none). `authority` is the mint's authority and signs; it becomes the record's update authority,
|
|
30
|
+
* the only one who can change the record later. `payer` pays its rent and Token Metadata's fee (`METADATA_FEE`).
|
|
31
|
+
* The mint is listed read-only: this path of Token Metadata writes nothing to a mint that exists. That is the
|
|
32
|
+
* program's behaviour, not a lock: in a setup's own transaction the mint is writable all the same (the same
|
|
33
|
+
* transaction makes it), so the tests read its supply and authorities back after the record is made. The record says
|
|
34
|
+
* a fungible token whatever the decimals (CreateMetadataAccountV3 would call one of 0 decimals an asset, which
|
|
35
|
+
* wallets list with collectibles).
|
|
36
|
+
*/
|
|
37
|
+
export function createFungibleMetadataIx({ mint, authority, payer = authority, name, symbol, uri = '' }) {
|
|
38
|
+
for (const [field, value] of Object.entries({ name, symbol, uri })) {
|
|
39
|
+
if (typeof value !== 'string') throw new TypeError(`A token's ${field} is text`);
|
|
40
|
+
if (utf8(value).length > METADATA_LIMITS[field])
|
|
41
|
+
throw new RangeError(`A token's ${field} is at most ${METADATA_LIMITS[field]} bytes`);
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
programId: METAPLEX_METADATA_PROGRAM,
|
|
45
|
+
keys: [
|
|
46
|
+
meta(metadataAddress(mint), false, true),
|
|
47
|
+
meta(METAPLEX_METADATA_PROGRAM, false, false), // no edition: a fungible token has none
|
|
48
|
+
meta(mint, false, false),
|
|
49
|
+
meta(authority, true, false),
|
|
50
|
+
meta(payer, true, true),
|
|
51
|
+
meta(authority, true, false), // the update authority
|
|
52
|
+
meta(SYSTEM_PROGRAM, false, false),
|
|
53
|
+
meta(SYSVAR_INSTRUCTIONS, false, false),
|
|
54
|
+
meta(TOKEN_PROGRAM, false, false),
|
|
55
|
+
],
|
|
56
|
+
data: concat(
|
|
57
|
+
Uint8Array.of(CREATE, CREATE_V1),
|
|
58
|
+
text(name),
|
|
59
|
+
text(symbol),
|
|
60
|
+
text(uri),
|
|
61
|
+
uintLE(0, 2), // no resale royalty
|
|
62
|
+
NONE, // no creators
|
|
63
|
+
Uint8Array.of(0, 1), // no primary sale yet; the record can be changed
|
|
64
|
+
Uint8Array.of(FUNGIBLE),
|
|
65
|
+
NONE, // no collection
|
|
66
|
+
NONE, // no uses
|
|
67
|
+
NONE, // no collection details
|
|
68
|
+
NONE, // no rule set
|
|
69
|
+
NONE, // decimals: the mint's own
|
|
70
|
+
NONE, // no print supply
|
|
71
|
+
),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The update authority a Token Metadata record names (who can change its name, ticker and link). */
|
|
76
|
+
export const metadataUpdateAuthority = data => (data?.length >= 33 ? encodeBase58(data.subarray(1, 33)) : null);
|
package/js/economy/check.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import { TOKEN_2022_PROGRAM, TOKEN_PROGRAM } from '../chain/pda.js';
|
|
|
14
14
|
import { addressBytes, encodeBase58 } from '../chain/base58.js';
|
|
15
15
|
import { isOnCurve } from '../chain/curve.js';
|
|
16
16
|
import { utf8 } from '../chain/bytes.js';
|
|
17
|
+
import { METADATA_LIMITS } from '../chain/metadata.js';
|
|
17
18
|
import { sha256 } from '../chain/sha256.js';
|
|
18
19
|
import {
|
|
19
20
|
FEES,
|
|
@@ -1013,6 +1014,33 @@ export function check(
|
|
|
1013
1014
|
`“${t.key}” is a token the project makes when the economy goes live, but it names the mint ${t.mint}: the setup would never make it, and would mint from an address the project has no say over.`,
|
|
1014
1015
|
'Remove "mint" for a token the setup makes, or make it an existing token (source "existing", with its "decimals").',
|
|
1015
1016
|
);
|
|
1017
|
+
// A token the setup makes is named on chain as it is made (its Token Metadata record), and the record takes so
|
|
1018
|
+
// many bytes of each, not characters: a longer one would be refused on chain, in the middle of going live. One
|
|
1019
|
+
// 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 already is not named again: its words here stay labels, as they were.
|
|
1021
|
+
if (source === 'game' && !economy.live?.mints?.[t.key])
|
|
1022
|
+
for (const [field, what] of [
|
|
1023
|
+
['name', 'name'],
|
|
1024
|
+
['symbol', 'ticker'],
|
|
1025
|
+
['uri', 'link'],
|
|
1026
|
+
]) {
|
|
1027
|
+
const value = String(tok[field] ?? '');
|
|
1028
|
+
const bytes = utf8(value).length;
|
|
1029
|
+
if (bytes > METADATA_LIMITS[field])
|
|
1030
|
+
refuse(
|
|
1031
|
+
'E-TOKEN',
|
|
1032
|
+
`${where}.${field}`,
|
|
1033
|
+
`The ${what} of “${t.key}” is ${bytes} bytes, and a token’s ${what} on chain is at most ${METADATA_LIMITS[field]} (a letter outside plain English takes two or more).`,
|
|
1034
|
+
`Shorten it to ${METADATA_LIMITS[field]} bytes${field === 'uri' ? ', for instance with a shorter path or host' : ''}.`,
|
|
1035
|
+
);
|
|
1036
|
+
else if (field !== 'uri' && !value.trim())
|
|
1037
|
+
refuse(
|
|
1038
|
+
'E-TOKEN',
|
|
1039
|
+
`${where}.${field}`,
|
|
1040
|
+
`“${t.key}” has an empty ${what}: going live writes it on chain as it is, and wallets would show a token with no ${what}.`,
|
|
1041
|
+
`Give it a ${what}, or leave "${field}" out (${field === 'symbol' ? 'its key in capitals is used' : 'its ticker is used'}).`,
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1016
1044
|
if (t.supply !== undefined && source !== 'game')
|
|
1017
1045
|
refuse(
|
|
1018
1046
|
'E-TOKEN',
|
package/js/economy/cost.mjs
CHANGED
|
@@ -47,11 +47,19 @@ export function setupCost(plan, economy) {
|
|
|
47
47
|
const m = model(economy);
|
|
48
48
|
// An account that grows (a live stash taking more NFTs) pays rent for its new bytes only.
|
|
49
49
|
const rentOf = c => (c.grows ? BigInt(c.bytes) * RENT_PER_BYTE : rent(c.bytes));
|
|
50
|
+
// `fee`: what the program that makes an account keeps in it on top of its rent (Token Metadata's, for a token's name
|
|
51
|
+
// on chain). It is not rent: it never comes back.
|
|
50
52
|
const accounts = plan.transactions
|
|
51
53
|
.flatMap(t => t.steps)
|
|
52
54
|
.flatMap(s => s.creates ?? [])
|
|
53
|
-
.map(c => ({
|
|
55
|
+
.map(c => ({
|
|
56
|
+
...c,
|
|
57
|
+
lamports: rentOf(c).toString(),
|
|
58
|
+
sol: sol(rentOf(c)),
|
|
59
|
+
...(c.fee ? { feeSol: sol(BigInt(c.fee)) } : {}),
|
|
60
|
+
}));
|
|
54
61
|
const rentLamports = accounts.reduce((n, a) => n + BigInt(a.lamports), 0n);
|
|
62
|
+
const metadataLamports = accounts.reduce((n, a) => n + BigInt(a.fee ?? 0), 0n);
|
|
55
63
|
const charged = plan.transactions.flatMap(t => t.steps).filter(s => !NO_FEE_STEP.test(s.id));
|
|
56
64
|
const feeLamports = BigInt(charged.length) * FEES.project;
|
|
57
65
|
const networkLamports = plan.transactions.reduce((n, t) => n + BigInt(t.signers.length) * SIGNATURE_FEE, 0n);
|
|
@@ -76,7 +84,7 @@ export function setupCost(plan, economy) {
|
|
|
76
84
|
for (const l of m.lotteries) if (steps.has(`rent:lottery:${l.key}`)) seedLamports += roundRentOf(l, m);
|
|
77
85
|
for (const sc of m.schedules)
|
|
78
86
|
if (sc.sealed && (sc.fundFrom ?? 'wallet') === 'wallet') putIn(sc.currency, sc.fund, `fund:${sc.key}`);
|
|
79
|
-
const total = rentLamports + feeLamports + networkLamports + seedLamports;
|
|
87
|
+
const total = rentLamports + metadataLamports + feeLamports + networkLamports + seedLamports;
|
|
80
88
|
const notes = [];
|
|
81
89
|
if (accounts.some(a => a.approx))
|
|
82
90
|
notes.push('Accounts marked “about” are sized roughly (a Core collection or an external token account).');
|
|
@@ -96,6 +104,7 @@ export function setupCost(plan, economy) {
|
|
|
96
104
|
return {
|
|
97
105
|
accounts,
|
|
98
106
|
rentSol: sol(rentLamports),
|
|
107
|
+
metadataFeesSol: sol(metadataLamports),
|
|
99
108
|
programFeesSol: sol(feeLamports),
|
|
100
109
|
networkFeesSol: sol(networkLamports),
|
|
101
110
|
seeds,
|
|
@@ -483,6 +492,8 @@ export function explainCost(c) {
|
|
|
483
492
|
];
|
|
484
493
|
lines.push(
|
|
485
494
|
` rent ${s.rentSol} SOL for ${s.accounts.length} accounts; program fees ${s.programFeesSol} SOL; network fees ${s.networkFeesSol} SOL${
|
|
495
|
+
s.metadataFeesSol ? `; Token Metadata’s fees ${s.metadataFeesSol} SOL (a token’s name on chain)` : ''
|
|
496
|
+
}${
|
|
486
497
|
Object.keys(s.seeds).length
|
|
487
498
|
? `; seeds ${Object.entries(s.seeds)
|
|
488
499
|
.map(([k, v]) => `${v} ${k}`)
|
|
@@ -490,7 +501,10 @@ export function explainCost(c) {
|
|
|
490
501
|
: ''
|
|
491
502
|
}.`,
|
|
492
503
|
);
|
|
493
|
-
for (const a of s.accounts)
|
|
504
|
+
for (const a of s.accounts)
|
|
505
|
+
lines.push(
|
|
506
|
+
` ${a.approx ? 'about ' : ''}${a.sol} SOL: ${a.what} (${a.bytes} bytes)${a.feeSol ? `, and ${a.feeSol} SOL Token Metadata keeps for it` : ''}`,
|
|
507
|
+
);
|
|
494
508
|
lines.push('While it runs (for a player, quote the “in all” number: it is what leaves their wallet for good):');
|
|
495
509
|
for (const r of c.actions) {
|
|
496
510
|
const deposits = r.deposits.map(d => `${d.sol} SOL for ${d.what} (back: ${d.back})`).join('; ');
|
package/js/economy/explain.mjs
CHANGED
|
@@ -204,6 +204,18 @@ export function explainEconomy(economy, { now = Math.floor(Date.now() / 1000) }
|
|
|
204
204
|
);
|
|
205
205
|
if (economy.prompt) lines.push(`Asked for: “${economy.prompt}”`);
|
|
206
206
|
|
|
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.
|
|
209
|
+
section(
|
|
210
|
+
'The tokens going live makes',
|
|
211
|
+
[...m.tokens.values()]
|
|
212
|
+
.filter(t => t.source === 'game' && !economy.live?.mints?.[t.key])
|
|
213
|
+
.map(
|
|
214
|
+
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, and these tools don’t change a live token’s yet: get them right first.`,
|
|
216
|
+
),
|
|
217
|
+
);
|
|
218
|
+
|
|
207
219
|
const fee = num(sim.fees.playerSol);
|
|
208
220
|
const actions = [];
|
|
209
221
|
for (const it of sim.items) {
|
|
@@ -559,6 +571,8 @@ export function explainEconomy(economy, { now = Math.floor(Date.now() / 1000) }
|
|
|
559
571
|
lines.push('', 'What setting it up costs');
|
|
560
572
|
lines.push(
|
|
561
573
|
`- About ${num(s.totalSol)} SOL in all, in ${plural(s.transactions, 'transaction')} (${plural(s.approvals, 'wallet approval')}): rent ${num(s.rentSol)} SOL for ${plural(s.accounts.length, 'account')}, program fees ${num(s.programFeesSol)} SOL, network fees ${num(s.networkFeesSol)} SOL${
|
|
574
|
+
s.metadataFeesSol ? `, Token Metadata’s fees ${num(s.metadataFeesSol)} SOL (a token’s name on chain)` : ''
|
|
575
|
+
}${
|
|
562
576
|
Object.keys(s.seeds).length
|
|
563
577
|
? `, seeds ${Object.entries(s.seeds)
|
|
564
578
|
.map(([k, v]) => `${num(v)} ${k}`)
|
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). loot does not put a token on a market: for trading, launch it on a token launchpad or an AMM, then name the mint.',
|
|
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, and changing them in a spec that is already live changes nothing on chain yet. loot does not put a token on a market: for trading, launch it on a token launchpad or an AMM, then name the mint.',
|
|
14
14
|
acts: 'nobody: a token is a fact',
|
|
15
15
|
},
|
|
16
16
|
currency: {
|
package/js/economy/plan.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// The setup plan: every instruction that puts an economy live on loot, in order, built with the client and
|
|
2
2
|
// packed into transactions that fit. Nothing here signs or talks to the chain. Pure.
|
|
3
3
|
//
|
|
4
|
-
// A step is { id, part, says, signers, creates: [{ what, bytes }], build(ctx) → instructions }
|
|
4
|
+
// A step is { id, part, says, signers, creates: [{ what, bytes, fee? }], build(ctx) → instructions } (`fee`: lamports
|
|
5
|
+
// the program that makes the account keeps in it, on top of its rent: Token Metadata's). `ctx` names the
|
|
5
6
|
// addresses a step needs: { owner, authority, project, collection, tree, treasury, mints: { tokenKey: mint }, keys: {
|
|
6
7
|
// name: address } } where `keys` are the fresh keys this setup makes ("new:collection", "new:tree", "new:mint/points"), each of
|
|
7
8
|
// which signs the step that creates it. `placeholderContext` fills every address with a stand-in, so a plan can be
|
|
@@ -20,6 +21,7 @@ import {
|
|
|
20
21
|
TOKEN_PROGRAM,
|
|
21
22
|
} from '../chain/pda.js';
|
|
22
23
|
import { createTableIx, extendTableIx } from '../chain/lookup.js';
|
|
24
|
+
import { createFungibleMetadataIx, METADATA_BYTES, METADATA_FEE, METADATA_LIMITS } from '../chain/metadata.js';
|
|
23
25
|
import { MEMO_PROGRAM } from '../chain/token2022.js';
|
|
24
26
|
import {
|
|
25
27
|
computeUnitLimit,
|
|
@@ -732,18 +734,40 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
|
|
|
732
734
|
const fresh = !live?.project;
|
|
733
735
|
|
|
734
736
|
// Tokens the project mints: a classic mint with the owner as its authority, handed to the project right after.
|
|
737
|
+
// Each is named on chain in the same step (its Token Metadata record: the name, the ticker and the link wallets
|
|
738
|
+
// show), while the owner is still its mint authority: Token Metadata takes a record from nobody else, so a mint whose
|
|
739
|
+
// minting was handed on or turned off without one can never have one. The owner stays the record's update authority.
|
|
735
740
|
for (const t of m.tokens.values()) {
|
|
736
741
|
if (t.source !== 'game' || live?.mints?.[t.key] || (t.mint && !t.mint.startsWith('@'))) continue;
|
|
737
742
|
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, which these tools don’t do yet, and Token Metadata keeps ${Number(METADATA_FEE) / 1e9} SOL for it)`;
|
|
744
|
+
const record = { what: `the name of ${t.symbol} on chain`, bytes: METADATA_BYTES, fee: Number(METADATA_FEE) };
|
|
745
|
+
// 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, whose words are only labels. Such a token is made here only when a live economy is
|
|
747
|
+
// planned as if it were new (explain's cost of the whole setup, which is never sent): its record is left out.
|
|
748
|
+
const fits = ['name', 'symbol', 'uri'].every(f => utf8(String(t[f] ?? '')).length <= METADATA_LIMITS[f]);
|
|
749
|
+
const name = ctx =>
|
|
750
|
+
fits
|
|
751
|
+
? [
|
|
752
|
+
createFungibleMetadataIx({
|
|
753
|
+
mint: ctx.keys[key],
|
|
754
|
+
authority: ctx.owner,
|
|
755
|
+
name: t.name,
|
|
756
|
+
symbol: t.symbol,
|
|
757
|
+
uri: t.uri ?? '',
|
|
758
|
+
}),
|
|
759
|
+
]
|
|
760
|
+
: [];
|
|
738
761
|
if (t.fixedSupply) {
|
|
739
762
|
const supply = amount(t.supply, t);
|
|
740
763
|
add({
|
|
741
764
|
id: `token:${t.key}`,
|
|
742
765
|
part: `token:${t.key}`,
|
|
743
|
-
says: `Make the token ${t.symbol} (${t.decimals} decimals) with a fixed supply of ${t.supply}, all of it to your wallet, then turn its minting off for good`,
|
|
766
|
+
says: `Make the token ${t.symbol} (${t.decimals} decimals) with a fixed supply of ${t.supply}, all of it to your wallet, ${named}, then turn its minting off for good`,
|
|
744
767
|
signers: [owner, key],
|
|
745
768
|
creates: [
|
|
746
769
|
{ what: `the mint of ${t.symbol}`, bytes: MINT_BYTES },
|
|
770
|
+
...(fits ? [record] : []),
|
|
747
771
|
{ what: `your ${t.symbol} account`, bytes: 165 },
|
|
748
772
|
],
|
|
749
773
|
build: ctx => [
|
|
@@ -755,6 +779,7 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
|
|
|
755
779
|
owner: TOKEN_PROGRAM,
|
|
756
780
|
}),
|
|
757
781
|
initializeMint2Ix({ mint: ctx.keys[key], decimals: t.decimals, authority: ctx.owner }),
|
|
782
|
+
...name(ctx),
|
|
758
783
|
createAssociatedTokenAccountIdempotent(ctx.owner, ctx.owner, ctx.keys[key]),
|
|
759
784
|
mintToIx({
|
|
760
785
|
mint: ctx.keys[key],
|
|
@@ -770,9 +795,9 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
|
|
|
770
795
|
add({
|
|
771
796
|
id: `token:${t.key}`,
|
|
772
797
|
part: `token:${t.key}`,
|
|
773
|
-
says: `Make the token ${t.symbol} (${t.decimals} decimals), then hand its minting to the project`,
|
|
798
|
+
says: `Make the token ${t.symbol} (${t.decimals} decimals), ${named}, then hand its minting to the project`,
|
|
774
799
|
signers: [owner, key],
|
|
775
|
-
creates: [{ what: `the mint of ${t.symbol}`, bytes: MINT_BYTES }],
|
|
800
|
+
creates: [{ what: `the mint of ${t.symbol}`, bytes: MINT_BYTES }, ...(fits ? [record] : [])],
|
|
776
801
|
build: ctx => [
|
|
777
802
|
createAccountIx({
|
|
778
803
|
from: ctx.owner,
|
|
@@ -782,6 +807,7 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
|
|
|
782
807
|
owner: TOKEN_PROGRAM,
|
|
783
808
|
}),
|
|
784
809
|
initializeMint2Ix({ mint: ctx.keys[key], decimals: t.decimals, authority: ctx.owner }),
|
|
810
|
+
...name(ctx),
|
|
785
811
|
setAuthorityIx({ account: ctx.keys[key], current: ctx.owner, newAuthority: ctx.project }),
|
|
786
812
|
],
|
|
787
813
|
});
|
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).",
|
|
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, and a change to them in a spec that is already live changes nothing on chain yet.",
|
|
367
367
|
"properties": {
|
|
368
368
|
"key": {
|
|
369
369
|
"$ref": "#/$defs/key"
|
|
@@ -401,11 +401,12 @@
|
|
|
401
401
|
"description": "Where the token trades, recorded for the app and not read or enforced by the tools: loot doesn't launch or trade tokens. A token launched elsewhere is `existing`, by its mint. `games.fun` is the older spelling of `xgames.fun`."
|
|
402
402
|
},
|
|
403
403
|
"uri": {
|
|
404
|
-
"$ref": "#/$defs/link"
|
|
404
|
+
"$ref": "#/$defs/link",
|
|
405
|
+
"description": "`project` only: a link to a JSON file that names the token's picture (`{ \"name\", \"symbol\", \"image\": \"https://…\" }`), at most 200 bytes. Going live writes it on chain with the token's name and ticker; without it wallets show the name and ticker and no picture."
|
|
405
406
|
},
|
|
406
407
|
"image": {
|
|
407
408
|
"$ref": "#/$defs/link",
|
|
408
|
-
"description": "A picture of it, recorded for the app: the tools don't read it, and the setup doesn't write it
|
|
409
|
+
"description": "A picture of it, recorded for the app: the tools don't read it, and the setup doesn't write it on chain (what wallets show is the `image` the JSON file at `uri` names)."
|
|
409
410
|
},
|
|
410
411
|
"powers": {
|
|
411
412
|
"type": "object",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xgamesfun/solana",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.2",
|
|
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": "20e40ee24d808d10091a0bd9973c3c4883e9dd35da4a6beac97557dd85a362c8"
|
|
8
8
|
}
|
|
@@ -9,8 +9,8 @@ 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
|
-
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.2 <guide|examples|check|simulate|cost|explain|review|publish|status> [xsolana.json]`,
|
|
13
|
+
or `npx -y @xgamesfun/solana@0.26.2 tool <name> '<json>'` for any tool (always with the exact version).
|
|
14
14
|
|
|
15
15
|
## Rules (always)
|
|
16
16
|
|
|
@@ -121,7 +121,7 @@ or `npx -y @xgamesfun/solana@0.26.1 tool <name> '<json>'` for any tool (always w
|
|
|
121
121
|
level would cost. Run it again after every change that touches a route that pays.
|
|
122
122
|
9. **When they say "put it live"**: `xsolana_publish` lists every step and its cost and gives the command. Tell them to
|
|
123
123
|
run it themselves in the project folder, exactly as `xsolana_publish` gives it (it names the exact version:
|
|
124
|
-
`npx -y @xgamesfun/solana@0.26.
|
|
124
|
+
`npx -y @xgamesfun/solana@0.26.2 approve xsolana.json`; never drop the version). It opens a page on their computer where
|
|
125
125
|
they connect a devnet wallet and approve; what lands is written back into `xsolana.json`. You publish nothing.
|
|
126
126
|
10. **After it is live**: `xsolana_status` shows each part on chain; read it before acting on what the person says
|
|
127
127
|
happened. A later change is the same loop: save, publish, and the approval sends only what changed. What only the
|
|
@@ -117,6 +117,13 @@ Don'ts:
|
|
|
117
117
|
otherwise, and until it is filled those prizes pay nothing.
|
|
118
118
|
- **A token the project mints** (`source: "project"`) is how points, tickets, attempts and shards work; an existing coin
|
|
119
119
|
needs its mint address and decimals from the person (ask; never guess an address).
|
|
120
|
+
- **A token the setup makes is named on chain as it goes live**: its `name`, its `symbol` and its `uri` (a link to a
|
|
121
|
+
JSON file the project hosts, `{ "name", "symbol", "image": "https://…" }`), which is what wallets and explorers show.
|
|
122
|
+
Ask for the name, the ticker and a picture before going live: with no `uri` it shows by name and ticker with no
|
|
123
|
+
picture. Only the owner's wallet can change the three later (Token Metadata's rule, not loot's), and these tools
|
|
124
|
+
don't change a live token's yet, so get them right first. The record costs about 0.014 SOL (`xsolana_cost` says it:
|
|
125
|
+
rent, and 0.01 SOL Token Metadata keeps). A token that went live before the tools wrote one has none and
|
|
126
|
+
can't be given one: only a mint's own authority can name it, and its minting was handed on or turned off.
|
|
120
127
|
- **NFTs the project has no authority over** (any collection but its own): they count as they are. Say that
|
|
121
128
|
whatever their collection lets be written on an NFT (even by its holder) is what a gate or a boost reads, and that
|
|
122
129
|
the project's notes about one (levels, uses) live in its Sheet, which the authority writes and pays for. Every
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** A record's size: Token Metadata pads every one to it, whatever its name, ticker and link. */
|
|
2
|
+
export declare const METADATA_BYTES = 607;
|
|
3
|
+
/** What Token Metadata keeps in each record it makes, on top of the record's rent: its own fee. */
|
|
4
|
+
export declare const METADATA_FEE = 10000000n;
|
|
5
|
+
/** The longest name, ticker and link a record takes, in UTF-8 bytes. */
|
|
6
|
+
export declare const METADATA_LIMITS: Readonly<{
|
|
7
|
+
name: 32;
|
|
8
|
+
symbol: 10;
|
|
9
|
+
uri: 200;
|
|
10
|
+
}>;
|
|
11
|
+
/** The address of a mint's Token Metadata record. */
|
|
12
|
+
export declare const metadataAddress: (mint: any) => any;
|
|
13
|
+
/**
|
|
14
|
+
* Token Metadata's CreateV1 for a fungible token whose mint exists: its name, its ticker and a link to a JSON file with
|
|
15
|
+
* its picture ('' for none). `authority` is the mint's authority and signs; it becomes the record's update authority,
|
|
16
|
+
* the only one who can change the record later. `payer` pays its rent and Token Metadata's fee (`METADATA_FEE`).
|
|
17
|
+
* The mint is listed read-only: this path of Token Metadata writes nothing to a mint that exists. That is the
|
|
18
|
+
* program's behaviour, not a lock: in a setup's own transaction the mint is writable all the same (the same
|
|
19
|
+
* transaction makes it), so the tests read its supply and authorities back after the record is made. The record says
|
|
20
|
+
* a fungible token whatever the decimals (CreateMetadataAccountV3 would call one of 0 decimals an asset, which
|
|
21
|
+
* wallets list with collectibles).
|
|
22
|
+
*/
|
|
23
|
+
export declare function createFungibleMetadataIx({ mint, authority, payer, name, symbol, uri }: {
|
|
24
|
+
authority: any;
|
|
25
|
+
mint: any;
|
|
26
|
+
name: any;
|
|
27
|
+
payer?: any;
|
|
28
|
+
symbol: any;
|
|
29
|
+
uri?: string | undefined;
|
|
30
|
+
}): {
|
|
31
|
+
programId: string;
|
|
32
|
+
keys: {
|
|
33
|
+
pubkey: any;
|
|
34
|
+
isSigner: any;
|
|
35
|
+
isWritable: any;
|
|
36
|
+
}[];
|
|
37
|
+
data: Uint8Array<any>;
|
|
38
|
+
};
|
|
39
|
+
/** The update authority a Token Metadata record names (who can change its name, ticker and link). */
|
|
40
|
+
export declare const metadataUpdateAuthority: (data: any) => string | null;
|
package/types/economy/cost.d.mts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
export declare function setupCost(plan: any, economy: any): {
|
|
3
3
|
accounts: any;
|
|
4
4
|
rentSol: any;
|
|
5
|
+
metadataFeesSol: any;
|
|
5
6
|
programFeesSol: any;
|
|
6
7
|
networkFeesSol: any;
|
|
7
8
|
seeds: {};
|
|
@@ -51,6 +52,7 @@ export declare function cost(economy: any, { now, only, live }?: {
|
|
|
51
52
|
setup: {
|
|
52
53
|
accounts: any;
|
|
53
54
|
rentSol: any;
|
|
55
|
+
metadataFeesSol: any;
|
|
54
56
|
programFeesSol: any;
|
|
55
57
|
networkFeesSol: any;
|
|
56
58
|
seeds: {};
|
package/types/economy/plan.d.mts
CHANGED
|
@@ -30,11 +30,11 @@ export declare function tableBatches(t: any): never[][];
|
|
|
30
30
|
export declare function partPrize(o: any, m: any): "Raffled" | {
|
|
31
31
|
Stash: any[];
|
|
32
32
|
} | {
|
|
33
|
-
Item?: undefined;
|
|
34
33
|
Compressed: {
|
|
35
34
|
item: any;
|
|
36
35
|
tree: string;
|
|
37
36
|
};
|
|
37
|
+
Item?: undefined;
|
|
38
38
|
Held?: undefined;
|
|
39
39
|
Nft?: undefined;
|
|
40
40
|
Roll?: undefined;
|
|
@@ -137,8 +137,8 @@ export declare function boxArgs(b: any, m: any): {
|
|
|
137
137
|
Item: any[];
|
|
138
138
|
Collection?: undefined;
|
|
139
139
|
} | {
|
|
140
|
-
Item?: undefined;
|
|
141
140
|
Collection: any[];
|
|
141
|
+
Item?: undefined;
|
|
142
142
|
};
|
|
143
143
|
key: any;
|
|
144
144
|
test: any;
|
|
@@ -159,8 +159,8 @@ export declare function boxArgs(b: any, m: any): {
|
|
|
159
159
|
Item: any[];
|
|
160
160
|
Collection?: undefined;
|
|
161
161
|
} | {
|
|
162
|
-
Item?: undefined;
|
|
163
162
|
Collection: any[];
|
|
163
|
+
Item?: undefined;
|
|
164
164
|
};
|
|
165
165
|
key: any;
|
|
166
166
|
test: any;
|
|
@@ -341,11 +341,11 @@ export declare function scheduleArgs(s: any, m: any): {
|
|
|
341
341
|
* of the pool), an item (a compressed NFT of it into the project's tree, "@tree"), one NFT of a stash, or a roll of a box.
|
|
342
342
|
*/
|
|
343
343
|
export declare function prizeArgs(p: any, m: any): "Raffled" | {
|
|
344
|
-
Item?: undefined;
|
|
345
344
|
Compressed: {
|
|
346
345
|
item: any;
|
|
347
346
|
tree: string;
|
|
348
347
|
};
|
|
348
|
+
Item?: undefined;
|
|
349
349
|
Held?: undefined;
|
|
350
350
|
Nft?: undefined;
|
|
351
351
|
Roll?: undefined;
|
|
@@ -464,8 +464,8 @@ export declare function lotteryArgs(l: any, m: any): {
|
|
|
464
464
|
Item: any[];
|
|
465
465
|
Collection?: undefined;
|
|
466
466
|
} | {
|
|
467
|
-
Item?: undefined;
|
|
468
467
|
Collection: any[];
|
|
468
|
+
Item?: undefined;
|
|
469
469
|
};
|
|
470
470
|
key: any;
|
|
471
471
|
test: any;
|
|
@@ -514,8 +514,8 @@ export declare function lotteryArgs(l: any, m: any): {
|
|
|
514
514
|
Item: any[];
|
|
515
515
|
Collection?: undefined;
|
|
516
516
|
} | {
|
|
517
|
-
Item?: undefined;
|
|
518
517
|
Collection: any[];
|
|
518
|
+
Item?: undefined;
|
|
519
519
|
};
|
|
520
520
|
key: any;
|
|
521
521
|
test: any;
|
|
@@ -608,8 +608,8 @@ export declare function contestArgs(c: any, m: any, at?: any): {
|
|
|
608
608
|
Item: any[];
|
|
609
609
|
Collection?: undefined;
|
|
610
610
|
} | {
|
|
611
|
-
Item?: undefined;
|
|
612
611
|
Collection: any[];
|
|
612
|
+
Item?: undefined;
|
|
613
613
|
};
|
|
614
614
|
key: any;
|
|
615
615
|
test: any;
|
|
@@ -619,11 +619,11 @@ export declare function contestArgs(c: any, m: any, at?: any): {
|
|
|
619
619
|
min_pot: bigint;
|
|
620
620
|
stake: bigint;
|
|
621
621
|
prize: string | {
|
|
622
|
-
Item?: undefined;
|
|
623
622
|
Compressed: {
|
|
624
623
|
item: any;
|
|
625
624
|
tree: string;
|
|
626
625
|
};
|
|
626
|
+
Item?: undefined;
|
|
627
627
|
Held?: undefined;
|
|
628
628
|
Nft?: undefined;
|
|
629
629
|
Roll?: undefined;
|
|
@@ -682,8 +682,8 @@ export declare function contestArgs(c: any, m: any, at?: any): {
|
|
|
682
682
|
Item: any[];
|
|
683
683
|
Collection?: undefined;
|
|
684
684
|
} | {
|
|
685
|
-
Item?: undefined;
|
|
686
685
|
Collection: any[];
|
|
686
|
+
Item?: undefined;
|
|
687
687
|
};
|
|
688
688
|
key: any;
|
|
689
689
|
test: any;
|
|
@@ -700,8 +700,8 @@ export declare function farmArgs(f: any, m: any): {
|
|
|
700
700
|
Item: any[];
|
|
701
701
|
Collection?: undefined;
|
|
702
702
|
} | {
|
|
703
|
-
Item?: undefined;
|
|
704
703
|
Collection: any[];
|
|
704
|
+
Item?: undefined;
|
|
705
705
|
};
|
|
706
706
|
key: any;
|
|
707
707
|
test: any;
|
|
@@ -780,8 +780,8 @@ export declare function auctionArgs(a: any, m: any): {
|
|
|
780
780
|
Item: any[];
|
|
781
781
|
Collection?: undefined;
|
|
782
782
|
} | {
|
|
783
|
-
Item?: undefined;
|
|
784
783
|
Collection: any[];
|
|
784
|
+
Item?: undefined;
|
|
785
785
|
};
|
|
786
786
|
key: any;
|
|
787
787
|
test: any;
|
|
@@ -792,8 +792,8 @@ export declare function auctionArgs(a: any, m: any): {
|
|
|
792
792
|
Item: any[];
|
|
793
793
|
Collection?: undefined;
|
|
794
794
|
} | {
|
|
795
|
-
Item?: undefined;
|
|
796
795
|
Collection: any[];
|
|
796
|
+
Item?: undefined;
|
|
797
797
|
};
|
|
798
798
|
key: any;
|
|
799
799
|
test: any;
|
|
@@ -801,11 +801,11 @@ export declare function auctionArgs(a: any, m: any): {
|
|
|
801
801
|
};
|
|
802
802
|
export declare function grantArgs(g: any, m: any): {
|
|
803
803
|
gives: string | {
|
|
804
|
-
Item?: undefined;
|
|
805
804
|
Compressed: {
|
|
806
805
|
item: any;
|
|
807
806
|
tree: string;
|
|
808
807
|
};
|
|
808
|
+
Item?: undefined;
|
|
809
809
|
Held?: undefined;
|
|
810
810
|
Nft?: undefined;
|
|
811
811
|
Roll?: undefined;
|
|
@@ -911,8 +911,8 @@ export declare function grantArgs(g: any, m: any): {
|
|
|
911
911
|
Item: any[];
|
|
912
912
|
Collection?: undefined;
|
|
913
913
|
} | {
|
|
914
|
-
Item?: undefined;
|
|
915
914
|
Collection: any[];
|
|
915
|
+
Item?: undefined;
|
|
916
916
|
};
|
|
917
917
|
key: any;
|
|
918
918
|
test: any;
|