@xgamesfun/solana 0.25.1 → 0.25.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.
@@ -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,8 +1,8 @@
1
1
  {
2
2
  "name": "@xgamesfun/solana",
3
- "version": "0.25.1",
3
+ "version": "0.25.2",
4
4
  "mcpName": "io.github.XGamesFun/solana-kit",
5
- "description": "One shared Solana program for game and community economies, its JS client, and the tools that let an AI agent design, check, simulate and launch an economy on it.",
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": {
@@ -15,13 +15,32 @@
15
15
  },
16
16
  "keywords": [
17
17
  "solana",
18
- "economy",
19
- "loot-box",
20
- "staking",
18
+ "solana-program",
19
+ "web3",
21
20
  "nft",
22
- "game",
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
- "agent"
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": "78b426dc003d261537934856302fe9767db149fd37b814af2569b95cc1333624"
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.1 <guide|examples|check|simulate|cost|explain|review|publish|status> [xsolana.json]`,
13
- or `npx -y @xgamesfun/solana@0.25.1 tool <name> '<json>'` for any tool (always with the exact version).
12
+ The tools are the `xsolana_*` MCP tools. Without MCP, the same from a terminal: `npx -y @xgamesfun/solana@0.25.2 <guide|examples|check|simulate|cost|explain|review|publish|status> [xsolana.json]`,
13
+ or `npx -y @xgamesfun/solana@0.25.2 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.1 approve xsolana.json`; never drop the version). It opens a page on their computer where
116
+ `npx -y @xgamesfun/solana@0.25.2 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
@@ -6,4 +6,5 @@ export * from './sizes.js';
6
6
  export * from './policy.js';
7
7
  export * from './compressed.js';
8
8
  export * from './traits.js';
9
+ export * from './kit.js';
9
10
  export * as random from './random.js';
@@ -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
+ };
@@ -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;