@xgamesfun/solana 0.24.0 → 0.25.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 +1 -1
- package/NOTICE +4 -4
- package/README.md +100 -52
- package/bin/xsolana.mjs +32 -11
- package/examples/blind-forge.json +2 -4
- package/examples/coin-flip-arena.json +2 -5
- package/examples/duel-arena.json +1 -4
- package/examples/fair-presale.json +1 -3
- package/examples/hero-trials.json +14 -2
- package/examples/ideas.json +788 -788
- package/examples/nft-raffle.json +1 -0
- package/examples/partner-vault.json +19 -3
- package/examples/sacrifice-raffle.json +2 -10
- package/examples/shelter-fair.json +1 -0
- package/examples/stakers-draw.json +4 -7
- package/examples/trade-in-shop.json +2 -3
- package/js/approve/details.mjs +117 -48
- package/js/approve/ed25519.mjs +5 -1
- package/js/approve/live-check.mjs +83 -26
- package/js/approve/page.html +217 -78
- package/js/approve/page.js +43 -15
- package/js/approve/server.mjs +276 -74
- package/js/chain/base58.js +7 -1
- package/js/chain/borsh.js +15 -11
- package/js/chain/bytes.js +11 -8
- package/js/chain/core.js +21 -5
- package/js/chain/curve.js +1 -1
- package/js/chain/lookup.js +118 -36
- package/js/chain/mint.js +6 -2
- package/js/chain/pda.js +2 -2
- package/js/chain/programs.js +6 -6
- package/js/chain/rpc.js +40 -14
- package/js/chain/token.js +13 -2
- package/js/chain/token2022.js +55 -16
- package/js/chain/tx.js +32 -22
- package/js/client/accounts.js +270 -80
- package/js/client/compressed.js +4 -3
- package/js/client/grant.js +165 -40
- package/js/client/idl/loot.js +1 -1
- package/js/client/idl/loot.json +104 -16
- package/js/client/ix.js +2121 -457
- package/js/client/pda.js +20 -18
- package/js/client/policy.js +12 -5
- package/js/client/random.js +2 -2
- package/js/client/sizes.js +220 -65
- package/js/client/traits.js +14 -5
- package/js/connect/connect.mjs +59 -17
- package/js/economy/check.mjs +3884 -690
- package/js/economy/cost.mjs +381 -57
- package/js/economy/examples.mjs +176 -35
- package/js/economy/explain.mjs +468 -110
- package/js/economy/guide.mjs +282 -59
- package/js/economy/legal.mjs +12 -6
- package/js/economy/live.mjs +62 -20
- package/js/economy/model.mjs +320 -97
- package/js/economy/plan.mjs +1022 -203
- package/js/economy/review.mjs +760 -0
- package/js/economy/schema.json +134 -372
- package/js/economy/secrets.mjs +17 -7
- package/js/economy/simulate.mjs +771 -178
- package/js/economy/trust.mjs +15 -0
- package/js/economy/txsize.mjs +15 -3
- package/js/economy/validate.mjs +61 -24
- package/js/index.js +1 -0
- package/js/mcp/server.mjs +43 -15
- package/js/mcp/tools.mjs +318 -80
- package/js/play/index.js +1960 -461
- package/js/play/keys.js +35 -9
- package/js/results/index.js +225 -32
- package/js/results/server.js +230 -0
- package/js/results/wire.js +64 -16
- package/package.json +6 -4
- package/release.json +5 -5
- package/skills/xsolana-economy/SKILL.md +52 -22
- package/skills/xsolana-economy/references/after-launch.md +32 -21
- package/skills/xsolana-economy/references/apps.md +47 -36
- package/skills/xsolana-economy/references/sentence-to-spec.md +149 -129
- package/skills/xsolana-economy/references/telling-the-person.md +15 -6
- package/skills/xsolana-economy/references/trust-and-cheating.md +126 -0
- package/types/chain/core.d.ts +1 -1
- package/types/chain/lookup.d.ts +1 -1
- package/types/client/accounts.d.ts +24 -3
- package/types/client/grant.d.ts +2 -2
- package/types/client/idl/loot.d.ts +22 -0
- package/types/client/ix.d.ts +54 -45
- package/types/client/pda.d.ts +3 -1
- package/types/economy/cost.d.mts +1 -1
- package/types/economy/explain.d.mts +3 -0
- package/types/economy/guide.d.mts +5 -0
- package/types/economy/model.d.mts +48 -3
- package/types/economy/plan.d.mts +44 -2
- package/types/economy/review.d.mts +45 -0
- package/types/economy/trust.d.mts +8 -0
- package/types/index.d.ts +1 -0
- package/types/mcp/tools.d.mts +62 -0
- package/types/play/index.d.ts +14 -25
- package/types/results/index.d.ts +8 -7
- package/types/results/server.d.ts +126 -0
- package/types/results/wire.d.ts +1 -1
package/js/economy/guide.mjs
CHANGED
|
@@ -4,29 +4,108 @@ import { FEES, LAMPORTS_PER_SOL, ORACLE_FEE } from './model.mjs';
|
|
|
4
4
|
import { SCHEMA } from './validate.mjs';
|
|
5
5
|
import { IDL } from '../client/ix.js';
|
|
6
6
|
|
|
7
|
-
const sol =
|
|
7
|
+
const sol = lamports => Number(lamports) / Number(LAMPORTS_PER_SOL);
|
|
8
8
|
|
|
9
9
|
/** Each part: what it is for, who acts at play time and its fee, and the nested shapes worth showing. */
|
|
10
10
|
export const PARTS = {
|
|
11
|
-
tokens: {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
11
|
+
tokens: {
|
|
12
|
+
def: 'token',
|
|
13
|
+
says: 'The currencies: SOL, any existing mint by address (classic or Token-2022, coins from a token launchpad or an AMM included), or a token the game makes when it goes live: one the game mints as its rules say (points, tickets, shards, an emission), or one with a fixed `supply` minted once to the owner with minting turned off for good (a community token you keep). loot does not put a token on a market: for trading, launch it on a token launchpad or an AMM, then name the mint.',
|
|
14
|
+
acts: 'nobody: a token is a fact',
|
|
15
|
+
},
|
|
16
|
+
currency: {
|
|
17
|
+
says: 'The key of the token that is the game’s own currency: what the shop is priced in and what the treasury holds. Fixed once live.',
|
|
18
|
+
},
|
|
19
|
+
collection: {
|
|
20
|
+
def: 'collection',
|
|
21
|
+
says: 'The Metaplex Core collection the project’s collectibles are minted into, made at setup; `false` makes none (a project of tokens only). `collections` adds Core collections your wallet already controls (as many as the project’s setup transaction carries, about 25 in all): an NFT of any of them is the project’s own, and an item can be minted into one with its `collection` (once you add the project as an update delegate of it). `projectNumber` numbers the project among your wallet’s (its address is ["project", owner, number]), so one wallet can run several economies.',
|
|
22
|
+
},
|
|
23
|
+
items: {
|
|
24
|
+
def: 'item',
|
|
25
|
+
nested: ['rule'],
|
|
26
|
+
says: 'What the game sells, drops or makes: collectibles (minted to the player, with attributes), and normal items (kept by the project’s backend). A price puts an item in the shop, sold in one step (`buy`: no order, nothing to settle). `{serial}` in its `uri` is each unit’s serial, so each can have its own art.',
|
|
27
|
+
acts: `the player buys (${sol(FEES.player)} SOL player fee), and gets it in the same transaction (a normal item: the project’s backend delivers it from the receipt)`,
|
|
28
|
+
},
|
|
29
|
+
pools: {
|
|
30
|
+
def: 'pool',
|
|
31
|
+
says: 'Vaults the game’s parts pay from (a prize pool, a buyback reserve), which no player can take from. The authority can also move what a pool holds (`move_tokens`, a payout decision), so players trust it with what is in them; a lottery’s pot and what a farm owes its stakers are vaults it can’t touch (a farm’s rewards land in a vault of the farm’s own). `seed` is what the owner puts in at the start.',
|
|
32
|
+
acts: 'nobody directly: other parts fill and drain them',
|
|
33
|
+
},
|
|
34
|
+
stashes: {
|
|
35
|
+
def: 'stash',
|
|
36
|
+
says: 'NFTs the game holds to give away as prizes, of any collection and any kind (Core with or without the game’s authority, Token Metadata legacy and pNFT, compressed): a partner’s NFTs, a treasury’s rare pieces, one artwork to raffle. Your wallet puts in each NFT it lists when the economy goes live (it must hold them then; a compressed one goes in after, with `loot.proveLeafIx` and `loot.stashIx`). A prize { "gives": "nft", "stash": key, "nft": address } gives that one NFT at once (once: the first winner takes it); a box outcome { "gives": "stash", "stash": key } gives one of them at random, taken out at the roll, so two winners never get the same one; a draw that lands on an empty stash isn’t made, and gives back that draw’s worth. Several stashes with weights are rarity tiers. Give each route its own stash when a shop and a raffle must not empty it for each other.',
|
|
37
|
+
acts: `the owner or the authority puts an NFT in or takes one out (${sol(FEES.project)} SOL); anyone settles a roll that won one`,
|
|
38
|
+
},
|
|
39
|
+
boxes: {
|
|
40
|
+
def: 'box',
|
|
41
|
+
nested: ['outcome', 'odds', 'gate', 'rule', 'effect', 'rise', 'fall', 'split', 'perWalletEach'],
|
|
42
|
+
says: 'A paid (or free) roll on chain with MagicBlock’s verifiable randomness, optionally on an NFT (`rollsOn`: an enchant, a mission’s result, a risky forge), whose outcomes then move its numbers (`shifts`) and send it back, burn it or give it away (`then`; a box that can burn or give it away holds it until the roll settles): each of up to 128 outcomes gives an item, a fixed prize from a pool, tokens minted (`tokens`), an NFT of a stash (picked at random, or a named one: `nft`), a roll of another box (`roll`), up to 14 of these together (`bundle`: { "gives": "bundle", "prizes": [...] }, a pack; it gives all it names or isn’t given, so it names a pool once, a stash’s random NFT beside nothing else of that stash, and two of an item need two left), or nothing. A box of one outcome with nothing random and no NFT it rolls on is bought in one step (`buy`): the price split and what it gives handed over at once, with no order and no oracle. A box takes an NFT as its price through `rollsOn` (which NFTs) and `then` (where the one paid goes: burned, given, or into a stash, a trade-in that becomes another player’s prize). A box is also the prize any route gives: a grant (`gives: { "roll": box }`), a lottery’s place (`{ "percent", "roll" }`), a contest’s winners (`roll`), an auction’s lot (`{ "roll": box }`), a mission’s tier and a recipe roll it for a winner, who gets whatever it gives; such a box is left `active: false`. A roll is on its written odds whatever is in stock: an outcome drawn that can’t be given then makes no draw, no other takes its place, and that draw’s worth of the price goes back at the settle; with no draw made a paid open fails and is refunded, and a free roll goes back to waiting. A sale in one step sells only while its pools hold every fixed prize. A prize won is set aside for its winner at once. An edit of a box’s roll fails only a purchase still waiting (for its refund); any other roll waiting rolls on the box as edited. A gate (a collectible shown, a few wallets, or a list of any size: `{ "list": [address, …] }`, an allowlist whose proof each player passes) and odds can read what a player shows; `opens`/`closes` time a drop; `perWallet`/`perWalletEach` limit each wallet (one ticket per wallet). A split share "chosen" lets the buyer name a referrer; "chosen:<item>" the holder of a collectible they name (plot rent).',
|
|
43
|
+
acts: `the player opens (${sol(FEES.player)} SOL + ${sol(ORACLE_FEE)} SOL to the oracle); its player, a key they named or the project’s keys settle. Chance that pays money is flagged`,
|
|
44
|
+
},
|
|
45
|
+
payments: {
|
|
46
|
+
def: 'payment',
|
|
47
|
+
nested: ['split'],
|
|
48
|
+
says: 'A player pays for something given off chain (an attempt, an entry, a tip), split up to four ways at once. With `any`, the player chooses the amount, at least `amount` (tips, donations). Its split may name one `"chosen"` share, a wallet the payer names each time they pay (a referrer: `app.pay(key, { chosenRecipient })`), never their own; not `"chosen:<item>"`.',
|
|
49
|
+
acts: `the player pays (${sol(FEES.player)} SOL)`,
|
|
50
|
+
},
|
|
51
|
+
recipes: {
|
|
52
|
+
def: 'recipe',
|
|
53
|
+
nested: ['input', 'output'],
|
|
54
|
+
says: 'Collectibles and tokens in, collectibles and tokens out, all or nothing: merges, upgrades, salvage, breeding, exchanges, repairs, a daily claim. `raises` buy a level or a stat on a collectible kept (+1 level for 100 DUST, up to 10), or with `fromNow` move a time (a day of fuel, a day off a lock); a `staked` line takes a collectible only while it is staked in the farm it names (`farm`; a set bound into a badge, its raises moving that farm’s `lockAttribute`), a `show` line takes any NFT of a collection, the project’s or not (its stats live in the project’s Sheet of it), so players can train an NFT the project didn’t make, and a `trade` line takes one and keeps it: burned, given on, or into a stash (`then`), a trade-in. Each `out` line is a prize (the same prize line every route takes: an item, tokens minted or from a pool, a share of a pool, an NFT of a stash, a roll of a box), given in the craft; `{ "gives": "roll", "box": key, "on": place }` rolls on a collectible it keeps (a risky upgrade).',
|
|
55
|
+
acts: `the player crafts (${sol(FEES.player)} SOL)`,
|
|
56
|
+
},
|
|
57
|
+
schedules: {
|
|
58
|
+
def: 'schedule',
|
|
59
|
+
says: 'On a timer: mint a game token into a pool, burn from a pool, move between pools, or pay a wallet, an account, a collectible’s holder, a lottery’s pot ("lottery:<key>") or a farm’s rewards ("farm:<key>"). `start` sets the first run (a cliff), `maxRuns` the last (a vesting of 12 monthly runs), `halveEvery` halves it every N runs; halving with `maxRuns` caps the total for good (an emission with a fixed most). A run missed is missed: one sent late pays once, for the latest run due, and the runs before it lapse; only a sealed schedule’s fixed amount pays every run due. `sealed` (with `fund`): it pays a wallet, the treasury or a burn from a vault of its own that nobody, the owner included, can change, stop or take back; its last run pays what is left.',
|
|
60
|
+
acts: 'the project’s keys, or who it pays, run it when it is due (a sealed one needs nobody else)',
|
|
61
|
+
},
|
|
62
|
+
lotteries: {
|
|
63
|
+
def: 'lottery',
|
|
64
|
+
nested: ['gate', 'rule', 'split'],
|
|
65
|
+
says: 'Tickets sold in rounds, a draw on a timer by the oracle, up to 64 runs of places (a place with `times` is a run of places alike: { "percent": 1, "times": 100 } is a hundred). It names where its places are paid from, exactly one of two, never a default: `"pot": 0` (a number 0 to 15) keeps a pot of its own from round to round (nothing but its winners’ payouts takes from it): each place takes its percent of what the pot holds when the draw is asked, with the round’s ticket money, so `seed`, a sponsor, a share of other sales ("lottery:<key>"), yield and what earlier rounds’ places left all count, and the next round always starts with what the round before left; or `"rest": "<destination>"` pays each round from its own ticket money alone, which nothing from outside can change (no seed, no gifts, no carry-over), and sends what its places leave there ("treasury", "burn", "wallet:<address>", "holder:<collectible>", "account:<token account>", a pool, a farm or another lottery; never "chosen"; a pool, farm or lottery named here counts the leftover only once someone pulls it). A no-loss lottery needs a pot. `split` sends part of ticket money elsewhere first (a charity, a referrer the buyer names with "chosen", the treasury). Leave out `every` for a raffle that draws once and ends. `perWallet` caps each wallet’s tickets in a round. A place can win a prize instead of (or as well as) its percent: `{ "percent": 0, "prize": { "gives": "nft", "stash": key, "nft": address } }` raffles one NFT, which its winner pulls after the draw (if that NFT is gone from its stash, a stranger’s pull is refused: the winner or a project key passes it over); a lottery that draws once with `nfts` (put in its own stash at go-live) and places { "prize": { "gives": "raffled" }, "times": n } raffles many, each winner drawing one at random; `{ "gives": "roll", "box": key }` makes it a random prize. `ticketNft` ({ "rule", "then" }) makes a ticket cost one NFT instead of tokens: held until the draw, then burned, given on or stashed (back to its buyer if the draw is cancelled; no price, no split). The draw’s ask fixes the round’s money and opens the next round at once; then anyone pulls each part (a refund, a place, a share) in any order, and the last pull closes the round. The owner puts SOL in the lottery at go-live for its rounds’ records and every ticket’s entry (`roundRent`, on by default and sized from `expect`: rounds at once and purchases a round; back at each close), so buyers pay no program rent; 0 turns it off, and then a round’s first buyer fronts its record and each buyer pays their entry’s bytes. `noLoss: true` is a no-loss lottery: every ticket (tokens or an NFT) goes back to its buyer once the round is drawn, and the winners take only the pot, what is paid into it (`seed`, a sponsor, a share of other sales to "lottery:<key>"); no split, no cut, no `ticketCurrency`. Tickets are not weighted by time: someone may buy most of a round’s tickets right before the draw and risk only the fees.',
|
|
66
|
+
acts: `players buy tickets (${sol(FEES.player)} SOL); anyone draws and pays (free). Flagged as chance`,
|
|
67
|
+
},
|
|
68
|
+
farms: {
|
|
69
|
+
def: 'farm',
|
|
70
|
+
nested: ['tier', 'rule', 'effect', 'growth'],
|
|
71
|
+
says: 'Staking: players stake a token or NFTs of any kind (the game’s, or any collection its gate names: Core, Token Metadata legacy and pNFT, compressed) and share what lands in the farm’s own rewards vault by weight (`rewards` names its token; fund it with `seed`, a share "farm:<key>" or a schedule that pays it; what lands is released to the seats over the farm’s longest lock, a day at least, so a stake earns only while it stays and nobody takes what was funded before them at once), or earn a fixed `rate` a day each (minted, or from the vault while it lasts, as most NFT staking pays; `shared` makes it the whole farm’s a day, shared by weight, and `halveEvery` halves it), with lock tiers (x1 to x11) and up to 32 boosts from what a collectible says. `growth` writes points (and a level, from `levels`) on a staked collectible for each day in, at its tier and boosts, with nobody’s signature: on the NFT when the game has authority over it, in its Sheet otherwise; a boost per level, a gate, box odds, a recipe’s need or a grant then read that level. A boost counts a level from when it is written, by a seat’s refresh, so a farm whose boost reads what its own growth writes should have its page or a keeper refresh seats (anyone may, for free). A tier’s `prize` (a prize line, as everywhere; it needs a `lock` above 0) makes it a mission: a collectible that stays its lock (topped up, not forfeited) leaves with it, given as it leaves: tokens (minted or from a pool, grown by its boosts), an item, an NFT of a stash, or a chest (`{ "gives": "roll", "box": key }`, settled after the oracle). A tier’s `roll` of a box with `rollsOn` is a roll on the NFT leaving it (success, a stat lost, the NFT lost). A collectible can also boost a token seat. `growth` takes up to 32 (+1 "level" and +2 "affection" a day; without `levels` the points are the level; up to 128 levels each), `gate` up to 32 rules an NFT must all pass, and `runs` ends a farm that long after it goes live (a mission open for a week): it takes no stake at its end or after it, nor one whose lock would run past it, and in a farm that shares what lands, what is not released by the end stays in its vault, as does what a seat forfeits after it, so fund it at least its release span before the end. A tier’s `earnsFor` is a term: a seat earns and grows that long after its lock started, and what it earned by then is exact however late it is next touched: nobody has to act at its end. A farm with a term or an upkeep that pays keeps a record of those times: each new one costs whoever writes it 44 bytes of rent (about 0.00022 SOL), not refunded on leaving, and a farm idle past very many of them is walked in parts first (`app.walkFarm`, anyone’s, free). A `rate` of 0 pays nothing: a mission that only grows or gives prizes. Escrow is the default (the game holds the NFT); Freeze locks it in the wallet, with release dependent on Core handling later issuer metadata changes; Watch leaves it free, earning only while its staker holds it, so one NFT can sit in a farm and its missions at once. Soulbound collectibles require Freeze. A farm of collectibles can keep two times on the NFT, as numbers recipes and rolls move: `lockAttribute` (its lock, written as the time it ends: a recipe that raises it by "-1d" `fromNow` for a price is a way out early, a roll that adds "1d" is jail) and `upkeepAttribute` (fuel: it earns and grows only until that time; a recipe sells it "+1d" `fromNow`, up to `upTo` ahead, and it runs out by itself). A recipe reaches a collectible staked in escrow on its seat. Never edited once open: a new mission is a new farm, added to the spec and published.',
|
|
72
|
+
acts: `the player stakes, unstakes and harvests (${sol(FEES.player)} SOL each); pause and suspend do not stop an unstake; external custody controls still apply`,
|
|
73
|
+
},
|
|
74
|
+
contests: {
|
|
75
|
+
def: 'contest',
|
|
76
|
+
nested: ['split', 'gate', 'rule'],
|
|
77
|
+
says: 'Players back a side; the side that wins shares the pot by stake, after the rake, as the pot stands when the side is named: each winner’s part is written then, whatever order they claim in, and anything sent to the pot after it goes to nobody (burned at the close). The authority names it, or a `deciders` key (a results server) signs it, so the authority’s key never sits on a server. One side and a `minPot` is an all-or-nothing crowdfund (short of it, everyone takes their money back); a wallet gate and a `stake` is a 1v1 wager. A contest whose result is overdue is off and pays everyone back. `prize` gives every winning entry a prize too, when it is claimed (a collectible, tokens, an NFT, a roll of a box); one scarce prize goes to the entry claimed first. `stakeNft` (a rule) makes each entry stake one NFT instead of tokens: the NFTs are the pot, each winner takes its own back and picks its share of the losers’ (`losersEach` caps it: 1 for “winner takes one”), and the opener gets what the winners leave.',
|
|
78
|
+
acts: `the authority opens and decides (${sol(FEES.project)} SOL each); players enter (${sol(FEES.player)} SOL). Flagged as betting`,
|
|
79
|
+
},
|
|
80
|
+
auctions: {
|
|
81
|
+
def: 'auction',
|
|
82
|
+
nested: ['split', 'rule'],
|
|
83
|
+
says: 'An English auction, opened when the economy goes live: the highest bid when the time runs out wins one new unit of an item, any Core NFT the owner holds (give its `collection` when it isn’t the game’s), or a roll of one of the game’s boxes (`{ "roll": box }`, a mystery lot; its price goes to the treasury). Bids in any token, SOL included; each reaches the reserve and tops the last by the least raise; the bid it tops goes back to its bidder at once. A late bid moves the end. With no bid the lot goes home; an auction nobody ends waits, and still sells to its high bidder whenever someone ends it. A seller of an NFT whose account can’t take the money doesn’t hold up the winner: the lot goes, and the auction stays open owing it until a later end pays and closes it. `bidNfts` (a rule) makes a bid a bundle of up to three Core or Token Metadata NFTs instead of tokens (never compressed ones: a bid hands the one it tops back at once, and a compressed NFT’s tree can be changed under it): the most NFTs wins, and the reserve counts NFTs.',
|
|
84
|
+
acts: `players bid (${sol(FEES.player)} SOL each); anyone ends it once it is over (free)`,
|
|
85
|
+
},
|
|
86
|
+
holds: {
|
|
87
|
+
def: 'hold',
|
|
88
|
+
says: 'A player hands a collectible to the project for a time they choose (`for`), or until a set time (`until`: a raid at 18:00); until it is up the authority says where it goes, after it anyone sends it home. A hold’s address names its end, so a late award never lands on a later hold of the NFT.',
|
|
89
|
+
acts: `the player hands it in (${sol(FEES.player)} SOL); the authority releases it (${sol(FEES.project)} SOL)`,
|
|
90
|
+
},
|
|
91
|
+
grants: {
|
|
92
|
+
def: 'grant',
|
|
93
|
+
nested: ['gate', 'rule', 'perWalletEach'],
|
|
94
|
+
says: 'Rewards handed out to whoever earned them: a `prize` given in the claim itself (a collectible, tokens minted or from a pool, one NFT of a stash), or a roll of a box (`{ "gives": "roll", "box": key }`: a quest chest, a daily spin, the grant’s funds paying its order). A claim of a prize it can’t give is refused, and keeps its round. Only the winning wallet claims: it signs every claim, whatever proves it won (a voucher, a list, an open grant); anyone may pay its fees, nobody claims for it. A voucher key (a results server: the results server, or one you run) signs who earned it, or one of `keys` (one per promo code: the page signs with the code’s key, and `perKey: 1` pays each code once), or the wallet is on a list. With none of them it is open: any wallet claims within its limits (with a gate, each collectible shown claims once too). The program keeps every claim within its limits per wallet (in all and per window), per person, per key, per day and in total. A grant of a fixed amount of tokens paid by a voucher or a list pays each claim the amount the voucher (or the list’s leaf) names, up to the grant’s own: one grant for rewards by rank or share (1st 500, 2nd 300, …), with the grant’s amount as the most any claim takes; 0 or none named pays the grant’s amount.',
|
|
95
|
+
acts: `anyone sends a claim and pays the ${sol(FEES.player)} SOL player fee; the grant’s funds pay its rent, and the sender what they can’t`,
|
|
96
|
+
},
|
|
97
|
+
decisions: {
|
|
98
|
+
def: 'decision',
|
|
99
|
+
says: 'The authority’s rules in words: what only the game knows (who cleared the level, who won) and what the program then does (a payout, a burn, a mint, a collectible made, rewritten or burned, a held collectible released, a roll).',
|
|
100
|
+
acts: `the authority signs each one (${sol(FEES.project)} SOL)`,
|
|
101
|
+
},
|
|
102
|
+
keys: {
|
|
103
|
+
says: '`authority`: "owner" (the owner’s wallet is both keys) or another wallet’s public address (a server’s). Never a private key.',
|
|
104
|
+
},
|
|
105
|
+
scenario: {
|
|
106
|
+
def: 'scenario',
|
|
107
|
+
says: 'What the quick simulation assumes: how many of each thing a day ("box:flip": 1000), days, how much is staked in each farm.',
|
|
108
|
+
},
|
|
30
109
|
};
|
|
31
110
|
|
|
32
111
|
export const CONVENTIONS = [
|
|
@@ -53,15 +132,27 @@ export const CONVENTIONS = [
|
|
|
53
132
|
* then offer the nearest versions with their numbers, best first, and build the one the person picks. A feature that
|
|
54
133
|
* ships later moves its entry to HOW.
|
|
55
134
|
*/
|
|
135
|
+
/**
|
|
136
|
+
* What no code removes, only makes smaller: a wallet is not a person, the page can't be trusted, and bots play well.
|
|
137
|
+
* Said once in the guide, so every design starts from it.
|
|
138
|
+
*/
|
|
139
|
+
export const SAFETY = [
|
|
140
|
+
'A wallet is not a person: wallets are free, so a limit per wallet never limits a person. What bounds a reward is its totals ("perDay", "total": the most it can ever pay), and what makes farming cost something: a gate on holding a collectible the game sells or a locked stake, or vouchers from your server that count people ("perPerson": the voucher signer names a person hash, which needs an outside sign-in such as X; a wallet-only player has none).',
|
|
141
|
+
'Open grants (no voucher, keys or list) pay any wallet: keep their prizes small or soulbound, gate them, and set "perDay". `xsolana_simulate` says what a crowd of wallets could take next to one.',
|
|
142
|
+
'The page can’t be trusted: a game runs in the player’s browser, so a result worth paying is decided or checked by your server, which signs the voucher. A server checks what a wallet holds with `collectiblesOf` / `holds`, pays only wallets a player proved (`proofRequest` / `verifyProof`), and makes promo codes with `makeCodes` (80 bits: a code’s key is public, so a short code can be guessed offline) and `codeAddresses` for the grant’s `keys` (all from `@xgamesfun/solana/results`). How far a signed result can be trusted is what the server checks: it decides the result (runs the game or match; the page sends only inputs), replays it (re-runs the page’s inputs from randomness it keeps), or only bounds it (pace, order, caps: a script at human speed gets through). Say which on each voucher grant (`"trust"`), and run xsolana_review before going live: it says what a cheater gets on every route.',
|
|
143
|
+
'Bots play well: even a verified result pays a bot what it pays a good player. Keep token rewards for results small and capped (a grant’s limits), give SOL prizes only by chance (the oracle) or by what your server decides, and don’t let a minted farm rate run without end into what its token buys (it compounds: give it an end, a term or an upkeep).',
|
|
144
|
+
'A gate costs a sybil most when it asks for something the game sells: a rule takes any of several items (`{ "items": ["oak", "pine"] }`, up to 16), and can read the attributes only the program writes on every minted collectible, `item_id`, `rarity`, `serial` and `made` (`{ "attribute": "rarity", "atLeast": 2 }`: any of the game’s Rare or better).',
|
|
145
|
+
];
|
|
146
|
+
|
|
56
147
|
export const CANNOT = [
|
|
57
148
|
{
|
|
58
149
|
what: 'a prize that is a share of what a pool holds (a jackpot paying a tenth of itself)',
|
|
59
150
|
words: 'jackpot grows growing share percent of the pool tenth prize progressive pot winner takes share of pool',
|
|
60
151
|
why: 'a prize is what its record says, all of it or none, on every route: never what an account holds at the time',
|
|
61
152
|
nearest: [
|
|
62
|
-
'a lottery: its pot is what its tickets fill, it grows round by round, and its places take percents of it; gives up a win at any time for a draw on a timer',
|
|
153
|
+
'a lottery with a pot ("pot": 0): its pot is what its tickets fill, it grows round by round with what its places leave, and its places take percents of it; gives up a win at any time for a draw on a timer',
|
|
63
154
|
'a fixed prize from a pool a share of each sale fills (`{ "gives": "pay", "pool": key, "amount": n }`); gives up a prize that grows, and a draw that lands on it while the pool is short gives back its worth',
|
|
64
|
-
|
|
155
|
+
"a voucher grant that names each claim's own amount, up to the grant's, worked out by the project's server from what the pool holds; gives up the rule living on chain",
|
|
65
156
|
],
|
|
66
157
|
},
|
|
67
158
|
{
|
|
@@ -90,7 +181,10 @@ export const CANNOT = [
|
|
|
90
181
|
what: 'a sealed-bid auction (nobody sees the bids until the end), a second-price (Vickrey) auction, or one decided by a random draw per block',
|
|
91
182
|
words: 'sealed blind vickrey second-price hidden',
|
|
92
183
|
why: 'every bid is a public transaction',
|
|
93
|
-
nearest: [
|
|
184
|
+
nearest: [
|
|
185
|
+
'an open English auction (`auctions`: every bid public, the highest wins, the outbid refunded at once)',
|
|
186
|
+
'a shop listing whose price falls with time to a floor (a Dutch-style sale)',
|
|
187
|
+
],
|
|
94
188
|
},
|
|
95
189
|
{
|
|
96
190
|
what: 'lending against tokens, liquidation by price, leverage, perpetuals, options or yield vaults',
|
|
@@ -106,7 +200,10 @@ export const CANNOT = [
|
|
|
106
200
|
what: 'liquid staking receipts or transferable share tokens of a vault',
|
|
107
201
|
words: 'liquid lst xtoken transferable-stake',
|
|
108
202
|
why: 'a token farm’s seat belongs to a wallet and isn’t transferable while staked',
|
|
109
|
-
nearest: [
|
|
203
|
+
nearest: [
|
|
204
|
+
'for NFTs, a farm with `custody: "watch"`: the NFT stays free in the wallet, earns while held, can be sold, and its next holder takes the seat over',
|
|
205
|
+
'a token farm with lock tiers and boosts (the stake stays with its wallet)',
|
|
206
|
+
],
|
|
110
207
|
},
|
|
111
208
|
{
|
|
112
209
|
what: 'paying every holder of a collection without any step by the holder',
|
|
@@ -131,7 +228,10 @@ export const CANNOT = [
|
|
|
131
228
|
what: 'a treasury that needs several people to sign (a multisig)',
|
|
132
229
|
words: 'multisig multi-sig 3 of 5 several signers officers council squads dao treasury approval',
|
|
133
230
|
why: 'loot isn’t a multisig, but any wallet can receive and any key can be the spending key',
|
|
134
|
-
nearest: [
|
|
231
|
+
nearest: [
|
|
232
|
+
'payments and sales go to a multisig vault: `"treasury": "wallet:<vault>"` or a `wallet:<vault>` share; spending from it needs the vault’s approvals in the multisig’s own app',
|
|
233
|
+
'the multisig vault as `keys.authority`, so moving the game’s pools needs its approvals (sent from the multisig’s own app, not the approve page)',
|
|
234
|
+
],
|
|
135
235
|
},
|
|
136
236
|
{
|
|
137
237
|
what: 'a ranking the chain works out by itself (the week’s top tipper, a leaderboard)',
|
|
@@ -149,7 +249,12 @@ export const CANNOT = [
|
|
|
149
249
|
why: 'every move from a player is signed by that player',
|
|
150
250
|
nearest: ['a page where each member pays (a payment per week), and a list of who paid from its receipts'],
|
|
151
251
|
},
|
|
152
|
-
{
|
|
252
|
+
{
|
|
253
|
+
what: 'mainnet',
|
|
254
|
+
words: 'mainnet real money production',
|
|
255
|
+
why: 'the program isn’t audited yet',
|
|
256
|
+
nearest: ['devnet now; mainnet after the audit'],
|
|
257
|
+
},
|
|
153
258
|
];
|
|
154
259
|
|
|
155
260
|
/**
|
|
@@ -157,45 +262,144 @@ export const CANNOT = [
|
|
|
157
262
|
* person's words match.
|
|
158
263
|
*/
|
|
159
264
|
export const HOW = [
|
|
160
|
-
{
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
{
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
{
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
{
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
265
|
+
{
|
|
266
|
+
what: 'a pity counter that resets on a rare and can’t be traded (gacha pity, a guaranteed legendary by the 90th pull)',
|
|
267
|
+
words: 'pity counter guaranteed hard pity soft pity gacha resets non-transferable soulbound charm',
|
|
268
|
+
how: 'a soulbound collectible each player holds (a charm: `"soulbound": true`, free or cheap, `perWallet` 1) that the pull box rolls on (`"rollsOn": { "item": "charm" }, "then": "back"`): each miss shifts its "pity" attribute by +1 (`shifts`, `upTo` the cap), the rare shifts it by -1000000 (a number never goes below 0, so it resets), and the rare’s `odds` add a percent per point of "pity". It counts per charm, never trades, and resets on a rare; a pity token (a token minted on each miss, spent in a recipe) can be sold and never resets. One pull at a time per charm (the box holds it until the roll settles)',
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
what: 'an NFT raffle (a seller raffles one NFT; the winner gets it, the seller the ticket money)',
|
|
272
|
+
words: 'raffle nft raffle sell tickets winner gets nft one-of-one',
|
|
273
|
+
how: 'a lottery with `"raffle": "<NFT address>"` and `"rest": "wallet:<seller>"` (the ticket money, less any cut, to the seller) (or a first place `{ "prize": { "gives": "nft", "nft": "<address>", "held": true } }`): the NFT waits in the lottery’s own stash, which nothing but the draw can empty; the winner pulls it after the draw; `minSold` cancels and refunds a round that sold fewer. A seller runs it as the owner of a project of their own (`app.raffle`)',
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
what: 'rent an NFT out (a cabin, a club, a hero for a scholar)',
|
|
277
|
+
words: 'rent rental renter lease borrow use scholarship cabin club hero lend for a week',
|
|
278
|
+
how: 'a listing for rent: `openListingIx` with `deal: { Rent: { price, lasts, max_periods } }` (`app.listForRent`); a renter pays by the period (`app.rentListing`) and is its user until the end; nobody ends it early; the game reads the renter with one account read (`app.rentedTo(asset)`). On-chain gates don’t read rentals: the game checks them',
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
what: 'a presale or fair launch: a token split by what each put in',
|
|
282
|
+
words: 'presale fair launch pro rata proportional raise contribute allocation by amount crowdsale ido',
|
|
283
|
+
how: 'a contest with one side, a `minPot`, a split of 100% to the treasury, and a token `prize` with `"split": true`: the whole prize is set aside when it is decided, and each backer claims their part in proportion to what they put in; short of `minPot`, everyone is refunded',
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
what: 'vesting nobody can take back (a team’s, an investor’s unlock, a sponsor’s prize stream)',
|
|
287
|
+
words: 'vesting vest unlock cliff team allocation investor locked tokens sealed escrow stream streamflow',
|
|
288
|
+
how: 'a schedule with `"sealed": true`, `"does": "pay"`, `fund`, `start` (the cliff), `every` and `maxRuns`: it pays from a vault of its own that nobody, the owner included, can change, stop or empty; the recipient runs it whenever they like and catches up; its last run pays what is left',
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
what: 'leave a stake early for a fee, compounding',
|
|
292
|
+
words: 'early exit unstake penalty exit fee paper hands compound auto-compound',
|
|
293
|
+
how: 'a farm of tokens: a tier’s `exitFee` (percent left to `exitFeeTo`: a burn, a pool, or "rewards", its own vault, shared by the stakers who stay; leaving early keeps the rewards it was credited), and `app.compound(farm)` when rewards are the staked token (the lock keeps running). A farm of collectibles leaves early by a recipe that lowers its `lockAttribute`. loot has no unbonding wait and no harvest tax',
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
what: 'different amounts per wallet from a list (points to tokens, monthly vesting by list)',
|
|
297
|
+
words:
|
|
298
|
+
'different amounts points-to-tokens conversion allocation weighted-airdrop pro-rata proportional list amounts monthly',
|
|
299
|
+
how: 'a grant’s `list` entries `{ "wallet", "amount", "round" }`: each claims its own amount (up to the grant’s); the same wallet under another `round` claims again',
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
what: 'stake only to qualify (launchpad tiers, stake to enter)',
|
|
303
|
+
words: 'stake to qualify launchpad tier tiers stake gate no rewards stake-to-enter staked qualify allocation',
|
|
304
|
+
how: 'a farm of tokens that nothing funds (no `seed`, no share or schedule sends it anything) and a locked tier, and a gate `{ "staked": { "farm": key, "atLeast": n } }` on a box, item, lottery, contest, auction or grant (counted while its lock runs)',
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
what: 'a referral link: a different referrer paid on each sale',
|
|
308
|
+
words: 'referral referrer refer affiliate invite link commission streamer creator-pick',
|
|
309
|
+
how: 'a box’s, shop item’s or lottery’s split share `{ "to": "chosen", "percent": 10 }`: the buyer’s page passes the referrer from the link (`app.buy(key, { listing, chosenRecipient: referrer })`), paid in the same transaction; no server (with none named the share goes to the treasury, so the page makes the choice required). A payment’s split takes a `chosen` share too (`app.pay(key, { chosenRecipient: referrer })`), a wallet only',
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
what: 'a share to whoever holds the collectible the buyer picks (rent to the plot built on, a stud fee to the stallion’s owner)',
|
|
313
|
+
words: 'plot land rent landlord stud stallion owner whichever holder per sale',
|
|
314
|
+
how: 'a split share `{ "to": "chosen:<item key>", "percent": 10 }`: the buyer names a collectible of that item and whoever holds it then is paid, in the same transaction, after any resale (the sender opens their account if it is missing). The buyer must name one a player holds: a purchase naming none, or one burned or held by the project (staked in escrow, held, up for auction), is refused (BadChoice), so the page offers only collectibles players hold. Its part goes to the treasury only when the holder can’t take it by the time an order settles (staked in escrow since, or their account frozen)',
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
what: 'an all-or-nothing crowdfund with refunds',
|
|
318
|
+
words: 'crowdfund crowdfunding goal kickstarter all-or-nothing pledge backers refund',
|
|
319
|
+
how: 'a contest with one side, a `minPot` (the goal) and a split of 100% to whoever it funds: short of the goal when entries close, every backer claims their pledge back; a contest `prize` gives each backer a badge',
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
what: 'money held until someone approves, refunded if nobody does by a deadline (a bounty paid when the work is accepted, a paid DM held until it is answered, a deposit)',
|
|
323
|
+
words: 'escrow held until approve approved accepted refund deadline bounty deposit answered released',
|
|
324
|
+
how: 'a contest with one side, opened for each case, whose split sends the pot to the receiver (`wallet:<address>`, less any fee), decided by the approver (the authority, or a `deciders` key); `resultDue` is the deadline: after it the contest is off by itself and every entry takes its money back (anyone claims it). The receiver is fixed when it opens',
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
what: 'limits per wallet: one each, or N a day each',
|
|
328
|
+
words: 'one-per-wallet per-wallet once-a-day daily-limit max-per-wallet one each',
|
|
329
|
+
how: '`perWallet` (in all) and `perWalletEach` (`{ "count": 1, "every": "1d" }`) on items, boxes, recipes and grants (a grant’s list pays each listed entry once: list a wallet again under another `round` to pay it again)',
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
what: 'a falling price on its own clock, however many sell (a Dutch drop)',
|
|
333
|
+
words: 'dutch falling price drop every minute goes down fixed clock',
|
|
334
|
+
how: 'a listing’s `fall` (`step`, `every`, `floor`) with `"clock": "shared"`',
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
what: 'promo codes and QR claims without a server',
|
|
338
|
+
words: 'promo code coupon redeem qr secret code flyer',
|
|
339
|
+
how: 'a grant with `keys` (one keypair per code, made offline; only public keys go in the spec) and `limits.perKey: 1`: the page derives the key from the typed code and signs for the player’s own wallet. A code used at every visit (one QR at the register) needs a rotating code or a results server',
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
what: 'renewing or extending a pass',
|
|
343
|
+
words: 'renew renewal extend membership monthly pass valid until expires',
|
|
344
|
+
how: 'a time attribute on the pass raised by a recipe (`raises`: `{ "key": "paid_until", "by": "30d", "fromNow": true }` with a `pay` line), read by gates with `"time": "ahead"`. An item’s `life.lasts` is read only by recipes, never by gates',
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
what: 'a limited number of uses for something a box rolls on (breed a mare 3 times)',
|
|
348
|
+
words: 'breed limit times uses breeding-count three times',
|
|
349
|
+
how: 'a recipe that `use`s the NFT (`life.uses`, `cooldown`: the kit writes them into each recipe that takes the item, as raises of a count of uses and of a time of next use written on the NFT) and gives a permit item; the box `rollsOn` the permit (`then: "burn"`). A box never counts `uses` itself',
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
what: 'rewards by rank or by points from one grant',
|
|
353
|
+
words: 'rank ranks top-10 first-second-third leaderboard-payout points-to-tokens',
|
|
354
|
+
how: 'a grant of a fixed amount of tokens paid by vouchers: each voucher names its claim’s amount, up to the grant’s',
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
what: 'NFT badges or an airdrop for thousands of wallets without rent for each one (POAPs, attendance, cNFTs)',
|
|
358
|
+
words: 'compressed cnft cnfts bubblegum poap attendance badge badges airdrop thousands cheap mint rent-free',
|
|
359
|
+
how: 'a prize `{ "gives": "item", "item": key, "compressed": true }` (on a grant, a lottery place, a contest, a mission or a recipe): a compressed NFT of the collectible, minted into the project’s tree (`compressedTree.holds`, 16,384 by default; its rent, about 0.23 SOL, paid once at the setup). The setup’s collection takes them when the spec has one before it goes live; not soulbound, not in a box',
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
what: 'a no-loss prize draw',
|
|
363
|
+
words: 'no-loss lossless prize-savings never-lose',
|
|
364
|
+
how: 'a lottery with `"noLoss": true` and `"pot": 0`: tickets come back after the draw; the pot is the seed plus what is paid into it (`lottery:<key>` in a split, or a schedule that pays it)',
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
what: 'a pack of cards from one purchase (a booster of five, a guaranteed rare, a hand of different cards)',
|
|
368
|
+
words:
|
|
369
|
+
'pack packs booster five cards ten cards guaranteed rare hand deck several items one open gacha pull multi-pull',
|
|
370
|
+
how: 'a box with `"slots"`: each slot so many draws among the outcomes it names (all, when it names none), `"distinct": true` for no outcome twice: `[{ "count": 4, "share": 60 }, { "count": 1, "from": [8, 9, 10], "share": 40 }]` is four cards and a rare. Slots that name outcomes each say what they are worth (`share`, 100 together): a draw that can’t be made gives that much back. All its cards come from one purchase and one oracle answer, and the player may open several packs at once: one wallet prompt. Its order holds a collectible’s rent for each card until it settles; the player or a key they named settles it, in parts when the cards don’t fit one transaction (14 Core cards a part).',
|
|
371
|
+
},
|
|
180
372
|
];
|
|
181
373
|
|
|
182
374
|
function typeOf(node) {
|
|
183
375
|
if (!node) return 'any';
|
|
184
376
|
if (node.$ref) return node.$ref.split('/').pop();
|
|
185
377
|
if (node.const !== undefined) return JSON.stringify(node.const);
|
|
186
|
-
if (node.enum) return node.enum.map(
|
|
378
|
+
if (node.enum) return node.enum.map(v => JSON.stringify(v)).join(' | ');
|
|
187
379
|
if (node.anyOf ?? node.oneOf) return (node.anyOf ?? node.oneOf).map(typeOf).join(' | ');
|
|
188
|
-
if (node.type === 'object' && node.properties)
|
|
189
|
-
|
|
190
|
-
|
|
380
|
+
if (node.type === 'object' && node.properties)
|
|
381
|
+
return `{ ${Object.keys(node.properties)
|
|
382
|
+
.map(k => (node.required?.includes(k) ? k : `${k}?`))
|
|
383
|
+
.join(', ')} }`;
|
|
384
|
+
if (node.type === 'array')
|
|
385
|
+
return `list of ${typeOf(node.items)}${node.maxItems ? ` (at most ${node.maxItems})` : ''}`;
|
|
386
|
+
if (node.type === 'integer' || node.type === 'number')
|
|
387
|
+
return `${node.type}${node.minimum !== undefined || node.maximum !== undefined ? ` ${node.minimum ?? ''}..${node.maximum ?? ''}` : ''}`;
|
|
191
388
|
return node.type ?? 'object';
|
|
192
389
|
}
|
|
193
390
|
|
|
194
391
|
function fieldsOf(def) {
|
|
195
392
|
const required = new Set(def.required ?? []);
|
|
196
|
-
return Object.entries(def.properties ?? {}).map(([name, p]) => ({
|
|
393
|
+
return Object.entries(def.properties ?? {}).map(([name, p]) => ({
|
|
394
|
+
name,
|
|
395
|
+
type: typeOf(p),
|
|
396
|
+
...(required.has(name) ? { required: true } : {}),
|
|
397
|
+
...(p.default !== undefined ? { default: p.default } : {}),
|
|
398
|
+
...(p.description ? { says: p.description } : {}),
|
|
399
|
+
}));
|
|
197
400
|
}
|
|
198
|
-
const fieldLine =
|
|
401
|
+
const fieldLine = f =>
|
|
402
|
+
` ${f.name}${f.required ? '*' : ''}: ${f.type}${f.default !== undefined ? ` (default ${JSON.stringify(f.default)})` : ''}${f.says ? ` — ${f.says}` : ''}`;
|
|
199
403
|
|
|
200
404
|
/** The parts (one, or all) with their fields from the schema. */
|
|
201
405
|
export function catalog(only) {
|
|
@@ -205,8 +409,19 @@ export function catalog(only) {
|
|
|
205
409
|
.map(([name, p]) => {
|
|
206
410
|
const node = SCHEMA.properties[name];
|
|
207
411
|
const def = p.def ? defs[p.def] : node;
|
|
208
|
-
const nested = (only ? p.nested ?? [] : []).map(
|
|
209
|
-
|
|
412
|
+
const nested = (only ? (p.nested ?? []) : []).map(n => ({
|
|
413
|
+
name: n,
|
|
414
|
+
says: defs[n].description ?? '',
|
|
415
|
+
fields: defs[n].type === 'array' ? [{ name: '(list)', type: typeOf(defs[n]) }] : fieldsOf(defs[n]),
|
|
416
|
+
}));
|
|
417
|
+
return {
|
|
418
|
+
part: name,
|
|
419
|
+
type: typeOf(node),
|
|
420
|
+
says: p.says,
|
|
421
|
+
...(p.acts ? { atPlayTime: p.acts } : {}),
|
|
422
|
+
fields: def?.properties ? fieldsOf(def) : [],
|
|
423
|
+
...(nested.length ? { nested } : {}),
|
|
424
|
+
};
|
|
210
425
|
});
|
|
211
426
|
}
|
|
212
427
|
|
|
@@ -214,17 +429,25 @@ export function catalog(only) {
|
|
|
214
429
|
export function guideText(only) {
|
|
215
430
|
const parts = catalog(only);
|
|
216
431
|
return [
|
|
217
|
-
only
|
|
218
|
-
|
|
432
|
+
only
|
|
433
|
+
? `The part “${only}” of a loot economy (* = required):`
|
|
434
|
+
: `A loot economy is one JSON spec (xsolana.json) on the shared loot program ${IDL.metadata.version}, devnet only. Nobody writes a contract: the spec is checked, simulated, and put live by the owner approving in their own wallet. Its parts (* = required field):`,
|
|
435
|
+
...parts.flatMap(p => [
|
|
436
|
+
`\n${p.part} (${p.type}): ${p.says}${p.atPlayTime ? `\n At play time: ${p.atPlayTime}.` : ''}`,
|
|
437
|
+
...p.fields.map(fieldLine),
|
|
438
|
+
...(p.nested ?? []).flatMap(n => [` ${n.name}: ${n.says}`, ...n.fields.map(f => ` ${fieldLine(f)}`)]),
|
|
439
|
+
]),
|
|
219
440
|
...(only
|
|
220
441
|
? []
|
|
221
442
|
: [
|
|
222
443
|
'\nWriting it:',
|
|
223
|
-
...CONVENTIONS.map(
|
|
444
|
+
...CONVENTIONS.map(c => `- ${c}`),
|
|
445
|
+
'\nDesigning against farming and cheating (no code removes these; a design makes them small):',
|
|
446
|
+
...SAFETY.map(x => `- ${x}`),
|
|
224
447
|
'\nAsks that look impossible but are one field or a short combination:',
|
|
225
|
-
...HOW.map(
|
|
448
|
+
...HOW.map(h => `- ${h.what}: ${h.how}.`),
|
|
226
449
|
'\nWhat no part of loot does (before saying so, try combining the parts above: most ideas are several parts together). Say what is missing, then offer the nearest versions, best first, and build the one the person picks; never end on "it can’t":',
|
|
227
|
-
...CANNOT.map(
|
|
450
|
+
...CANNOT.map(c => `- ${c.what} (${c.why}). Nearest: ${c.nearest.join('; or ')}.`),
|
|
228
451
|
`\nFees: ${sol(FEES.player)} SOL once when a player signs for something of their own (a grant's claim too), never again for what follows from it; ${sol(FEES.project)} SOL each time the owner or the authority calls loot, setting up included; ${sol(ORACLE_FEE)} SOL to MagicBlock’s oracle per random roll.`,
|
|
229
452
|
'\nNext: xsolana_examples to find the nearest example, then write the spec and run xsolana_check.',
|
|
230
453
|
]),
|
package/js/economy/legal.mjs
CHANGED
|
@@ -4,12 +4,18 @@
|
|
|
4
4
|
|
|
5
5
|
/** Each legal flag's sentence, for the person who approves, by the checker's warning code. */
|
|
6
6
|
export const LEGAL = Object.freeze({
|
|
7
|
-
'W-CHANCE-PRIZE':
|
|
8
|
-
|
|
9
|
-
'W-
|
|
10
|
-
|
|
7
|
+
'W-CHANCE-PRIZE':
|
|
8
|
+
'Players pay for a chance at money (a box or a lottery that pays a prize, a bet on an outcome). That is gambling in many places: before real money, get a lawyer, and gate it by age and region.',
|
|
9
|
+
'W-CHANCE-ITEMS':
|
|
10
|
+
'Players pay for random items they can resell (a loot box). Some places treat that as gambling and many require the odds shown: show them on the page, and get a lawyer before real money.',
|
|
11
|
+
'W-AUCTION-SHILL':
|
|
12
|
+
'Anyone, the seller included, can bid to push an auction’s price up (shill bidding): a reserve is the honest way to set a floor.',
|
|
13
|
+
'W-REVENUE-SHARE':
|
|
14
|
+
'Stakers are paid from what players spend, which regulators look at when they decide whether a token is a security: before real money, get a lawyer, or pay rewards from emissions instead.',
|
|
11
15
|
});
|
|
12
16
|
|
|
13
17
|
/** The legal flags a checker verdict raises, each once, in the order the checker raised them: [{ code, says }]. */
|
|
14
|
-
export const legalFlags =
|
|
15
|
-
[...new Set((verdict?.warnings ?? []).map(
|
|
18
|
+
export const legalFlags = verdict =>
|
|
19
|
+
[...new Set((verdict?.warnings ?? []).map(w => w.code))]
|
|
20
|
+
.filter(code => Object.hasOwn(LEGAL, code))
|
|
21
|
+
.map(code => ({ code, says: LEGAL[code] }));
|