@xgamesfun/solana 0.28.1 → 0.28.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +16 -4
- package/js/approve/server.mjs +31 -26
- package/js/approve/with-key.mjs +7 -4
- package/js/chain/borsh.js +5 -3
- 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 +1 -1
- 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 +48 -38
- package/js/play/sponsor.js +4 -4
- package/js/results/index.js +36 -14
- 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/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 +14 -14
- 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 +14 -7
- 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
|
});
|
|
@@ -4917,7 +4922,12 @@ export function solanaApp({
|
|
|
4917
4922
|
only(others, ['server', 'token', 'fetchImpl'], 'claimWins');
|
|
4918
4923
|
const base = server.replace(/\/$/, '');
|
|
4919
4924
|
const ask = async (path, init) => {
|
|
4920
|
-
|
|
4925
|
+
// (one server answers for both clusters: a request for mainnet says so, one for devnet names none, as before)
|
|
4926
|
+
const at = path.startsWith('http') ? path : `${base}${path}`;
|
|
4927
|
+
const on = loot.cluster();
|
|
4928
|
+
const url =
|
|
4929
|
+
on === 'devnet' || /[?&]cluster=/.test(at) ? at : `${at}${at.includes('?') ? '&' : '?'}cluster=${on}`;
|
|
4930
|
+
const r = await fetchImpl(url, init);
|
|
4921
4931
|
// The status first: a proxy's error page is no JSON, and says nothing a player can act on but its status.
|
|
4922
4932
|
const body = await r.json().catch(() => null);
|
|
4923
4933
|
if (!r.ok)
|
|
@@ -5050,7 +5060,7 @@ export function solanaApp({
|
|
|
5050
5060
|
const slot = await slotNow();
|
|
5051
5061
|
const time = BigInt(now());
|
|
5052
5062
|
const chainTime = await chainNow();
|
|
5053
|
-
for (const { pubkey, account } of await client.programAccounts(loot.
|
|
5063
|
+
for (const { pubkey, account } of await client.programAccounts(loot.lootProgram(), [
|
|
5054
5064
|
tag('Order'),
|
|
5055
5065
|
at(1, project),
|
|
5056
5066
|
at(33, me),
|
|
@@ -5068,7 +5078,7 @@ export function solanaApp({
|
|
|
5068
5078
|
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
5079
|
});
|
|
5070
5080
|
}
|
|
5071
|
-
for (const { pubkey, account } of await client.programAccounts(loot.
|
|
5081
|
+
for (const { pubkey, account } of await client.programAccounts(loot.lootProgram(), [
|
|
5072
5082
|
tag('Contest'),
|
|
5073
5083
|
at(1, project),
|
|
5074
5084
|
])) {
|
|
@@ -5076,7 +5086,7 @@ export function solanaApp({
|
|
|
5076
5086
|
const off = loot.contestOff(contest, time);
|
|
5077
5087
|
if (!(off || contest.verdict === 'Won')) continue;
|
|
5078
5088
|
const list = await client.account(loot.contestEntries(pubkey, contest));
|
|
5079
|
-
const entries = list?.owner === loot.
|
|
5089
|
+
const entries = list?.owner === loot.lootProgram() ? loot.decodeEntries(list.data) : [];
|
|
5080
5090
|
const entry = entries.find(e => e.player === me);
|
|
5081
5091
|
if (!entry) continue;
|
|
5082
5092
|
const won = !off && Number(contest.winner) === Number(entry.side);
|
|
@@ -5134,7 +5144,7 @@ export function solanaApp({
|
|
|
5134
5144
|
// wallet made: one search of the project's holds finds them all.
|
|
5135
5145
|
const g = await projectNow();
|
|
5136
5146
|
const holdsOf = [me, ...(g.treasury === me ? [project] : [])];
|
|
5137
|
-
const holds = (await client.programAccounts(loot.
|
|
5147
|
+
const holds = (await client.programAccounts(loot.lootProgram(), [tag('Hold'), at(1, project)]))
|
|
5138
5148
|
.map(({ pubkey, account }) => ({ pubkey, hold: loot.decode('Hold', account.data) }))
|
|
5139
5149
|
.filter(({ hold }) => holdsOf.includes(hold.player) || hold.debt?.lender === me);
|
|
5140
5150
|
// Whether the project still holds each one's NFT: whoever controls its collection can take it back or burn it.
|
|
@@ -5157,7 +5167,7 @@ export function solanaApp({
|
|
|
5157
5167
|
out.push({ ...claim, says: 'A loan you made is overdue and unpaid: take its NFT.' });
|
|
5158
5168
|
}
|
|
5159
5169
|
// 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.
|
|
5170
|
+
for (const { account } of await client.programAccounts(loot.lootProgram(), [tag('Proven')])) {
|
|
5161
5171
|
const leaf = (() => {
|
|
5162
5172
|
try {
|
|
5163
5173
|
return loot.decodeProven(account.data);
|
|
@@ -5335,7 +5345,7 @@ export function solanaApp({
|
|
|
5335
5345
|
out.push({ kind: 'vest', key: sc.key, says: `What has vested of “${sc.name ?? sc.key}”: claim it.` });
|
|
5336
5346
|
}
|
|
5337
5347
|
// 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.
|
|
5348
|
+
const mineListed = await client.programAccounts(loot.lootProgram(), [tag('Listing'), at(1, project), at(33, me)]);
|
|
5339
5349
|
for (const { account } of mineListed) {
|
|
5340
5350
|
const listing = loot.decode('Listing', account.data);
|
|
5341
5351
|
if (listing.deal.Rent && listing.user !== NOBODY && !loot.renterOf(listing, chainTime))
|
|
@@ -5535,7 +5545,7 @@ export function solanaApp({
|
|
|
5535
5545
|
: await read('Lottery', loot.pda.lottery(project, l.id));
|
|
5536
5546
|
const key = c.round ?? loot.currentRound(loot.pda.lottery(project, l.id), lottery);
|
|
5537
5547
|
const found = await client.account(key);
|
|
5538
|
-
const r = found?.owner === loot.
|
|
5548
|
+
const r = found?.owner === loot.lootProgram() ? loot.decodeRound(found.data) : null;
|
|
5539
5549
|
// The ask: the current round's once due, or an older round's again (after the timeout, it cancels the round).
|
|
5540
5550
|
if (c.kind === 'reveal' || c.kind === 'cancel-draw') {
|
|
5541
5551
|
const steps = await drawStep(l, lottery, c.round ?? null, r, me);
|
|
@@ -5617,7 +5627,7 @@ export function solanaApp({
|
|
|
5617
5627
|
const carryOn = async signature => {
|
|
5618
5628
|
if (build || !partial) return signature;
|
|
5619
5629
|
const after = await client.account(key);
|
|
5620
|
-
const r2 = after?.owner === loot.
|
|
5630
|
+
const r2 = after?.owner === loot.lootProgram() ? loot.decodeRound(after.data) : null;
|
|
5621
5631
|
const moved = r2 && (Number(r2.assigned) > Number(r.assigned) || Number(r2.owed) < Number(r.owed));
|
|
5622
5632
|
if (!moved) return signature;
|
|
5623
5633
|
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,12 +130,12 @@ 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 };
|
|
136
137
|
const all = [await signResult(signer, result, cluster)];
|
|
137
|
-
const response = await fetchImpl(
|
|
138
|
+
const response = await fetchImpl(serverUrl(server, '/v1/results', cluster), {
|
|
138
139
|
method: 'POST',
|
|
139
140
|
headers: { 'content-type': 'application/json' },
|
|
140
141
|
body: JSON.stringify({ ...result, cert, signatures: all }),
|
|
@@ -164,9 +165,16 @@ async function answerOf(response) {
|
|
|
164
165
|
}
|
|
165
166
|
|
|
166
167
|
/** A wallet's vouchers for a round it won: [{ grant, publicKey, signature, round, expires, … }]. */
|
|
167
|
-
export async function fetchVouchers({
|
|
168
|
+
export async function fetchVouchers({
|
|
169
|
+
server,
|
|
170
|
+
game,
|
|
171
|
+
round,
|
|
172
|
+
wallet,
|
|
173
|
+
cluster = onCluster(),
|
|
174
|
+
fetchImpl = globalThis.fetch,
|
|
175
|
+
}) {
|
|
168
176
|
const response = await fetchImpl(
|
|
169
|
-
|
|
177
|
+
serverUrl(server, `/v1/vouchers?game=${q(game)}&round=${q(round)}&wallet=${q(wallet)}`, cluster),
|
|
170
178
|
);
|
|
171
179
|
return (await answerOf(response)).vouchers;
|
|
172
180
|
}
|
|
@@ -225,9 +233,19 @@ export async function signVoucher({
|
|
|
225
233
|
};
|
|
226
234
|
}
|
|
227
235
|
|
|
236
|
+
/**
|
|
237
|
+
* A path on a results server, for the cluster the request is on. One server answers for both clusters, and a request
|
|
238
|
+
* that names none is devnet's: so nothing is added on devnet (as every request was before), and `cluster=mainnet` on
|
|
239
|
+
* mainnet.
|
|
240
|
+
*/
|
|
241
|
+
export function serverUrl(server, path, cluster = onCluster()) {
|
|
242
|
+
const url = `${String(server).replace(/\/$/, '')}${path}`;
|
|
243
|
+
return cluster === 'devnet' ? url : `${url}${url.includes('?') ? '&' : '?'}cluster=${encodeURIComponent(cluster)}`;
|
|
244
|
+
}
|
|
245
|
+
|
|
228
246
|
/** Posts JSON to the server and answers its body, or throws its { code, why }. */
|
|
229
|
-
async function post(server, path, body, fetchImpl) {
|
|
230
|
-
const response = await fetchImpl(
|
|
247
|
+
async function post(server, path, body, fetchImpl, cluster = onCluster()) {
|
|
248
|
+
const response = await fetchImpl(serverUrl(server, path, cluster), {
|
|
231
249
|
method: 'POST',
|
|
232
250
|
headers: { 'content-type': 'application/json' },
|
|
233
251
|
body: JSON.stringify(body),
|
|
@@ -236,7 +254,8 @@ async function post(server, path, body, fetchImpl) {
|
|
|
236
254
|
}
|
|
237
255
|
|
|
238
256
|
/** Gets JSON from the server and answers its body, or throws its { code, why }. */
|
|
239
|
-
const fetchJson = async (server, path, fetchImpl
|
|
257
|
+
const fetchJson = async (server, path, fetchImpl, cluster = onCluster()) =>
|
|
258
|
+
answerOf(await fetchImpl(serverUrl(server, path, cluster)));
|
|
240
259
|
|
|
241
260
|
/**
|
|
242
261
|
* A draw, from the organizer's side and the players'. `signMessage(text) → signature bytes` is the organizer's wallet
|
|
@@ -274,16 +293,17 @@ export const draws = {
|
|
|
274
293
|
token = null,
|
|
275
294
|
wallet = null,
|
|
276
295
|
signMessage = null,
|
|
277
|
-
cluster =
|
|
296
|
+
cluster = onCluster(),
|
|
278
297
|
fetchImpl = globalThis.fetch,
|
|
279
298
|
}) => {
|
|
280
|
-
if (token) return post(server, `/v1/draws/${seg(game)}/${seg(draw)}/entries`, { token }, fetchImpl);
|
|
299
|
+
if (token) return post(server, `/v1/draws/${seg(game)}/${seg(draw)}/entries`, { token }, fetchImpl, cluster);
|
|
281
300
|
const text = entryText({ cluster, game, draw });
|
|
282
301
|
return post(
|
|
283
302
|
server,
|
|
284
303
|
`/v1/draws/${seg(game)}/${seg(draw)}/entries`,
|
|
285
304
|
{ wallet, text, signature: encodeBase58(await signMessage(new TextEncoder().encode(text))) },
|
|
286
305
|
fetchImpl,
|
|
306
|
+
cluster,
|
|
287
307
|
);
|
|
288
308
|
},
|
|
289
309
|
/** Locks the players the server holds now to `beacon`, a drand round still to come. Checks their hash itself. */
|
|
@@ -295,11 +315,11 @@ export const draws = {
|
|
|
295
315
|
signMessage,
|
|
296
316
|
by,
|
|
297
317
|
cert = null,
|
|
298
|
-
cluster =
|
|
318
|
+
cluster = onCluster(),
|
|
299
319
|
net = 'quicknet',
|
|
300
320
|
fetchImpl = globalThis.fetch,
|
|
301
321
|
}) => {
|
|
302
|
-
const { draw: view } = await fetchJson(server, `/v1/draws/${seg(game)}/${seg(draw)}`, fetchImpl);
|
|
322
|
+
const { draw: view } = await fetchJson(server, `/v1/draws/${seg(game)}/${seg(draw)}`, fetchImpl, cluster);
|
|
303
323
|
const text = lockText({
|
|
304
324
|
cluster,
|
|
305
325
|
game,
|
|
@@ -319,6 +339,7 @@ export const draws = {
|
|
|
319
339
|
...(cert ? { cert } : {}),
|
|
320
340
|
},
|
|
321
341
|
fetchImpl,
|
|
342
|
+
cluster,
|
|
322
343
|
);
|
|
323
344
|
},
|
|
324
345
|
/** The draw as anyone may check it; settled, with its result, once its beacon round is out. */
|
|
@@ -345,7 +366,7 @@ export const giveaways = {
|
|
|
345
366
|
endedAt,
|
|
346
367
|
winners,
|
|
347
368
|
grant,
|
|
348
|
-
cluster =
|
|
369
|
+
cluster = onCluster(),
|
|
349
370
|
fetchImpl = globalThis.fetch,
|
|
350
371
|
}) => {
|
|
351
372
|
const signed = winners.map(w => ({ wallet: w.wallet, place: w.place, grant }));
|
|
@@ -357,6 +378,7 @@ export const giveaways = {
|
|
|
357
378
|
`/v1/giveaways/${seg(game)}/${seg(giveaway)}/result`,
|
|
358
379
|
{ endedAt, winners: winners.map(({ wallet, place }) => ({ wallet, place })), signature },
|
|
359
380
|
fetchImpl,
|
|
381
|
+
cluster,
|
|
360
382
|
);
|
|
361
383
|
},
|
|
362
384
|
};
|
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.3",
|
|
4
4
|
"mcpName": "io.github.XGamesFun/solana-kit",
|
|
5
5
|
"description": "Loot boxes, gacha, shops, lotteries, auctions, NFT staking, crafting and rewards on Solana without writing a smart contract: one shared program, its JS client, a CLI, and an MCP server and skill so Claude Code, Codex or any AI agent can design, check, simulate and launch an economy.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -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
|
}
|