@xgamesfun/solana 0.27.1 → 0.27.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -17
- package/bin/xsolana.mjs +72 -7
- package/js/approve/details.mjs +1 -1
- package/js/approve/keyfile.mjs +242 -0
- package/js/approve/live-check.mjs +5 -2
- package/js/approve/server.mjs +94 -13
- package/js/approve/with-key.mjs +298 -0
- package/js/chain/lookup.js +11 -0
- package/js/chain/programs.js +49 -1
- package/js/chain/rpc.js +53 -6
- package/js/economy/check.mjs +2 -2
- package/js/economy/cost.mjs +20 -4
- package/js/economy/explain.mjs +3 -3
- package/js/economy/guide.mjs +2 -2
- package/js/economy/live.mjs +1 -1
- package/js/economy/plan.mjs +34 -6
- package/js/economy/review.mjs +1 -1
- package/js/economy/schema.json +1 -1
- package/js/economy/txsize.mjs +3 -2
- package/js/mcp/server.mjs +3 -3
- package/js/mcp/tools.mjs +50 -13
- package/js/play/index.js +21 -7
- package/package.json +5 -1
- package/release.json +1 -1
- package/skills/xsolana-economy/SKILL.md +58 -19
- package/skills/xsolana-economy/references/after-launch.md +43 -12
- package/skills/xsolana-economy/references/apps.md +1 -1
- package/skills/xsolana-economy/references/sentence-to-spec.md +3 -3
- package/skills/xsolana-economy/references/telling-the-person.md +18 -11
- package/skills/xsolana-economy/references/trust-and-cheating.md +4 -2
- package/types/approve/keyfile.d.mts +36 -0
- package/types/chain/lookup.d.ts +12 -0
- package/types/chain/programs.d.ts +22 -0
- package/types/chain/rpc.d.ts +15 -1
- package/types/economy/plan.d.mts +21 -4
- package/types/mcp/server.d.mts +4 -3
- package/types/mcp/tools.d.mts +14 -5
package/js/chain/rpc.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// (devnet: about 100 requests per 10 s per IP, 40 for any one method), so 429s are retried with backoff, and so are a
|
|
3
3
|
// gateway's passing failures (502, 503, 504).
|
|
4
4
|
import { fromBase64, toBase64 } from './bytes.js';
|
|
5
|
+
import { transactionId } from './tx.js';
|
|
5
6
|
|
|
6
7
|
// The options of a call made without any: read-only, and typed so the generated declarations take any option.
|
|
7
8
|
const NO_OPTIONS = Object.freeze(/** @type {Record<string, any>} */ ({}));
|
|
@@ -21,11 +22,19 @@ const BEHIND = [-32005, -32016];
|
|
|
21
22
|
|
|
22
23
|
/** A JSON-RPC failure, with the node's own message (and logs for a failed simulation). */
|
|
23
24
|
export class RpcError extends Error {
|
|
24
|
-
|
|
25
|
+
/**
|
|
26
|
+
* `code`: the node's own error code, when a node answered. `logs`: a failed dry run's program logs. `err`: the
|
|
27
|
+
* node's own name for why a transaction was refused ("BlockhashNotFound", "AlreadyProcessed", or the program's
|
|
28
|
+
* error as the chain writes it). `maybeTaken`: a send whose earlier try got no answer, so a node may have taken the
|
|
29
|
+
* transaction although the last try was refused.
|
|
30
|
+
*/
|
|
31
|
+
constructor(message, { code, logs, err, maybeTaken } = NO_OPTIONS) {
|
|
25
32
|
super(message);
|
|
26
33
|
this.name = 'RpcError';
|
|
27
34
|
this.code = code;
|
|
28
35
|
this.logs = logs;
|
|
36
|
+
this.err = err;
|
|
37
|
+
this.maybeTaken = Boolean(maybeTaken);
|
|
29
38
|
}
|
|
30
39
|
}
|
|
31
40
|
|
|
@@ -72,10 +81,27 @@ export function programError(err, logs) {
|
|
|
72
81
|
return last ?? (typeof err === 'string' ? err : JSON.stringify(err));
|
|
73
82
|
}
|
|
74
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Whether a send was refused because the cluster has this very transaction already (it was sent before): the node's
|
|
86
|
+
* own word for it (`err`), or, from a node that gives only a sentence, its sentence. Never a program's log line: a
|
|
87
|
+
* dry run that failed inside a program is that program's refusal, whatever its words.
|
|
88
|
+
*/
|
|
89
|
+
export const alreadyProcessed = error =>
|
|
90
|
+
error?.err === 'AlreadyProcessed' ||
|
|
91
|
+
(error?.err === undefined &&
|
|
92
|
+
!error?.logs?.length &&
|
|
93
|
+
/this transaction has already been processed/i.test(String(error?.message ?? '')));
|
|
94
|
+
|
|
95
|
+
/** Whether a send's refusal is the dry run not knowing the transaction's blockhash. */
|
|
96
|
+
const blockhashNotFound = error =>
|
|
97
|
+
error?.data?.err === 'BlockhashNotFound' || /blockhash not found/i.test(String(error?.message ?? ''));
|
|
98
|
+
|
|
75
99
|
/** A client for one RPC URL. `fetchImpl` and `sleepImpl` are for tests. */
|
|
76
100
|
export function rpcClient(url, { fetchImpl = globalThis.fetch, sleepImpl = sleep } = {}) {
|
|
77
101
|
let id = 0;
|
|
78
102
|
async function call(method, params = []) {
|
|
103
|
+
// A try that got no answer (the connection dropped, a gateway failed): a node may have taken what it carried.
|
|
104
|
+
let unanswered = false;
|
|
79
105
|
for (let attempt = 0; ; attempt++) {
|
|
80
106
|
let response;
|
|
81
107
|
try {
|
|
@@ -86,6 +112,7 @@ export function rpcClient(url, { fetchImpl = globalThis.fetch, sleepImpl = sleep
|
|
|
86
112
|
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
87
113
|
});
|
|
88
114
|
} catch (error) {
|
|
115
|
+
unanswered = true;
|
|
89
116
|
if (attempt < RETRIES) {
|
|
90
117
|
await sleepImpl(500 * 2 ** attempt);
|
|
91
118
|
continue;
|
|
@@ -98,6 +125,7 @@ export function rpcClient(url, { fetchImpl = globalThis.fetch, sleepImpl = sleep
|
|
|
98
125
|
continue;
|
|
99
126
|
}
|
|
100
127
|
// Sending the same signed transaction again is safe: it has one signature, so it lands once at most.
|
|
128
|
+
if (PASSING.includes(response.status)) unanswered = true;
|
|
101
129
|
if (PASSING.includes(response.status) && attempt < RETRIES) {
|
|
102
130
|
await sleepImpl(500 * 2 ** attempt);
|
|
103
131
|
continue;
|
|
@@ -115,13 +143,25 @@ export function rpcClient(url, { fetchImpl = globalThis.fetch, sleepImpl = sleep
|
|
|
115
143
|
await sleepImpl(500 * 2 ** attempt);
|
|
116
144
|
continue;
|
|
117
145
|
}
|
|
146
|
+
// A send refused in its dry run for a blockhash this node has not seen yet ("Blockhash not found"): the node
|
|
147
|
+
// that gave the blockhash was a moment ahead of this one. Asked again, it (or another) knows it. A blockhash
|
|
148
|
+
// that is truly gone is refused the same way each time, and the last refusal is the answer.
|
|
149
|
+
if (method === 'sendTransaction' && blockhashNotFound(body?.error) && attempt < RETRIES) {
|
|
150
|
+
await sleepImpl(500 * 2 ** attempt);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
118
153
|
// A send whose preflight failed ("Transaction simulation failed: … custom program error: 0x17b5") carries the
|
|
119
154
|
// logs, where the program says why in words.
|
|
120
155
|
const logs = body?.error?.data?.logs;
|
|
121
156
|
const said = logs?.length
|
|
122
157
|
? `The network refused this in a dry run: ${programError(body.error.data.err, logs)}`
|
|
123
158
|
: message;
|
|
124
|
-
throw new RpcError(said, {
|
|
159
|
+
throw new RpcError(said, {
|
|
160
|
+
code: body?.error?.code,
|
|
161
|
+
logs,
|
|
162
|
+
err: body?.error?.data?.err,
|
|
163
|
+
maybeTaken: method === 'sendTransaction' && unanswered,
|
|
164
|
+
});
|
|
125
165
|
}
|
|
126
166
|
return body.result;
|
|
127
167
|
}
|
|
@@ -211,10 +251,17 @@ export function rpcClient(url, { fetchImpl = globalThis.fetch, sleepImpl = sleep
|
|
|
211
251
|
};
|
|
212
252
|
},
|
|
213
253
|
async send(wire) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
254
|
+
try {
|
|
255
|
+
return await call('sendTransaction', [
|
|
256
|
+
toBase64(wire),
|
|
257
|
+
{ encoding: 'base64', preflightCommitment: 'confirmed', maxRetries: 3 },
|
|
258
|
+
]);
|
|
259
|
+
} catch (e) {
|
|
260
|
+
// A transaction the cluster has taken already is refused as already processed: that is what a second send
|
|
261
|
+
// gets when the answer to the first was lost on its way back. It was sent; its signature is its first.
|
|
262
|
+
if (alreadyProcessed(e)) return transactionId(wire);
|
|
263
|
+
throw e;
|
|
264
|
+
}
|
|
218
265
|
},
|
|
219
266
|
signatureStatus,
|
|
220
267
|
/** Wait until a sent transaction is confirmed; throws with the program's reason when it failed. */
|
package/js/economy/check.mjs
CHANGED
|
@@ -5673,7 +5673,7 @@ export function check(
|
|
|
5673
5673
|
warn(
|
|
5674
5674
|
'W-AUTHORITY',
|
|
5675
5675
|
'economy.keys.authority',
|
|
5676
|
-
`This economy needs the authority while it runs, for what only the game knows: ${needs.join('; ')}. ${m.authority === 'owner' ? 'That is
|
|
5676
|
+
`This economy needs the authority while it runs, for what only the game knows: ${needs.join('; ')}. ${m.authority === 'owner' ? 'That is the project’s owner key, which signs each one.' : `That is the key ${m.authority}.`}`,
|
|
5677
5677
|
);
|
|
5678
5678
|
const voucherKeys = [...new Set(m.grants.flatMap(g => [...(g.vouchers ?? []), ...(g.keys ?? [])]))];
|
|
5679
5679
|
if (voucherKeys.length)
|
|
@@ -5802,7 +5802,7 @@ export function check(
|
|
|
5802
5802
|
refuse(
|
|
5803
5803
|
'E-TX-SIZE',
|
|
5804
5804
|
paths[kind] ? `economy.${paths[kind]}${part ? `.${key}` : ''}` : 'economy',
|
|
5805
|
-
`${named} takes ${bytes} bytes to set up in one transaction, more than the ${BUDGET} a setup transaction may hold (${MAX_TRANSACTION_BYTES}, less ${WALLET_ROOM} kept for
|
|
5805
|
+
`${named} takes ${bytes} bytes to set up in one transaction, more than the ${BUDGET} a setup transaction may hold (${MAX_TRANSACTION_BYTES}, less ${WALLET_ROOM} kept for the compute unit limit and price it leads with), so it could never go live.`,
|
|
5806
5806
|
fix,
|
|
5807
5807
|
);
|
|
5808
5808
|
}
|
package/js/economy/cost.mjs
CHANGED
|
@@ -3,7 +3,16 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Rent is (bytes + 128) × 5,080 lamports (model.mjs); the loot program's own records are sized byte for byte from
|
|
5
5
|
// their Borsh layout, others (a Core collection, a farm) roughly, and marked so.
|
|
6
|
-
import {
|
|
6
|
+
import { priorityFee } from '../chain/programs.js';
|
|
7
|
+
import {
|
|
8
|
+
boxOrderBytes,
|
|
9
|
+
plan as compile,
|
|
10
|
+
contestBytes,
|
|
11
|
+
entryShape,
|
|
12
|
+
farmEnds,
|
|
13
|
+
roundRentOf,
|
|
14
|
+
withSetupBudget,
|
|
15
|
+
} from './plan.mjs';
|
|
7
16
|
import {
|
|
8
17
|
ACCOUNT_BYTES,
|
|
9
18
|
CONTEST_ENTRY_BYTES,
|
|
@@ -63,7 +72,12 @@ export function setupCost(plan, economy) {
|
|
|
63
72
|
const metadataLamports = accounts.reduce((n, a) => n + BigInt(a.fee ?? 0), 0n);
|
|
64
73
|
const charged = plan.transactions.flatMap(t => t.steps).filter(s => !NO_FEE_STEP.test(s.id));
|
|
65
74
|
const feeLamports = BigInt(charged.length) * FEES.project;
|
|
66
|
-
|
|
75
|
+
// The network's fee: 5,000 lamports a signature, and each transaction's own priority fee (its unit limit times the
|
|
76
|
+
// unit price the plan sets: 2 lamports; none for a transaction listed without its instructions).
|
|
77
|
+
const networkLamports = plan.transactions.reduce(
|
|
78
|
+
(n, t) => n + BigInt(t.signers.length) * SIGNATURE_FEE + priorityFee(t.instructions ?? []),
|
|
79
|
+
0n,
|
|
80
|
+
);
|
|
67
81
|
// Only the seeds and grant funds this plan puts in: a change to a live economy doesn't pay its seeds again.
|
|
68
82
|
const steps = new Set(plan.transactions.flatMap(t => t.steps).map(s => s.id));
|
|
69
83
|
const seeds = {};
|
|
@@ -101,7 +115,9 @@ export function setupCost(plan, economy) {
|
|
|
101
115
|
notes.push(
|
|
102
116
|
'The total includes the SOL put in grants’ funds, which pays their claims; what is left goes back to you when a grant closes.',
|
|
103
117
|
);
|
|
104
|
-
notes.push(
|
|
118
|
+
notes.push(
|
|
119
|
+
`The network fee is 5,000 lamports a signature and a priority fee of ${priorityFee(withSetupBudget([]))} lamports a transaction: each sets its own compute unit limit and price, so a wallet that adds a priority fee of its own leaves it as it is.`,
|
|
120
|
+
);
|
|
105
121
|
return {
|
|
106
122
|
accounts,
|
|
107
123
|
rentSol: sol(rentLamports),
|
|
@@ -489,7 +505,7 @@ export function cost(economy, { now = Math.floor(Date.now() / 1000), only = null
|
|
|
489
505
|
export function explainCost(c) {
|
|
490
506
|
const s = c.setup;
|
|
491
507
|
const lines = [
|
|
492
|
-
`Going live takes ${s.transactions} transaction${s.transactions === 1 ? '' : 's'}
|
|
508
|
+
`Going live takes ${s.transactions} transaction${s.transactions === 1 ? '' : 's'} and about ${s.totalSol} SOL:`,
|
|
493
509
|
];
|
|
494
510
|
lines.push(
|
|
495
511
|
` rent ${s.rentSol} SOL for ${s.accounts.length} accounts; program fees ${s.programFeesSol} SOL; network fees ${s.networkFeesSol} SOL${
|
package/js/economy/explain.mjs
CHANGED
|
@@ -560,7 +560,7 @@ export function explainEconomy(economy, { now = Math.floor(Date.now() / 1000) }
|
|
|
560
560
|
]);
|
|
561
561
|
const authority = play.filter(s => s.signer.startsWith('authority'));
|
|
562
562
|
section(
|
|
563
|
-
`What only the authority signs while it runs (${m.authority === 'owner' ? '
|
|
563
|
+
`What only the authority signs while it runs (${m.authority === 'owner' ? 'the project’s owner key' : `the key ${m.authority}`})`,
|
|
564
564
|
authority.map(s => `${s.says} (${s.part}, ${s.ix}).`),
|
|
565
565
|
);
|
|
566
566
|
if (authority.length)
|
|
@@ -571,7 +571,7 @@ export function explainEconomy(economy, { now = Math.floor(Date.now() / 1000) }
|
|
|
571
571
|
const s = c.setup;
|
|
572
572
|
lines.push('', 'What setting it up costs');
|
|
573
573
|
lines.push(
|
|
574
|
-
`- About ${num(s.totalSol)} SOL in all, in ${plural(s.transactions, 'transaction')}
|
|
574
|
+
`- About ${num(s.totalSol)} SOL in all, in ${plural(s.transactions, 'transaction')}: rent ${num(s.rentSol)} SOL for ${plural(s.accounts.length, 'account')}, program fees ${num(s.programFeesSol)} SOL, network fees ${num(s.networkFeesSol)} SOL${
|
|
575
575
|
s.metadataFeesSol ? `, Token Metadata’s fees ${num(s.metadataFeesSol)} SOL (a token’s name on chain)` : ''
|
|
576
576
|
}${
|
|
577
577
|
Object.keys(s.seeds).length
|
|
@@ -613,7 +613,7 @@ export function explainEconomy(economy, { now = Math.floor(Date.now() / 1000) }
|
|
|
613
613
|
);
|
|
614
614
|
lines.push(
|
|
615
615
|
'',
|
|
616
|
-
'Devnet only: nothing here is real money. Going live is
|
|
616
|
+
'Devnet only: nothing here is real money. Going live is signed on your own machine by the project’s key file, which is what “your wallet” means above. The command first prints everything it would send and sends nothing; it sends only when run again with the code it printed, which your agent is told to do only after your yes. No seed phrase or wallet key is ever asked for.',
|
|
617
617
|
);
|
|
618
618
|
return {
|
|
619
619
|
ok: true,
|
package/js/economy/guide.mjs
CHANGED
|
@@ -230,7 +230,7 @@ export const CANNOT = [
|
|
|
230
230
|
why: 'loot isn’t a multisig, but any wallet can receive and any key can be the spending key',
|
|
231
231
|
nearest: [
|
|
232
232
|
'payments and sales go to a multisig vault: `"treasury": "wallet:<vault>"` or a `wallet:<vault>` share; spending from it needs the vault’s approvals in the multisig’s own app',
|
|
233
|
-
'the multisig vault as `keys.authority`, so moving the game’s pools needs its approvals (sent from the multisig’s own app, not
|
|
233
|
+
'the multisig vault as `keys.authority`, so moving the game’s pools needs its approvals (sent from the multisig’s own app, not by `xsolana approve`)',
|
|
234
234
|
],
|
|
235
235
|
},
|
|
236
236
|
{
|
|
@@ -431,7 +431,7 @@ export function guideText(only) {
|
|
|
431
431
|
return [
|
|
432
432
|
only
|
|
433
433
|
? `The part “${only}” of a loot economy (* = required):`
|
|
434
|
-
: `A loot economy is one JSON spec (xsolana.json) on the shared loot program ${IDL.metadata.version}, devnet only. Nobody writes a contract: the spec is checked, simulated, and put live
|
|
434
|
+
: `A loot economy is one JSON spec (xsolana.json) on the shared loot program ${IDL.metadata.version}, devnet only. Nobody writes a contract: the spec is checked, simulated, and put live on the owner’s own machine, signed by the project’s key file once they have said yes. Its parts (* = required field):`,
|
|
435
435
|
...parts.flatMap(p => [
|
|
436
436
|
`\n${p.part} (${p.type}): ${p.says}${p.atPlayTime ? `\n At play time: ${p.atPlayTime}.` : ''}`,
|
|
437
437
|
...p.fields.map(fieldLine),
|
package/js/economy/live.mjs
CHANGED
|
@@ -189,4 +189,4 @@ export function changedParts(economy) {
|
|
|
189
189
|
* of that is decided when the change is approved.
|
|
190
190
|
*/
|
|
191
191
|
export const renamesNote = renames =>
|
|
192
|
-
`The spec changes what ${renames.join(', ')} ${renames.length === 1 ? 'is' : 'are'} called. A token’s name on chain is its name record: approving reads it, and writes the new name, ticker and link there when the token has a record its owner can still change (and nothing when the record says them already). A token that went live before setups named tokens has no record and can’t be given one, and a record handed on, made unchangeable or given a royalty outside these tools is left as it is:
|
|
192
|
+
`The spec changes what ${renames.join(', ')} ${renames.length === 1 ? 'is' : 'are'} called. A token’s name on chain is its name record: approving reads it, and writes the new name, ticker and link there when the token has a record its owner can still change (and nothing when the record says them already). A token that went live before setups named tokens has no record and can’t be given one, and a record handed on, made unchangeable or given a royalty outside these tools is left as it is: approving says so, sends nothing for that token, and the change stays listed here until the spec gives its old name back.`;
|
package/js/economy/plan.mjs
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
createAssociatedTokenAccountIdempotent,
|
|
35
35
|
syncNative,
|
|
36
36
|
transferLamports,
|
|
37
|
+
withComputeBudget,
|
|
37
38
|
} from '../chain/programs.js';
|
|
38
39
|
import {
|
|
39
40
|
createAccountIx,
|
|
@@ -43,7 +44,7 @@ import {
|
|
|
43
44
|
setAuthorityIx,
|
|
44
45
|
transferCheckedIx,
|
|
45
46
|
} from '../chain/token.js';
|
|
46
|
-
import { compileMessage, transactionSize } from '../chain/tx.js';
|
|
47
|
+
import { compileMessage, MAX_TRANSACTION_BYTES, transactionSize } from '../chain/tx.js';
|
|
47
48
|
import { sha256 } from '../chain/sha256.js';
|
|
48
49
|
import { utf8 } from '../chain/bytes.js';
|
|
49
50
|
import * as loot from '../client/index.js';
|
|
@@ -1878,13 +1879,33 @@ export const stepBytes = (instructions, ctx) =>
|
|
|
1878
1879
|
const budgeted = (instructions, compute) =>
|
|
1879
1880
|
compute ? [computeUnitLimit(MAX_COMPUTE_UNITS), ...instructions] : instructions;
|
|
1880
1881
|
|
|
1881
|
-
/** The bytes of a transaction of `step` alone, as `pack`
|
|
1882
|
+
/** The bytes of a transaction of `step` alone, as `pack` holds it against the budget (a box's compute limit included). */
|
|
1882
1883
|
export const stepAloneBytes = (step, ctx) => stepBytes(budgeted(step.build(ctx), Boolean(step.compute)), ctx);
|
|
1883
1884
|
|
|
1884
1885
|
/**
|
|
1885
|
-
*
|
|
1886
|
-
*
|
|
1887
|
-
*
|
|
1886
|
+
* The compute unit price every setup transaction sets, in micro-lamports a unit: 1, the least that is not 0 (a wallet
|
|
1887
|
+
* may read 0 as no price set). With the unit limit below it comes to 1.4 lamports, charged as 2 lamports a transaction
|
|
1888
|
+
* on top of the 5,000 a signature.
|
|
1889
|
+
*/
|
|
1890
|
+
export const SETUP_UNIT_PRICE = 1n;
|
|
1891
|
+
|
|
1892
|
+
/**
|
|
1893
|
+
* A setup transaction's instructions as the owner's wallet is handed them: led by a compute unit limit and a compute
|
|
1894
|
+
* unit price, each once. A wallet that adds its own priority fee (Phantom does) adds it only to a transaction that
|
|
1895
|
+
* sets neither, and the approval page sends a transaction only as it prepared it, so every setup transaction sets both
|
|
1896
|
+
* itself. The limit is all a transaction can have (1,400,000 units): nothing here measures what a step takes, a box's
|
|
1897
|
+
* write needs it, and no step may fail for want of compute. A limit or a price the instructions already carry is
|
|
1898
|
+
* kept.
|
|
1899
|
+
*/
|
|
1900
|
+
export const withSetupBudget = instructions =>
|
|
1901
|
+
withComputeBudget(instructions, { units: MAX_COMPUTE_UNITS, microLamports: SETUP_UNIT_PRICE });
|
|
1902
|
+
|
|
1903
|
+
/**
|
|
1904
|
+
* Packs steps into transactions under the size budget, one signer set at a time; each step whole. Every transaction
|
|
1905
|
+
* leads with its compute budget (`withSetupBudget`): `BUDGET` keeps 64 bytes of a transaction free, and the two
|
|
1906
|
+
* instructions take 52 of them (12 where a box's write, `compute`, had its limit counted with the steps already). So
|
|
1907
|
+
* the steps pack as they did before the plan set a price, and `bytes`, the size of the transaction as it is sent, is
|
|
1908
|
+
* held to what one transaction carries.
|
|
1888
1909
|
*/
|
|
1889
1910
|
export function pack(steps, ctx) {
|
|
1890
1911
|
const transactions = [];
|
|
@@ -1922,7 +1943,14 @@ export function pack(steps, ctx) {
|
|
|
1922
1943
|
};
|
|
1923
1944
|
transactions.push(current);
|
|
1924
1945
|
}
|
|
1925
|
-
for (const t of transactions)
|
|
1946
|
+
for (const t of transactions) {
|
|
1947
|
+
t.instructions = withSetupBudget(t.instructions);
|
|
1948
|
+
t.bytes = sizeOf(t.instructions);
|
|
1949
|
+
if (t.bytes > MAX_TRANSACTION_BYTES)
|
|
1950
|
+
throw new Error(
|
|
1951
|
+
`The steps “${t.steps.map(s => s.says).join('”, “')}” take ${t.bytes} bytes with their compute budget: too big for one transaction.`,
|
|
1952
|
+
);
|
|
1953
|
+
}
|
|
1926
1954
|
return transactions;
|
|
1927
1955
|
}
|
|
1928
1956
|
|
package/js/economy/review.mjs
CHANGED
|
@@ -670,7 +670,7 @@ export function review(economy, { now = Math.floor(Date.now() / 1000) } = {}) {
|
|
|
670
670
|
m.authority === 'owner' ? 'info' : 'low',
|
|
671
671
|
'economy',
|
|
672
672
|
`${owner[0].toUpperCase()}${owner.slice(1)} alone changes every price, rate, grant, limit and voucher key (publishing again), and closes parts. ${m.authority === 'owner' ? 'It is also the authority, which' : `The authority, ${authority},`} mints the project’s tokens, moves what pools hold, names contest results and releases holds, with no caps. ${m.treasuryWallet ? `What sales leave goes to the wallet ${m.treasuryWallet}.` : 'What sales leave goes to the owner’s wallet (the treasury).'} Money held for a record (a farm’s stakes and rewards, a pot, an order) leaves only by its record’s rules: neither key pays it out.`,
|
|
673
|
-
'Keep these keys
|
|
673
|
+
'Keep these keys safe: on devnet, the key file the setup was signed with, readable by you alone and never pasted anywhere; with real money, a hardware wallet or a multisig. Never on a server that players can reach, unless it is an authority made for that server.',
|
|
674
674
|
);
|
|
675
675
|
for (const w of warned('W-MINT-POWERS')) {
|
|
676
676
|
const key = /^economy\.tokens\.([^.]+)/.exec(w.where)?.[1];
|
package/js/economy/schema.json
CHANGED
|
@@ -1824,7 +1824,7 @@
|
|
|
1824
1824
|
"signedBy": {
|
|
1825
1825
|
"enum": ["page", "server"],
|
|
1826
1826
|
"default": "page",
|
|
1827
|
-
"description": "page: the authority
|
|
1827
|
+
"description": "page: the authority signs each one itself (a script with its key file, or its browser wallet). server: a server you run holds the authority key (keys.authority is its public address)."
|
|
1828
1828
|
},
|
|
1829
1829
|
"proof": {
|
|
1830
1830
|
"type": "object",
|
package/js/economy/txsize.mjs
CHANGED
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
// - instruction data is sized byte for byte from the program's IDL (idl.mjs), and other programs' data from their
|
|
12
12
|
// documented layouts at its longest;
|
|
13
13
|
// - no address lookup tables are assumed.
|
|
14
|
-
// A
|
|
15
|
-
//
|
|
14
|
+
// A transaction's compute budget instructions (a unit limit and a unit price) take 52 bytes. A setup transaction
|
|
15
|
+
// leads with its own (plan.mjs `withSetupBudget`), and a player's wallet may add its own to a transaction that sets
|
|
16
|
+
// none. So an estimate keeps WALLET_ROOM free below the network's 1,232-byte limit.
|
|
16
17
|
|
|
17
18
|
export const MAX_TRANSACTION_BYTES = 1232;
|
|
18
19
|
export const WALLET_ROOM = 64;
|
package/js/mcp/server.mjs
CHANGED
|
@@ -10,18 +10,18 @@ import { PACKAGE_VERSION, PROGRAM_INFO, REMOTE_TOOLS, tools, VERSION } from './t
|
|
|
10
10
|
|
|
11
11
|
export const INSTRUCTIONS = [
|
|
12
12
|
'loot is one shared Solana program for game and community economies (shops, loot boxes, lotteries, auctions, staking farms, crafting, contests, schedules, perks for holders of any NFT, capped rewards). An economy is a JSON spec, not a new smart contract: never write Anchor or Rust for what loot covers.',
|
|
13
|
-
'Start with xsolana_guide, then xsolana_examples with the person’s own words; write the spec; xsolana_check after every change; xsolana_simulate and xsolana_explain before telling them what it does; xsolana_review before going live (fix its high findings, say honestly what each voucher server checks, and tell the person what a cheater gets); xsolana_save; xsolana_publish when they say to put it live
|
|
13
|
+
'Start with xsolana_guide, then xsolana_examples with the person’s own words; write the spec; xsolana_check after every change; xsolana_simulate and xsolana_explain before telling them what it does; xsolana_review before going live (fix its high findings, say honestly what each voucher server checks, and tell the person what a cheater gets); xsolana_save; xsolana_publish when they say to put it live. Going live is signed by a key file on their own machine (xsolana keygen makes one; never ask for, read or print a key) with xsolana approve --keypair, which prints everything it would send and sends nothing until it is run again with the code it printed: do that only after they have read the printout and said yes. That key owns the project for good: a change to an economy that is already live is signed by the key that owns it, never by a new one. A browser wallet’s signing page (approve --browser) only if they ask for one, and they run it themselves.',
|
|
14
14
|
`Program ${PROGRAM_INFO.program} on ${PROGRAM_INFO.cluster} (loot ${VERSION}). Devnet only until audited.`,
|
|
15
15
|
].join(' ');
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* What a hosted server (js/mcp/http.mjs) tells the agent: the same work, with the spec passed whole in every call,
|
|
19
|
-
* and going live on the person's own computer, since a hosted server can neither write their files nor
|
|
19
|
+
* and going live on the person's own computer, since a hosted server can neither write their files nor sign there.
|
|
20
20
|
*/
|
|
21
21
|
export const REMOTE_INSTRUCTIONS = [
|
|
22
22
|
INSTRUCTIONS.split(' Start with')[0],
|
|
23
23
|
'Start with xsolana_guide, then xsolana_examples with the person’s own words; write the spec and pass it whole as "spec" to every tool (this hosted server reads no files); xsolana_check after every change; xsolana_simulate and xsolana_explain before telling them what it does; xsolana_review before going live (fix its high findings, say honestly what each voucher server checks, and tell the person what a cheater gets).',
|
|
24
|
-
`To go live,
|
|
24
|
+
`To go live, the spec goes in a file named xsolana.json on the person’s own computer and is signed there by a key file. In that folder: npx -y @xgamesfun/solana@${PACKAGE_VERSION} keygen ~/.config/xsolana/<name>.json makes the key file (it prints the address, never the key; the person sends that address devnet SOL), then npx -y @xgamesfun/solana@${PACKAGE_VERSION} approve xsolana.json --keypair <that file> prints everything that would be sent and sends nothing. When the person has read that and said yes, the same command with --yes <the code it printed> sends it. Run them yourself if you have a terminal there; otherwise hand them to the person. Never ask for, read or print a key. Say once what the key file is: the project’s owner for good (whoever can read it controls the project; they back it up themselves; nothing restores a lost one) and, unless the spec names a treasury, where what the economy earns lands. A change to an economy that is already live is signed by the key that owns it: never make a new key for it. A browser wallet’s signing page (the same approve command with --browser in place of --keypair), which the person runs themselves, only if they ask for one or a browser wallet owns the project. (xsolana_save and xsolana_publish, which tool texts may name, run only in the local server.)`,
|
|
25
25
|
`Program ${PROGRAM_INFO.program} on ${PROGRAM_INFO.cluster} (loot ${VERSION}). Devnet only until audited.`,
|
|
26
26
|
].join(' ');
|
|
27
27
|
|
package/js/mcp/tools.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// The xsolana tools: what an agent needs to turn a person's idea into an economy on the shared loot program, check it,
|
|
2
|
-
// simulate it, price it, explain it, save it and
|
|
2
|
+
// simulate it, price it, explain it, save it and say how it is put live: signed by a key file on the person's own
|
|
3
|
+
// machine, once they have read what would be sent and said yes.
|
|
3
4
|
// Each tool is { name, description, inputSchema, annotations, handler(input, ctx) → { text, ...data } }; the server
|
|
4
5
|
// (server.mjs) and the terminal (`xsolana tool`) run them the same way. No tool signs, holds a key or talks to a wallet.
|
|
5
6
|
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
@@ -48,15 +49,30 @@ export function plainName(rel, paths = path) {
|
|
|
48
49
|
const NOT_PLAIN = rel => `${JSON.stringify(String(rel))} is not a plain file name`;
|
|
49
50
|
const RENAME = 'use letters, digits and . _ - (and / for a folder), as in xsolana.json';
|
|
50
51
|
/**
|
|
51
|
-
* The command
|
|
52
|
-
*
|
|
53
|
-
*
|
|
52
|
+
* The command that puts an economy live, pinned to this package's exact version, with the spec's file (relative to the
|
|
53
|
+
* project folder, by `paths`' rules: the system's own unless given) as one plain word. A name that is not plain
|
|
54
|
+
* throws. How it is signed goes after it: `--keypair <key file>` (the usual way), or `--browser` when the person
|
|
55
|
+
* asks for a browser wallet.
|
|
54
56
|
*/
|
|
55
57
|
export function approveCommand(file = '', paths = path) {
|
|
56
58
|
const name = file ? plainName(file, paths) : '';
|
|
57
59
|
if (name === null) throw new Error(`${NOT_PLAIN(file)}: rename it (${RENAME}).`);
|
|
58
60
|
return `npx -y @xgamesfun/solana@${PACKAGE_VERSION} approve${name ? ` ${name}` : ''}`;
|
|
59
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Where a project's key file is kept unless the person names another place: outside any repository, under the
|
|
64
|
+
* economy's own name (`~` is the home folder, in any shell: the commands read it themselves).
|
|
65
|
+
*/
|
|
66
|
+
export function keyFileFor(name) {
|
|
67
|
+
const slug = String(name ?? '')
|
|
68
|
+
.toLowerCase()
|
|
69
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
70
|
+
.replace(/^-+|-+$/g, '')
|
|
71
|
+
.slice(0, 40);
|
|
72
|
+
return `~/.config/xsolana/${slug || 'project'}.json`;
|
|
73
|
+
}
|
|
74
|
+
/** The command that makes a project's key file (it prints the key's address, never the key). */
|
|
75
|
+
export const keygenCommand = keyFile => `npx -y @xgamesfun/solana@${PACKAGE_VERSION} keygen ${keyFile}`;
|
|
60
76
|
export const CLUSTER = 'devnet';
|
|
61
77
|
export const DEVNET_RPC = 'https://api.devnet.solana.com';
|
|
62
78
|
/** What every answer carries, so an agent never has to remember the program's address. */
|
|
@@ -235,7 +251,7 @@ function flagsToTell(economy, now) {
|
|
|
235
251
|
const flags = legalFlags(check(economy, { now }));
|
|
236
252
|
return flags.length
|
|
237
253
|
? [
|
|
238
|
-
"If you haven't told the person these yet, say each once, in a plain sentence (
|
|
254
|
+
"If you haven't told the person these yet, say each once, in a plain sentence (what `approve` prints before anything is sent shows them too; after that, only when they ask):",
|
|
239
255
|
...flags.map(f => `- ${f.says}`),
|
|
240
256
|
'',
|
|
241
257
|
]
|
|
@@ -422,7 +438,7 @@ export const tools = [
|
|
|
422
438
|
name: 'xsolana_cost',
|
|
423
439
|
annotations: READ,
|
|
424
440
|
description:
|
|
425
|
-
'What going live costs (each account’s rent, the program’s fees, network fees, the seeds, the transactions
|
|
441
|
+
'What going live costs (each account’s rent, the program’s fees, network fees, the seeds, the transactions it takes) and what each action costs whoever signs it while it runs.',
|
|
426
442
|
inputSchema: { type: 'object', properties: SOURCE, additionalProperties: false },
|
|
427
443
|
async handler(input, ctx) {
|
|
428
444
|
const { economy, from } = await load(input, ctx);
|
|
@@ -565,7 +581,7 @@ export const tools = [
|
|
|
565
581
|
{
|
|
566
582
|
name: 'xsolana_publish',
|
|
567
583
|
annotations: { ...READ, readOnlyHint: true },
|
|
568
|
-
description: `When the person says to put it live: returns what going live does (every step, in order, and its cost) and the
|
|
584
|
+
description: `When the person says to put it live: returns what going live does (every step, in order, and its cost) and the commands that do it (each starts npx -y @xgamesfun/solana@${PACKAGE_VERSION} for this exact version: give them as they are). Going live is signed by a key file on the person’s own machine: \`keygen\` makes one for the project (it prints the address, never the key), and \`approve … --keypair <file>\` prints everything that would be sent and sends nothing until it is run again with the code it printed, which you do only after the person has read that printout and said yes. A live economy is changed only by the key that owns it: never make a new key for one. Never ask for, read or print a key, and open no signing page: the browser-wallet page (\`approve … --browser\`) is for a person who asks for one, or whose browser wallet owns the project, and they run it themselves.`,
|
|
569
585
|
inputSchema: { type: 'object', properties: SOURCE, additionalProperties: false },
|
|
570
586
|
async handler(input, ctx) {
|
|
571
587
|
const { economy, from, file } = await load(input, ctx);
|
|
@@ -608,21 +624,41 @@ export const tools = [
|
|
|
608
624
|
steps: [],
|
|
609
625
|
text: `Not published: ${NOT_PLAIN(target)}, and the approve command carries the file's name as it is. Ask the person to rename it (${RENAME}), then run xsolana_publish again with the new name.${footer}`,
|
|
610
626
|
};
|
|
611
|
-
const
|
|
627
|
+
const live = Boolean(change.scope.live);
|
|
628
|
+
const keyFile = keyFileFor(economy.name);
|
|
629
|
+
// (a live project is signed only by the key that owns it: no command here makes another)
|
|
630
|
+
const keygen = live ? null : keygenCommand(keyFile);
|
|
631
|
+
const browserCommand = `${approveCommand(target)} --browser`;
|
|
632
|
+
const command = `${approveCommand(target)} --keypair ${keyFile}`;
|
|
612
633
|
const text = [
|
|
613
634
|
...flagsToTell(economy, nowOf(ctx)),
|
|
614
|
-
`${change.scope.live ? 'Publishing this change to the live economy' : 'Going live'}: ${renaming ? 'at most ' : ''}${plural(p.transactions.length, 'transaction')},
|
|
635
|
+
`${change.scope.live ? 'Publishing this change to the live economy' : 'Going live'}: ${renaming ? 'at most ' : ''}${plural(p.transactions.length, 'transaction')}, about ${c.setup.totalSol} SOL on devnet:`,
|
|
615
636
|
...steps.map((s, i) => `${i + 1}. ${s}`),
|
|
616
637
|
'',
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
638
|
+
`${live ? 'To publish it' : 'To put it live'}, in a terminal in the project folder (you run these; nothing opens, and nothing is sent before the person’s yes):`,
|
|
639
|
+
...(live
|
|
640
|
+
? [
|
|
641
|
+
`1. Only the key that owns the project can change it: the key file it went live with (${keyFile} if it was made there; give the file’s own path if it is elsewhere). If there is no such file, a browser wallet owns it: use the --browser command below. Never make a new key for a live project (a new key can change nothing), and never remove the file’s live section because a key was refused. If you can’t tell, ask the person how it went live. The owner’s address needs about ${c.setup.totalSol} SOL of devnet SOL.`,
|
|
642
|
+
]
|
|
643
|
+
: [
|
|
644
|
+
`1. A key file for this project, unless it has one: ${keygen}`,
|
|
645
|
+
' It prints the key’s address, never the key. The key file is the project’s owner: whoever can read it controls the project, and which key goes live is for good (every later change is signed by it; these tools can’t hand a project to another owner). It stays where it is: never in a chat, a spec, a repository or a page, and you never open, read or print it. Tell the person to back the file up themselves: if it is lost, nothing can change the economy or move what its address holds.',
|
|
646
|
+
` Everything the setup takes from the owner must be at that address first: the devnet SOL (about ${c.setup.totalSol} SOL here; https://faucet.solana.com gives some), any seed in a coin that already exists, and every NFT a stash, a raffle or an auction names (a collection they already have needs that address as its update authority). If they would rather not move those, offer the browser wallet.`,
|
|
647
|
+
' The steps above call the owner’s address “your wallet”: with a key file, that is the key’s address. Unless the spec names a treasury, what sales leave lands there, and it signs what only the authority signs. If the economy takes money in, ask for the public address of a wallet the person holds and set "treasury": "wallet:<address>" first: it can’t be changed through the spec once live.',
|
|
648
|
+
]),
|
|
649
|
+
`2. ${command}`,
|
|
650
|
+
' It prints every step with what it pays and trusts, the warnings, the notice (the program is not independently audited and comes as is, with no warranty; they use it at their own risk, and making sure the economy is legal where it is offered is up to them: DISCLAIMER.md in the package) and a code. It sends nothing.',
|
|
651
|
+
'3. Show the person that printout and ask. When they say yes, run the same command with --yes <code>: the code stands for exactly what was printed, and anything else is refused. What lands is written back into the file. If it stops part way, run it again without --yes: it prints what is left and a new code. Go on with that code; if it shows a step or a warning they have not seen, ask again.',
|
|
652
|
+
`${live ? 'For a project a browser wallet owns' : 'Only if the person asks to sign in a browser wallet instead'}: give them this to run in their own terminal: ${browserCommand} (it serves a page on their own computer where they connect a wallet set to devnet and approve, ${plural(p.approvals, 'approval')}, and keeps running until the page says it is live). Don’t run or open it otherwise.${live ? ' A project a key file owns can’t be signed in a browser wallet.' : ''}`,
|
|
653
|
+
...(file ? [] : [`(Save it first with xsolana_save: \`approve\` reads ${DEFAULT_FILE}.)`]),
|
|
621
654
|
...(change.notes ? [change.notes.trim()] : []),
|
|
622
655
|
].join('\n');
|
|
623
656
|
return {
|
|
624
657
|
ok: true,
|
|
625
658
|
command,
|
|
659
|
+
...(keygen ? { keygen } : {}),
|
|
660
|
+
keyFile,
|
|
661
|
+
browserCommand,
|
|
626
662
|
transactions: p.transactions.length,
|
|
627
663
|
approvals: p.approvals,
|
|
628
664
|
setupSol: c.setup.totalSol,
|
|
@@ -861,6 +897,7 @@ export const tools = [
|
|
|
861
897
|
const text = [
|
|
862
898
|
`loot ${VERSION} on ${CLUSTER}: program ${LOOT_PROGRAM}. Never write another address from memory: take it from this package.`,
|
|
863
899
|
'Package: npm install @xgamesfun/solana (plain ES modules, no Solana SDK needed; works in the browser).',
|
|
900
|
+
' What only the project’s owner or authority signs (a contest’s result, a payout, a schedule’s run) is not a page’s: a script on the owner’s machine signs it with the project’s key file, `readKeyFile(file)` from "@xgamesfun/solana/keyfile" (an address and a way to sign, never the key; it is also a `wallet` for solanaApp). The skill’s references/after-launch.md has the script.',
|
|
864
901
|
' import { solanaApp, walletStandardSigner } from "@xgamesfun/solana/play";',
|
|
865
902
|
' const app = solanaApp({ spec /* the live xsolana.json */, rpcUrl, wallet: walletStandardSigner(wallet, account) });',
|
|
866
903
|
' app.shop(), app.buy(key, { listing }), app.boxes(), app.openBox(key, { listing }), app.pay(key) (or app.pay(key, { chosenRecipient })),',
|
package/js/play/index.js
CHANGED
|
@@ -37,12 +37,14 @@ import {
|
|
|
37
37
|
requestHeapFrame,
|
|
38
38
|
syncNative,
|
|
39
39
|
transferLamports,
|
|
40
|
+
withComputeBudget,
|
|
40
41
|
withSolWrap,
|
|
41
42
|
} from '../chain/programs.js';
|
|
42
43
|
import { checkCluster, notMainnet, rpcClient } from '../chain/rpc.js';
|
|
43
44
|
import { extension, mayStartFrozen, mintPowers, resolveTransfers } from '../chain/token2022.js';
|
|
44
45
|
import {
|
|
45
46
|
compileFitting,
|
|
47
|
+
compileKeepingKind,
|
|
46
48
|
closeTableIx,
|
|
47
49
|
deactivateTableIx,
|
|
48
50
|
readTable,
|
|
@@ -74,6 +76,9 @@ export {
|
|
|
74
76
|
|
|
75
77
|
// The options of a call made without any: read-only, and typed so the generated declarations take any option.
|
|
76
78
|
/** A lottery's steps in `claims()`: each depends on the one before, so `claimAll` takes one of a lottery's a call. */
|
|
79
|
+
/** The compute units a sponsored step asks for: all a transaction can have. It sets a price of 0, so they cost nothing. */
|
|
80
|
+
const SPONSORED_UNITS = 1_400_000;
|
|
81
|
+
|
|
77
82
|
const LOTTERY_STEPS = new Set(['reveal', 'cancel-draw', 'pull', 'payout']);
|
|
78
83
|
/** The claims that are one transaction of the wallet's: `claimAll` signs them together. */
|
|
79
84
|
const BATCHED = new Set(['contest', 'release', 'lookup-table', 'close-leaf', ...LOTTERY_STEPS]);
|
|
@@ -799,17 +804,26 @@ export function solanaApp({
|
|
|
799
804
|
const { blockhash, lastValidBlockHeight } = await client.latestBlockhash();
|
|
800
805
|
// Lent the most, the step is tried; then sent lent the same, the player giving back last all but what it costs in
|
|
801
806
|
// fees and rent: a step can need more SOL midway than it costs (a wrapped-SOL account it opens and closes).
|
|
802
|
-
|
|
803
|
-
|
|
807
|
+
// It leads with a compute unit limit (all a transaction can have) and a unit price of 0, the price a sponsor
|
|
808
|
+
// pays unless its server says more: a wallet that adds its own priority fee only where a transaction sets none
|
|
809
|
+
// (Phantom does, to one with no signature on it yet) is then handed one it signs as it stands, and the sponsor's
|
|
810
|
+
// server, which refuses a price it did not agree to, takes it. A step keeps the kind of transaction it had: one
|
|
811
|
+
// that fits a legacy transaction stays one (without the two where they would not fit), and only a step that
|
|
812
|
+
// needed the project's lookup tables before goes as v0.
|
|
813
|
+
const compile = (back = 0n) => {
|
|
814
|
+
const list = [
|
|
815
|
+
sponsorTopUp(sponsor.address, me, sponsorMax),
|
|
816
|
+
...instructions,
|
|
817
|
+
...(back > 0n ? [sponsorGiveBack(sponsor.address, me, back)] : []),
|
|
818
|
+
];
|
|
819
|
+
return compileKeepingKind({
|
|
804
820
|
payer: sponsor.address,
|
|
805
821
|
blockhash,
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
...instructions,
|
|
809
|
-
...(back > 0n ? [sponsorGiveBack(sponsor.address, me, back)] : []),
|
|
810
|
-
],
|
|
822
|
+
led: withComputeBudget(list, { units: SPONSORED_UNITS, microLamports: 0n }),
|
|
823
|
+
plain: list,
|
|
811
824
|
tables,
|
|
812
825
|
});
|
|
826
|
+
};
|
|
813
827
|
const tried = compile();
|
|
814
828
|
if (!tried) return null;
|
|
815
829
|
const addresses = namedIn([sponsorTopUp(sponsor.address, me, 0n), ...instructions]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xgamesfun/solana",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.2",
|
|
4
4
|
"mcpName": "io.github.XGamesFun/solana-kit",
|
|
5
5
|
"description": "Loot boxes, gacha, shops, lotteries, auctions, NFT staking, crafting and rewards on Solana without writing a smart contract: one shared program, its JS client, a CLI, and an MCP server and skill so Claude Code, Codex or any AI agent can design, check, simulate and launch an economy.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -70,6 +70,10 @@
|
|
|
70
70
|
"types": "./types/results/index.d.ts",
|
|
71
71
|
"default": "./js/results/index.js"
|
|
72
72
|
},
|
|
73
|
+
"./keyfile": {
|
|
74
|
+
"types": "./types/approve/keyfile.d.mts",
|
|
75
|
+
"default": "./js/approve/keyfile.mjs"
|
|
76
|
+
},
|
|
73
77
|
"./idl.json": "./js/client/idl/loot.json",
|
|
74
78
|
"./package.json": "./package.json",
|
|
75
79
|
"./release.json": "./release.json",
|
package/release.json
CHANGED
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
"sbfBytes": 1794664,
|
|
5
5
|
"sbfSha256": "3b11c460548a4fec865fd902431a94ac13a71c3a1013e4516783515e5d266bd6",
|
|
6
6
|
"idlSha256": "62364c5fd5be8aa001f4d72fb87b121b045dea07668f9b5ec9856d323d6cf445",
|
|
7
|
-
"clientSha256": "
|
|
7
|
+
"clientSha256": "7790b667386bb981218acc8621088b2f7799c92347193b3a97d59b9332c4ed29"
|
|
8
8
|
}
|