@xgamesfun/solana 0.26.2 → 0.26.3
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 +111 -2
- package/js/approve/page.js +22 -6
- package/js/approve/server.mjs +44 -8
- package/js/chain/mint.js +55 -0
- package/js/chain/token2022.js +89 -0
- package/js/economy/check.mjs +33 -3
- package/js/economy/explain.mjs +1 -1
- package/js/economy/guide.mjs +2 -2
- package/js/economy/legal.mjs +2 -0
- package/js/mcp/tools.mjs +144 -0
- 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 +3 -1
- package/types/chain/mint.d.ts +29 -0
- package/types/chain/token2022.d.ts +38 -0
- package/types/economy/legal.d.mts +1 -0
- package/types/mcp/server.d.mts +53 -1
- package/types/mcp/tools.d.mts +111 -4
package/js/chain/token2022.js
CHANGED
|
@@ -20,10 +20,16 @@ export const EXECUTE_DISCRIMINATOR = Uint8Array.of(105, 37, 101, 197, 75, 251, 1
|
|
|
20
20
|
|
|
21
21
|
const ACCOUNT_TYPE_MINT = 1;
|
|
22
22
|
const EXTENSION = {
|
|
23
|
+
TransferFeeConfig: 1,
|
|
24
|
+
MintCloseAuthority: 3,
|
|
25
|
+
ConfidentialTransferMint: 4,
|
|
23
26
|
DefaultAccountState: 6,
|
|
24
27
|
MemoTransfer: 8,
|
|
28
|
+
NonTransferable: 9,
|
|
29
|
+
InterestBearingConfig: 10,
|
|
25
30
|
PermanentDelegate: 12,
|
|
26
31
|
TransferHook: 14,
|
|
32
|
+
ScaledUiAmount: 25,
|
|
27
33
|
PausableConfig: 26,
|
|
28
34
|
};
|
|
29
35
|
const META_BYTES = 35;
|
|
@@ -109,6 +115,89 @@ export function mintPowers({ owner, data }) {
|
|
|
109
115
|
return out;
|
|
110
116
|
}
|
|
111
117
|
|
|
118
|
+
/**
|
|
119
|
+
* What a mint's own keys can do, as an economy spec's `powers` says it (schema `token.powers`), read from the mint's
|
|
120
|
+
* account (`owner`, its token program; `data`), so nobody has to look it up and type it in. Only what is so is named:
|
|
121
|
+
* a plain mint nobody can mint more of reads `{ mintAuthority: 'none' }`. `project`: the loot project's address, when
|
|
122
|
+
* known, whose own tokens read `mintAuthority: 'project'`; any other minter is `'someone else'`.
|
|
123
|
+
* freeze: a freeze authority; permanentDelegate; transferHook (a hook program, or an authority who can name one);
|
|
124
|
+
* transferFeePercent and feeChangeable (its authority is still there); closeAuthority; pausable; defaultState (the
|
|
125
|
+
* mint sets what new accounts start as) and defaultFrozen (they start frozen); nonTransferable;
|
|
126
|
+
* confidentialTransfers.
|
|
127
|
+
* A mint holds two transfer fees: the one in force until an epoch, and the one from that epoch on. With `epoch` (the
|
|
128
|
+
* chain's current one) the fee read is the one that applies: the newer once its epoch has come, else the higher of
|
|
129
|
+
* the two (the fee today, or the one already set to follow). Without it, the higher of the two: never less than what
|
|
130
|
+
* a transfer pays, and a fee since lowered still reads at its old rate.
|
|
131
|
+
* Returns null when the account is not a mint of either token program.
|
|
132
|
+
*/
|
|
133
|
+
export function powersOf({ owner, data }, { project = null, epoch = null } = {}) {
|
|
134
|
+
const classic = owner === TOKEN_PROGRAM;
|
|
135
|
+
if (!classic && owner !== TOKEN_2022_PROGRAM) return null;
|
|
136
|
+
const extended = !classic && data.length > TOKEN_ACCOUNT_BYTES && data[TOKEN_ACCOUNT_BYTES] === ACCOUNT_TYPE_MINT;
|
|
137
|
+
if (data.length !== MINT_BYTES && !extended) return null;
|
|
138
|
+
if (data[45] !== 1) return null;
|
|
139
|
+
const minter = readUintLE(data, 0, 4) === 1n ? encodeBase58(data.subarray(4, 36)) : null;
|
|
140
|
+
const out = { mintAuthority: !minter ? 'none' : project && minter === project ? 'project' : 'someone else' };
|
|
141
|
+
if (readUintLE(data, 46, 4) === 1n) out.freeze = true;
|
|
142
|
+
if (!extended) return out;
|
|
143
|
+
/** An address a Token-2022 extension holds at `at`, or null when it holds none (all zeros). */
|
|
144
|
+
const held = (value, at = 0) => {
|
|
145
|
+
const a = value && value.length >= at + 32 ? encodeBase58(value.subarray(at, at + 32)) : null;
|
|
146
|
+
return a === NO_ADDRESS ? null : a;
|
|
147
|
+
};
|
|
148
|
+
if (held(extension(data, EXTENSION.PermanentDelegate))) out.permanentDelegate = true;
|
|
149
|
+
const hook = extension(data, EXTENSION.TransferHook);
|
|
150
|
+
if (held(hook, 0) || held(hook, 32)) out.transferHook = true;
|
|
151
|
+
// TransferFeeConfig: its authority, the withdraw authority, what is withheld, then the fee in force and the one to
|
|
152
|
+
// come, each { epoch u64, most u64, basis points u16 }.
|
|
153
|
+
const fee = extension(data, EXTENSION.TransferFeeConfig);
|
|
154
|
+
if (fee && fee.length >= 108) {
|
|
155
|
+
const older = Number(readUintLE(fee, 88, 2)),
|
|
156
|
+
newer = Number(readUintLE(fee, 106, 2));
|
|
157
|
+
const since = readUintLE(fee, 90, 8);
|
|
158
|
+
const points = epoch !== null && BigInt(epoch) >= since ? newer : Math.max(older, newer);
|
|
159
|
+
if (points > 0) out.transferFeePercent = points / 100;
|
|
160
|
+
if (held(fee, 0)) out.feeChangeable = true;
|
|
161
|
+
}
|
|
162
|
+
if (held(extension(data, EXTENSION.MintCloseAuthority))) out.closeAuthority = true;
|
|
163
|
+
const pause = extension(data, EXTENSION.PausableConfig);
|
|
164
|
+
if (pause && (held(pause) || (pause.length > 32 && pause[32] !== 0))) out.pausable = true;
|
|
165
|
+
const state = extension(data, EXTENSION.DefaultAccountState);
|
|
166
|
+
if (state) {
|
|
167
|
+
out.defaultState = true;
|
|
168
|
+
if (state[0] === 2) out.defaultFrozen = true;
|
|
169
|
+
}
|
|
170
|
+
if (extension(data, EXTENSION.NonTransferable)) out.nonTransferable = true;
|
|
171
|
+
if (extension(data, EXTENSION.ConfidentialTransferMint)) out.confidentialTransfers = true;
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* What a Token-2022 mint's state does today that its powers don't say: `paused` (a pause is on: nothing moves),
|
|
177
|
+
* `scaled` (ScaledUiAmount or InterestBearing: wallets show amounts multiplied by a rate, not the base units the
|
|
178
|
+
* program moves and an economy counts in) and `scaler` (somebody can still change that rate). All false for a classic
|
|
179
|
+
* mint, or one with none of them.
|
|
180
|
+
*/
|
|
181
|
+
export function mintState({ owner, data }) {
|
|
182
|
+
const out = { paused: false, scaled: false, scaler: false };
|
|
183
|
+
if (
|
|
184
|
+
owner !== TOKEN_2022_PROGRAM ||
|
|
185
|
+
data.length <= TOKEN_ACCOUNT_BYTES ||
|
|
186
|
+
data[TOKEN_ACCOUNT_BYTES] !== ACCOUNT_TYPE_MINT
|
|
187
|
+
)
|
|
188
|
+
return out;
|
|
189
|
+
const set = value => Boolean(value) && value.length >= 32 && encodeBase58(value.subarray(0, 32)) !== NO_ADDRESS;
|
|
190
|
+
const pause = extension(data, EXTENSION.PausableConfig);
|
|
191
|
+
out.paused = Boolean(pause) && pause.length > 32 && pause[32] !== 0;
|
|
192
|
+
for (const kind of [EXTENSION.ScaledUiAmount, EXTENSION.InterestBearingConfig]) {
|
|
193
|
+
const value = extension(data, kind);
|
|
194
|
+
if (!value) continue;
|
|
195
|
+
out.scaled = true;
|
|
196
|
+
if (set(value)) out.scaler = true;
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
|
|
112
201
|
/** Whether a token account requires a memo on every transfer into it. */
|
|
113
202
|
export function requiresMemo(accountData) {
|
|
114
203
|
const value = accountData.length > TOKEN_ACCOUNT_BYTES ? extension(accountData, EXTENSION.MemoTransfer) : null;
|
package/js/economy/check.mjs
CHANGED
|
@@ -996,7 +996,7 @@ export function check(
|
|
|
996
996
|
'E-TOKEN',
|
|
997
997
|
where,
|
|
998
998
|
`“${t.key}” is an existing token, so the economy needs its mint address and its decimals.`,
|
|
999
|
-
'Add "mint" (the token’s address) and "decimals"
|
|
999
|
+
'Add "mint" (the token’s address) and "decimals": `xsolana token <mint>` (xsolana_token) reads the mint and gives the whole entry. `xsolana approve` reads the mint too and refuses decimals that differ.',
|
|
1000
1000
|
);
|
|
1001
1001
|
if (source === 'game' && t.program === 'token-2022')
|
|
1002
1002
|
refuse(
|
|
@@ -1110,7 +1110,7 @@ export function check(
|
|
|
1110
1110
|
'W-MINT-POWERS',
|
|
1111
1111
|
where,
|
|
1112
1112
|
`The checker cannot read the mint of “${t.key}” from here, so it does not know what its owner can do (freeze, a permanent delegate, a transfer hook, which no rolled box can give a prize in, or fee).`,
|
|
1113
|
-
|
|
1113
|
+
`Run \`xsolana token ${t.mint ?? '<mint>'}\` (xsolana_token): it reads the mint and gives this entry with "powers" filled in.`,
|
|
1114
1114
|
);
|
|
1115
1115
|
}
|
|
1116
1116
|
if (!m.currency) {
|
|
@@ -5003,11 +5003,41 @@ export function check(
|
|
|
5003
5003
|
`${label(c)} splits a prize of 0 among its winners: the program opens the contest but can never name its winner (PrizeNotCovered), so it runs past its result and is called off, every entry refunded.`,
|
|
5004
5004
|
'Give the prize an amount above 0, or leave the prize out.',
|
|
5005
5005
|
);
|
|
5006
|
+
// A presale: one side, entries paid in money (SOL, or a coin made elsewhere), and another token split among the
|
|
5007
|
+
// backers by what each put in: people pay for a share of a token, which is what the law on selling securities
|
|
5008
|
+
// looks at first. The prize's own token is read (a pool's is what the pool holds), and entries paid in points
|
|
5009
|
+
// the project mints are no sale.
|
|
5010
|
+
const sold = cp?.gives === 'tokens' ? prizeToken(c.prize, m) : null;
|
|
5011
|
+
if (
|
|
5012
|
+
c.prize?.split &&
|
|
5013
|
+
c.sides === 1 &&
|
|
5014
|
+
sold &&
|
|
5015
|
+
!sold.wsol &&
|
|
5016
|
+
sold.source !== 'sol' &&
|
|
5017
|
+
tok &&
|
|
5018
|
+
tok.source !== 'game' &&
|
|
5019
|
+
sold.key !== tok.key
|
|
5020
|
+
)
|
|
5021
|
+
warn(
|
|
5022
|
+
'W-TOKEN-SALE',
|
|
5023
|
+
`${where}.prize`,
|
|
5024
|
+
`${label(c)} sells ${sold.symbol ?? sold.key} for ${tok.symbol ?? tok.key}: each backer pays in and takes a share of the tokens by what they put in. Selling a token to raise money is what securities law looks at first, and many places limit who may buy.`,
|
|
5025
|
+
'Fine on devnet. Before mainnet, ask counsel.',
|
|
5026
|
+
);
|
|
5006
5027
|
// One NFT, the last units of an item, a pool's prize: the winning entries share it in the order they are claimed.
|
|
5007
5028
|
const scarce =
|
|
5008
5029
|
cp &&
|
|
5009
5030
|
(cp.gives === 'nft' || (cp.gives === 'item' && Number(m.item.get(cp.item)?.supply ?? 0) > 0) || prizePool(cp));
|
|
5010
|
-
|
|
5031
|
+
// A prize split by stake is set aside whole when the winner is named, or that decision is refused: no entry takes
|
|
5032
|
+
// another's part, but the pool must hold all of it by then.
|
|
5033
|
+
if (scarce && c.prize.split && prizePool(cp))
|
|
5034
|
+
warn(
|
|
5035
|
+
'W-CONTEST-PRIZE',
|
|
5036
|
+
`${where}.prize`,
|
|
5037
|
+
`${label(c)} splits ${prizeWords(cp, m)} among its winners: all of it is set aside when the winner is named, and naming one is refused while “${prizePool(cp)}” holds less (PrizeNotCovered), so the contest would run past its result and be called off.`,
|
|
5038
|
+
'Keep the pool holding the whole prize until the winner is named: seed it with that much, and let nothing else pay out of it.',
|
|
5039
|
+
);
|
|
5040
|
+
else if (scarce)
|
|
5011
5041
|
warn(
|
|
5012
5042
|
'W-CONTEST-PRIZE',
|
|
5013
5043
|
`${where}.prize`,
|
package/js/economy/explain.mjs
CHANGED
|
@@ -603,7 +603,7 @@ export function explainEconomy(economy, { now = Math.floor(Date.now() / 1000) }
|
|
|
603
603
|
);
|
|
604
604
|
for (const n of sim.notes) lines.push(`- ${n}`);
|
|
605
605
|
|
|
606
|
-
const flagged = verdict.warnings.filter(w =>
|
|
606
|
+
const flagged = verdict.warnings.filter(w => ['W-CHANCE-PRIZE', 'W-REVENUE-SHARE', 'W-TOKEN-SALE'].includes(w.code));
|
|
607
607
|
const others = verdict.warnings.filter(w => !flagged.includes(w));
|
|
608
608
|
if (flagged.length) section('Flagged for the law, once it is real money', [...new Set(flagged.map(w => w.message))]);
|
|
609
609
|
section(
|
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, 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.',
|
|
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. `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: {
|
|
@@ -160,7 +160,7 @@ export const CANNOT = [
|
|
|
160
160
|
words: 'bonding curve market trading fees launch pump.fun meteora swap liquidity sell back creator coin',
|
|
161
161
|
why: 'loot runs no market: it moves and sells what the game owns, it doesn’t trade between strangers',
|
|
162
162
|
nearest: [
|
|
163
|
-
'launch the coin on a token launchpad or an AMM (a launchpad’s creator fees pay on trades), then name its mint as an existing token: loot’s shops, boxes, farms and gates use it; gives up
|
|
163
|
+
'launch the coin on a token launchpad or an AMM (a launchpad’s creator fees pay on trades), then name its mint as an existing token (`xsolana token <mint>` reads it into its entry): loot’s shops, boxes, farms and gates use it; gives up minting, since a coin launched this way usually has nobody left who can mint it, so the economy pays out only what its owner puts into its pools',
|
|
164
164
|
'a shop listing whose price rises after each sale (`rise`), and a recipe that buys the token back at a fixed price from a reserve pool a share of each sale fills (`in`: a `pay` line burned; `out`: tokens from the pool): a one-way curve with a floor; gives up a market price, and the reserve is the authority’s to spend',
|
|
165
165
|
'a game token with a fixed `supply` minted once to the owner, minting off for good: a community token the owner keeps and hands out; gives up a public market',
|
|
166
166
|
],
|
package/js/economy/legal.mjs
CHANGED
|
@@ -12,6 +12,8 @@ export const LEGAL = Object.freeze({
|
|
|
12
12
|
'Anyone, the seller included, can bid to push an auction’s price up (shill bidding): a reserve is the honest way to set a floor.',
|
|
13
13
|
'W-REVENUE-SHARE':
|
|
14
14
|
'Stakers are paid from what players spend, which regulators look at when they decide whether a token is a security: before real money, get a lawyer, or pay rewards from emissions instead.',
|
|
15
|
+
'W-TOKEN-SALE':
|
|
16
|
+
'People pay for a share of a new token (a presale). Selling a token to raise money is what securities law looks at first, and many places limit who may buy: before real money, get a lawyer.',
|
|
15
17
|
});
|
|
16
18
|
|
|
17
19
|
/** The legal flags a checker verdict raises, each once, in the order the checker raised them: [{ code, says }]. */
|
package/js/mcp/tools.mjs
CHANGED
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
6
6
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
7
7
|
import path from 'node:path';
|
|
8
|
+
import { addressBytes } from '../chain/base58.js';
|
|
9
|
+
import { metadataAddress } from '../chain/metadata.js';
|
|
10
|
+
import { parseMint, tokenEntry } from '../chain/mint.js';
|
|
8
11
|
import { checkCluster, notMainnet, rpcClient } from '../chain/rpc.js';
|
|
12
|
+
import { mintState } from '../chain/token2022.js';
|
|
9
13
|
import { decodeAny } from '../client/accounts.js';
|
|
10
14
|
import { IDL } from '../client/ix.js';
|
|
11
15
|
import { LOOT_PROGRAM, pda } from '../client/pda.js';
|
|
@@ -82,6 +86,7 @@ export const REMOTE_TOOLS = new Set([
|
|
|
82
86
|
'xsolana_explain',
|
|
83
87
|
'xsolana_review',
|
|
84
88
|
'xsolana_status',
|
|
89
|
+
'xsolana_token',
|
|
85
90
|
'xsolana_client',
|
|
86
91
|
]);
|
|
87
92
|
|
|
@@ -688,6 +693,145 @@ export const tools = [
|
|
|
688
693
|
return { ok: true, rows, text: text + footer };
|
|
689
694
|
},
|
|
690
695
|
},
|
|
696
|
+
{
|
|
697
|
+
name: 'xsolana_token',
|
|
698
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
699
|
+
description:
|
|
700
|
+
'A token made elsewhere, as the chain has it, for an economy that uses it: give its mint’s address and get its ready `tokens` entry (`source: "existing"`) with its name, ticker, decimals and token program, and `powers` filled in: what the mint’s own keys can do (mint more, freeze, a transfer fee, a transfer hook, a permanent delegate, a pause…), in words too. Use it instead of asking the person to look the mint up. Reads devnet (or XSOLANA_RPC_URL): the mint and its name record only, never the link the record names.',
|
|
701
|
+
inputSchema: {
|
|
702
|
+
type: 'object',
|
|
703
|
+
properties: {
|
|
704
|
+
mint: { type: 'string', description: 'The token’s mint address.' },
|
|
705
|
+
key: {
|
|
706
|
+
type: 'string',
|
|
707
|
+
pattern: '^[a-z][a-z0-9-]{0,31}$',
|
|
708
|
+
description: 'What the spec will call it (default: its ticker, in the letters a key takes).',
|
|
709
|
+
},
|
|
710
|
+
project: {
|
|
711
|
+
type: 'string',
|
|
712
|
+
description:
|
|
713
|
+
'The loot project’s address, when the token is one the project is to mint on a mint of its own: its mint authority then reads as the project’s, or as not handed over yet.',
|
|
714
|
+
},
|
|
715
|
+
},
|
|
716
|
+
required: ['mint'],
|
|
717
|
+
additionalProperties: false,
|
|
718
|
+
},
|
|
719
|
+
async handler(input, ctx) {
|
|
720
|
+
const mint = String(input.mint ?? '').trim();
|
|
721
|
+
try {
|
|
722
|
+
addressBytes(mint);
|
|
723
|
+
} catch {
|
|
724
|
+
return {
|
|
725
|
+
ok: false,
|
|
726
|
+
text: `${JSON.stringify(mint)} is not a Solana address. Give the token’s mint address.${footer}`,
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
// Devnet only: a mainnet URL is refused, and so is a chain whose first block is mainnet's or testnet's.
|
|
730
|
+
const rpc = (ctx?.rpc ?? rpcClient)(notMainnet(process.env.XSOLANA_RPC_URL || DEVNET_RPC));
|
|
731
|
+
if (!ctx?.rpc) await checkCluster(rpc);
|
|
732
|
+
let project = null;
|
|
733
|
+
if (input.project !== undefined) {
|
|
734
|
+
project = String(input.project).trim();
|
|
735
|
+
try {
|
|
736
|
+
addressBytes(project);
|
|
737
|
+
} catch {
|
|
738
|
+
return {
|
|
739
|
+
ok: false,
|
|
740
|
+
text: `${JSON.stringify(project)} is not a Solana address. Give the project’s address, or leave it out.${footer}`,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
const [account, record] = await rpc.accounts([mint, metadataAddress(mint)]);
|
|
745
|
+
// A transfer fee changes on an epoch: with the chain's current one, a fee since lowered reads as it is now.
|
|
746
|
+
const epoch =
|
|
747
|
+
account && typeof rpc.call === 'function'
|
|
748
|
+
? await rpc.call('getEpochInfo').then(
|
|
749
|
+
e => Number(e.epoch),
|
|
750
|
+
() => null,
|
|
751
|
+
)
|
|
752
|
+
: null;
|
|
753
|
+
const token = tokenEntry(mint, account, record?.data ?? null, {
|
|
754
|
+
...(input.key ? { key: input.key } : {}),
|
|
755
|
+
project,
|
|
756
|
+
epoch,
|
|
757
|
+
});
|
|
758
|
+
if (!token)
|
|
759
|
+
return {
|
|
760
|
+
ok: false,
|
|
761
|
+
text: `${
|
|
762
|
+
account
|
|
763
|
+
? `${mint} is not a token mint on ${CLUSTER} (a token account or a wallet is not the mint: an explorer’s token page names it)`
|
|
764
|
+
: `There is nothing at ${mint} on ${CLUSTER}. A coin launched on mainnet is not on ${CLUSTER}, where loot runs for now`
|
|
765
|
+
}.${footer}`,
|
|
766
|
+
};
|
|
767
|
+
if (token.source === 'sol')
|
|
768
|
+
return {
|
|
769
|
+
ok: true,
|
|
770
|
+
token,
|
|
771
|
+
text: `${mint} is wrapped SOL: in a spec that is SOL itself.\n\nIn the spec’s "tokens":\n${JSON.stringify(token)}${footer}`,
|
|
772
|
+
};
|
|
773
|
+
const read = parseMint(mint, account.owner, account.data, record?.data ?? null);
|
|
774
|
+
const supply = Number(BigInt(read.supply)) / 10 ** read.decimals;
|
|
775
|
+
const p = token.powers;
|
|
776
|
+
const state = mintState(account);
|
|
777
|
+
// The name and ticker are the token's own writing, anyone's to set: given as the entry has them (one line, cut
|
|
778
|
+
// to a spec's lengths), in quotes, as labels and never as words of this tool.
|
|
779
|
+
const called =
|
|
780
|
+
token.symbol || token.name
|
|
781
|
+
? `calls itself ${[token.name && `“${token.name}”`, token.symbol && `ticker “${token.symbol}”`].filter(Boolean).join(', ')} (its own record’s words: a label anyone can write, not a fact)`
|
|
782
|
+
: 'has no name record, so wallets show it as an unknown token';
|
|
783
|
+
// What each power lets whoever holds it do, in the checker's words (check.mjs W-MINT-POWERS).
|
|
784
|
+
const can = [
|
|
785
|
+
p.mintAuthority === 'someone else' && `mint more of it whenever they want (${read.mintAuthority} can)`,
|
|
786
|
+
p.mintAuthority === 'project' && null,
|
|
787
|
+
p.freeze && 'freeze any account holding it, an economy’s pools and vaults included',
|
|
788
|
+
p.permanentDelegate && 'move or burn it out of any account',
|
|
789
|
+
p.transferHook && 'run a program of their own on every transfer, which can refuse one',
|
|
790
|
+
p.transferFeePercent > 0 && `keep ${p.transferFeePercent}% of every transfer`,
|
|
791
|
+
p.feeChangeable && 'raise its transfer fee, up to all of a transfer',
|
|
792
|
+
p.closeAuthority && 'close the mint and make it again with other powers',
|
|
793
|
+
p.pausable && 'pause every transfer of it',
|
|
794
|
+
p.defaultFrozen && 'have every new account of it start frozen',
|
|
795
|
+
!p.defaultFrozen && p.defaultState && p.freeze && 'switch every new account of it to start frozen',
|
|
796
|
+
].filter(Boolean);
|
|
797
|
+
const also = [
|
|
798
|
+
p.nonTransferable && 'It cannot be transferred at all: nothing can be paid, staked or given as a prize in it.',
|
|
799
|
+
p.confidentialTransfers &&
|
|
800
|
+
'Its holders can turn plain transfers into their account away (confidential transfers).',
|
|
801
|
+
state.paused && 'It is paused now: nothing of it moves until its issuer ends the pause.',
|
|
802
|
+
state.scaled &&
|
|
803
|
+
`Wallets show its amounts multiplied by a rate its mint carries${state.scaler ? ', which its issuer can change' : ''}: an economy counts the amounts under it, so a price in a spec is not the number a wallet shows.`,
|
|
804
|
+
p.mintAuthority === 'project' &&
|
|
805
|
+
'Its mint authority is the project given: the project mints it, as its rules say.',
|
|
806
|
+
project &&
|
|
807
|
+
p.mintAuthority === 'someone else' &&
|
|
808
|
+
`Its mint authority is not the project given (${project}): until it is handed over, nothing the economy mints in it lands.`,
|
|
809
|
+
].filter(Boolean);
|
|
810
|
+
const text = [
|
|
811
|
+
`The token at ${mint} on ${CLUSTER}: ${read.program === 'token-2022' ? 'a Token-2022' : 'a classic SPL'} token, ${plural(read.decimals, 'decimal')}, ${supply.toLocaleString('en-US')} in existence${p.mintAuthority === 'none' ? ', and nobody can mint more' : ''}. It ${called}.`,
|
|
812
|
+
can.length
|
|
813
|
+
? `Whoever controls its mint can:\n${can.map(c => `- ${c}`).join('\n')}\nAn economy in it is only as safe as the token: say so where players pay, stake or are paid.`
|
|
814
|
+
: `Its mint has no key that can freeze it, take it, charge on it or stop a transfer${p.mintAuthority === 'project' ? ' (the project mints it)' : ''}.`,
|
|
815
|
+
...also,
|
|
816
|
+
`In the spec’s "tokens" (xsolana_check then says what each power means for the economy’s parts):\n${JSON.stringify(token, null, 2)}`,
|
|
817
|
+
].join('\n\n');
|
|
818
|
+
return {
|
|
819
|
+
ok: true,
|
|
820
|
+
token,
|
|
821
|
+
mint: {
|
|
822
|
+
address: mint,
|
|
823
|
+
program: read.program,
|
|
824
|
+
decimals: read.decimals,
|
|
825
|
+
supply: read.supply,
|
|
826
|
+
name: token.name ?? null,
|
|
827
|
+
symbol: token.symbol ?? null,
|
|
828
|
+
mintAuthority: read.mintAuthority,
|
|
829
|
+
freezeAuthority: read.freezeAuthority,
|
|
830
|
+
},
|
|
831
|
+
text: text + footer,
|
|
832
|
+
};
|
|
833
|
+
},
|
|
834
|
+
},
|
|
691
835
|
{
|
|
692
836
|
name: 'xsolana_client',
|
|
693
837
|
annotations: READ,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xgamesfun/solana",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.3",
|
|
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": "8c8c89bfca22961d24379b2cc63964dfa2e68d3b65cdcbb33d52352677464533"
|
|
8
8
|
}
|
|
@@ -9,8 +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
|
-
|
|
12
|
+
The tools are the `xsolana_*` MCP tools. Without MCP, the same from a terminal: `npx -y @xgamesfun/solana@0.26.3 <guide|examples|check|simulate|cost|explain|review|publish|status> [xsolana.json]`,
|
|
13
|
+
`npx -y @xgamesfun/solana@0.26.3 token <mint>` for a coin made elsewhere,
|
|
14
|
+
or `npx -y @xgamesfun/solana@0.26.3 tool <name> '<json>'` for any tool (always with the exact version).
|
|
14
15
|
|
|
15
16
|
## Rules (always)
|
|
16
17
|
|
|
@@ -26,8 +27,9 @@ or `npx -y @xgamesfun/solana@0.26.2 tool <name> '<json>'` for any tool (always w
|
|
|
26
27
|
`xsolana_simulate`, `xsolana_cost` and `xsolana_explain`, never your own arithmetic. What a player pays is the tools'
|
|
27
28
|
"in all" number for that action (price, fee, oracle, network and rent that never comes back): quote it as is.
|
|
28
29
|
6. **Flag the law once.** The first time the checker raises `W-CHANCE-PRIZE` (paid chance at money: boxes with
|
|
29
|
-
prizes, lotteries, contests), `W-CHANCE-ITEMS` (paid random collectibles a player can resell: loot boxes, gacha)
|
|
30
|
-
`W-REVENUE-SHARE` (stakers paid from sales)
|
|
30
|
+
prizes, lotteries, contests), `W-CHANCE-ITEMS` (paid random collectibles a player can resell: loot boxes, gacha),
|
|
31
|
+
`W-REVENUE-SHARE` (stakers paid from sales) or `W-TOKEN-SALE` (a presale: people pay for a share of a new token),
|
|
32
|
+
say it in one plain sentence. Don't repeat it in later answers, or add
|
|
31
33
|
legal notes to pages and apps you build, unless the person asks or a flag of a new kind appears. When they ask about
|
|
32
34
|
the law, answer what you know and leave the rest to their lawyer: you are not one. Never call anything an
|
|
33
35
|
investment, a return or passive income, and never promise it is safe. The approval page shows the flags and that
|
|
@@ -121,13 +123,33 @@ or `npx -y @xgamesfun/solana@0.26.2 tool <name> '<json>'` for any tool (always w
|
|
|
121
123
|
level would cost. Run it again after every change that touches a route that pays.
|
|
122
124
|
9. **When they say "put it live"**: `xsolana_publish` lists every step and its cost and gives the command. Tell them to
|
|
123
125
|
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.
|
|
126
|
+
`npx -y @xgamesfun/solana@0.26.3 approve xsolana.json`; never drop the version). It opens a page on their computer where
|
|
125
127
|
they connect a devnet wallet and approve; what lands is written back into `xsolana.json`. You publish nothing.
|
|
126
128
|
10. **After it is live**: `xsolana_status` shows each part on chain; read it before acting on what the person says
|
|
127
129
|
happened. A later change is the same loop: save, publish, and the approval sends only what changed. What only the
|
|
128
130
|
owner signs (open or decide a contest, pay out) goes on an owner page they approve in their own wallet:
|
|
129
131
|
`references/after-launch.md`.
|
|
130
132
|
|
|
133
|
+
## When they ask for a token
|
|
134
|
+
|
|
135
|
+
"A token for my game" is one of four things. Find out which from what they say it is for, before writing anything
|
|
136
|
+
(ask only when they haven't said):
|
|
137
|
+
|
|
138
|
+
| They mean | It is | In the spec |
|
|
139
|
+
| --------------------------------------------------------------------------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
140
|
+
| points, tickets, shards: what players earn and spend in the game | a token the project mints as its rules say | `source: "project"`, given by a recipe's or a grant's prize (`"from": "mint"`), a farm's `rate` (`"minted": true`) or a schedule (`"does": "issue"`) |
|
|
141
|
+
| "our own coin, 10 million, we hand it out", a community token | a fixed supply, all of it to the owner, no more ever | `source: "project"` with `supply`; pools are seeded from it, and a presale (a contest), sealed vesting (a schedule) and a farm hand it out |
|
|
142
|
+
| "a coin people can trade", "on pump.fun", "a bonding curve", "creator fees" | a market, which loot does not run | they launch it on a launchpad or an AMM, in their own wallet; then `xsolana_token {"mint": …}` gives its entry, and loot's shops, boxes, farms and gates use it |
|
|
143
|
+
| "our token", "the coin we launched", "this mint" | a coin that exists | `xsolana_token {"mint": …}`: its entry with decimals, program and `powers` read from the chain. Never type them by hand, and never guess an address |
|
|
144
|
+
|
|
145
|
+
Say what each gives up. The first two have no public market: nobody trades them unless the project lists them
|
|
146
|
+
somewhere itself. A coin launched on a market usually has nobody left who can mint it (`xsolana_token` says so): the
|
|
147
|
+
economy pays out only what its owner puts into its pools. On devnet a coin trades nothing of value.
|
|
148
|
+
|
|
149
|
+
A token the setup makes is named on chain as it goes live, so ask for its name, its ticker and a picture first
|
|
150
|
+
(`references/telling-the-person.md`). Whatever the kind, rule 6 holds: never call a token an investment or promise it
|
|
151
|
+
a price.
|
|
152
|
+
|
|
131
153
|
## Building the app around it
|
|
132
154
|
|
|
133
155
|
A game, a site or a bot calls the live economy with the player's own wallet, through the package's player module
|
|
@@ -119,7 +119,7 @@ maps to a part of the spec (`xsolana_guide` has every field).
|
|
|
119
119
|
| "holders of the ticket claim a free badge once" | an open grant: no `vouchers`, `keys` or `list`, a `gate` of the ticket, and `limits.perWallet: 1` (each ticket shown claims once too) |
|
|
120
120
|
| "bring your own NFT", "any NFT of a partner collection can play", "holders of X get" | a `gate` (or a farm's, or odds') rule with `collection`: the collection's address; the project's notes about each NFT (a level) go in its Sheet |
|
|
121
121
|
| "our own token, 10 million, we keep it", "a fixed supply", "a community token" | `source: "project"` with `supply`: going live mints all of it to the owner and turns minting off for good; pools are seeded from it |
|
|
122
|
-
| "our token", "the coin we already launched", "this mint" | `source: "existing"
|
|
122
|
+
| "our token", "the coin we already launched", "this mint" | `source: "existing"`: ask for the mint's address, and `xsolana_token {"mint": …}` gives the whole entry (decimals, program, `powers`). loot doesn't put a token on a market: for trading, launch it on a token launchpad or an exchange first (SKILL.md, "When they ask for a token") |
|
|
123
123
|
| "Token-2022", "a transfer fee" | `program: "token-2022"` and `powers.transferFeePercent` |
|
|
124
124
|
| "tokens only, no NFTs" | `"collection": false`: no collection is made (no collectibles; normal items and tokens work) |
|
|
125
125
|
| "mint into our existing collection", "two collections" | `collections`: Core collections the owner's wallet already controls, each with the project added as an update delegate; an item's `collection` names the one it is minted into |
|
|
@@ -26,7 +26,9 @@ and the spec differ (a limit, an amount, a key not set yet), change one of them
|
|
|
26
26
|
6. **Flags for the law, once.** `W-CHANCE-PRIZE`: "Players pay for a chance at money. That is gambling in many places once
|
|
27
27
|
it is real money; this runs on devnet only." `W-CHANCE-ITEMS`: "Players pay for random items they can resell: some
|
|
28
28
|
places treat that as gambling, and many require the odds to be shown." `W-REVENUE-SHARE`: "Stakers are paid from
|
|
29
|
-
what players spend; regulators look at that when they decide whether a token is a security."
|
|
29
|
+
what players spend; regulators look at that when they decide whether a token is a security." `W-TOKEN-SALE`:
|
|
30
|
+
"People pay for a share of a new token; selling a token to raise money is what securities law looks at first."
|
|
31
|
+
Say each once, the
|
|
30
32
|
first time the checker raises it; then leave them, saying them again only when the person asks or a flag of a new
|
|
31
33
|
kind appears, and keep them out of pages and apps unless asked. The approval page shows them, with the program's
|
|
32
34
|
status (not independently audited, as is, at their own risk, theirs to make legal where they offer it), and keeps
|
package/types/chain/mint.d.ts
CHANGED
|
@@ -23,3 +23,32 @@ export declare function parseMint(address: any, owner: any, data: any, metadataD
|
|
|
23
23
|
symbol: string | null;
|
|
24
24
|
uri: string | null;
|
|
25
25
|
} | null;
|
|
26
|
+
/**
|
|
27
|
+
* A token made elsewhere as an economy spec names it: its `tokens` entry (`source: "existing"`), every field read from
|
|
28
|
+
* the chain so nothing is typed in by hand: the mint, its decimals and token program, its name and ticker when it has
|
|
29
|
+
* a name record, and `powers` (token2022.js `powersOf`: what the mint's own keys can do). `account`: the mint's
|
|
30
|
+
* ({ owner, data }); `record`: its Token Metadata record's data, when there is one. `key`: what the spec will call it
|
|
31
|
+
* (default: its ticker in the letters a key takes, else "token"). `project`: the loot project, whose own tokens read
|
|
32
|
+
* `mintAuthority: "project"`; `epoch`: the chain's current epoch, for a transfer fee since changed (`powersOf`). The
|
|
33
|
+
* name and ticker are cut to what a spec takes, on one line. Wrapped SOL is SOL (`{ key: "sol", source: "sol" }`).
|
|
34
|
+
* Null when `account` is no mint.
|
|
35
|
+
*/
|
|
36
|
+
export declare function tokenEntry(address: any, account: any, record?: null, { key, project, epoch }?: {
|
|
37
|
+
epoch?: null | undefined;
|
|
38
|
+
key?: null | undefined;
|
|
39
|
+
project?: null | undefined;
|
|
40
|
+
}): {
|
|
41
|
+
key: string;
|
|
42
|
+
source: string;
|
|
43
|
+
} | {
|
|
44
|
+
key: string;
|
|
45
|
+
source: string;
|
|
46
|
+
name?: string | undefined;
|
|
47
|
+
symbol?: string | undefined;
|
|
48
|
+
decimals: any;
|
|
49
|
+
program: string;
|
|
50
|
+
mint: any;
|
|
51
|
+
powers: {
|
|
52
|
+
mintAuthority: string;
|
|
53
|
+
} | null;
|
|
54
|
+
} | null;
|
|
@@ -26,6 +26,44 @@ export declare function mintPowers({ owner, data }: {
|
|
|
26
26
|
data: any;
|
|
27
27
|
owner: any;
|
|
28
28
|
}): any[];
|
|
29
|
+
/**
|
|
30
|
+
* What a mint's own keys can do, as an economy spec's `powers` says it (schema `token.powers`), read from the mint's
|
|
31
|
+
* account (`owner`, its token program; `data`), so nobody has to look it up and type it in. Only what is so is named:
|
|
32
|
+
* a plain mint nobody can mint more of reads `{ mintAuthority: 'none' }`. `project`: the loot project's address, when
|
|
33
|
+
* known, whose own tokens read `mintAuthority: 'project'`; any other minter is `'someone else'`.
|
|
34
|
+
* freeze: a freeze authority; permanentDelegate; transferHook (a hook program, or an authority who can name one);
|
|
35
|
+
* transferFeePercent and feeChangeable (its authority is still there); closeAuthority; pausable; defaultState (the
|
|
36
|
+
* mint sets what new accounts start as) and defaultFrozen (they start frozen); nonTransferable;
|
|
37
|
+
* confidentialTransfers.
|
|
38
|
+
* A mint holds two transfer fees: the one in force until an epoch, and the one from that epoch on. With `epoch` (the
|
|
39
|
+
* chain's current one) the fee read is the one that applies: the newer once its epoch has come, else the higher of
|
|
40
|
+
* the two (the fee today, or the one already set to follow). Without it, the higher of the two: never less than what
|
|
41
|
+
* a transfer pays, and a fee since lowered still reads at its old rate.
|
|
42
|
+
* Returns null when the account is not a mint of either token program.
|
|
43
|
+
*/
|
|
44
|
+
export declare function powersOf({ owner, data }: {
|
|
45
|
+
data: any;
|
|
46
|
+
owner: any;
|
|
47
|
+
}, { project, epoch }?: {
|
|
48
|
+
epoch?: null | undefined;
|
|
49
|
+
project?: null | undefined;
|
|
50
|
+
}): {
|
|
51
|
+
mintAuthority: string;
|
|
52
|
+
} | null;
|
|
53
|
+
/**
|
|
54
|
+
* What a Token-2022 mint's state does today that its powers don't say: `paused` (a pause is on: nothing moves),
|
|
55
|
+
* `scaled` (ScaledUiAmount or InterestBearing: wallets show amounts multiplied by a rate, not the base units the
|
|
56
|
+
* program moves and an economy counts in) and `scaler` (somebody can still change that rate). All false for a classic
|
|
57
|
+
* mint, or one with none of them.
|
|
58
|
+
*/
|
|
59
|
+
export declare function mintState({ owner, data }: {
|
|
60
|
+
data: any;
|
|
61
|
+
owner: any;
|
|
62
|
+
}): {
|
|
63
|
+
paused: boolean;
|
|
64
|
+
scaled: boolean;
|
|
65
|
+
scaler: boolean;
|
|
66
|
+
};
|
|
29
67
|
/** Whether a token account requires a memo on every transfer into it. */
|
|
30
68
|
export declare function requiresMemo(accountData: any): boolean;
|
|
31
69
|
/** The hook's list of extra accounts for this mint. */
|
|
@@ -4,6 +4,7 @@ export declare const LEGAL: Readonly<{
|
|
|
4
4
|
'W-CHANCE-ITEMS': "Players pay for random items they can resell (a loot box). Some places treat that as gambling and many require the odds shown: show them on the page, and get a lawyer before real money.";
|
|
5
5
|
'W-AUCTION-SHILL': "Anyone, the seller included, can bid to push an auction’s price up (shill bidding): a reserve is the honest way to set a floor.";
|
|
6
6
|
'W-REVENUE-SHARE': "Stakers are paid from what players spend, which regulators look at when they decide whether a token is a security: before real money, get a lawyer, or pay rewards from emissions instead.";
|
|
7
|
+
'W-TOKEN-SALE': "People pay for a share of a new token (a presale). Selling a token to raise money is what securities law looks at first, and many places limit who may buy: before real money, get a lawyer.";
|
|
7
8
|
}>;
|
|
8
9
|
/** The legal flags a checker verdict raises, each once, in the order the checker raised them: [{ code, says }]. */
|
|
9
10
|
export declare const legalFlags: (verdict: any) => {
|