@xgamesfun/solana 0.28.0 → 0.28.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/DISCLAIMER.md +3 -1
- package/README.md +18 -16
- package/bin/xsolana.mjs +27 -1
- package/js/approve/details.mjs +3 -3
- package/js/approve/live-check.mjs +2 -2
- package/js/approve/page.html +5 -5
- package/js/approve/page.js +24 -4
- package/js/approve/server.mjs +66 -78
- package/js/approve/wallet-adds.mjs +180 -0
- package/js/approve/with-key.mjs +7 -4
- package/js/chain/borsh.js +5 -3
- package/js/chain/pda.js +6 -0
- package/js/chain/rpc.js +26 -10
- package/js/client/accounts.js +6 -6
- package/js/client/cluster.js +136 -0
- package/js/client/grant.js +2 -2
- package/js/client/ix.js +16 -16
- package/js/client/pda.js +18 -5
- package/js/economy/check.mjs +28 -13
- package/js/economy/explain.mjs +2 -1
- package/js/economy/guide.mjs +6 -4
- package/js/economy/live.mjs +2 -2
- package/js/economy/plan.mjs +3 -3
- package/js/economy/schema.json +2 -2
- package/js/mcp/server.mjs +35 -4
- package/js/mcp/tools.mjs +56 -45
- package/js/mcp/with-cluster.mjs +25 -0
- package/js/play/index.js +42 -37
- package/js/play/sponsor.js +4 -4
- package/js/results/index.js +7 -6
- package/js/results/server.js +4 -3
- package/js/results/wire.js +9 -8
- package/package.json +6 -1
- package/release.json +6 -1
- package/skills/xsolana-economy/SKILL.md +13 -9
- package/skills/xsolana-economy/references/apps.md +2 -1
- package/skills/xsolana-economy/references/telling-the-person.md +1 -1
- package/skills/xsolana-economy/references/trust-and-cheating.md +1 -1
- package/types/chain/borsh.d.ts +4 -2
- package/types/chain/pda.d.ts +6 -0
- package/types/chain/rpc.d.ts +10 -4
- package/types/client/cluster.d.ts +61 -0
- package/types/client/idl/loot.d.ts +9 -9
- package/types/client/pda.d.ts +1 -1
- package/types/economy/check.d.mts +1 -2
- package/types/economy/plan.d.mts +16 -16
- package/types/mcp/server.d.mts +1 -1
- package/types/mcp/tools.d.mts +9 -7
- package/types/mcp/with-cluster.d.mts +8 -0
- package/types/play/index.d.ts +2 -2
- package/types/results/index.d.ts +6 -6
- package/types/results/server.d.ts +3 -3
- package/types/results/wire.d.ts +8 -8
package/js/play/index.js
CHANGED
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
withComputeBudget,
|
|
41
41
|
withSolWrap,
|
|
42
42
|
} from '../chain/programs.js';
|
|
43
|
-
import {
|
|
43
|
+
import { rightCluster, rpcClient } from '../chain/rpc.js';
|
|
44
44
|
import { extension, mayStartFrozen, mintPowers, resolveTransfers } from '../chain/token2022.js';
|
|
45
45
|
import {
|
|
46
46
|
compileFitting,
|
|
@@ -84,14 +84,13 @@ const LOTTERY_STEPS = new Set(['reveal', 'cancel-draw', 'pull', 'payout']);
|
|
|
84
84
|
const BATCHED = new Set(['contest', 'release', 'lookup-table', 'close-leaf', ...LOTTERY_STEPS]);
|
|
85
85
|
const NO_OPTIONS = Object.freeze(/** @type {Record<string, any>} */ ({}));
|
|
86
86
|
|
|
87
|
-
const CHAIN = 'solana:devnet';
|
|
88
87
|
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
|
89
88
|
const MPL_CORE = loot.MPL_CORE_PROGRAM;
|
|
90
89
|
const NOBODY = '11111111111111111111111111111111';
|
|
91
90
|
/** A table made this many slots ago whose transaction never landed was never sent: its blockhashes ran out long since. */
|
|
92
91
|
const TABLE_UNSENT_SLOTS = 300n;
|
|
93
92
|
/** A compressed NFT's `prove_leaf`: its record stays good while the leaf is unchanged, so it may go in a transaction of its own first. */
|
|
94
|
-
const isProveLeaf = ix => ix.programId === loot.
|
|
93
|
+
const isProveLeaf = ix => ix.programId === loot.lootProgram() && loot.instructionOf(ix.data)?.name === 'prove_leaf';
|
|
95
94
|
/** A system account must keep this much (rent for 0 bytes) or be emptied. */
|
|
96
95
|
const RENT_EMPTY = 890_880n;
|
|
97
96
|
const SIGNATURE_FEE = 5_000n;
|
|
@@ -101,7 +100,7 @@ const SIGNATURE_FEE = 5_000n;
|
|
|
101
100
|
* `solana:signMessage`, also `signMessage(bytes)`: the app's settle keys are made from one signature of it a session,
|
|
102
101
|
* so a page that closes can make them again (`recover`).
|
|
103
102
|
*/
|
|
104
|
-
export function walletStandardSigner(wallet, account, chain =
|
|
103
|
+
export function walletStandardSigner(wallet, account, chain = loot.walletChain()) {
|
|
105
104
|
const feature = wallet.features?.['solana:signTransaction'];
|
|
106
105
|
if (!feature) throw new Error(`${wallet.name} cannot sign transactions.`);
|
|
107
106
|
const messages = wallet.features?.['solana:signMessage'];
|
|
@@ -165,18 +164,18 @@ const show = (u, decimals) => {
|
|
|
165
164
|
};
|
|
166
165
|
|
|
167
166
|
/**
|
|
168
|
-
* `client`, kept to
|
|
169
|
-
* chain's genesis block is checked, once (`checkCluster`), so an RPC whose URL hides
|
|
167
|
+
* `client`, kept to this page's cluster: a URL that names another is refused at once, and before its first request goes
|
|
168
|
+
* out the chain's genesis block is checked, once (`checkCluster`), so an RPC whose URL hides another cluster is refused
|
|
170
169
|
* whatever it says. Every RPC reached over http(s) is checked: the app's own client, and one given whose `url` is
|
|
171
170
|
* http(s). A client with no such URL (a local chain of the caller's own, like the tests' LiteSVM) is used as it is.
|
|
172
171
|
*/
|
|
173
|
-
function
|
|
174
|
-
if (typeof client.url === 'string')
|
|
172
|
+
function onCluster(client) {
|
|
173
|
+
if (typeof client.url === 'string') rightCluster(client.url, loot.cluster());
|
|
175
174
|
if (!/^(https?|wss?):/i.test(String(client.url ?? ''))) return client;
|
|
176
175
|
let checked = null;
|
|
177
176
|
// A check that could not be made (the network did not answer) is made again on the next request.
|
|
178
177
|
const ready = () =>
|
|
179
|
-
(checked ??=
|
|
178
|
+
(checked ??= loot.checkOnCluster(client).catch(e => {
|
|
180
179
|
checked = null;
|
|
181
180
|
throw e;
|
|
182
181
|
}));
|
|
@@ -203,7 +202,7 @@ function devnetOnly(client) {
|
|
|
203
202
|
*/
|
|
204
203
|
export function solanaApp({
|
|
205
204
|
spec,
|
|
206
|
-
rpcUrl =
|
|
205
|
+
rpcUrl = null,
|
|
207
206
|
rpc = null,
|
|
208
207
|
wallet,
|
|
209
208
|
programs = {},
|
|
@@ -217,7 +216,13 @@ export function solanaApp({
|
|
|
217
216
|
throw new Error(
|
|
218
217
|
'The rpc given has no call(method, params): a custom rpc needs it (the devnet check, the slot and compressed NFTs are read through it), as well as accounts, account, latestBlockhash, send, confirm and programAccounts.',
|
|
219
218
|
);
|
|
220
|
-
|
|
219
|
+
// The economy's file names its cluster (devnet when it names none), and this page works on it and no other.
|
|
220
|
+
loot.useCluster(loot.networkOf(spec));
|
|
221
|
+
if (live.program && live.program !== loot.lootProgram())
|
|
222
|
+
throw new Error(
|
|
223
|
+
`This economy went live on the program ${live.program}, which is not loot’s on ${loot.cluster()} (${loot.lootProgram()}): its "network" and its "live" section name two clusters.`,
|
|
224
|
+
);
|
|
225
|
+
const client = onCluster(rpc ?? rpcClient(loot.clusterRpcUrl(rpcUrl)));
|
|
221
226
|
const project = live.project;
|
|
222
227
|
/** The settle keys, each named by its job's account (`keys.js`): made again after a reload (`recover`). */
|
|
223
228
|
const keys = wallet?.address ? keyring({ wallet, project, store: keyStore }) : null;
|
|
@@ -252,13 +257,13 @@ export function solanaApp({
|
|
|
252
257
|
const read = async (name, address) => {
|
|
253
258
|
const a = await client.account(address);
|
|
254
259
|
if (!a) throw new Error(`Nothing at ${address}: is the economy live on this cluster?`);
|
|
255
|
-
if (a.owner !== loot.
|
|
260
|
+
if (a.owner !== loot.lootProgram()) throw new Error(`${address} is not a loot account.`);
|
|
256
261
|
return loot.decode(name, a.data);
|
|
257
262
|
};
|
|
258
263
|
/** A loot account, or null when there is none yet. */
|
|
259
264
|
const readMaybe = async (name, address) => {
|
|
260
265
|
const a = await client.account(address);
|
|
261
|
-
return a?.owner === loot.
|
|
266
|
+
return a?.owner === loot.lootProgram() ? loot.decode(name, a.data) : null;
|
|
262
267
|
};
|
|
263
268
|
/** The key of the token a lottery's tickets are paid in: the spec's `ticketCurrency`, or the token of its mint. */
|
|
264
269
|
const ticketCurrencyOf = (l, lottery) =>
|
|
@@ -530,7 +535,7 @@ export function solanaApp({
|
|
|
530
535
|
const sheetOf = async nft => {
|
|
531
536
|
const found = await client.account(loot.pda.sheet(project, nft));
|
|
532
537
|
return Object.fromEntries(
|
|
533
|
-
(loot.decodeSheet(found?.owner === loot.
|
|
538
|
+
(loot.decodeSheet(found?.owner === loot.lootProgram() ? found.data : null)?.attributes ?? []).map(a => [
|
|
534
539
|
a.key,
|
|
535
540
|
a.value,
|
|
536
541
|
]),
|
|
@@ -653,7 +658,7 @@ export function solanaApp({
|
|
|
653
658
|
if (loot.readsProfile(rule)) {
|
|
654
659
|
const found = await client.account(loot.pda.profile(project, wallet.address));
|
|
655
660
|
const attributes = Object.fromEntries(
|
|
656
|
-
(loot.decodeProfile(found?.owner === loot.
|
|
661
|
+
(loot.decodeProfile(found?.owner === loot.lootProgram() ? found.data : null)?.attributes ?? []).map(a => [
|
|
657
662
|
a.key,
|
|
658
663
|
a.value,
|
|
659
664
|
]),
|
|
@@ -925,7 +930,7 @@ export function solanaApp({
|
|
|
925
930
|
const said = String(e?.message ?? '');
|
|
926
931
|
const m = /custom program error: 0x([0-9a-f]+)/i.exec(said);
|
|
927
932
|
const failed = e?.logs?.find(line => / failed: custom program error: 0x/i.test(line));
|
|
928
|
-
const known = m && (!failed || failed.includes(loot.
|
|
933
|
+
const known = m && (!failed || failed.includes(loot.lootProgram())) ? loot.ERRORS[parseInt(m[1], 16)] : null;
|
|
929
934
|
if (!known) throw e;
|
|
930
935
|
throw Object.assign(new Error(`${known.msg} (${known.name}).`, { cause: e }), {
|
|
931
936
|
code: parseInt(m[1], 16),
|
|
@@ -1080,14 +1085,14 @@ export function solanaApp({
|
|
|
1080
1085
|
const watch = [
|
|
1081
1086
|
...new Set(
|
|
1082
1087
|
instructions
|
|
1083
|
-
.filter(ix => ix.programId === loot.
|
|
1088
|
+
.filter(ix => ix.programId === loot.lootProgram())
|
|
1084
1089
|
.flatMap(ix => ix.keys.filter(k => k.isWritable && !k.isSigner).map(k => k.pubkey)),
|
|
1085
1090
|
),
|
|
1086
1091
|
];
|
|
1087
1092
|
const after = (await dryRun(wallet.address, instructions, watch)).accounts;
|
|
1088
1093
|
const waiting = watch.flatMap((address, i) => {
|
|
1089
1094
|
const a = after[i];
|
|
1090
|
-
if (a?.owner !== loot.
|
|
1095
|
+
if (a?.owner !== loot.lootProgram() || loot.accountKind(a.data) !== 'Order') return [];
|
|
1091
1096
|
const order = loot.decode('Order', a.data);
|
|
1092
1097
|
return order.status === 'Waiting' && order.player === wallet.address ? [[address, order]] : [];
|
|
1093
1098
|
});
|
|
@@ -1191,7 +1196,7 @@ export function solanaApp({
|
|
|
1191
1196
|
// `landed`: the slot one's own open landed in; the order is read from a node that has seen it.
|
|
1192
1197
|
const current = async () => {
|
|
1193
1198
|
const account = await client.account(orderAddress, 'confirmed', landed != null ? { minContextSlot: landed } : {});
|
|
1194
|
-
return account?.owner === loot.
|
|
1199
|
+
return account?.owner === loot.lootProgram() ? loot.decode('Order', account.data) : null;
|
|
1195
1200
|
};
|
|
1196
1201
|
let order = await current();
|
|
1197
1202
|
// Right after one's own confirmed open, no order is "not seen yet", never "settled already": a node of the RPC may
|
|
@@ -2298,7 +2303,7 @@ export function solanaApp({
|
|
|
2298
2303
|
const listing = await readMaybe('Listing', listingAt);
|
|
2299
2304
|
if (listing) out.push({ kind: 'listing', address: listingAt, record: listing });
|
|
2300
2305
|
const tag = { memcmp: { offset: 0, bytes: encodeBase58(Uint8Array.of(loot.TAGS.Hold)) } };
|
|
2301
|
-
const holds = await client.programAccounts(loot.
|
|
2306
|
+
const holds = await client.programAccounts(loot.lootProgram(), [
|
|
2302
2307
|
tag,
|
|
2303
2308
|
{ memcmp: { offset: 1, bytes: project } },
|
|
2304
2309
|
{ memcmp: { offset: 33, bytes: asset } },
|
|
@@ -2317,7 +2322,7 @@ export function solanaApp({
|
|
|
2317
2322
|
*/
|
|
2318
2323
|
async function stashesOf(asset) {
|
|
2319
2324
|
const at = (offset, bytes) => ({ memcmp: { offset, bytes } });
|
|
2320
|
-
const found = await client.programAccounts(loot.
|
|
2325
|
+
const found = await client.programAccounts(loot.lootProgram(), [
|
|
2321
2326
|
at(0, encodeBase58(Uint8Array.of(loot.TAGS.Table))),
|
|
2322
2327
|
at(1, project),
|
|
2323
2328
|
at(37, encodeBase58(Uint8Array.of(loot.PAYLOAD.Nft))),
|
|
@@ -2532,7 +2537,7 @@ export function solanaApp({
|
|
|
2532
2537
|
const found = await client.accounts(keys);
|
|
2533
2538
|
const rounds = keys.map((key, i) => ({
|
|
2534
2539
|
address: key,
|
|
2535
|
-
round: found[i]?.owner === loot.
|
|
2540
|
+
round: found[i]?.owner === loot.lootProgram() ? loot.decodeRound(found[i].data) : null,
|
|
2536
2541
|
}));
|
|
2537
2542
|
return { address, lottery, current: rounds[0].round, rounds: rounds.filter(r => r.round) };
|
|
2538
2543
|
}
|
|
@@ -2587,7 +2592,7 @@ export function solanaApp({
|
|
|
2587
2592
|
first && lottery?.owing && lottery.owing !== NOBODY && loot.potIdOf(terms) != null
|
|
2588
2593
|
? await client.account(lottery.owing)
|
|
2589
2594
|
: null;
|
|
2590
|
-
const owing = owingAt?.owner === loot.
|
|
2595
|
+
const owing = owingAt?.owner === loot.lootProgram() ? loot.decodeRound(owingAt.data) : null;
|
|
2591
2596
|
// That round's answer says what it left the pot: until it lands (or the round is cancelled), this draw waits.
|
|
2592
2597
|
if (owing?.status === 'Drawing')
|
|
2593
2598
|
throw new Error(
|
|
@@ -3000,13 +3005,13 @@ export function solanaApp({
|
|
|
3000
3005
|
const tag = t => ({ memcmp: { offset: 0, bytes: encodeBase58(Uint8Array.of(loot.TAGS[t])) } });
|
|
3001
3006
|
const at = (offset, bytes) => ({ memcmp: { offset, bytes } });
|
|
3002
3007
|
const orders = new Map();
|
|
3003
|
-
for (const { pubkey, account } of await client.programAccounts(loot.
|
|
3008
|
+
for (const { pubkey, account } of await client.programAccounts(loot.lootProgram(), [
|
|
3004
3009
|
tag('Order'),
|
|
3005
3010
|
at(1, project),
|
|
3006
3011
|
at(33, me),
|
|
3007
3012
|
]))
|
|
3008
3013
|
orders.set(pubkey, loot.decode('Order', account.data));
|
|
3009
|
-
const holds = (await client.programAccounts(loot.
|
|
3014
|
+
const holds = (await client.programAccounts(loot.lootProgram(), [tag('Hold'), at(1, project), at(65, me)])).map(
|
|
3010
3015
|
h => h.pubkey,
|
|
3011
3016
|
);
|
|
3012
3017
|
// Keys are named by an order of the wallet's, a hold (a loan's release), or an auction (the releases after its end).
|
|
@@ -3140,7 +3145,7 @@ export function solanaApp({
|
|
|
3140
3145
|
async payNft(asset, { to = null, reference, ...others } = NO_OPTIONS) {
|
|
3141
3146
|
only(others, ['to', 'reference'], 'payNft');
|
|
3142
3147
|
// An NFT paid goes to a wallet: never the project's address or an account of the program (ToTheProject).
|
|
3143
|
-
if (to && (to === project || (await client.account(to))?.owner === loot.
|
|
3148
|
+
if (to && (to === project || (await client.account(to))?.owner === loot.lootProgram()))
|
|
3144
3149
|
throw new Error('An NFT paid can’t go to the project or an account of the program: name a wallet, or burn it.');
|
|
3145
3150
|
const { before, ...nft } = await nftOf(asset);
|
|
3146
3151
|
return send([...before, loot.payNftIx({ player: wallet.address, project, asset, to, ...nft, reference })]);
|
|
@@ -3158,7 +3163,7 @@ export function solanaApp({
|
|
|
3158
3163
|
const there = await client.account(loot.pda.lottery(project, l.id));
|
|
3159
3164
|
// The terms on sale are the round's own copy once someone bought into it, the lottery's before.
|
|
3160
3165
|
const { lottery, current } =
|
|
3161
|
-
there?.owner === loot.
|
|
3166
|
+
there?.owner === loot.lootProgram() ? await roundOf(l) : { lottery: null, current: null };
|
|
3162
3167
|
const copy = current ?? lottery;
|
|
3163
3168
|
const terms = copy?.terms;
|
|
3164
3169
|
const currency = lottery ? ticketCurrencyOf(l, lottery) : (l.ticketCurrency ?? currencyKey);
|
|
@@ -3718,7 +3723,7 @@ export function solanaApp({
|
|
|
3718
3723
|
const nfts = f.stakes === 'collectibles';
|
|
3719
3724
|
// A seat names its player (offset 1), not its farm: a seat of this farm is the one at its own address here.
|
|
3720
3725
|
const found = (
|
|
3721
|
-
await client.programAccounts(loot.
|
|
3726
|
+
await client.programAccounts(loot.lootProgram(), [
|
|
3722
3727
|
{ memcmp: { offset: 0, bytes: encodeBase58(Uint8Array.of(loot.TAGS.Seat)) } },
|
|
3723
3728
|
{ memcmp: { offset: 1, bytes: player } },
|
|
3724
3729
|
])
|
|
@@ -4545,7 +4550,7 @@ export function solanaApp({
|
|
|
4545
4550
|
...(maker ? [{ memcmp: { offset: 33, bytes: maker } }] : []),
|
|
4546
4551
|
];
|
|
4547
4552
|
const time = await chainNow();
|
|
4548
|
-
const found = (await client.programAccounts(loot.
|
|
4553
|
+
const found = (await client.programAccounts(loot.lootProgram(), filters)).map(({ pubkey, account }) => {
|
|
4549
4554
|
const listing = loot.decode('Listing', account.data);
|
|
4550
4555
|
return { listing: pubkey, ...listing, renter: loot.renterOf(listing, time) };
|
|
4551
4556
|
});
|
|
@@ -5050,7 +5055,7 @@ export function solanaApp({
|
|
|
5050
5055
|
const slot = await slotNow();
|
|
5051
5056
|
const time = BigInt(now());
|
|
5052
5057
|
const chainTime = await chainNow();
|
|
5053
|
-
for (const { pubkey, account } of await client.programAccounts(loot.
|
|
5058
|
+
for (const { pubkey, account } of await client.programAccounts(loot.lootProgram(), [
|
|
5054
5059
|
tag('Order'),
|
|
5055
5060
|
at(1, project),
|
|
5056
5061
|
at(33, me),
|
|
@@ -5068,7 +5073,7 @@ export function solanaApp({
|
|
|
5068
5073
|
says: 'A roll you won (a mission’s, a recipe’s, a prize’s): ask for it and see what it gives (one whose box has nothing left to give waits until it has).',
|
|
5069
5074
|
});
|
|
5070
5075
|
}
|
|
5071
|
-
for (const { pubkey, account } of await client.programAccounts(loot.
|
|
5076
|
+
for (const { pubkey, account } of await client.programAccounts(loot.lootProgram(), [
|
|
5072
5077
|
tag('Contest'),
|
|
5073
5078
|
at(1, project),
|
|
5074
5079
|
])) {
|
|
@@ -5076,7 +5081,7 @@ export function solanaApp({
|
|
|
5076
5081
|
const off = loot.contestOff(contest, time);
|
|
5077
5082
|
if (!(off || contest.verdict === 'Won')) continue;
|
|
5078
5083
|
const list = await client.account(loot.contestEntries(pubkey, contest));
|
|
5079
|
-
const entries = list?.owner === loot.
|
|
5084
|
+
const entries = list?.owner === loot.lootProgram() ? loot.decodeEntries(list.data) : [];
|
|
5080
5085
|
const entry = entries.find(e => e.player === me);
|
|
5081
5086
|
if (!entry) continue;
|
|
5082
5087
|
const won = !off && Number(contest.winner) === Number(entry.side);
|
|
@@ -5134,7 +5139,7 @@ export function solanaApp({
|
|
|
5134
5139
|
// wallet made: one search of the project's holds finds them all.
|
|
5135
5140
|
const g = await projectNow();
|
|
5136
5141
|
const holdsOf = [me, ...(g.treasury === me ? [project] : [])];
|
|
5137
|
-
const holds = (await client.programAccounts(loot.
|
|
5142
|
+
const holds = (await client.programAccounts(loot.lootProgram(), [tag('Hold'), at(1, project)]))
|
|
5138
5143
|
.map(({ pubkey, account }) => ({ pubkey, hold: loot.decode('Hold', account.data) }))
|
|
5139
5144
|
.filter(({ hold }) => holdsOf.includes(hold.player) || hold.debt?.lender === me);
|
|
5140
5145
|
// Whether the project still holds each one's NFT: whoever controls its collection can take it back or burn it.
|
|
@@ -5157,7 +5162,7 @@ export function solanaApp({
|
|
|
5157
5162
|
out.push({ ...claim, says: 'A loan you made is overdue and unpaid: take its NFT.' });
|
|
5158
5163
|
}
|
|
5159
5164
|
// A compressed NFT's proof record this wallet paid for and left open: closing it brings its rent back.
|
|
5160
|
-
for (const { account } of await client.programAccounts(loot.
|
|
5165
|
+
for (const { account } of await client.programAccounts(loot.lootProgram(), [tag('Proven')])) {
|
|
5161
5166
|
const leaf = (() => {
|
|
5162
5167
|
try {
|
|
5163
5168
|
return loot.decodeProven(account.data);
|
|
@@ -5335,7 +5340,7 @@ export function solanaApp({
|
|
|
5335
5340
|
out.push({ kind: 'vest', key: sc.key, says: `What has vested of “${sc.name ?? sc.key}”: claim it.` });
|
|
5336
5341
|
}
|
|
5337
5342
|
// The player's listings whose rental is over: their NFT can go home (or wait for the next renter).
|
|
5338
|
-
const mineListed = await client.programAccounts(loot.
|
|
5343
|
+
const mineListed = await client.programAccounts(loot.lootProgram(), [tag('Listing'), at(1, project), at(33, me)]);
|
|
5339
5344
|
for (const { account } of mineListed) {
|
|
5340
5345
|
const listing = loot.decode('Listing', account.data);
|
|
5341
5346
|
if (listing.deal.Rent && listing.user !== NOBODY && !loot.renterOf(listing, chainTime))
|
|
@@ -5535,7 +5540,7 @@ export function solanaApp({
|
|
|
5535
5540
|
: await read('Lottery', loot.pda.lottery(project, l.id));
|
|
5536
5541
|
const key = c.round ?? loot.currentRound(loot.pda.lottery(project, l.id), lottery);
|
|
5537
5542
|
const found = await client.account(key);
|
|
5538
|
-
const r = found?.owner === loot.
|
|
5543
|
+
const r = found?.owner === loot.lootProgram() ? loot.decodeRound(found.data) : null;
|
|
5539
5544
|
// The ask: the current round's once due, or an older round's again (after the timeout, it cancels the round).
|
|
5540
5545
|
if (c.kind === 'reveal' || c.kind === 'cancel-draw') {
|
|
5541
5546
|
const steps = await drawStep(l, lottery, c.round ?? null, r, me);
|
|
@@ -5617,7 +5622,7 @@ export function solanaApp({
|
|
|
5617
5622
|
const carryOn = async signature => {
|
|
5618
5623
|
if (build || !partial) return signature;
|
|
5619
5624
|
const after = await client.account(key);
|
|
5620
|
-
const r2 = after?.owner === loot.
|
|
5625
|
+
const r2 = after?.owner === loot.lootProgram() ? loot.decodeRound(after.data) : null;
|
|
5621
5626
|
const moved = r2 && (Number(r2.assigned) > Number(r.assigned) || Number(r2.owed) < Number(r.owed));
|
|
5622
5627
|
if (!moved) return signature;
|
|
5623
5628
|
const left =
|
package/js/play/sponsor.js
CHANGED
|
@@ -36,7 +36,7 @@ import { transferLamports } from '../chain/programs.js';
|
|
|
36
36
|
import { encodeBase58 } from '../chain/base58.js';
|
|
37
37
|
import { fromBase64, toBase64 } from '../chain/bytes.js';
|
|
38
38
|
import { readMessage, readTransaction, withSignature } from '../chain/tx.js';
|
|
39
|
-
import {
|
|
39
|
+
import { lootProgram, MPL_CORE_PROGRAM, pda, VRF_PROGRAM } from '../client/index.js';
|
|
40
40
|
|
|
41
41
|
const SYSTEM_PROGRAM = '11111111111111111111111111111111';
|
|
42
42
|
/** What a signature costs the fee payer, in lamports. */
|
|
@@ -82,7 +82,7 @@ export function sponsoredCost({ addresses, pre, post, project, skip = [], progra
|
|
|
82
82
|
}
|
|
83
83
|
if (before) continue;
|
|
84
84
|
const opened =
|
|
85
|
-
after.owner ===
|
|
85
|
+
after.owner === lootProgram() ||
|
|
86
86
|
(after.owner === MPL_CORE_PROGRAM && !programs.includes(MPL_CORE_PROGRAM)) ||
|
|
87
87
|
((after.owner === TOKEN_PROGRAM || after.owner === TOKEN_2022_PROGRAM) &&
|
|
88
88
|
after.data?.length >= 32 &&
|
|
@@ -97,7 +97,7 @@ export function stepShape(instructions) {
|
|
|
97
97
|
return {
|
|
98
98
|
programs: [...new Set(instructions.map(ix => ix.programId))],
|
|
99
99
|
lootNamed: [
|
|
100
|
-
...new Set(instructions.filter(ix => ix.programId ===
|
|
100
|
+
...new Set(instructions.filter(ix => ix.programId === lootProgram()).flatMap(ix => ix.keys.map(k => k.pubkey))),
|
|
101
101
|
],
|
|
102
102
|
};
|
|
103
103
|
}
|
|
@@ -180,7 +180,7 @@ export async function checkSponsored(
|
|
|
180
180
|
if (program === COMPUTE_BUDGET_PROGRAM && ix.data[0] === 2 && ix.data.length >= 5)
|
|
181
181
|
units = BigInt(u32At(ix.data, 1));
|
|
182
182
|
if (program === COMPUTE_BUDGET_PROGRAM && ix.data[0] === 3 && ix.data.length >= 9) price = u64At(ix.data, 1);
|
|
183
|
-
if (program ===
|
|
183
|
+
if (program === lootProgram() && accounts.includes(project)) ours = true;
|
|
184
184
|
}
|
|
185
185
|
if (!ours) return no('It does nothing in this project.');
|
|
186
186
|
if (price > BigInt(maxPriorityMicroLamports)) return no('Its priority fee is more than this sponsor pays.');
|
package/js/results/index.js
CHANGED
|
@@ -22,6 +22,7 @@ import { decodeBase58, encodeBase58 } from '../chain/base58.js';
|
|
|
22
22
|
import { toHex } from '../chain/bytes.js';
|
|
23
23
|
import { pda, voucherBytes } from '../client/index.js';
|
|
24
24
|
import { canonicalJson, drawText, entryText, lockText, resultBytes, rosterHash } from './wire.js';
|
|
25
|
+
import { cluster as onCluster } from '../client/cluster.js';
|
|
25
26
|
|
|
26
27
|
export {
|
|
27
28
|
canonicalJson,
|
|
@@ -40,7 +41,7 @@ export {
|
|
|
40
41
|
rosterHash,
|
|
41
42
|
} from './wire.js';
|
|
42
43
|
|
|
43
|
-
/** xgames.fun's results server, on its play host
|
|
44
|
+
/** xgames.fun's results server, on its play host. One host answers for devnet and for mainnet: each request names its cluster. */
|
|
44
45
|
export const RESULTS_SERVER = 'https://play.xgames.fun/xsolana';
|
|
45
46
|
|
|
46
47
|
const subtle = () => globalThis.crypto.subtle;
|
|
@@ -112,7 +113,7 @@ function base64url(text) {
|
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
/** One signer's signature of a result: { key, signature }. `signer` is a key from newKey, or anything with sign(). */
|
|
115
|
-
export async function signResult(signer, result, cluster =
|
|
116
|
+
export async function signResult(signer, result, cluster = onCluster()) {
|
|
116
117
|
return { key: signer.address, signature: encodeBase58(await signer.sign(resultBytes({ ...result, cluster }))) };
|
|
117
118
|
}
|
|
118
119
|
|
|
@@ -129,7 +130,7 @@ export async function report({
|
|
|
129
130
|
round,
|
|
130
131
|
endedAt,
|
|
131
132
|
winners,
|
|
132
|
-
cluster =
|
|
133
|
+
cluster = onCluster(),
|
|
133
134
|
fetchImpl = globalThis.fetch,
|
|
134
135
|
}) {
|
|
135
136
|
const result = { game, round, endedAt, winners };
|
|
@@ -274,7 +275,7 @@ export const draws = {
|
|
|
274
275
|
token = null,
|
|
275
276
|
wallet = null,
|
|
276
277
|
signMessage = null,
|
|
277
|
-
cluster =
|
|
278
|
+
cluster = onCluster(),
|
|
278
279
|
fetchImpl = globalThis.fetch,
|
|
279
280
|
}) => {
|
|
280
281
|
if (token) return post(server, `/v1/draws/${seg(game)}/${seg(draw)}/entries`, { token }, fetchImpl);
|
|
@@ -295,7 +296,7 @@ export const draws = {
|
|
|
295
296
|
signMessage,
|
|
296
297
|
by,
|
|
297
298
|
cert = null,
|
|
298
|
-
cluster =
|
|
299
|
+
cluster = onCluster(),
|
|
299
300
|
net = 'quicknet',
|
|
300
301
|
fetchImpl = globalThis.fetch,
|
|
301
302
|
}) => {
|
|
@@ -345,7 +346,7 @@ export const giveaways = {
|
|
|
345
346
|
endedAt,
|
|
346
347
|
winners,
|
|
347
348
|
grant,
|
|
348
|
-
cluster =
|
|
349
|
+
cluster = onCluster(),
|
|
349
350
|
fetchImpl = globalThis.fetch,
|
|
350
351
|
}) => {
|
|
351
352
|
const signed = winners.map(w => ({ wallet: w.wallet, place: w.place, grant }));
|
package/js/results/server.js
CHANGED
|
@@ -19,6 +19,7 @@ import { MPL_CORE_PROGRAM, ownedAssetFilters, parseCoreAsset } from '../chain/co
|
|
|
19
19
|
import { rpcClient } from '../chain/rpc.js';
|
|
20
20
|
import { sha256 } from '../chain/sha256.js';
|
|
21
21
|
import { signVoucher } from './index.js';
|
|
22
|
+
import { cluster as onCluster } from '../client/cluster.js';
|
|
22
23
|
|
|
23
24
|
const subtle = () => globalThis.crypto.subtle;
|
|
24
25
|
const rpcOf = rpc => (typeof rpc === 'string' ? rpcClient(rpc) : rpc);
|
|
@@ -70,7 +71,7 @@ const oneLine = (name, value) => {
|
|
|
70
71
|
};
|
|
71
72
|
|
|
72
73
|
/** The exact text a wallet signs to prove it is its owner's: fixed lines, so nothing else can be passed off as it. */
|
|
73
|
-
export const proofText = ({ cluster =
|
|
74
|
+
export const proofText = ({ cluster = onCluster(), game, wallet, purpose, nonce, expires }) =>
|
|
74
75
|
[
|
|
75
76
|
PROOF_TITLE,
|
|
76
77
|
...PROOF_FIELDS.map(f => `${f}: ${oneLine(f, { cluster, game, wallet, purpose, nonce, expires }[f])}`),
|
|
@@ -83,7 +84,7 @@ export const proofText = ({ cluster = 'devnet', game, wallet, purpose, nonce, ex
|
|
|
83
84
|
* (unix seconds, `seconds` from now) and takes each nonce once, so a signed proof can't be replayed.
|
|
84
85
|
*/
|
|
85
86
|
export function proofRequest({
|
|
86
|
-
cluster =
|
|
87
|
+
cluster = onCluster(),
|
|
87
88
|
game,
|
|
88
89
|
wallet,
|
|
89
90
|
purpose = 'sign in',
|
|
@@ -127,7 +128,7 @@ export async function verifyProof({
|
|
|
127
128
|
wallet,
|
|
128
129
|
game,
|
|
129
130
|
purpose = null,
|
|
130
|
-
cluster =
|
|
131
|
+
cluster = onCluster(),
|
|
131
132
|
now = Math.floor(Date.now() / 1000),
|
|
132
133
|
}) {
|
|
133
134
|
const fields = readProof(text);
|
package/js/results/wire.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// with whoever signs them: a host server, a player's game page, an owner's wallet. Browser-safe: no Node imports.
|
|
3
3
|
import { addressBytes } from '../chain/base58.js';
|
|
4
4
|
import { concat, fromHex, uintLE, utf8 } from '../chain/bytes.js';
|
|
5
|
+
import { cluster as onCluster } from '../client/cluster.js';
|
|
5
6
|
|
|
6
7
|
export const RESULT_DOMAIN = 'loot-result-v1';
|
|
7
8
|
export const NO_GRANT = 0xffff;
|
|
@@ -31,10 +32,10 @@ const write = (kind, values) =>
|
|
|
31
32
|
[CERT_KINDS[kind].title, ...CERT_KINDS[kind].fields.map(f => `${f}: ${field(f, values[f])}`)].join('\n');
|
|
32
33
|
|
|
33
34
|
/** The text the owner signs to let `host` report `game`'s results until `until` (unix seconds). */
|
|
34
|
-
export const hostCertText = ({ cluster =
|
|
35
|
+
export const hostCertText = ({ cluster = onCluster(), game, host, until }) =>
|
|
35
36
|
write('host', { cluster, game, host, until });
|
|
36
37
|
/** The text the owner signs to take `host` back. */
|
|
37
|
-
export const revokeText = ({ cluster =
|
|
38
|
+
export const revokeText = ({ cluster = onCluster(), game, host }) => write('revoke', { cluster, game, host });
|
|
38
39
|
|
|
39
40
|
/**
|
|
40
41
|
* The text the organizer (the game's owner or authority, or a host key they certified) signs to open a draw: a round
|
|
@@ -45,7 +46,7 @@ export const revokeText = ({ cluster = 'devnet', game, host }) => write('revoke'
|
|
|
45
46
|
* JSON), or 'default'.
|
|
46
47
|
*/
|
|
47
48
|
export const drawText = ({
|
|
48
|
-
cluster =
|
|
49
|
+
cluster = onCluster(),
|
|
49
50
|
game,
|
|
50
51
|
draw,
|
|
51
52
|
engine,
|
|
@@ -69,19 +70,19 @@ export const drawText = ({
|
|
|
69
70
|
rates,
|
|
70
71
|
});
|
|
71
72
|
/** The text the organizer signs to lock a draw's players (`roster`: rosterHash) to a beacon round still to come. */
|
|
72
|
-
export const lockText = ({ cluster =
|
|
73
|
+
export const lockText = ({ cluster = onCluster(), game, draw, roster, players, beacon, net = 'quicknet' }) =>
|
|
73
74
|
write('lock', { cluster, game, draw, roster, players, beacon: `${net} ${beacon}` });
|
|
74
75
|
/** The text a player's wallet signs to enter a draw (identity 'wallet'). */
|
|
75
|
-
export const entryText = ({ cluster =
|
|
76
|
+
export const entryText = ({ cluster = onCluster(), game, draw }) => write('entry', { cluster, game, draw });
|
|
76
77
|
|
|
77
78
|
/**
|
|
78
79
|
* The text the owner (or authority) signs to appoint a giveaway host: `who` is "x:<X account id>" (the host signs in
|
|
79
80
|
* with that X account through the play host) or a wallet address; their giveaways pay from grant `grant` alone.
|
|
80
81
|
*/
|
|
81
|
-
export const giveawayHostText = ({ cluster =
|
|
82
|
+
export const giveawayHostText = ({ cluster = onCluster(), game, who, slug, grant, until }) =>
|
|
82
83
|
write('giveaway', { cluster, game, who, slug, grant, until });
|
|
83
84
|
/** The text the owner (or authority) signs to take a giveaway host back. */
|
|
84
|
-
export const revokeGiveawayText = ({ cluster =
|
|
85
|
+
export const revokeGiveawayText = ({ cluster = onCluster(), game, who }) =>
|
|
85
86
|
write('revokeGiveaway', { cluster, game, who });
|
|
86
87
|
|
|
87
88
|
/** A draw's players, as one hash: each `id<TAB>wallet`, sorted by id, one a line; sha256, hex. */
|
|
@@ -113,7 +114,7 @@ export function canonicalJson(value) {
|
|
|
113
114
|
* Every field has one length, so one set of bytes means one result: an unknown cluster, a round that is not 16 bytes, a
|
|
114
115
|
* place past 255 and more than 255 winners are refused, never written as something else.
|
|
115
116
|
*/
|
|
116
|
-
export function resultBytes({ cluster =
|
|
117
|
+
export function resultBytes({ cluster = onCluster(), game, round, endedAt, winners }) {
|
|
117
118
|
if (!Object.hasOwn(CLUSTERS, cluster))
|
|
118
119
|
throw new RangeError(
|
|
119
120
|
`Unknown cluster ${JSON.stringify(cluster)}: a result names ${Object.keys(CLUSTERS).join(' or ')}.`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xgamesfun/solana",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.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",
|
|
@@ -80,6 +80,10 @@
|
|
|
80
80
|
"./mcp/http": {
|
|
81
81
|
"types": "./types/mcp/http.d.mts",
|
|
82
82
|
"default": "./js/mcp/http.mjs"
|
|
83
|
+
},
|
|
84
|
+
"./with-cluster": {
|
|
85
|
+
"types": "./types/mcp/with-cluster.d.mts",
|
|
86
|
+
"default": "./js/mcp/with-cluster.mjs"
|
|
83
87
|
}
|
|
84
88
|
},
|
|
85
89
|
"sideEffects": [
|
|
@@ -102,6 +106,7 @@
|
|
|
102
106
|
],
|
|
103
107
|
"scripts": {
|
|
104
108
|
"test": "node --test \"js/test/**/*.test.mjs\"",
|
|
109
|
+
"test:mainnet": "node scripts/test-mainnet.mjs",
|
|
105
110
|
"lint": "eslint . && prettier --check .",
|
|
106
111
|
"format": "prettier --write .",
|
|
107
112
|
"types": "tsc -p tsconfig.types.json",
|
package/release.json
CHANGED
|
@@ -4,5 +4,10 @@
|
|
|
4
4
|
"sbfBytes": 1794616,
|
|
5
5
|
"sbfSha256": "ac8b09ed2c3a3f7daa2dfab55d2f6dea19fa43e4c98b44a82da2a68a7bc8ebbe",
|
|
6
6
|
"idlSha256": "ef3b8043d62c606e850df75773b4535cdb08206216e17aca653111a77403abd7",
|
|
7
|
-
"clientSha256": "
|
|
7
|
+
"clientSha256": "164ed8ee25b5a2714f6c8f5de243d6b650ba0741637ee3dd4c2886f15af3a6c5",
|
|
8
|
+
"mainnet": {
|
|
9
|
+
"programId": "xgames8C758PyweWB9sDw8evvwsTVwyA2fGRUeAXVPy",
|
|
10
|
+
"sbfBytes": 1794616,
|
|
11
|
+
"sbfSha256": "f78e4f0ef38b5f1673936044263293e91172b4bac92c741cc7bb9567617bbdf6"
|
|
12
|
+
}
|
|
8
13
|
}
|
|
@@ -8,11 +8,12 @@ description: Build a web3, token or NFT economy on Solana without writing a smar
|
|
|
8
8
|
An economy is one JSON spec (`xsolana.json` in the project) on **loot**, a Solana program every project shares. Nobody
|
|
9
9
|
writes or deploys a contract: the spec is checked, simulated and priced by the `xsolana_*` tools, and it goes live
|
|
10
10
|
signed by a key file on the owner's own machine, once they have read what will be sent and said yes. It runs on
|
|
11
|
-
**devnet**
|
|
11
|
+
**devnet**, where nothing is real money, and on **mainnet**, where everything is: an economy is on devnet unless its
|
|
12
|
+
file says `"network": "mainnet"`.
|
|
12
13
|
|
|
13
|
-
The tools are the `xsolana_*` MCP tools. Without MCP, the same from a terminal: `npx -y @xgamesfun/solana@0.28.
|
|
14
|
-
`npx -y @xgamesfun/solana@0.28.
|
|
15
|
-
or `npx -y @xgamesfun/solana@0.28.
|
|
14
|
+
The tools are the `xsolana_*` MCP tools. Without MCP, the same from a terminal: `npx -y @xgamesfun/solana@0.28.2 <guide|examples|check|simulate|cost|explain|review|publish|status> [xsolana.json]`,
|
|
15
|
+
`npx -y @xgamesfun/solana@0.28.2 token <mint>` for a coin made elsewhere,
|
|
16
|
+
or `npx -y @xgamesfun/solana@0.28.2 tool <name> '<json>'` for any tool (always with the exact version).
|
|
16
17
|
|
|
17
18
|
## Rules (always)
|
|
18
19
|
|
|
@@ -33,7 +34,10 @@ or `npx -y @xgamesfun/solana@0.28.0 tool <name> '<json>'` for any tool (always w
|
|
|
33
34
|
owner: whoever can read it controls the project. Never open it, copy it or print it, and never put it in a chat, a
|
|
34
35
|
spec, a repository or a page. Never ask for a seed phrase or the key of a wallet the person already has; if they
|
|
35
36
|
paste one, tell them to treat it as exposed and move its funds, and write it nowhere.
|
|
36
|
-
3. **Devnet only.**
|
|
37
|
+
3. **Devnet first; mainnet only on the person's own word.** Build, check and try every economy on devnet, and say
|
|
38
|
+
that nothing there is real money. Write `"network": "mainnet"` only when the person asks for mainnet in their own
|
|
39
|
+
words, never to make something feel real. Tell them then, before anything is sent: it is real money, loot has not
|
|
40
|
+
been independently audited, what is sent can be lost, and what the review and the checker's legal warnings say.
|
|
37
41
|
4. **The checker decides.** Never save or publish a spec with refusals. Fix each refusal the way its `fix` says.
|
|
38
42
|
5. **Numbers come from the tools.** What a box gives back, how long a pool lasts, what going live costs: quote
|
|
39
43
|
`xsolana_simulate`, `xsolana_cost` and `xsolana_explain`, never your own arithmetic. What a player pays is the tools'
|
|
@@ -137,7 +141,7 @@ or `npx -y @xgamesfun/solana@0.28.0 tool <name> '<json>'` for any tool (always w
|
|
|
137
141
|
9. **When they say "put it live"**: `xsolana_publish` lists every step and its cost and gives the commands, which you
|
|
138
142
|
run in the project folder exactly as given (they name the exact version; never drop it):
|
|
139
143
|
- **A new economy gets a key file**, unless the project has one:
|
|
140
|
-
`npx -y @xgamesfun/solana@0.28.
|
|
144
|
+
`npx -y @xgamesfun/solana@0.28.2 keygen ~/.config/xsolana/<name>.json`. Give the person the address it prints. A
|
|
141
145
|
Solana key file they already have and name themselves (`~/.config/solana/id.json`) works the same; never go
|
|
142
146
|
looking for one.
|
|
143
147
|
- **Say what the key file is** before anything is sent: the project's owner, and, unless the spec says otherwise,
|
|
@@ -148,11 +152,11 @@ or `npx -y @xgamesfun/solana@0.28.0 tool <name> '<json>'` for any tool (always w
|
|
|
148
152
|
set `"treasury": "wallet:<address>"` first: it can't be changed through the spec once live. Tell them to back
|
|
149
153
|
the file up themselves (a USB stick, a password manager): if it is lost, nobody can change the economy, decide
|
|
150
154
|
a contest or move what its address holds, and nothing restores it. You never open or copy it.
|
|
151
|
-
- **Everything the setup takes from the owner must be at the key's address first**: the
|
|
152
|
-
|
|
155
|
+
- **Everything the setup takes from the owner must be at the key's address first**: the SOL (`xsolana_cost`;
|
|
156
|
+
devnet SOL for an economy on devnet, real SOL for one on mainnet), any seed in a coin that already exists, and every NFT a stash, a raffle or an auction names. A
|
|
153
157
|
collection they already have needs the key's address as its update authority. If they would rather not move
|
|
154
158
|
those, offer the browser wallet.
|
|
155
|
-
- **Show it**: `npx -y @xgamesfun/solana@0.28.
|
|
159
|
+
- **Show it**: `npx -y @xgamesfun/solana@0.28.2 approve xsolana.json --keypair <key file>` prints every step with
|
|
156
160
|
what it pays and trusts, the warnings, the notice and a code, and sends nothing. Give the person that printout as
|
|
157
161
|
it is, not a summary of it, and ask.
|
|
158
162
|
- **On their yes**, the same command with `--yes <code>`. What lands is written back into `xsolana.json`. If it
|
|
@@ -76,7 +76,8 @@ await app.balances(); // what they hold of each of the economy's tokens
|
|
|
76
76
|
}
|
|
77
77
|
</script>
|
|
78
78
|
```
|
|
79
|
-
Tell the person the one command that starts it, and
|
|
79
|
+
Tell the person the one command that starts it, and which cluster its economy is on (devnet unless its file says
|
|
80
|
+
mainnet).
|
|
80
81
|
- **What a roll looks like**: a box of several outcomes is rolled by the oracle a few seconds after the open; `openBox`
|
|
81
82
|
waits for it. Show the player that it is rolling.
|
|
82
83
|
- **A page that closes before the settle**: the SOL its settle key holds is not lost in the same browser (on another device, with nothing stored, it can't be found yet). Build the wallet with
|
|
@@ -29,7 +29,7 @@ and the spec differ (a limit, an amount, a key not set yet), change one of them
|
|
|
29
29
|
a score): "Checking that a player really did it is up to you, on your own server: a game runs in the player's
|
|
30
30
|
browser, and the program trusts the authority's signature."
|
|
31
31
|
6. **Flags for the law, once.** `W-CHANCE-PRIZE`: "Players pay for a chance at money. That is gambling in many places once
|
|
32
|
-
it is real money; this
|
|
32
|
+
it is real money; this one is on devnet, which is not." (On mainnet: "this one is on mainnet, so it is.") `W-CHANCE-ITEMS`: "Players pay for random items they can resell: some
|
|
33
33
|
places treat that as gambling, and many require the odds to be shown." `W-REVENUE-SHARE`: "Stakers are paid from
|
|
34
34
|
what players spend; regulators look at that when they decide whether a token is a security." `W-TOKEN-SALE`:
|
|
35
35
|
"People pay for a share of a new token; selling a token to raise money is what securities law looks at first."
|
|
@@ -104,7 +104,7 @@ Run `xsolana_review` and go through what it reports; these are the points behind
|
|
|
104
104
|
11. **The authority and the treasury.** The owner's wallet alone changes every price, rate, grant, limit and voucher
|
|
105
105
|
key; the authority mints, moves pool funds, decides contests and releases holds, with no caps. On devnet the
|
|
106
106
|
owner's key is the key file the setup was signed with: readable by its user alone, outside any repository, never
|
|
107
|
-
pasted anywhere. With real money (
|
|
107
|
+
pasted anywhere. With real money (an economy on mainnet) those keys belong in a hardware wallet or a multisig; a
|
|
108
108
|
server that signs gets an authority key of its own, never the owner's.
|
|
109
109
|
12. **The page is untrusted.** A page builds the player's transactions, and a cheater builds their own: the program's
|
|
110
110
|
checks are what count, never the page's. Anything the page decides (a score, a reward amount, who won) is only as
|