@xgamesfun/solana 0.25.1 → 0.25.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 +61 -12
- package/bin/xsolana.mjs +10 -2
- package/js/client/index.js +1 -0
- package/js/client/kit.js +36 -0
- package/js/mcp/http.mjs +136 -0
- package/js/mcp/server.mjs +33 -9
- package/js/mcp/tools.mjs +24 -1
- package/js/play/index.js +116 -2
- package/js/play/sponsor.js +302 -0
- package/package.json +36 -9
- package/release.json +1 -1
- package/skills/xsolana-economy/SKILL.md +6 -3
- package/skills/xsolana-economy/references/apps.md +28 -0
- package/types/client/index.d.ts +1 -0
- package/types/client/kit.d.ts +49 -0
- package/types/economy/plan.d.mts +14 -14
- package/types/mcp/http.d.mts +30 -0
- package/types/mcp/server.d.mts +321 -0
- package/types/mcp/tools.d.mts +9 -1
- package/types/play/index.d.ts +9 -7
- package/types/play/sponsor.d.ts +149 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// A project pays its players' fees: a sponsor. The sponsor is a plain key on the project's own server, with no role in
|
|
2
|
+
// the program: it is the transaction's fee payer, and it lends the player SOL in the same transaction, the player
|
|
3
|
+
// giving back, last, all but what the step costs them beyond its price (the platform's player fee, the oracle's fee
|
|
4
|
+
// for a roll, and the rent of what the program opens or mints for them). It lends first and takes back after because a
|
|
5
|
+
// step can need more SOL midway than it costs (a wrapped-SOL account it opens and closes). A player with no SOL can then
|
|
6
|
+
// play with what they hold. The price itself is never sponsored.
|
|
7
|
+
//
|
|
8
|
+
// The app (`solanaApp({ sponsor })`) builds the transaction with the sponsor as payer, the player's wallet signs it,
|
|
9
|
+
// and the project's server checks it (`checkSponsored`) and signs it (`sponsorSigner`, `sponsorHandler`) before the
|
|
10
|
+
// app sends it. The server trusts nothing the page says: it reads the transaction itself and dry-runs it. What it
|
|
11
|
+
// holds to:
|
|
12
|
+
//
|
|
13
|
+
// - the sponsor appears in no instruction but its one loan, a plain transfer to a signer of the transaction (the
|
|
14
|
+
// player), and plain transfers back to it: no instruction can move the sponsor's SOL but the loan, sign as it, or
|
|
15
|
+
// name it otherwise;
|
|
16
|
+
// - at least one instruction is this project's (a loot instruction naming the project), and none pays the project's
|
|
17
|
+
// fee box or the oracle straight from the page;
|
|
18
|
+
// - no priority fee past the server's limit (none by default);
|
|
19
|
+
// - in the dry run, what the sponsor is out (its loan less what came back) leaves the player no richer than they began,
|
|
20
|
+
// and is at most what the step cost in fees and rent: what the project's fee box and the oracle took, and the rent of
|
|
21
|
+
// what the program's own moves opened (`sponsoredCost`: a loot account, a token account of a mint the step's loot
|
|
22
|
+
// instructions name, a Core asset when the page calls Core itself nowhere). An account the page opens for itself (a
|
|
23
|
+
// plain key, a token account of any mint) is never sponsored: the player could close it and keep the rent;
|
|
24
|
+
// - the loan and the network fee together are at most `maxLamports`, and the project's `budget` (per player, per day)
|
|
25
|
+
// agrees.
|
|
26
|
+
//
|
|
27
|
+
// What a sponsor can still lose: rent the program later gives back to the player (an order's, when it settles; a token
|
|
28
|
+
// account the player closes), up to `maxLamports` a transaction, and a fee for a transaction that passed its dry run
|
|
29
|
+
// but failed on chain. `budget` is where a project caps those.
|
|
30
|
+
//
|
|
31
|
+
// Browser-safe: the server side needs only an RPC and the sponsor's own `sign`, which the project's server provides
|
|
32
|
+
// (this package never holds a key).
|
|
33
|
+
import { readTable } from '../chain/lookup.js';
|
|
34
|
+
import { COMPUTE_BUDGET_PROGRAM, TOKEN_2022_PROGRAM, TOKEN_PROGRAM } from '../chain/pda.js';
|
|
35
|
+
import { transferLamports } from '../chain/programs.js';
|
|
36
|
+
import { encodeBase58 } from '../chain/base58.js';
|
|
37
|
+
import { fromBase64, toBase64 } from '../chain/bytes.js';
|
|
38
|
+
import { readMessage, readTransaction, withSignature } from '../chain/tx.js';
|
|
39
|
+
import { LOOT_PROGRAM, MPL_CORE_PROGRAM, pda, VRF_PROGRAM } from '../client/index.js';
|
|
40
|
+
|
|
41
|
+
const SYSTEM_PROGRAM = '11111111111111111111111111111111';
|
|
42
|
+
/** What a signature costs the fee payer, in lamports. */
|
|
43
|
+
export const SIGNATURE_LAMPORTS = 5_000n;
|
|
44
|
+
/** The most a sponsored transaction may cost the sponsor by default: its top-up and its network fee, 0.01 SOL. */
|
|
45
|
+
export const SPONSOR_MAX_LAMPORTS = 10_000_000n;
|
|
46
|
+
|
|
47
|
+
const lamportsOf = a => (a ? BigInt(a.lamports) : 0n);
|
|
48
|
+
const u64At = (data, at) => new DataView(data.buffer, data.byteOffset).getBigUint64(at, true);
|
|
49
|
+
const u32At = (data, at) => new DataView(data.buffer, data.byteOffset).getUint32(at, true);
|
|
50
|
+
|
|
51
|
+
/** The top-up: the sponsor's transfer to the player, first in the transaction. */
|
|
52
|
+
export const sponsorTopUp = (sponsor, player, lamports) => transferLamports(sponsor, player, BigInt(lamports));
|
|
53
|
+
|
|
54
|
+
/** The give-back: what the step didn't need of the top-up, from the player back to the sponsor, last in the transaction. */
|
|
55
|
+
export const sponsorGiveBack = (sponsor, player, lamports) => transferLamports(player, sponsor, BigInt(lamports));
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* What a step cost in fees and rent, from the accounts it names before and after a dry run (`pre`, `post`: one each
|
|
59
|
+
* per address, null when there is none): what the project's fee box and the oracle took, and the full rent of each
|
|
60
|
+
* account it opened that loot, Core or a token program owns. Only what the program's own moves open counts: a token
|
|
61
|
+
* account counts when its mint is named by one of the step's loot instructions (`lootNamed`), and a Core asset when no
|
|
62
|
+
* instruction of the step calls Core itself (`programs`: the programs its instructions call), so a player can't open
|
|
63
|
+
* accounts of their own on the sponsor and close them for the rent. `skip`: the payer and the player.
|
|
64
|
+
* @param {{ addresses: string[], pre: ({ owner: string, lamports: number | bigint, data?: Uint8Array } | null)[],
|
|
65
|
+
* post: ({ owner: string, lamports: number | bigint, data?: Uint8Array } | null)[], project: string, skip?: string[],
|
|
66
|
+
* programs?: string[], lootNamed?: string[] }} input
|
|
67
|
+
* @returns {bigint}
|
|
68
|
+
*/
|
|
69
|
+
export function sponsoredCost({ addresses, pre, post, project, skip = [], programs = [], lootNamed = [] }) {
|
|
70
|
+
const feeBox = pda.fees(project);
|
|
71
|
+
const named = new Set(lootNamed);
|
|
72
|
+
let cost = 0n;
|
|
73
|
+
for (const [i, address] of addresses.entries()) {
|
|
74
|
+
if (skip.includes(address)) continue;
|
|
75
|
+
const before = pre[i];
|
|
76
|
+
const after = post[i];
|
|
77
|
+
if (!after) continue;
|
|
78
|
+
const grew = lamportsOf(after) - lamportsOf(before);
|
|
79
|
+
if (address === feeBox || after.owner === VRF_PROGRAM) {
|
|
80
|
+
cost += grew > 0n ? grew : 0n;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (before) continue;
|
|
84
|
+
const opened =
|
|
85
|
+
after.owner === LOOT_PROGRAM ||
|
|
86
|
+
(after.owner === MPL_CORE_PROGRAM && !programs.includes(MPL_CORE_PROGRAM)) ||
|
|
87
|
+
((after.owner === TOKEN_PROGRAM || after.owner === TOKEN_2022_PROGRAM) &&
|
|
88
|
+
after.data?.length >= 32 &&
|
|
89
|
+
named.has(encodeBase58(after.data.subarray(0, 32))));
|
|
90
|
+
if (opened) cost += lamportsOf(after);
|
|
91
|
+
}
|
|
92
|
+
return cost;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The programs a step's instructions call, and every account its loot instructions name: what `sponsoredCost` reads. */
|
|
96
|
+
export function stepShape(instructions) {
|
|
97
|
+
return {
|
|
98
|
+
programs: [...new Set(instructions.map(ix => ix.programId))],
|
|
99
|
+
lootNamed: [
|
|
100
|
+
...new Set(instructions.filter(ix => ix.programId === LOOT_PROGRAM).flatMap(ix => ix.keys.map(k => k.pubkey))),
|
|
101
|
+
],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* The top-up a step needs: what it cost the player (`spend`, before any top-up) bounded by its fees and rent
|
|
107
|
+
* (`cost`), never below 0. A step that leaves the player richer, or costs nothing, needs none.
|
|
108
|
+
*/
|
|
109
|
+
export const topUpFor = ({ spend, cost }) => {
|
|
110
|
+
const t = spend < cost ? spend : cost;
|
|
111
|
+
return t > 0n ? t : 0n;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/** The transaction's accounts in order: its static keys, then each lookup table's writable, then read-only ones. */
|
|
115
|
+
async function accountKeys(message, rpc) {
|
|
116
|
+
const writable = [];
|
|
117
|
+
const readonly = [];
|
|
118
|
+
for (const l of message.lookups) {
|
|
119
|
+
const table = readTable(await rpc.account(l.address));
|
|
120
|
+
if (!table || table.deactivationSlot !== null) throw new Error(`the lookup table ${l.address} is not active`);
|
|
121
|
+
const at = i => {
|
|
122
|
+
if (i >= table.addresses.length) throw new Error(`the lookup table ${l.address} has no address ${i}`);
|
|
123
|
+
return table.addresses[i];
|
|
124
|
+
};
|
|
125
|
+
writable.push(...l.writable.map(at));
|
|
126
|
+
readonly.push(...l.readonly.map(at));
|
|
127
|
+
}
|
|
128
|
+
return [...message.keys, ...writable, ...readonly];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Whether the project's sponsor should sign this transaction (a wire, the player's signature in it): read it, dry-run
|
|
133
|
+
* it, and hold it to the rules above. Returns { ok: true, player, topUp, cost, lamports, message } (`topUp`: what the
|
|
134
|
+
* player kept of it, the top-up less the give-back; `lamports`: what it costs the sponsor, that and the network fee)
|
|
135
|
+
* or { ok: false, reason } in words.
|
|
136
|
+
* @param {Uint8Array} wire
|
|
137
|
+
* @param {{ rpc: any, project: string, sponsor: string, maxLamports?: bigint | number, maxPriorityMicroLamports?: bigint | number,
|
|
138
|
+
* budget?: ((ask: { player: string, lamports: bigint, topUp: bigint }) => boolean | Promise<boolean>) | null }} options
|
|
139
|
+
*/
|
|
140
|
+
export async function checkSponsored(
|
|
141
|
+
wire,
|
|
142
|
+
{ rpc, project, sponsor, maxLamports = SPONSOR_MAX_LAMPORTS, maxPriorityMicroLamports = 0n, budget = null },
|
|
143
|
+
) {
|
|
144
|
+
const no = reason => ({ ok: false, reason });
|
|
145
|
+
let t;
|
|
146
|
+
let m;
|
|
147
|
+
let keys;
|
|
148
|
+
try {
|
|
149
|
+
t = readTransaction(wire);
|
|
150
|
+
m = readMessage(t.message);
|
|
151
|
+
keys = await accountKeys(m, rpc);
|
|
152
|
+
} catch (e) {
|
|
153
|
+
return no(`It is not a transaction this server can read: ${e.message}.`);
|
|
154
|
+
}
|
|
155
|
+
if (keys[0] !== sponsor) return no('The sponsor is not its fee payer.');
|
|
156
|
+
const signers = keys.slice(0, m.header.numRequiredSignatures);
|
|
157
|
+
if (signers.length < 2) return no('Only the sponsor signs it: a sponsored transaction is a player’s.');
|
|
158
|
+
let topUp = null;
|
|
159
|
+
let back = 0n;
|
|
160
|
+
let ours = false;
|
|
161
|
+
let units = 200_000n * BigInt(m.instructions.length);
|
|
162
|
+
let price = 0n;
|
|
163
|
+
for (const ix of m.instructions) {
|
|
164
|
+
const program = keys[ix.programIndex];
|
|
165
|
+
const accounts = ix.accountIndexes.map(i => keys[i]);
|
|
166
|
+
if (program === sponsor) return no('It calls the sponsor as a program.');
|
|
167
|
+
const isTransfer = program === SYSTEM_PROGRAM && ix.data.length === 12 && u32At(ix.data, 0) === 2;
|
|
168
|
+
if (isTransfer && accounts[0] === sponsor && accounts.length === 2) {
|
|
169
|
+
if (topUp) return no('It tops up more than once.');
|
|
170
|
+
if (!signers.includes(accounts[1]) || accounts[1] === sponsor)
|
|
171
|
+
return no('Its top-up goes to someone who doesn’t sign it.');
|
|
172
|
+
topUp = { to: accounts[1], lamports: u64At(ix.data, 4) };
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (isTransfer && accounts[1] === sponsor && accounts[0] !== sponsor && accounts.length === 2) {
|
|
176
|
+
back += u64At(ix.data, 4);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (accounts.includes(sponsor)) return no('The sponsor is named in an instruction other than the top-up.');
|
|
180
|
+
if (program === COMPUTE_BUDGET_PROGRAM && ix.data[0] === 2 && ix.data.length >= 5)
|
|
181
|
+
units = BigInt(u32At(ix.data, 1));
|
|
182
|
+
if (program === COMPUTE_BUDGET_PROGRAM && ix.data[0] === 3 && ix.data.length >= 9) price = u64At(ix.data, 1);
|
|
183
|
+
if (program === LOOT_PROGRAM && accounts.includes(project)) ours = true;
|
|
184
|
+
}
|
|
185
|
+
if (!ours) return no('It does nothing in this project.');
|
|
186
|
+
if (price > BigInt(maxPriorityMicroLamports)) return no('Its priority fee is more than this sponsor pays.');
|
|
187
|
+
const fee = SIGNATURE_LAMPORTS * BigInt(signers.length) + (units * price + 999_999n) / 1_000_000n;
|
|
188
|
+
const player = topUp?.to ?? signers.find(s => s !== sponsor);
|
|
189
|
+
const lent = topUp?.lamports ?? 0n;
|
|
190
|
+
if (lent + fee > BigInt(maxLamports))
|
|
191
|
+
return no(`It would cost the sponsor ${lent + fee} lamports, more than the ${maxLamports} it pays a transaction.`);
|
|
192
|
+
// What the sponsor is out once the player gives back what the step didn't need.
|
|
193
|
+
const net = lent > back ? lent - back : 0n;
|
|
194
|
+
const lamports = net + fee;
|
|
195
|
+
const addresses = [...new Set(keys)];
|
|
196
|
+
const at = address => addresses.indexOf(address);
|
|
197
|
+
let pre;
|
|
198
|
+
try {
|
|
199
|
+
pre = await rpc.accounts(addresses);
|
|
200
|
+
} catch (e) {
|
|
201
|
+
return no(`Its accounts could not be read: ${e.message}`);
|
|
202
|
+
}
|
|
203
|
+
const feeBox = pda.fees(project);
|
|
204
|
+
const steps = m.instructions.map(ix => ({
|
|
205
|
+
programId: keys[ix.programIndex],
|
|
206
|
+
keys: ix.accountIndexes.map(i => ({ pubkey: keys[i] })),
|
|
207
|
+
}));
|
|
208
|
+
// The fee box and the oracle are paid by the program's own moves, never straight from an instruction of the page's.
|
|
209
|
+
for (const ix of steps.filter(x => x.programId === SYSTEM_PROGRAM))
|
|
210
|
+
if (ix.keys.some(k => k.pubkey === feeBox || pre[at(k.pubkey)]?.owner === VRF_PROGRAM))
|
|
211
|
+
return no('It pays the fee box or the oracle straight from an instruction of its own.');
|
|
212
|
+
let post;
|
|
213
|
+
try {
|
|
214
|
+
post = (await rpc.simulate(wire, { accounts: addresses })).accounts;
|
|
215
|
+
} catch (e) {
|
|
216
|
+
return no(`It would fail: ${e.message}`);
|
|
217
|
+
}
|
|
218
|
+
const cost = sponsoredCost({ addresses, pre, post, project, skip: [sponsor, player], ...stepShape(steps) });
|
|
219
|
+
// A player who keeps none of the top-up (it was all given back) may end richer by what the step itself pays them
|
|
220
|
+
// (an order's rent, back at its settle): the sponsor paid only the fee.
|
|
221
|
+
if (net > 0n) {
|
|
222
|
+
if (lamportsOf(post[at(player)]) > lamportsOf(pre[at(player)]))
|
|
223
|
+
return no('Its top-up is more than the step costs the player: they would keep some.');
|
|
224
|
+
if (net > cost) return no('Its top-up is more than the step’s fees and rent: the price is the player’s.');
|
|
225
|
+
}
|
|
226
|
+
if (budget && !(await budget({ player, lamports, topUp: net })))
|
|
227
|
+
return no('This project’s sponsor has paid as much as it pays for now.');
|
|
228
|
+
return { ok: true, player, topUp: net, cost, lamports, message: t.message };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The project's server side: `sign(wire)` checks a transaction (`checkSponsored`) and, when it passes, returns it with
|
|
233
|
+
* the sponsor's signature; otherwise it throws the reason (`refused: true`). `sponsor.sign(message)`: the sponsor key's
|
|
234
|
+
* ed25519 signature of the message bytes, made by the project's server, which alone holds that key.
|
|
235
|
+
* @param {{ rpc: any, project: string, sponsor: { address: string, sign: (message: Uint8Array) => Uint8Array | Promise<Uint8Array> },
|
|
236
|
+
* maxLamports?: bigint | number, maxPriorityMicroLamports?: bigint | number,
|
|
237
|
+
* budget?: ((ask: { player: string, lamports: bigint, topUp: bigint }) => boolean | Promise<boolean>) | null }} options
|
|
238
|
+
*/
|
|
239
|
+
export function sponsorSigner({ rpc, project, sponsor, ...limits }) {
|
|
240
|
+
return {
|
|
241
|
+
address: sponsor.address,
|
|
242
|
+
async sign(wire) {
|
|
243
|
+
const checked = await checkSponsored(wire, { rpc, project, sponsor: sponsor.address, ...limits });
|
|
244
|
+
if (!checked.ok) throw Object.assign(new Error(checked.reason), { refused: true });
|
|
245
|
+
return withSignature(wire, sponsor.address, await sponsor.sign(checked.message));
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* The sponsor as an HTTP endpoint (a Next.js route, a Worker): POST { transaction } (base64) answers { transaction }
|
|
252
|
+
* signed, or 403 { error } in words. `signer`: a `sponsorSigner`.
|
|
253
|
+
* @param {Request} request
|
|
254
|
+
* @param {{ address: string, sign: (wire: Uint8Array) => Promise<Uint8Array> }} signer
|
|
255
|
+
* @returns {Promise<Response>}
|
|
256
|
+
*/
|
|
257
|
+
export async function sponsorHandler(request, signer) {
|
|
258
|
+
const answer = (body, status = 200) =>
|
|
259
|
+
new Response(JSON.stringify(body), {
|
|
260
|
+
status,
|
|
261
|
+
headers: { 'content-type': 'application/json', 'cache-control': 'no-store' },
|
|
262
|
+
});
|
|
263
|
+
if (request.method !== 'POST') return answer({ error: 'POST a transaction.' }, 405);
|
|
264
|
+
let wire;
|
|
265
|
+
try {
|
|
266
|
+
const body = await request.json();
|
|
267
|
+
wire = fromBase64(String(body.transaction));
|
|
268
|
+
if (wire.length > 1232) throw new Error('too long');
|
|
269
|
+
} catch {
|
|
270
|
+
return answer({ error: 'Send { "transaction": "<base64>" }.' }, 400);
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
return answer({ transaction: toBase64(await signer.sign(wire)), sponsor: signer.address });
|
|
274
|
+
} catch (e) {
|
|
275
|
+
return answer({ error: e.message }, e.refused ? 403 : 500);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* The app's side of a sponsor behind `url` (`sponsorHandler`): what `solanaApp({ sponsor })` takes. `address`: the
|
|
281
|
+
* sponsor's public key; `maxLamports`: the most it pays a transaction, as its server says.
|
|
282
|
+
* @param {{ url: string, address: string, maxLamports?: bigint | number, fetchImpl?: typeof fetch }} options
|
|
283
|
+
*/
|
|
284
|
+
export function httpSponsor({ url, address, maxLamports = SPONSOR_MAX_LAMPORTS, fetchImpl = fetch }) {
|
|
285
|
+
return {
|
|
286
|
+
address,
|
|
287
|
+
maxLamports: BigInt(maxLamports),
|
|
288
|
+
async sign(wire) {
|
|
289
|
+
const r = await fetchImpl(url, {
|
|
290
|
+
method: 'POST',
|
|
291
|
+
headers: { 'content-type': 'application/json' },
|
|
292
|
+
body: JSON.stringify({ transaction: toBase64(wire) }),
|
|
293
|
+
});
|
|
294
|
+
const body = await r.json().catch(() => ({}));
|
|
295
|
+
if (!r.ok || !body.transaction)
|
|
296
|
+
throw Object.assign(new Error(body.error ?? `The sponsor answered ${r.status}.`), {
|
|
297
|
+
refused: r.status === 403,
|
|
298
|
+
});
|
|
299
|
+
return fromBase64(body.transaction);
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
}
|
package/package.json
CHANGED
|
@@ -1,27 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xgamesfun/solana",
|
|
3
|
-
"version": "0.25.
|
|
3
|
+
"version": "0.25.3",
|
|
4
4
|
"mcpName": "io.github.XGamesFun/solana-kit",
|
|
5
|
-
"description": "
|
|
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",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
10
|
"url": "git+https://github.com/XGamesFun/solana-kit.git"
|
|
11
11
|
},
|
|
12
|
-
"homepage": "https://
|
|
12
|
+
"homepage": "https://xgames.fun/solana",
|
|
13
13
|
"bugs": {
|
|
14
14
|
"url": "https://github.com/XGamesFun/solana-kit/issues"
|
|
15
15
|
},
|
|
16
16
|
"keywords": [
|
|
17
17
|
"solana",
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"staking",
|
|
18
|
+
"solana-program",
|
|
19
|
+
"web3",
|
|
21
20
|
"nft",
|
|
22
|
-
"
|
|
21
|
+
"nft-staking",
|
|
22
|
+
"staking",
|
|
23
|
+
"loot-box",
|
|
24
|
+
"gacha",
|
|
25
|
+
"lottery",
|
|
26
|
+
"raffle",
|
|
27
|
+
"auction",
|
|
28
|
+
"crafting",
|
|
29
|
+
"game-economy",
|
|
30
|
+
"web3-gaming",
|
|
31
|
+
"gamefi",
|
|
32
|
+
"token",
|
|
33
|
+
"spl-token",
|
|
34
|
+
"metaplex-core",
|
|
35
|
+
"no-smart-contract",
|
|
36
|
+
"vrf",
|
|
23
37
|
"mcp",
|
|
24
|
-
"
|
|
38
|
+
"mcp-server",
|
|
39
|
+
"model-context-protocol",
|
|
40
|
+
"claude-code",
|
|
41
|
+
"codex",
|
|
42
|
+
"ai-agent",
|
|
43
|
+
"agent-skill"
|
|
25
44
|
],
|
|
26
45
|
"engines": {
|
|
27
46
|
"node": ">=22"
|
|
@@ -43,13 +62,21 @@
|
|
|
43
62
|
"types": "./types/play/index.d.ts",
|
|
44
63
|
"default": "./js/play/index.js"
|
|
45
64
|
},
|
|
65
|
+
"./sponsor": {
|
|
66
|
+
"types": "./types/play/sponsor.d.ts",
|
|
67
|
+
"default": "./js/play/sponsor.js"
|
|
68
|
+
},
|
|
46
69
|
"./results": {
|
|
47
70
|
"types": "./types/results/index.d.ts",
|
|
48
71
|
"default": "./js/results/index.js"
|
|
49
72
|
},
|
|
50
73
|
"./idl.json": "./js/client/idl/loot.json",
|
|
51
74
|
"./package.json": "./package.json",
|
|
52
|
-
"./release.json": "./release.json"
|
|
75
|
+
"./release.json": "./release.json",
|
|
76
|
+
"./mcp/http": {
|
|
77
|
+
"types": "./types/mcp/http.d.mts",
|
|
78
|
+
"default": "./js/mcp/http.mjs"
|
|
79
|
+
}
|
|
53
80
|
},
|
|
54
81
|
"sideEffects": [
|
|
55
82
|
"./bin/xsolana.mjs",
|
package/release.json
CHANGED
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
"sbfBytes": 1741840,
|
|
5
5
|
"sbfSha256": "ea19d15d03211f7bd0b25179d51f75544342225127fcb631e687eae6750c9ba7",
|
|
6
6
|
"idlSha256": "41e621990c17b8645f59bb2a170271645ad370d1c1bf9f1dbdd04f817b82d4cf",
|
|
7
|
-
"clientSha256": "
|
|
7
|
+
"clientSha256": "849106b5d1a022aac13e1208172945c4cee0c4a55cafbd5f1c77e1be6948de04"
|
|
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.25.
|
|
13
|
-
or `npx -y @xgamesfun/solana@0.25.
|
|
12
|
+
The tools are the `xsolana_*` MCP tools. Without MCP, the same from a terminal: `npx -y @xgamesfun/solana@0.25.3 <guide|examples|check|simulate|cost|explain|review|publish|status> [xsolana.json]`,
|
|
13
|
+
or `npx -y @xgamesfun/solana@0.25.3 tool <name> '<json>'` for any tool (always with the exact version).
|
|
14
14
|
|
|
15
15
|
## Rules (always)
|
|
16
16
|
|
|
@@ -113,7 +113,7 @@ or `npx -y @xgamesfun/solana@0.25.1 tool <name> '<json>'` for any tool (always w
|
|
|
113
113
|
level would cost. Run it again after every change that touches a route that pays.
|
|
114
114
|
9. **When they say "put it live"**: `xsolana_publish` lists every step and its cost and gives the command. Tell them to
|
|
115
115
|
run it themselves in the project folder, exactly as `xsolana_publish` gives it (it names the exact version:
|
|
116
|
-
`npx -y @xgamesfun/solana@0.25.
|
|
116
|
+
`npx -y @xgamesfun/solana@0.25.3 approve xsolana.json`; never drop the version). It opens a page on their computer where
|
|
117
117
|
they connect a devnet wallet and approve; what lands is written back into `xsolana.json`. You publish nothing.
|
|
118
118
|
10. **After it is live**: `xsolana_status` shows each part on chain; read it before acting on what the person says
|
|
119
119
|
happened. A later change is the same loop: save, publish, and the approval sends only what changed. What only the
|
|
@@ -129,6 +129,9 @@ is due, list what a player owns). Pages call `claims()` when they open and show
|
|
|
129
129
|
`references/apps.md` shows a whole page; `xsolana_client` gives the program and the calls. Never invent account layouts,
|
|
130
130
|
instruction data or addresses, and never put a key or the authority's actions in a page players load.
|
|
131
131
|
For staking an NFT of another collection, show what custody means and get explicit acceptance before sending.
|
|
132
|
+
Players with no SOL can still play when the project sponsors their fees: a key on the project's own server pays them,
|
|
133
|
+
never the price (`references/apps.md`, "Sponsoring players' fees"). Offer it when the person wants free first steps or
|
|
134
|
+
players new to Solana.
|
|
132
135
|
|
|
133
136
|
## More
|
|
134
137
|
|
|
@@ -198,6 +198,34 @@ assetId, tree }` after a `loot.proveLeafIx` in the same transaction. `app.stakeN
|
|
|
198
198
|
`collectibles()`), one read at a time, and keep what is on screen when one read fails.
|
|
199
199
|
- **Errors** come back as plain sentences (a sold-out item, a gate not met, a price above the most they agreed to):
|
|
200
200
|
show them.
|
|
201
|
+
- **Sponsoring players' fees** (players with no SOL, a free item or box as a first step): the project runs a sponsor,
|
|
202
|
+
a plain key on its own server with no role in the program, funded with SOL. The page passes
|
|
203
|
+
`solanaApp({ ..., sponsor: httpSponsor({ url: '/sponsor', address: SPONSOR_ADDRESS }) })`; each step then has the
|
|
204
|
+
sponsor pay the network fee and lend the player SOL for the step, the player giving back, in the same transaction,
|
|
205
|
+
all but what it costs in fees and rent (the player fee, the oracle's fee for a roll, the rent of what the program
|
|
206
|
+
opens or mints). The price is never sponsored: a priced step still takes the player's own SOL or tokens. The server
|
|
207
|
+
route is a few lines:
|
|
208
|
+
|
|
209
|
+
```js
|
|
210
|
+
import { sponsorHandler, sponsorSigner } from '@xgamesfun/solana/sponsor';
|
|
211
|
+
const signer = sponsorSigner({
|
|
212
|
+
rpc, // the client's rpcClient, or any with accounts and simulate
|
|
213
|
+
project: spec.live.project,
|
|
214
|
+
sponsor: { address: SPONSOR_ADDRESS, sign: message => signWithServerKey(message) }, // the server's own ed25519 key
|
|
215
|
+
maxLamports: 10_000_000n, // the most one transaction costs it (0.01 SOL)
|
|
216
|
+
budget: ({ player, lamports }) => underTodaysCap(player, lamports), // the project's own per-player, per-day limit
|
|
217
|
+
});
|
|
218
|
+
export const POST = request => sponsorHandler(request, signer);
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
The server reads and dry-runs every transaction before it signs (`checkSponsored`): the sponsor may only pay the fee
|
|
222
|
+
and the one loan, the transaction must be this project's, and the player may keep no more than the step's fees and
|
|
223
|
+
rent. What a sponsor can still lose is rent the program later gives back to the player (an order's, at its settle)
|
|
224
|
+
and fees for transactions that fail on chain, so always give it a `budget`, keep only a few days' worth of SOL in
|
|
225
|
+
it, and never make it the owner or the authority. Sponsored, a box's roll takes two prompts (the open, then the
|
|
226
|
+
settle) instead of one, and a step too big for one transaction goes as the player's own. Tell the person what a
|
|
227
|
+
day of their players costs the sponsor (`xsolana_cost`: the player fee, the oracle's fee and rent per step).
|
|
228
|
+
|
|
201
229
|
- **Never in a page**: a private key, a keyed RPC link, or anything the authority signs (payouts, minting, rewriting
|
|
202
230
|
a collectible). Those are the owner's or the authority's to sign in their own wallet, or on a server they run.
|
|
203
231
|
- **Lower level**: `@xgamesfun/solana/client` has one builder per instruction and the account decoders, for what the
|
package/types/client/index.d.ts
CHANGED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** @solana/kit's AccountRole: bit 1 is a signer, bit 0 is writable. */
|
|
2
|
+
export declare const KIT_ACCOUNT_ROLE: Readonly<{
|
|
3
|
+
READONLY: 0;
|
|
4
|
+
WRITABLE: 1;
|
|
5
|
+
READONLY_SIGNER: 2;
|
|
6
|
+
WRITABLE_SIGNER: 3;
|
|
7
|
+
}>;
|
|
8
|
+
/**
|
|
9
|
+
* One instruction from a builder here, as @solana/kit takes it: `appendTransactionMessageInstruction(toKitInstruction(ix), m)`.
|
|
10
|
+
* @param {{ programId: string, keys: { pubkey: string, isSigner: boolean, isWritable: boolean }[], data: Uint8Array }} ix
|
|
11
|
+
* @returns {{ programAddress: string, accounts: { address: string, role: number }[], data: Uint8Array }}
|
|
12
|
+
*/
|
|
13
|
+
export declare function toKitInstruction(ix: {
|
|
14
|
+
programId: string;
|
|
15
|
+
keys: {
|
|
16
|
+
pubkey: string;
|
|
17
|
+
isSigner: boolean;
|
|
18
|
+
isWritable: boolean;
|
|
19
|
+
}[];
|
|
20
|
+
data: Uint8Array;
|
|
21
|
+
}): {
|
|
22
|
+
programAddress: string;
|
|
23
|
+
accounts: {
|
|
24
|
+
address: string;
|
|
25
|
+
role: number;
|
|
26
|
+
}[];
|
|
27
|
+
data: Uint8Array;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* A @solana/kit instruction in the shape this package's transactions take (js/chain/tx.js, js/play).
|
|
31
|
+
* @param {{ programAddress: string, accounts?: { address: string, role: number }[], data?: Uint8Array }} ix
|
|
32
|
+
* @returns {{ programId: string, keys: { pubkey: string, isSigner: boolean, isWritable: boolean }[], data: Uint8Array }}
|
|
33
|
+
*/
|
|
34
|
+
export declare function fromKitInstruction(ix: {
|
|
35
|
+
programAddress: string;
|
|
36
|
+
accounts?: {
|
|
37
|
+
address: string;
|
|
38
|
+
role: number;
|
|
39
|
+
}[];
|
|
40
|
+
data?: Uint8Array;
|
|
41
|
+
}): {
|
|
42
|
+
programId: string;
|
|
43
|
+
keys: {
|
|
44
|
+
pubkey: string;
|
|
45
|
+
isSigner: boolean;
|
|
46
|
+
isWritable: boolean;
|
|
47
|
+
}[];
|
|
48
|
+
data: Uint8Array;
|
|
49
|
+
};
|
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;
|
|
33
34
|
Compressed: {
|
|
34
35
|
item: any;
|
|
35
36
|
tree: string;
|
|
36
37
|
};
|
|
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
|
-
Collection: any[];
|
|
141
140
|
Item?: undefined;
|
|
141
|
+
Collection: any[];
|
|
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
|
-
Collection: any[];
|
|
163
162
|
Item?: undefined;
|
|
163
|
+
Collection: any[];
|
|
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;
|
|
344
345
|
Compressed: {
|
|
345
346
|
item: any;
|
|
346
347
|
tree: string;
|
|
347
348
|
};
|
|
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
|
-
Collection: any[];
|
|
468
467
|
Item?: undefined;
|
|
468
|
+
Collection: any[];
|
|
469
469
|
};
|
|
470
470
|
key: any;
|
|
471
471
|
test: any;
|
|
@@ -512,8 +512,8 @@ export declare function lotteryArgs(l: any, m: any): {
|
|
|
512
512
|
Item: any[];
|
|
513
513
|
Collection?: undefined;
|
|
514
514
|
} | {
|
|
515
|
-
Collection: any[];
|
|
516
515
|
Item?: undefined;
|
|
516
|
+
Collection: any[];
|
|
517
517
|
};
|
|
518
518
|
key: any;
|
|
519
519
|
test: any;
|
|
@@ -587,8 +587,8 @@ export declare function contestArgs(c: any, m: any, at?: any): {
|
|
|
587
587
|
Item: any[];
|
|
588
588
|
Collection?: undefined;
|
|
589
589
|
} | {
|
|
590
|
-
Collection: any[];
|
|
591
590
|
Item?: undefined;
|
|
591
|
+
Collection: any[];
|
|
592
592
|
};
|
|
593
593
|
key: any;
|
|
594
594
|
test: any;
|
|
@@ -598,11 +598,11 @@ export declare function contestArgs(c: any, m: any, at?: any): {
|
|
|
598
598
|
min_pot: bigint;
|
|
599
599
|
stake: bigint;
|
|
600
600
|
prize: string | {
|
|
601
|
+
Item?: undefined;
|
|
601
602
|
Compressed: {
|
|
602
603
|
item: any;
|
|
603
604
|
tree: string;
|
|
604
605
|
};
|
|
605
|
-
Item?: undefined;
|
|
606
606
|
Held?: undefined;
|
|
607
607
|
Nft?: undefined;
|
|
608
608
|
Roll?: undefined;
|
|
@@ -661,8 +661,8 @@ export declare function contestArgs(c: any, m: any, at?: any): {
|
|
|
661
661
|
Item: any[];
|
|
662
662
|
Collection?: undefined;
|
|
663
663
|
} | {
|
|
664
|
-
Collection: any[];
|
|
665
664
|
Item?: undefined;
|
|
665
|
+
Collection: any[];
|
|
666
666
|
};
|
|
667
667
|
key: any;
|
|
668
668
|
test: any;
|
|
@@ -679,8 +679,8 @@ export declare function farmArgs(f: any, m: any): {
|
|
|
679
679
|
Item: any[];
|
|
680
680
|
Collection?: undefined;
|
|
681
681
|
} | {
|
|
682
|
-
Collection: any[];
|
|
683
682
|
Item?: undefined;
|
|
683
|
+
Collection: any[];
|
|
684
684
|
};
|
|
685
685
|
key: any;
|
|
686
686
|
test: any;
|
|
@@ -759,8 +759,8 @@ export declare function auctionArgs(a: any, m: any): {
|
|
|
759
759
|
Item: any[];
|
|
760
760
|
Collection?: undefined;
|
|
761
761
|
} | {
|
|
762
|
-
Collection: any[];
|
|
763
762
|
Item?: undefined;
|
|
763
|
+
Collection: any[];
|
|
764
764
|
};
|
|
765
765
|
key: any;
|
|
766
766
|
test: any;
|
|
@@ -771,8 +771,8 @@ export declare function auctionArgs(a: any, m: any): {
|
|
|
771
771
|
Item: any[];
|
|
772
772
|
Collection?: undefined;
|
|
773
773
|
} | {
|
|
774
|
-
Collection: any[];
|
|
775
774
|
Item?: undefined;
|
|
775
|
+
Collection: any[];
|
|
776
776
|
};
|
|
777
777
|
key: any;
|
|
778
778
|
test: any;
|
|
@@ -780,11 +780,11 @@ export declare function auctionArgs(a: any, m: any): {
|
|
|
780
780
|
};
|
|
781
781
|
export declare function grantArgs(g: any, m: any): {
|
|
782
782
|
gives: string | {
|
|
783
|
+
Item?: undefined;
|
|
783
784
|
Compressed: {
|
|
784
785
|
item: any;
|
|
785
786
|
tree: string;
|
|
786
787
|
};
|
|
787
|
-
Item?: undefined;
|
|
788
788
|
Held?: undefined;
|
|
789
789
|
Nft?: undefined;
|
|
790
790
|
Roll?: undefined;
|
|
@@ -890,8 +890,8 @@ export declare function grantArgs(g: any, m: any): {
|
|
|
890
890
|
Item: any[];
|
|
891
891
|
Collection?: undefined;
|
|
892
892
|
} | {
|
|
893
|
-
Collection: any[];
|
|
894
893
|
Item?: undefined;
|
|
894
|
+
Collection: any[];
|
|
895
895
|
};
|
|
896
896
|
key: any;
|
|
897
897
|
test: any;
|