@xgamesfun/solana 0.0.0-stage → 0.20.1
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 +41 -0
- package/LICENSE +202 -0
- package/NOTICE +13 -0
- package/README.md +172 -2
- package/bin/xsolana.mjs +189 -0
- package/examples/artwork-auction.json +51 -0
- package/examples/blind-forge.json +128 -0
- package/examples/coin-flip-arena.json +108 -0
- package/examples/duel-arena.json +137 -0
- package/examples/elements-set.json +281 -0
- package/examples/event-badges.json +67 -0
- package/examples/expedition-guild.json +161 -0
- package/examples/fair-presale.json +48 -0
- package/examples/falling-drop.json +90 -0
- package/examples/guild-missions.json +144 -0
- package/examples/halving-seasons.json +112 -0
- package/examples/hero-trials.json +96 -0
- package/examples/ideas.json +789 -0
- package/examples/level-ladder.json +65 -0
- package/examples/merge-2048.json +128 -0
- package/examples/nft-raffle.json +33 -0
- package/examples/paper-hands-farm.json +56 -0
- package/examples/partner-vault.json +115 -0
- package/examples/sacrifice-raffle.json +118 -0
- package/examples/scorched-treasury.json +84 -0
- package/examples/shelter-fair.json +164 -0
- package/examples/spark-pity.json +153 -0
- package/examples/stakers-draw.json +125 -0
- package/examples/star-fleet.json +157 -0
- package/examples/starforce.json +116 -0
- package/examples/stud-farm.json +134 -0
- package/examples/team-vesting.json +70 -0
- package/examples/trade-in-shop.json +168 -0
- package/js/approve/details.mjs +262 -0
- package/js/approve/ed25519.mjs +36 -0
- package/js/approve/live-check.mjs +144 -0
- package/js/approve/page.html +86 -0
- package/js/approve/page.js +203 -0
- package/js/approve/server.mjs +711 -0
- package/js/chain/base58.js +73 -0
- package/js/chain/borsh.js +261 -0
- package/js/chain/bytes.js +141 -0
- package/js/chain/core.js +169 -0
- package/js/chain/curve.js +52 -0
- package/js/chain/lookup.js +273 -0
- package/js/chain/mint.js +96 -0
- package/js/chain/pda.js +66 -0
- package/js/chain/programs.js +102 -0
- package/js/chain/rpc.js +217 -0
- package/js/chain/sha256.js +68 -0
- package/js/chain/token.js +85 -0
- package/js/chain/token2022.js +247 -0
- package/js/chain/tx.js +224 -0
- package/js/client/accounts.js +347 -0
- package/js/client/compressed.js +46 -0
- package/js/client/grant.js +240 -0
- package/js/client/idl/loot.js +2 -0
- package/js/client/idl/loot.json +17064 -0
- package/js/client/index.js +9 -0
- package/js/client/ix.js +2156 -0
- package/js/client/pda.js +110 -0
- package/js/client/policy.js +48 -0
- package/js/client/sizes.js +54 -0
- package/js/client/traits.js +44 -0
- package/js/connect/connect.mjs +182 -0
- package/js/economy/check.mjs +1991 -0
- package/js/economy/cost.mjs +170 -0
- package/js/economy/examples.mjs +114 -0
- package/js/economy/explain.mjs +235 -0
- package/js/economy/guide.mjs +231 -0
- package/js/economy/legal.mjs +15 -0
- package/js/economy/live.mjs +107 -0
- package/js/economy/model.mjs +583 -0
- package/js/economy/plan.mjs +887 -0
- package/js/economy/schema.json +2923 -0
- package/js/economy/secrets.mjs +60 -0
- package/js/economy/simulate.mjs +952 -0
- package/js/economy/txsize.mjs +45 -0
- package/js/economy/validate.mjs +85 -0
- package/js/index.js +10 -0
- package/js/mcp/server.mjs +113 -0
- package/js/mcp/tools.mjs +479 -0
- package/js/play/index.js +2109 -0
- package/js/results/index.js +184 -0
- package/js/results/wire.js +90 -0
- package/package.json +100 -4
- package/release.json +8 -0
- package/skills/xsolana-economy/SKILL.md +105 -0
- package/skills/xsolana-economy/references/after-launch.md +114 -0
- package/skills/xsolana-economy/references/apps.md +157 -0
- package/skills/xsolana-economy/references/sentence-to-spec.md +172 -0
- package/skills/xsolana-economy/references/telling-the-person.md +93 -0
- package/types/chain/base58.d.ts +10 -0
- package/types/chain/borsh.d.ts +23 -0
- package/types/chain/bytes.d.ts +45 -0
- package/types/chain/core.d.ts +50 -0
- package/types/chain/curve.d.ts +1 -0
- package/types/chain/lookup.d.ts +191 -0
- package/types/chain/mint.d.ts +25 -0
- package/types/chain/pda.d.ts +25 -0
- package/types/chain/programs.d.ts +65 -0
- package/types/chain/rpc.d.ts +66 -0
- package/types/chain/sha256.d.ts +1 -0
- package/types/chain/token.d.ts +141 -0
- package/types/chain/token2022.d.ts +53 -0
- package/types/chain/tx.d.ts +60 -0
- package/types/client/accounts.d.ts +178 -0
- package/types/client/compressed.d.ts +29 -0
- package/types/client/grant.d.ts +125 -0
- package/types/client/idl/loot.d.ts +4467 -0
- package/types/client/index.d.ts +8 -0
- package/types/client/ix.d.ts +1687 -0
- package/types/client/pda.d.ts +91 -0
- package/types/client/policy.d.ts +23 -0
- package/types/client/sizes.d.ts +13 -0
- package/types/client/traits.d.ts +12 -0
- package/types/economy/check.d.mts +105 -0
- package/types/economy/cost.d.mts +68 -0
- package/types/economy/examples.d.mts +27 -0
- package/types/economy/explain.d.mts +19 -0
- package/types/economy/guide.d.mts +140 -0
- package/types/economy/legal.d.mts +12 -0
- package/types/economy/live.d.mts +109 -0
- package/types/economy/model.d.mts +363 -0
- package/types/economy/plan.d.mts +1013 -0
- package/types/economy/secrets.d.mts +4 -0
- package/types/economy/simulate.d.mts +156 -0
- package/types/economy/txsize.d.mts +17 -0
- package/types/economy/validate.d.mts +3 -0
- package/types/index.d.ts +9 -0
- package/types/mcp/tools.d.mts +627 -0
- package/types/play/index.d.ts +469 -0
- package/types/results/index.d.ts +155 -0
- package/types/results/wire.d.ts +122 -0
package/js/client/pda.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Every address the loot program derives, with the seeds the program uses. Each takes base58
|
|
2
|
+
// addresses and numbers and returns a base58 address. Browser-safe.
|
|
3
|
+
import { addressBytes } from '../chain/base58.js';
|
|
4
|
+
import { intLE, utf8, uintLE } from '../chain/bytes.js';
|
|
5
|
+
import { associatedTokenAddress, findProgramAddress, MPL_CORE_PROGRAM, TOKEN_PROGRAM } from '../chain/pda.js';
|
|
6
|
+
import IDL from './idl/loot.js';
|
|
7
|
+
|
|
8
|
+
export const LOOT_PROGRAM = IDL.address;
|
|
9
|
+
/** MagicBlock's VRF program and its default queue, the only queue the program accepts. */
|
|
10
|
+
export const VRF_PROGRAM = 'Vrf1RNUjXmQGjmQrQLvJHs9SNkvDJEsRVFPkfSQUwGz';
|
|
11
|
+
export const VRF_QUEUE = 'Cuj97ggrhhidhbu39TijNVqE74xvKJ69gDervRUXAxGh';
|
|
12
|
+
export { MPL_CORE_PROGRAM };
|
|
13
|
+
export const BPF_LOADER_UPGRADEABLE = 'BPFLoaderUpgradeab1e11111111111111111111111';
|
|
14
|
+
/** Compressed NFTs: Bubblegum, and the account compression program and noop of its v2 trees. */
|
|
15
|
+
export const BUBBLEGUM_PROGRAM = 'BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY';
|
|
16
|
+
export const COMPRESSION_PROGRAM = 'mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW';
|
|
17
|
+
export const NOOP_PROGRAM = 'mnoopTCrg4p8ry25e4bcWA9XZjbNjMTfgYVGGEdRsf3';
|
|
18
|
+
|
|
19
|
+
const seed = (s) => utf8(s);
|
|
20
|
+
const key = (a) => addressBytes(a);
|
|
21
|
+
const u8 = (n) => uintLE(n, 1);
|
|
22
|
+
const u16 = (n) => uintLE(n, 2);
|
|
23
|
+
const u32 = (n) => uintLE(n, 4);
|
|
24
|
+
const u64 = (n) => uintLE(n, 8);
|
|
25
|
+
const i64 = (n) => intLE(n, 8);
|
|
26
|
+
|
|
27
|
+
/** The PDA of these seeds under the loot program, with its bump. */
|
|
28
|
+
export const lootAddress = (seeds, program = LOOT_PROGRAM) => findProgramAddress(seeds, program);
|
|
29
|
+
const at = (...seeds) => lootAddress(seeds)[0];
|
|
30
|
+
const keyReceipt = (grant, incarnation, voucherKey) => at(seed('code'), key(grant), u64(incarnation), key(voucherKey));
|
|
31
|
+
|
|
32
|
+
export const pda = {
|
|
33
|
+
platform: () => at(seed('platform')),
|
|
34
|
+
/** A project: any wallet (its creator) opens as many as it likes, each with a number of its own (`id`, a u64). A
|
|
35
|
+
* new owner keeps the address. */
|
|
36
|
+
project: (creator, id = 0) => at(seed('project'), key(creator), u64(id)),
|
|
37
|
+
/** The project's fee box: where the platform's fees on its transactions wait. */
|
|
38
|
+
fees: (project) => at(seed('fees'), key(project)),
|
|
39
|
+
pool: (project, index) => at(seed('pool'), key(project), u8(index)),
|
|
40
|
+
item: (project, id) => at(seed('item'), key(project), u32(id)),
|
|
41
|
+
box: (project, id) => at(seed('box'), key(project), u32(id)),
|
|
42
|
+
/** NFTs the project holds to give away as prizes: a box entry of a stash gives one of them at random. */
|
|
43
|
+
stash: (project, id) => at(seed('stash'), key(project), u32(id)),
|
|
44
|
+
schedule: (project, id) => at(seed('schedule'), key(project), u32(id)),
|
|
45
|
+
lottery: (project, id) => at(seed('lottery'), key(project), u32(id)),
|
|
46
|
+
recipe: (project, id) => at(seed('recipe'), key(project), u32(id)),
|
|
47
|
+
contest: (project, id) => at(seed('contest'), key(project), u32(id)),
|
|
48
|
+
/** An auction: an id is its seller's own. Its bids wait in the project's vault for its mint (`vault`). */
|
|
49
|
+
auction: (project, seller, id) => at(seed('auction'), key(project), key(seller), u32(id)),
|
|
50
|
+
farm: (project, id) => at(seed('farm'), key(project), u32(id)),
|
|
51
|
+
grant: (project, id) => at(seed('grant'), key(project), u16(id)),
|
|
52
|
+
order: (project, player, nonce) => at(seed('order'), key(project), key(player), u64(nonce)),
|
|
53
|
+
/** Every collectible the program mints of its own accord (an order's, a prize's, an auction's lot): its address
|
|
54
|
+
* names its item and its serial, minted once at most. */
|
|
55
|
+
itemAsset: (item, serial) => at(seed('asset'), key(item), u32(serial)),
|
|
56
|
+
/** A contest's pot, or a lottery's: `["pot", contest or lottery]`. */
|
|
57
|
+
pot: (record) => at(seed('pot'), key(record)),
|
|
58
|
+
lotteryFunds: (lottery) => at(seed('lottery_funds'), key(lottery)),
|
|
59
|
+
lotteryReceipt: (lottery, incarnation, wallet) => at(seed('receipt'), key(lottery), u64(incarnation), key(wallet)),
|
|
60
|
+
/** One purchase of a lottery's tickets, at a nonce of its buyer's. */
|
|
61
|
+
contribution: (lottery, player, nonce) => at(seed('contribution'), key(lottery), key(player), u64(nonce)),
|
|
62
|
+
/** A wallet's entry in a contest: one, on one side (another side is `OtherSide`). */
|
|
63
|
+
entry: (contest, player) => at(seed('entry'), key(contest), key(player)),
|
|
64
|
+
/** A hold names its NFT and its end (Unix seconds): a release sent late never finds a later hold of the NFT. */
|
|
65
|
+
hold: (asset, freeAt) => at(seed('hold'), key(asset), i64(freeAt)),
|
|
66
|
+
/** An offer: an id is its maker's own. Its money waits in the project's vault for its mint (`vault`). */
|
|
67
|
+
offer: (project, maker, id) => at(seed('offer'), key(project), key(maker), u32(id)),
|
|
68
|
+
/** A lottery's own stash: the NFTs its places promise (`Prize::Held`), which leave only as its prizes. */
|
|
69
|
+
lotteryStash: (lottery) => at(seed('lottery_stash'), key(lottery)),
|
|
70
|
+
/** A contest's prize split by stake, set aside when it is decided: `["prize", contest]`. */
|
|
71
|
+
prizeVault: (contest) => at(seed('prize'), key(contest)),
|
|
72
|
+
/** A sealed schedule's own vault, which only its runs pay out of: `["vest", schedule]`. */
|
|
73
|
+
vest: (schedule) => at(seed('vest'), key(schedule)),
|
|
74
|
+
/** A listing of an NFT for sale or for rent: one per NFT a project holds, so its address names the NFT. */
|
|
75
|
+
listing: (project, asset) => at(seed('listing'), key(project), key(asset)),
|
|
76
|
+
staked: (farm) => at(seed('staked'), key(farm)),
|
|
77
|
+
rewards: (farm) => at(seed('rewards'), key(farm)),
|
|
78
|
+
/** A seat: a player's in a farm of tokens, a collectible's (its asset) in a farm of collectibles. */
|
|
79
|
+
seat: (farm, by) => at(seed('seat'), key(farm), key(by)),
|
|
80
|
+
/** What the project writes about an NFT it has no authority over (its Sheet): `nft` is a Core asset, a Token Metadata mint or a compressed NFT's asset id. */
|
|
81
|
+
sheet: (project, nft) => at(seed('sheet'), key(project), key(nft)),
|
|
82
|
+
/** A compressed NFT as last proven (`prove_leaf`), read while the leaf is unchanged and its root is still in the tree's change log. */
|
|
83
|
+
leaf: (asset) => at(seed('leaf'), key(asset)),
|
|
84
|
+
/** The root of the traits the project vouches for, under its own `key` (a collection's address, by convention). */
|
|
85
|
+
traits: (project, key_) => at(seed('traits'), key(project), key(key_)),
|
|
86
|
+
/** Bubblegum's config of a tree: `[tree]`. */
|
|
87
|
+
treeConfig: (tree) => findProgramAddress([key(tree)], BUBBLEGUM_PROGRAM)[0],
|
|
88
|
+
/** A compressed NFT's asset id: Bubblegum's `["asset", tree, nonce]`, `nonce` the count minted before it. */
|
|
89
|
+
compressedAsset: (tree, nonce) => findProgramAddress([seed('asset'), key(tree), u64(nonce)], BUBBLEGUM_PROGRAM)[0],
|
|
90
|
+
/** A wallet's counter on a box or recipe. Grants and lotteries also name their incarnation. */
|
|
91
|
+
receipt: (record, wallet) => at(seed('receipt'), key(record), key(wallet)),
|
|
92
|
+
grantReceipt: (grant, incarnation, wallet) => at(seed('receipt'), key(grant), u64(incarnation), key(wallet)),
|
|
93
|
+
person: (grant, incarnation, personHash) => at(seed('person'), key(grant), u64(incarnation), Uint8Array.from(personHash)),
|
|
94
|
+
/** How many times an NFT shown claimed an open grant its gate asks for one of: an address of its own, never a person's. */
|
|
95
|
+
badge: (grant, incarnation, nft) => at(seed('badge'), key(grant), u64(incarnation), key(nft)),
|
|
96
|
+
/** A voucher key's counter on a grant (a promo code's). */
|
|
97
|
+
code: keyReceipt,
|
|
98
|
+
/** The program's identity, which signs its requests to the oracle. */
|
|
99
|
+
identity: () => at(seed('identity')),
|
|
100
|
+
/** The identity the oracle signs callbacks with: the VRF program's PDA for this program. */
|
|
101
|
+
oracleIdentity: () => findProgramAddress([seed('identity'), key(LOOT_PROGRAM)], VRF_PROGRAM)[0],
|
|
102
|
+
/** The program's ProgramData account (its upgrade authority signs `init_platform`). */
|
|
103
|
+
programData: () => findProgramAddress([key(LOOT_PROGRAM)], BPF_LOADER_UPGRADEABLE)[0],
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/** `owner`'s associated token account of `mint` (owner may be a PDA: a project's vault, a pool is not one). */
|
|
107
|
+
export const ata = (owner, mint, tokenProgram = TOKEN_PROGRAM) => associatedTokenAddress(owner, mint, tokenProgram);
|
|
108
|
+
|
|
109
|
+
/** A project's vault for `mint`: its associated token account, where every open order's payment is held. */
|
|
110
|
+
export const vault = (project, mint, tokenProgram = TOKEN_PROGRAM) => ata(project, mint, tokenProgram);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Lifecycle rules shared by browsers and optional keepers, read from the decoded accounts. The program still checks
|
|
2
|
+
// every transition; these say when one is worth sending.
|
|
3
|
+
import IDL from './idl/loot.js';
|
|
4
|
+
|
|
5
|
+
const constant = name => BigInt(IDL.constants.find(value => value.name === name).value);
|
|
6
|
+
export const STALE_SLOTS = constant('STALE_SLOTS');
|
|
7
|
+
export const LOTTERY_TIMEOUT_SLOTS = constant('LOTTERY_TIMEOUT_SLOTS');
|
|
8
|
+
const U64_MAX = (1n << 64n) - 1n;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The earliest slot an order can be refunded in, or null when it never can be: a rolled order has an outcome anyone
|
|
12
|
+
* can carry out, and is settled, never refunded; a roll a mission or a recipe made waits for anyone to ask for it
|
|
13
|
+
* (`Waiting`), and once asked (`Asked`) is asked again if the oracle never answers (`rollAskable`).
|
|
14
|
+
*/
|
|
15
|
+
export function orderRefundSlot(order) {
|
|
16
|
+
let wait;
|
|
17
|
+
if (order.status === 'Failed') wait = 0n;
|
|
18
|
+
else if (order.status === 'Pending') wait = STALE_SLOTS;
|
|
19
|
+
else if (['Rolled', 'Waiting', 'Asked'].includes(order.status)) return null;
|
|
20
|
+
else throw new Error('Unknown order status.');
|
|
21
|
+
const deadline = BigInt(order.requested_slot) + wait;
|
|
22
|
+
// The program requires strictly greater than the saturating deadline; MAX + 1 is unreachable.
|
|
23
|
+
return (deadline > U64_MAX ? U64_MAX : deadline) + 1n;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Whether anyone may ask the oracle for a mission's or a recipe's roll at `slot`: never asked, or never answered. */
|
|
27
|
+
export const rollAskable = (order, slot) => order.status === 'Waiting' || (order.status === 'Asked' && BigInt(slot) > BigInt(order.requested_slot) + STALE_SLOTS);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Whether a contest is off at `now` (Unix seconds), as the program reads its verdict: called off, its result
|
|
31
|
+
* overdue, or short of its least pot once entries closed. Every entry of one that is off takes its money back.
|
|
32
|
+
*/
|
|
33
|
+
export function contestOff(contest, now) {
|
|
34
|
+
if (contest.verdict === 'Off') return true;
|
|
35
|
+
if (contest.verdict !== 'Open') return false;
|
|
36
|
+
const total = contest.sides.reduce((sum, side) => sum + BigInt(side), 0n);
|
|
37
|
+
return BigInt(now) > BigInt(contest.result_by) || (BigInt(now) >= BigInt(contest.closes_at) && total < BigInt(contest.min_pot ?? 0));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Whether a lottery's draw was asked for and the oracle has not answered within `STALE_SLOTS`: anyone may ask again. */
|
|
41
|
+
export const lotteryDrawStuck = (lottery, slot) => lottery.status === 'Drawing' && BigInt(slot) > BigInt(lottery.asked_slot) + STALE_SLOTS;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Whether a lottery's draw was asked for and the oracle has not answered within `LOTTERY_TIMEOUT_SLOTS` of the first
|
|
45
|
+
* ask (asking again doesn't move it): anyone may cancel the round and refund it. A round nobody asked to draw is never
|
|
46
|
+
* cancelled.
|
|
47
|
+
*/
|
|
48
|
+
export const lotteryCancellable = (lottery, slot) => lottery.status === 'Drawing' && BigInt(slot) > BigInt(lottery.asked_first) + LOTTERY_TIMEOUT_SLOTS;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Rent sizes from the current IDL. Fixed allocations reserve the largest enum and option forms,
|
|
2
|
+
// matching Anchor InitSpace. Dynamic records use their actual encoded fields.
|
|
3
|
+
import { encodeAccount } from '../chain/borsh.js';
|
|
4
|
+
import IDL from './idl/loot.js';
|
|
5
|
+
|
|
6
|
+
const scalar = { bool: 1, u8: 1, u16: 2, u32: 4, i32: 4, u64: 8, i64: 8, u128: 16, pubkey: 32 };
|
|
7
|
+
// The most an order holds: its shares, and a roll's fate (`MAX_SHIFTS` numbers, each key at most 32 bytes).
|
|
8
|
+
const bounds = { 'Order.shares': 4, 'Order.won': 3, 'Fate.shifts': 2, 'Project.collections': 8 };
|
|
9
|
+
const strings = { 'Shift.key': 32 };
|
|
10
|
+
function maximum(type, path) {
|
|
11
|
+
if (type === 'string') {
|
|
12
|
+
if (!(path in strings)) throw new Error(`No allocation bound for ${path}`);
|
|
13
|
+
return 4 + strings[path];
|
|
14
|
+
}
|
|
15
|
+
if (typeof type === 'string') {
|
|
16
|
+
if (!(type in scalar)) throw new Error(`Unbounded IDL type ${path}: ${type}`);
|
|
17
|
+
return scalar[type];
|
|
18
|
+
}
|
|
19
|
+
if (type.array) return type.array[1] * maximum(type.array[0], path);
|
|
20
|
+
if (type.option) return 1 + maximum(type.option, path);
|
|
21
|
+
if (type.vec) {
|
|
22
|
+
if (!(path in bounds)) throw new Error(`No allocation bound for ${path}`);
|
|
23
|
+
return 4 + bounds[path] * maximum(type.vec, path);
|
|
24
|
+
}
|
|
25
|
+
const name = type.defined.name;
|
|
26
|
+
const definition = IDL.types.find((entry) => entry.name === name)?.type;
|
|
27
|
+
if (!definition) throw new Error(`Missing current IDL type ${name}`);
|
|
28
|
+
const fields = (list) => (list ?? []).reduce((n, field) => n + maximum(field.type ?? field, `${name}.${field.name ?? ''}`), 0);
|
|
29
|
+
return definition.kind === 'enum' ? 1 + Math.max(...definition.variants.map((variant) => fields(variant.fields))) : fields(definition.fields);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The space reserved by a fixed-size account, including its discriminator. */
|
|
33
|
+
export function allocationBytes(name) {
|
|
34
|
+
const account = IDL.accounts.find((entry) => entry.name === name);
|
|
35
|
+
if (!account) throw new Error(`Missing current IDL account ${name}`);
|
|
36
|
+
return account.discriminator.length + maximum({ defined: { name } }, name);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The exact bytes of a dynamically allocated account. */
|
|
40
|
+
export const accountBytes = (name, value) => encodeAccount(IDL, name, value).length;
|
|
41
|
+
export const ACCOUNT_BYTES = Object.freeze({ ...Object.fromEntries(['Order', 'Seat', 'Hold', 'Entry', 'LotteryContribution'].map((name) => [name, allocationBytes(name)])) });
|
|
42
|
+
|
|
43
|
+
/** A project's bytes with `collections` collections, as the program sizes the account. */
|
|
44
|
+
export const projectBytes = (collections = 0) => 153 + 32 * collections;
|
|
45
|
+
|
|
46
|
+
// A receipt is not in the IDL and is read by hand: a 17-byte head (its tag, counts and how many rounds), then 8 bytes
|
|
47
|
+
// for each round it holds (see docs/program.md).
|
|
48
|
+
export const MAX_RECEIPT_ROUNDS = Number(IDL.constants.find((entry) => entry.name === 'MAX_RECEIPT_ROUNDS').value);
|
|
49
|
+
export const RECEIPT_BASE_BYTES = 17;
|
|
50
|
+
export const RECEIPT_ROUND_BYTES = 8;
|
|
51
|
+
export function receiptBytes(rounds = 0) {
|
|
52
|
+
if (!Number.isInteger(rounds) || rounds < 0 || rounds > MAX_RECEIPT_ROUNDS) throw new RangeError(`A receipt retains at most ${MAX_RECEIPT_ROUNDS} distinct voucher rounds`);
|
|
53
|
+
return RECEIPT_BASE_BYTES + RECEIPT_ROUND_BYTES * rounds;
|
|
54
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// The traits a project vouches for (`set_traits`, `prove_traits`): a Merkle tree of what NFTs' off-chain metadata says
|
|
2
|
+
// of them (a background, a class), whose root the project's owner or authority signs once. Anyone then writes an NFT's
|
|
3
|
+
// traits into its Sheet with its proof. Leaves and nodes are hashed exactly as the program hashes them. Browser-safe.
|
|
4
|
+
import { addressBytes } from '../chain/base58.js';
|
|
5
|
+
import { compareBytes, concat, uintLE, utf8 } from '../chain/bytes.js';
|
|
6
|
+
import { sha256 } from '../chain/sha256.js';
|
|
7
|
+
|
|
8
|
+
const text = (s) => concat(uintLE(utf8(s).length, 4), utf8(s));
|
|
9
|
+
|
|
10
|
+
/** A leaf: an NFT's address and its traits ([{ key, value }], in order). */
|
|
11
|
+
export const traitsLeaf = (nft, attributes) =>
|
|
12
|
+
sha256(concat(Uint8Array.of(0), addressBytes(nft), uintLE(attributes.length, 4), ...attributes.map((a) => concat(text(a.key), text(a.value)))));
|
|
13
|
+
|
|
14
|
+
/** Two nodes joined, the smaller first, so a proof needs no left or right. */
|
|
15
|
+
export const traitsNode = (a, b) => (compareBytes(a, b) <= 0 ? sha256(concat(Uint8Array.of(1), a, b)) : sha256(concat(Uint8Array.of(1), b, a)));
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The tree of `entries` ([{ nft, attributes }]): { root, proof(nft) → [node, …] bottom up }. A node left without a pair
|
|
19
|
+
* at a level goes up as it is.
|
|
20
|
+
*/
|
|
21
|
+
export function traitsTree(entries) {
|
|
22
|
+
const levels = [entries.map((e) => traitsLeaf(e.nft, e.attributes))];
|
|
23
|
+
while (levels.at(-1).length > 1) {
|
|
24
|
+
const below = levels.at(-1);
|
|
25
|
+
const up = [];
|
|
26
|
+
for (let i = 0; i < below.length; i += 2) up.push(i + 1 < below.length ? traitsNode(below[i], below[i + 1]) : below[i]);
|
|
27
|
+
levels.push(up);
|
|
28
|
+
}
|
|
29
|
+
const at = new Map(entries.map((e, i) => [e.nft, i]));
|
|
30
|
+
return {
|
|
31
|
+
root: levels.at(-1)[0],
|
|
32
|
+
proof(nft) {
|
|
33
|
+
let i = at.get(nft);
|
|
34
|
+
if (i === undefined) throw new Error(`${nft} is not in this traits tree.`);
|
|
35
|
+
const out = [];
|
|
36
|
+
for (const level of levels.slice(0, -1)) {
|
|
37
|
+
const sibling = i ^ 1;
|
|
38
|
+
if (sibling < level.length) out.push(level[sibling]);
|
|
39
|
+
i >>= 1;
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// `xsolana connect`: adds the xsolana MCP server and the xsolana-economy skill to the coding agents on this machine (Claude
|
|
2
|
+
// Code, Codex), through each agent's own commands. It never edits an agent's config file and never reads a
|
|
3
|
+
// credential. With `--local`, the server runs from this checkout (before the package is published); otherwise from npm.
|
|
4
|
+
// `--scope user` (the default) adds them for every project of this user; `--scope project`, for the project folder
|
|
5
|
+
// it runs in only.
|
|
6
|
+
import { exec, execFile } from 'node:child_process';
|
|
7
|
+
import { cp, mkdir, readFile, rm, stat } from 'node:fs/promises';
|
|
8
|
+
import os from 'node:os';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
|
|
12
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
|
13
|
+
const NAME = 'xsolana';
|
|
14
|
+
const SKILL = 'xsolana-economy';
|
|
15
|
+
|
|
16
|
+
/** The scopes `xsolana connect` knows: every project of this user, or the one project folder it runs in. */
|
|
17
|
+
export const SCOPES = ['user', 'project'];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The agents xsolana connect to, as data: a new agent is a row. `scopes`: where the agent can register an MCP server
|
|
21
|
+
* from its command line. `skills(scope, cwd)`: the folder its skills are read from.
|
|
22
|
+
*/
|
|
23
|
+
export const AGENTS = {
|
|
24
|
+
claude: {
|
|
25
|
+
name: 'Claude Code',
|
|
26
|
+
bin: 'claude',
|
|
27
|
+
scopes: ['user', 'project'],
|
|
28
|
+
add: (command, args, scope) => ['mcp', 'add', '--transport', 'stdio', '--scope', scope, NAME, '--', command, ...args],
|
|
29
|
+
remove: (scope) => ['mcp', 'remove', '--scope', scope, NAME],
|
|
30
|
+
get: ['mcp', 'get', NAME],
|
|
31
|
+
skills: (scope, cwd) => (scope === 'project' ? path.join(cwd, '.claude', 'skills') : path.join(os.homedir(), '.claude', 'skills')),
|
|
32
|
+
plugin: 'Or install the plugin: /plugin marketplace add XGamesFun/solana-kit, then /plugin install xsolana@xgamesfun.',
|
|
33
|
+
next: 'Start a new Claude Code session (or /mcp) to load the xsolana tools.',
|
|
34
|
+
},
|
|
35
|
+
codex: {
|
|
36
|
+
name: 'Codex',
|
|
37
|
+
bin: 'codex',
|
|
38
|
+
scopes: ['user'],
|
|
39
|
+
add: (command, args) => ['mcp', 'add', NAME, '--', command, ...args],
|
|
40
|
+
remove: () => ['mcp', 'remove', NAME],
|
|
41
|
+
get: ['mcp', 'get', NAME],
|
|
42
|
+
skills: (scope, cwd) => (scope === 'project' ? path.join(cwd, '.agents', 'skills') : path.join(os.homedir(), '.agents', 'skills')),
|
|
43
|
+
plugin: 'Or install the plugin: codex plugin marketplace add XGamesFun/solana-kit, then codex plugin add xsolana@xgamesfun.',
|
|
44
|
+
next: 'Start a new Codex session so it picks up the xsolana tools.',
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** One word for cmd.exe: as it is when it is plain, else in double quotes (a quote inside doubled). */
|
|
49
|
+
const winWord = (a) => (/^[\w@+=:,./\\-]+$/.test(a) ? a : `"${String(a).replace(/"/g, '""')}"`);
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Runs an agent's command and answers { code, out }. On Windows the agents are installed as .cmd shims, which only a
|
|
53
|
+
* shell can start: there the command goes to cmd.exe as one line, each word quoted as it needs.
|
|
54
|
+
*/
|
|
55
|
+
export function runAgent(bin, args, { platform = process.platform, execFileImpl = execFile, execImpl = exec } = {}) {
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
const done = (error, stdout, stderr) => resolve({ code: error ? (typeof error.code === 'number' ? error.code : 1) : 0, out: `${stdout ?? ''}${stderr ?? ''}`.trim() });
|
|
58
|
+
if (platform === 'win32') execImpl([bin, ...args].map(winWord).join(' '), { timeout: 60_000, windowsHide: true }, done);
|
|
59
|
+
else execFileImpl(bin, args, { timeout: 60_000 }, done);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The command that starts the server: this checkout's (`local`), or the published package's. On Windows, npx is a
|
|
65
|
+
* .cmd shim that an agent can only start through cmd.exe.
|
|
66
|
+
*/
|
|
67
|
+
export async function serverCommand({ local = false, platform = process.platform } = {}) {
|
|
68
|
+
if (local) return { command: process.execPath, args: [path.join(ROOT, 'bin', 'xsolana.mjs'), 'mcp'] };
|
|
69
|
+
const { version } = JSON.parse(await readFile(path.join(ROOT, 'package.json'), 'utf8'));
|
|
70
|
+
const npx = ['-y', `--package=@xgamesfun/solana@${version}`, 'xsolana', 'mcp'];
|
|
71
|
+
return platform === 'win32' ? { command: 'cmd', args: ['/c', 'npx', ...npx] } : { command: 'npx', args: npx };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* What is in a skill folder: 'none' (nothing there), 'ours' (xsolana's own skill: its SKILL.md names xsolana-economy in its
|
|
76
|
+
* frontmatter, so it may be replaced or removed), or 'other' (anything else, which xsolana never touches).
|
|
77
|
+
*/
|
|
78
|
+
export async function skillFolder(dir) {
|
|
79
|
+
try {
|
|
80
|
+
await stat(dir);
|
|
81
|
+
} catch {
|
|
82
|
+
return 'none';
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
const text = await readFile(path.join(dir, 'SKILL.md'), 'utf8');
|
|
86
|
+
const front = /^---\r?\n([\s\S]*?)\r?\n---/.exec(text)?.[1] ?? '';
|
|
87
|
+
return /^name:\s*["']?xsolana-economy["']?\s*$/m.test(front) ? 'ours' : 'other';
|
|
88
|
+
} catch {
|
|
89
|
+
return 'other';
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Connects (or with `remove`, disconnects) each agent found. Returns an exit code. */
|
|
94
|
+
export async function connect({ agent, local = false, remove = false, dryRun = false, scope = 'user', cwd = process.cwd(), log = (...a) => console.log(...a), run = runAgent } = {}) {
|
|
95
|
+
if (!SCOPES.includes(scope)) {
|
|
96
|
+
log(`No scope called “${scope}” (${SCOPES.join(', ')}).`);
|
|
97
|
+
return 1;
|
|
98
|
+
}
|
|
99
|
+
if (agent && !AGENTS[agent]) {
|
|
100
|
+
log(`No agent called “${agent}” (${Object.keys(AGENTS).join(', ')}).`);
|
|
101
|
+
return 1;
|
|
102
|
+
}
|
|
103
|
+
const ids = agent ? [agent] : Object.keys(AGENTS);
|
|
104
|
+
const { command, args } = await serverCommand({ local });
|
|
105
|
+
let failed = 0;
|
|
106
|
+
let found = 0;
|
|
107
|
+
for (const id of ids) {
|
|
108
|
+
const a = AGENTS[id];
|
|
109
|
+
const version = await run(a.bin, ['--version']);
|
|
110
|
+
if (version.code !== 0) {
|
|
111
|
+
if (agent) log(`${a.name}: not found on this machine.`);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
found++;
|
|
115
|
+
if (!a.scopes.includes(scope)) {
|
|
116
|
+
log(`${a.name}: adds MCP servers only for the whole user (--scope ${a.scopes.join(' or ')}), so it was left as it is. Run \`xsolana connect --agent ${id}\` for that.`);
|
|
117
|
+
if (agent) failed++;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
// Registered already (at some scope): taken out at this scope first. Before an add that may fail (it is at the
|
|
121
|
+
// other scope); for --remove it is the whole point, so whether it worked is told.
|
|
122
|
+
const steps = [];
|
|
123
|
+
const had = await run(a.bin, a.get);
|
|
124
|
+
if (had.code === 0) steps.push({ args: a.remove(scope), optional: true, removal: true });
|
|
125
|
+
if (!remove) steps.push({ args: a.add(command, args, scope) });
|
|
126
|
+
let ok = true;
|
|
127
|
+
let removed = false;
|
|
128
|
+
for (const { args: s, optional, removal } of steps) {
|
|
129
|
+
if (dryRun) {
|
|
130
|
+
log(`${a.bin} ${s.join(' ')}`);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const r = await run(a.bin, s);
|
|
134
|
+
if (removal && r.code === 0) removed = true;
|
|
135
|
+
if (r.code !== 0 && !optional) {
|
|
136
|
+
log(`${a.name}: ${a.bin} ${s.slice(0, 3).join(' ')} failed: ${r.out.split('\n').at(-1)}`);
|
|
137
|
+
failed++;
|
|
138
|
+
ok = false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// The skill: copied into the agent's skills folder for this scope. A folder of that name that is not xsolana's own
|
|
143
|
+
// skill is never replaced or removed.
|
|
144
|
+
const skills = a.skills(scope, cwd);
|
|
145
|
+
const skillDir = path.join(skills, SKILL);
|
|
146
|
+
const there = await skillFolder(skillDir);
|
|
147
|
+
let skillSaid;
|
|
148
|
+
if (there === 'other') {
|
|
149
|
+
log(`${a.name}: ${skillDir} is there already and is not xsolana's ${SKILL} skill, so it was left as it is${remove ? '' : ' and the skill was not copied'}. Move it away, then run \`xsolana connect\` again.`);
|
|
150
|
+
failed++;
|
|
151
|
+
ok = false;
|
|
152
|
+
} else if (remove) {
|
|
153
|
+
if (dryRun) log(there === 'ours' ? `remove ${skillDir}` : `(no skill at ${skillDir} to remove)`);
|
|
154
|
+
else if (there === 'ours') await rm(skillDir, { recursive: true, force: true });
|
|
155
|
+
skillSaid = there === 'ours' ? `; the ${SKILL} skill removed from ${skillDir}` : '';
|
|
156
|
+
} else {
|
|
157
|
+
if (dryRun) log(`${there === 'ours' ? 'replace the xsolana skill at' : 'copy skills/xsolana-economy to'} ${skillDir}`);
|
|
158
|
+
else {
|
|
159
|
+
await mkdir(skills, { recursive: true });
|
|
160
|
+
if (there === 'ours') await rm(skillDir, { recursive: true, force: true });
|
|
161
|
+
await cp(path.join(ROOT, 'skills', SKILL), skillDir, { recursive: true });
|
|
162
|
+
}
|
|
163
|
+
skillSaid = ` and the ${SKILL} skill copied to ${skillDir}`;
|
|
164
|
+
}
|
|
165
|
+
if (dryRun) continue;
|
|
166
|
+
if (remove) {
|
|
167
|
+
// Registered, but not at this scope: it is still there, and the person asked for it gone.
|
|
168
|
+
const stays = had.code === 0 && !removed;
|
|
169
|
+
if (stays) failed++;
|
|
170
|
+
const server = removed ? 'xsolana removed' : stays ? `xsolana’s server was not found at the ${scope} scope, so it was left where it is (\`${a.bin} mcp get ${NAME}\` shows where)` : 'xsolana’s server was not registered';
|
|
171
|
+
log(`${a.name}: ${server}${skillSaid ?? ''}.`);
|
|
172
|
+
} else if (ok) {
|
|
173
|
+
log(`${a.name}: xsolana tools added (${local ? 'from this checkout' : 'from npm'}, ${scope === 'project' ? `for ${cwd} only` : 'for every project'})${skillSaid}. ${a.next}`);
|
|
174
|
+
log(` ${a.plugin}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (!found) {
|
|
178
|
+
log('No coding agent found (Claude Code: npm install -g @anthropic-ai/claude-code; Codex: npm install -g @openai/codex).');
|
|
179
|
+
return 1;
|
|
180
|
+
}
|
|
181
|
+
return failed ? 1 : 0;
|
|
182
|
+
}
|