@xgamesfun/solana 0.20.1 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -10
- package/examples/coin-flip-arena.json +2 -7
- package/examples/elements-set.json +2 -9
- package/examples/expedition-guild.json +1 -8
- package/examples/guild-missions.json +3 -10
- package/examples/halving-seasons.json +4 -9
- package/examples/hero-trials.json +1 -4
- package/examples/merge-2048.json +5 -5
- package/examples/paper-hands-farm.json +4 -16
- package/examples/partner-vault.json +1 -4
- package/examples/stakers-draw.json +1 -8
- package/examples/star-fleet.json +1 -8
- package/examples/starforce.json +6 -6
- package/js/approve/details.mjs +8 -7
- package/js/approve/server.mjs +20 -5
- package/js/chain/borsh.js +9 -0
- package/js/chain/core.js +14 -2
- package/js/chain/lookup.js +8 -2
- package/js/chain/rpc.js +4 -2
- package/js/client/accounts.js +448 -116
- package/js/client/grant.js +125 -66
- package/js/client/idl/loot.js +1 -1
- package/js/client/idl/loot.json +2360 -2218
- package/js/client/index.js +1 -0
- package/js/client/ix.js +1204 -473
- package/js/client/pda.js +20 -16
- package/js/client/policy.js +44 -23
- package/js/client/random.js +70 -0
- package/js/client/sizes.js +255 -12
- package/js/economy/check.mjs +455 -210
- package/js/economy/cost.mjs +45 -30
- package/js/economy/examples.mjs +2 -2
- package/js/economy/explain.mjs +24 -20
- package/js/economy/guide.mjs +31 -30
- package/js/economy/live.mjs +30 -7
- package/js/economy/model.mjs +171 -56
- package/js/economy/plan.mjs +380 -112
- package/js/economy/schema.json +291 -143
- package/js/economy/simulate.mjs +154 -87
- package/js/mcp/tools.mjs +14 -8
- package/js/play/index.js +1121 -375
- package/js/play/keys.js +167 -0
- package/package.json +1 -1
- package/release.json +5 -5
- package/skills/xsolana-economy/SKILL.md +15 -11
- package/skills/xsolana-economy/references/after-launch.md +37 -27
- package/skills/xsolana-economy/references/apps.md +55 -19
- package/skills/xsolana-economy/references/sentence-to-spec.md +30 -22
- package/skills/xsolana-economy/references/telling-the-person.md +52 -11
- package/types/chain/core.d.ts +3 -1
- package/types/chain/lookup.d.ts +5 -2
- package/types/chain/rpc.d.ts +3 -1
- package/types/client/accounts.d.ts +195 -55
- package/types/client/grant.d.ts +72 -27
- package/types/client/idl/loot.d.ts +590 -559
- package/types/client/index.d.ts +1 -0
- package/types/client/ix.d.ts +3862 -234
- package/types/client/pda.d.ts +19 -15
- package/types/client/policy.d.ts +39 -14
- package/types/client/random.d.ts +26 -0
- package/types/client/sizes.d.ts +93 -5
- package/types/economy/check.d.mts +5 -10
- package/types/economy/cost.d.mts +2 -1
- package/types/economy/live.d.mts +16 -1
- package/types/economy/model.d.mts +105 -37
- package/types/economy/plan.d.mts +193 -192
- package/types/economy/simulate.d.mts +21 -13
- package/types/mcp/tools.d.mts +21 -13
- package/types/play/index.d.ts +450 -39
- package/types/play/keys.d.ts +51 -0
package/js/economy/model.mjs
CHANGED
|
@@ -16,12 +16,13 @@ export const FEES = Object.freeze({ player: 2_000_000n, project: 10_000n });
|
|
|
16
16
|
* on `open_box` (order.rs), whoever sends `draw_lottery` (lottery.rs, its `payer`), the authority on `roll`
|
|
17
17
|
* (roll.rs). Measured on devnet: 500,000 lamports per open.
|
|
18
18
|
*/
|
|
19
|
-
export const ORACLE_FEE =
|
|
19
|
+
export const ORACLE_FEE = 800_000n;
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
21
|
+
* What js/play's throwaway settle key is funded with, in the purchase's own transaction, for each prize its settle may
|
|
22
|
+
* open or mint (`loot.SETTLE_RENT_UNIT`): whoever settles pays that rent, the order lends nothing. What the settle
|
|
23
|
+
* doesn't spend goes back to the player when the key empties itself, and all of it when the roll fails.
|
|
23
24
|
*/
|
|
24
|
-
export const
|
|
25
|
+
export const SETTLE_RENT_UNIT = 6_000_000n;
|
|
25
26
|
/** Rent: (bytes + 128) × 5,080 lamports, as `solana rent` reads it on devnet and mainnet-beta. */
|
|
26
27
|
export const RENT_PER_BYTE = 5_080n;
|
|
27
28
|
export const ACCOUNT_OVERHEAD = 128n;
|
|
@@ -41,10 +42,17 @@ export const COLLECTIBLE_RENT = 3_100_000n;
|
|
|
41
42
|
export const SIGNATURE_FEE = 5_000n;
|
|
42
43
|
export const LAMPORTS_PER_SOL = 1_000_000_000n;
|
|
43
44
|
export const WSOL = 'So11111111111111111111111111111111111111112';
|
|
44
|
-
/** The
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
export const
|
|
45
|
+
/** The limits a spec meets: the chain's (the oracle's answer carries 21 accounts; a farm names a tier and a recipe a
|
|
46
|
+
* collectible's place in a byte; Bubblegum keeps a compressed NFT's link in 200 bytes and its name in 32) and the two
|
|
47
|
+
* caps that keep a farm's numbers from overflowing. Since loot 0.22 the program caps no count or size of its own. */
|
|
48
|
+
export const LIMITS = Object.freeze({
|
|
49
|
+
callbackAccounts: 21, tiers: 256, collectiblesPerCraft: 255, vouchers: 3, compressedNameBytes: 32, compressedUriBytes: 200, maxBoostBps: 1_000_000, maxTierBps: 110_000,
|
|
50
|
+
// The program's caps on a record's lists (constants.rs, not in the IDL): a spec over one is refused before anything is signed.
|
|
51
|
+
boxEntries: 128, boxSlots: 32, boxOdds: 32, boxFates: 64, entryPrizes: 14, itemTraits: 64, traitKeyBytes: 64, recipeLines: 48, farmRules: 32, growthSteps: 128, contestSides: 1024, deciders: 32, collections: 32, lotteryRuns: 64, farmEnds: 100_000,
|
|
52
|
+
});
|
|
53
|
+
/** Attribute names the program writes itself, which a recipe cannot copy and `Set` cannot change (craft.rs, make.rs),
|
|
54
|
+
* and the two a collectible's life keeps (`lifeLines`: its uses and its next use). */
|
|
55
|
+
export const RESERVED_ATTRIBUTES = Object.freeze(['made', 'used', 'next_use', 'item_id', 'rarity', 'serial', 'origin']);
|
|
48
56
|
export const RARITIES = Object.freeze(['Common', 'Uncommon', 'Rare', 'Epic', 'Legendary', 'Mythic']);
|
|
49
57
|
|
|
50
58
|
// ---------------------------------------------------------------- units
|
|
@@ -111,8 +119,8 @@ export function toSeconds(duration) {
|
|
|
111
119
|
}
|
|
112
120
|
|
|
113
121
|
/**
|
|
114
|
-
* How long what lands in a farm's rewards
|
|
115
|
-
*
|
|
122
|
+
* How long what lands in a farm's rewards vault takes to be released to its seats, in seconds (a farm that shares what
|
|
123
|
+
* lands, `farm.rs` `release_span`): its longest lock tier, and a day at least.
|
|
116
124
|
*/
|
|
117
125
|
export const releaseSeconds = (f) => Math.max(86_400, ...(f.tiers ?? []).map((t) => toSeconds(t.lock ?? 0)));
|
|
118
126
|
|
|
@@ -123,16 +131,54 @@ export function releaseWords(f) {
|
|
|
123
131
|
return s % 3_600 === 0 ? `${s / 3_600} hours` : `${s} seconds`;
|
|
124
132
|
}
|
|
125
133
|
|
|
134
|
+
/**
|
|
135
|
+
* What a run sent late does, in words (schedule.rs `catches_up`): a sealed schedule's fixed amount pays every run due;
|
|
136
|
+
* any other run pays once, for the latest run due, and the runs before it lapse.
|
|
137
|
+
*/
|
|
138
|
+
export const lateRunWords = (s) => (s.sealed && s.percent === undefined ? 'a run sent late pays every run due since' : 'a run missed is missed: one sent late pays once, for the latest run due, and the runs before it lapse');
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Whether a farm's boost reads a number its own growth writes (`refresh_seat` writes it): each level counts from when it is
|
|
142
|
+
* written, so a page or a keeper should refresh seats.
|
|
143
|
+
*/
|
|
144
|
+
export const boostReadsGrowth = (f) => (f.boosts ?? []).some((b) => b.when?.attribute && listOf(f.growth).some((g) => b.when.attribute === (g.attribute ?? 'xp') || ((g.levels ?? []).length > 0 && b.when.attribute === (g.level ?? 'level'))));
|
|
145
|
+
|
|
146
|
+
/** A box's roll against what is in stock, in words (order.rs): the odds stay as written, whatever is left. */
|
|
147
|
+
export const STOCK_WORDS = 'The odds stay as written whatever is in stock: a draw that lands on an outcome that can’t be given then (its pool short of the whole amount, its item or stash gone) is not made, no other outcome takes its place, and that draw’s worth of the price goes back at the settle; with no draw made at all, a paid open fails and is refunded, and a free roll goes back to waiting';
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* What a box that rolls does once an outcome can't be given (`whenOut`, order.rs `WhenOut`), in words. `sold`: it is
|
|
151
|
+
* sold (an open checks the stock); a box only other routes roll (a prize, a mission) never stops a sale.
|
|
152
|
+
*/
|
|
153
|
+
export function whenOutWords(b, { sold = b.active !== false } = {}) {
|
|
154
|
+
if (b.whenOut === 'drawLeft' && !sold)
|
|
155
|
+
return 'When an outcome runs out (an item sold out, a pool short of its prize, a stash empty), a roll draws among the outcomes still there by their written weights, so the others’ chances rise; with nothing left at all, a free roll waits to be asked again and a paid one is refunded';
|
|
156
|
+
if (b.whenOut === 'drawLeft')
|
|
157
|
+
return 'When an outcome runs out (an item sold out, a pool short of its prize, a stash empty), the box keeps selling while its first draw has anything left, and a roll draws among the outcomes still there by their written weights: the others’ chances rise as one runs out, so a player can empty a common outcome to make a rare one likelier (a box selling a rare prize should stop selling instead, the default)';
|
|
158
|
+
if (!sold) return STOCK_WORDS;
|
|
159
|
+
return 'When an outcome runs out, the box stops selling: it isn’t sold while any outcome couldn’t cover this purchase’s draws and those of purchases still waiting for the oracle (an item’s units left, a pool’s tokens, a stash’s NFTs), so every roll is on the odds as written and nobody can buy their way to a refund on every miss; it sells again once the project restocks or edits the box (an outcome whose odds shrink with stock needs no cover). An outcome emptied another way all the same (another route sharing its pool, item or stash) makes no draw, no other outcome taking its place, and gives back that draw’s worth of the price at the settle; with no draw made at all, the open is refunded';
|
|
160
|
+
}
|
|
161
|
+
|
|
126
162
|
/**
|
|
127
163
|
* What the end of a sharing farm that `runs` means, in words, or null (no end, or a fixed rate): its release keeps its
|
|
128
|
-
* pace, so what it has not let go by the end stays in
|
|
129
|
-
*
|
|
164
|
+
* pace, so what it has not let go by the end stays in its vault, free for the authority to take back, and so does what
|
|
165
|
+
* a seat forfeits after the end.
|
|
130
166
|
*/
|
|
131
167
|
export function farmEndWords(f) {
|
|
132
168
|
if (!f.runs || f.rate) return null;
|
|
133
169
|
const span = releaseWords(f);
|
|
134
|
-
|
|
135
|
-
|
|
170
|
+
return `what is not released by its end stays in its vault, credited to nobody (the authority may take back what is free there), and so does what a seat forfeits after the end: fund it at least ${span} before its end (what lands later is released only in part, and what lands at or after the end not at all)`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The token a farm's rewards are in (`rewards`, a token key): by default its staked token, or for a farm of
|
|
175
|
+
* collectibles the project's currency. One that names a pool (as farms did before they had a vault of their own) is
|
|
176
|
+
* read as that pool's token, and the checker refuses it.
|
|
177
|
+
*/
|
|
178
|
+
export function farmRewardsToken(f, tokens, currencyKey, poolCurrency = new Map()) {
|
|
179
|
+
if (f.rewards && tokens.has(f.rewards)) return f.rewards;
|
|
180
|
+
if (f.rewards && poolCurrency.has(f.rewards)) return poolCurrency.get(f.rewards);
|
|
181
|
+
return f.stakes && f.stakes !== 'collectibles' ? f.stakes : currencyKey;
|
|
136
182
|
}
|
|
137
183
|
|
|
138
184
|
/** A raise's or a shift's `by`: a number, or for a time a duration, which may be negative ("-1d"). */
|
|
@@ -206,21 +252,14 @@ export function model(economy, { now = 0 } = {}) {
|
|
|
206
252
|
const tokens = new Map((e.tokens ?? []).map((t) => [t.key, tokenOf(t)]));
|
|
207
253
|
const currencyKey = e.currency ?? (tokens.size === 1 ? [...tokens.keys()][0] : undefined);
|
|
208
254
|
const currency = tokens.get(currencyKey);
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
const unpaid = (e.farms ?? []).filter((f) => !f.rewards);
|
|
213
|
-
const hidden = unpaid.map((f) => ({ key: `${f.key}-rewards`, currency: f.stakes !== 'collectibles' ? f.stakes : currencyKey, about: `${f.key}: pays nothing`, farm: f.key, ...(Number.isInteger(f.rewardsIndex) ? { index: f.rewardsIndex } : {}) }));
|
|
214
|
-
const listed = [...(e.pools ?? []), ...hidden];
|
|
215
|
-
// Numbers are given in this order: the pools that have one, then the empty pool of a farm already live that has no
|
|
216
|
-
// number written (it went live before numbers were kept, and took the first free one then, before any pool added
|
|
217
|
-
// since), then the rest in the spec's order.
|
|
218
|
-
const landed = (key) => Number.isInteger(unpaid.find((f) => f.key === key)?.id);
|
|
219
|
-
const rank = (p) => (Number.isInteger(p.index) ? 0 : p.farm && landed(p.farm) ? 1 : 2);
|
|
220
|
-
const order = [...listed.keys()].sort((a, b) => rank(listed[a]) - rank(listed[b]));
|
|
255
|
+
// Numbers are given in this order: the pools that have one, then the rest in the spec's order.
|
|
256
|
+
const listed = e.pools ?? [];
|
|
257
|
+
const order = [...listed.keys()].sort((a, b) => Number(!Number.isInteger(listed[a].index)) - Number(!Number.isInteger(listed[b].index)));
|
|
221
258
|
const assigned = assignIds(order.map((i) => listed[i]), 'index', 0);
|
|
222
259
|
const pools = listed.map((_, i) => assigned[order.indexOf(i)]);
|
|
223
|
-
|
|
260
|
+
// A farm's rewards land in a vault of its own: `rewardsToken` is their token.
|
|
261
|
+
const poolCurrency = new Map(pools.map((p) => [p.key, p.currency]));
|
|
262
|
+
const farms = (e.farms ?? []).map((f) => ({ ...f, rewardsToken: farmRewardsToken(f, tokens, currencyKey, poolCurrency) }));
|
|
224
263
|
// A raffle: its NFT is first place's prize, held in the lottery's own stash.
|
|
225
264
|
const lotteries = (e.lotteries ?? []).map((l) => {
|
|
226
265
|
if (!l.raffle) return l;
|
|
@@ -247,6 +286,7 @@ export function model(economy, { now = 0 } = {}) {
|
|
|
247
286
|
items: assignIds(e.items ?? [], 'id', 1),
|
|
248
287
|
pools,
|
|
249
288
|
stashes: assignIds(e.stashes ?? [], 'id', 1),
|
|
289
|
+
tables: assignIds(e.tables ?? [], 'id', 1),
|
|
250
290
|
boxes: [],
|
|
251
291
|
payments: e.payments ?? [],
|
|
252
292
|
recipes: assignIds(e.recipes ?? [], 'id', 1),
|
|
@@ -290,6 +330,7 @@ export function model(economy, { now = 0 } = {}) {
|
|
|
290
330
|
m.item = index(m.items);
|
|
291
331
|
m.pool = index(m.pools);
|
|
292
332
|
m.stash = index(m.stashes);
|
|
333
|
+
m.table = index(m.tables);
|
|
293
334
|
m.box = index(m.allBoxes);
|
|
294
335
|
m.recipe = index(m.recipes);
|
|
295
336
|
m.farm = index(m.farms);
|
|
@@ -312,16 +353,17 @@ export function model(economy, { now = 0 } = {}) {
|
|
|
312
353
|
*/
|
|
313
354
|
export function stashNfts(s) {
|
|
314
355
|
return (s.nfts ?? []).map((n) => {
|
|
315
|
-
if (typeof n === 'string') return { kind: 'core', asset: n, collection: s.collection ?? null };
|
|
316
|
-
|
|
317
|
-
if (n.
|
|
318
|
-
return { kind: '
|
|
356
|
+
if (typeof n === 'string') return { kind: 'core', asset: n, collection: s.collection ?? null, weight: 1 };
|
|
357
|
+
const weight = n.weight ?? 1;
|
|
358
|
+
if (n.mint) return { kind: 'token-metadata', asset: n.mint, programmable: Boolean(n.programmable), ruleSet: n.ruleSet ?? null, weight };
|
|
359
|
+
if (n.compressed) return { kind: 'compressed', asset: n.compressed, weight };
|
|
360
|
+
return { kind: 'core', asset: n.asset, collection: n.collection ?? s.collection ?? null, weight };
|
|
319
361
|
});
|
|
320
362
|
}
|
|
321
363
|
|
|
322
364
|
/**
|
|
323
365
|
* A prize as the spec writes it (`$defs/prize`), read the one way: a line { gives, … } as a recipe's `out` lines are:
|
|
324
|
-
* "item" (`item`), "tokens" (`amount`, `from` "mint" with `currency`, or "pool:<key>"),
|
|
366
|
+
* "item" (`item`), "tokens" (`amount`, `from` "mint" with `currency`, or "pool:<key>"),
|
|
325
367
|
* "nft" (`nft`, an address the stash `stash` lists) or "roll" (a roll of `box`; in a recipe, on the collectible kept
|
|
326
368
|
* at `on`). A grant's older `gives` ({ item }, { pool, amount }, { roll }) reads as its line. Null for none.
|
|
327
369
|
*/
|
|
@@ -334,8 +376,8 @@ export function prizeLine(p) {
|
|
|
334
376
|
return null;
|
|
335
377
|
}
|
|
336
378
|
|
|
337
|
-
/** The pool a prize line pays from:
|
|
338
|
-
export const prizePool = (p) => (p?.gives === '
|
|
379
|
+
/** The pool a prize line pays from: tokens' "pool:<key>". */
|
|
380
|
+
export const prizePool = (p) => (p?.gives === 'tokens' && p.from?.startsWith('pool:') ? p.from.slice(5) : null);
|
|
339
381
|
|
|
340
382
|
/** The token a prize line pays in: its pool's, or the one it mints. */
|
|
341
383
|
export const prizeToken = (p, m) => (prizePool(p) ? m.poolToken(prizePool(p)) : p?.gives === 'tokens' ? m.tokens.get(p.currency ?? m.currencyKey) : null);
|
|
@@ -364,7 +406,8 @@ export function prizes(m) {
|
|
|
364
406
|
* canopy, bytes }
|
|
365
407
|
*/
|
|
366
408
|
export function compressedTree(m) {
|
|
367
|
-
|
|
409
|
+
const boxGives = m.allBoxes.some((b) => (b.outcomes ?? []).flatMap(outcomeParts).some((o) => o.gives === 'item' && o.compressed));
|
|
410
|
+
if (!boxGives && !prizes(m).some((p) => p.prize.gives === 'item' && p.prize.compressed)) return null;
|
|
368
411
|
const depth = Math.min(20, Math.max(14, Math.ceil(Math.log2(Math.max(1, m.treeHolds)))));
|
|
369
412
|
const canopy = Math.min(depth - 10, 8);
|
|
370
413
|
return { depth, buffer: 64, canopy, bytes: treeSize(depth, 64, canopy) };
|
|
@@ -406,13 +449,13 @@ export function rollsOf(m) {
|
|
|
406
449
|
return rolls;
|
|
407
450
|
}
|
|
408
451
|
|
|
409
|
-
/** A prize line in words: "one Badge", "100 GOLD from “rewards”", "100 GOLD, minted", "
|
|
452
|
+
/** A prize line in words: "one Badge", "100 GOLD from “rewards”", "100 GOLD, minted", "the NFT ab12…cd34 of “vault”", "a roll of “Chest”". */
|
|
410
453
|
export function prizeWords(p, m) {
|
|
411
454
|
const line = prizeLine(p);
|
|
412
455
|
if (!line) return 'nothing';
|
|
413
456
|
const sym = (key) => m.tokens.get(key)?.symbol ?? key;
|
|
414
457
|
if (line.gives === 'item') return `one ${m.item.get(line.item)?.name ?? line.item}${line.compressed ? ' (compressed)' : ''}`;
|
|
415
|
-
if (line.gives === '
|
|
458
|
+
if (line.gives === 'raffled') return 'an NFT drawn at random from the lottery’s own stash';
|
|
416
459
|
if (line.gives === 'nft' && line.held) return `the NFT ${String(line.nft).slice(0, 4)}…${String(line.nft).slice(-4)} (held in the lottery’s own stash until it is won)`;
|
|
417
460
|
if (line.gives === 'nft') return `the NFT ${String(line.nft).slice(0, 4)}…${String(line.nft).slice(-4)} of “${m.stash.get(line.stash)?.name ?? line.stash}”`;
|
|
418
461
|
if (line.gives === 'roll') return `a roll of “${m.box.get(line.box)?.name ?? line.box}”`;
|
|
@@ -422,30 +465,81 @@ export function prizeWords(p, m) {
|
|
|
422
465
|
}
|
|
423
466
|
|
|
424
467
|
/**
|
|
425
|
-
* What one box outcome gives (catalog.rs
|
|
426
|
-
* "
|
|
468
|
+
* What one box outcome gives (catalog.rs), each in the box's own words: "item", "pay" (a fixed amount from a pool),
|
|
469
|
+
* "stash" (an NFT of a stash at random), "mint"
|
|
427
470
|
* (tokens minted), "nft" (one NFT a stash lists) or "roll" (a roll of another box). A bundle gives its `prizes`; "burn"
|
|
428
471
|
* and "nothing" give none.
|
|
429
472
|
*/
|
|
430
473
|
export function outcomeParts(o) {
|
|
431
474
|
if (!o) return [];
|
|
432
475
|
if (o.gives === 'bundle') return (o.prizes ?? []).flatMap(outcomeParts);
|
|
433
|
-
if (o.gives === '
|
|
476
|
+
if (o.gives === 'nothing') return [];
|
|
434
477
|
if (o.gives === 'tokens') return [o.from?.startsWith('pool:') ? { gives: 'pay', pool: o.from.slice(5), amount: o.amount } : { gives: 'mint', currency: o.currency, amount: o.amount }];
|
|
435
478
|
return [o];
|
|
436
479
|
}
|
|
437
480
|
|
|
438
|
-
/** Whether an outcome burns the payment in place of the box's split: "burn", or a bundle that says `burns`. */
|
|
439
|
-
export const outcomeBurns = (o) => o?.gives === 'burn' || Boolean(o?.burns);
|
|
440
|
-
|
|
441
481
|
/** Whether a box leaves its outcome to the oracle (catalog.rs `rolls`): more than one outcome, or an NFT at random. */
|
|
442
|
-
|
|
482
|
+
/** How many draws one open of a box makes: its slots' counts, or one. */
|
|
483
|
+
export const drawsOf = (b) => ((b.slots ?? []).length ? b.slots.reduce((n, s) => n + Number(s.count), 0) : 1);
|
|
484
|
+
|
|
485
|
+
/** Whether a box's prize costs its settle a collectible's rent: an item minted as a Core NFT, or a fresh compressed one
|
|
486
|
+
* (its traits' Sheet). A plain compressed NFT costs no rent. */
|
|
487
|
+
export const holdsRent = (p, m) => p.gives === 'item' && (!p.compressed || (m?.item.get(p.item)?.drawn ?? []).length > 0);
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* The units of `SETTLE_RENT_UNIT` a settle of box `b`'s order may spend (`loot.settleRent`), which js/play funds its
|
|
491
|
+
* throwaway settle key with and gets back unspent: for each draw, each part of the richest outcome that opens an
|
|
492
|
+
* account (a collectible, a fresh compressed NFT's Sheet, a stash's NFT); once an order, each source of tokens it may
|
|
493
|
+
* pay, and the fate of the NFT it rolls on.
|
|
494
|
+
*/
|
|
495
|
+
export function settleUnits(b, m) {
|
|
496
|
+
const opens = (p) => holdsRent(p, m) || p.gives === 'stash' || p.gives === 'nft';
|
|
497
|
+
const parts = (b.outcomes ?? []).map((o) => outcomeParts(o));
|
|
498
|
+
const most = Math.max(0, ...parts.map((ps) => ps.filter(opens).length));
|
|
499
|
+
const sources = new Set(parts.flat().filter((p) => ['tokens', 'pay', 'mint'].includes(p.gives)).map((p) => prizePool(p) ?? `mint:${p.currency ?? ''}`));
|
|
500
|
+
return drawsOf(b) * most + sources.size + (b.rollsOn ? 1 : 0);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export const boxRolls = (b, m) => (b.outcomes ?? []).length > 1 || (b.outcomes ?? []).flatMap(outcomeParts).some((p) => p.gives === 'stash' || (p.gives === 'item' && (m?.item.get(p.item)?.drawn ?? []).length > 0));
|
|
443
504
|
|
|
444
|
-
/** Whether a box sells with `buy`, in one transaction (buy.rs): it doesn't roll
|
|
445
|
-
|
|
505
|
+
/** Whether a box sells with `buy`, in one transaction (buy.rs): it doesn't roll and rolls on no NFT, any number of draws
|
|
506
|
+
* a sale (a fixed pack: its prizes given once for each draw). */
|
|
507
|
+
export const boxBuys = (b, m) => !boxRolls(b, m) && !b.rollsOn;
|
|
508
|
+
|
|
509
|
+
/** The most a raise lowers a number by (an i32's least but one): a renew's, which counts a collectible's uses from 0. */
|
|
510
|
+
const RENEW_BY = -2_147_483_647;
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* What a recipe's lines owe the life of each collectible they take (an item's `life`), as the program's needs and
|
|
514
|
+
* raises (craft.rs `Need`, `Raise`), at each collectible's place in the recipe (each burned unit, then each kept one, in
|
|
515
|
+
* line order; a pay line takes none): ready after `readyAfter` (`made` older than it), for `lasts` (`made` within it),
|
|
516
|
+
* on every line that takes it; `uses` times (`used` raised by 1 up to it) and a `cooldown` (`next_use` passed, then
|
|
517
|
+
* raised to that far from now) on a line that uses it or takes it staked; a renew line counts its uses from 0 again.
|
|
518
|
+
* { needs, raises }, in the program's shapes.
|
|
519
|
+
*/
|
|
520
|
+
export function lifeLines(r, m) {
|
|
521
|
+
const places = (r.in ?? []).flatMap((l) => (l.takes === 'pay' ? [] : Array(l.takes === 'burn' ? l.count ?? 1 : 1).fill(l)));
|
|
522
|
+
const needs = [];
|
|
523
|
+
const raises = [];
|
|
524
|
+
places.forEach((l, place) => {
|
|
525
|
+
const it = l.item ? m.item.get(l.item) : null;
|
|
526
|
+
if (!it?.life || it.kind === 'normal') return;
|
|
527
|
+
const [ready, lasts, uses, cooldown] = [toSeconds(it.life.readyAfter ?? 0), toSeconds(it.life.lasts ?? 0), it.life.uses ?? 0, toSeconds(it.life.cooldown ?? 0)];
|
|
528
|
+
if (ready) needs.push({ place, key: 'made', test: { Older: [ready] } });
|
|
529
|
+
if (lasts) needs.push({ place, key: 'made', test: { Within: [lasts] } });
|
|
530
|
+
if (l.takes === 'renew') raises.push({ place, shift: { key: 'used', by: RENEW_BY, most: 0, from_now: false } });
|
|
531
|
+
if (l.takes !== 'use' && l.takes !== 'staked') return;
|
|
532
|
+
if (uses) raises.push({ place, shift: { key: 'used', by: 1, most: uses, from_now: false } });
|
|
533
|
+
if (cooldown) {
|
|
534
|
+
needs.push({ place, key: 'next_use', test: 'Passed' });
|
|
535
|
+
raises.push({ place, shift: { key: 'next_use', by: cooldown, most: cooldown, from_now: true } });
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
return { needs, raises };
|
|
539
|
+
}
|
|
446
540
|
|
|
447
541
|
/** A lottery's place as { percent, prize }: a number is a share of the pot and no prize. */
|
|
448
|
-
export const placeOf = (p) => (typeof p === 'object' && p !== null ? { percent: p.percent ?? 0, prize: prizeLine(p.prize) } : { percent: p, prize: null });
|
|
542
|
+
export const placeOf = (p) => (typeof p === 'object' && p !== null ? { percent: p.percent ?? 0, prize: prizeLine(p.prize), times: p.times ?? 1 } : { percent: p, prize: null, times: 1 });
|
|
449
543
|
|
|
450
544
|
/** "pool:house" → { kind: 'pool', key: 'house' }; "burn" → { kind: 'burn' }. */
|
|
451
545
|
export function destination(to) {
|
|
@@ -463,7 +557,8 @@ export const sharesOf = (split) => (split ?? []).map((s) => ({ ...destination(s.
|
|
|
463
557
|
* The program's `To` for a destination, with pool indexes resolved (the IDL's own shape). A wallet ("wallet:<address>")
|
|
464
558
|
* is paid at its own token account of the part's currency (the program opens it when it is not there); "treasury" is the game's treasury wallet's; "chosen"
|
|
465
559
|
* (or "chosen:<item>") is the one the buyer names when they buy; "lottery:<key>" is that lottery's pot
|
|
466
|
-
* ("@pot:<id>", which the plan resolves once the game is
|
|
560
|
+
* ("@pot:<id>") and "farm:<key>" that farm's rewards vault ("@rewards:<id>"), which the plan resolves once the game is
|
|
561
|
+
* known.
|
|
467
562
|
*/
|
|
468
563
|
export function toEnum(to, m) {
|
|
469
564
|
const d = destination(to);
|
|
@@ -473,6 +568,7 @@ export function toEnum(to, m) {
|
|
|
473
568
|
if (d.kind === 'account') return { Account: [d.key] };
|
|
474
569
|
if (d.kind === 'wallet') return { Wallet: [d.key] };
|
|
475
570
|
if (d.kind === 'lottery') return { Account: [`@pot:${m.lottery.get(d.key)?.id ?? 0}`] };
|
|
571
|
+
if (d.kind === 'farm') return { Account: [`@rewards:${m.farm.get(d.key)?.id ?? 0}`] };
|
|
476
572
|
if (d.kind === 'chosen') return { Chosen: { item: d.key ? (m.item.get(d.key)?.id ?? 0) : null } };
|
|
477
573
|
return { Holder: [d.key] };
|
|
478
574
|
}
|
|
@@ -544,8 +640,8 @@ export function weights(box) {
|
|
|
544
640
|
export function poolTakers(m) {
|
|
545
641
|
const takers = [];
|
|
546
642
|
const add = (pool, by, how) => pool && takers.push({ pool, by, how });
|
|
547
|
-
for (const b of m.allBoxes) for (const o of (b.outcomes ?? []).flatMap(outcomeParts)) if (o.gives === 'pay'
|
|
548
|
-
for (const p of prizes(m)) add(prizePool(p.prize), p.by,
|
|
643
|
+
for (const b of m.allBoxes) for (const o of (b.outcomes ?? []).flatMap(outcomeParts)) if (o.gives === 'pay') add(o.pool, `box:${b.key}`, 'pays prizes');
|
|
644
|
+
for (const p of prizes(m)) add(prizePool(p.prize), p.by, `pays prizes (${p.route})`);
|
|
549
645
|
for (const s of m.schedules) {
|
|
550
646
|
if (s.does === 'burn') add(s.pool, `schedule:${s.key}`, 'burns a share');
|
|
551
647
|
if (s.does === 'move') add(s.from, `schedule:${s.key}`, 'moves a share out');
|
|
@@ -553,12 +649,6 @@ export function poolTakers(m) {
|
|
|
553
649
|
// from a vault of its own and names no pool.
|
|
554
650
|
if (s.does === 'pay') add(s.from, `schedule:${s.key}`, 'pays out on a timer');
|
|
555
651
|
}
|
|
556
|
-
for (const f of m.farms) {
|
|
557
|
-
// Sharing what arrives releases all of the pool over its release span; a fixed rate from it takes the rate; a
|
|
558
|
-
// minted one takes nothing.
|
|
559
|
-
if (!f.rate) add(f.rewards, `farm:${f.key}`, 'releases everything to its seats');
|
|
560
|
-
else if (!f.rate.minted) add(f.rewards, `farm:${f.key}`, 'pays its fixed rate');
|
|
561
|
-
}
|
|
562
652
|
for (const d of m.decisions) if (d.does === 'payout' || d.does === 'burn') add(d.pool, `decision:${d.key}`, d.does === 'payout' ? 'pays out on the authority’s word' : 'burns on the authority’s word');
|
|
563
653
|
return takers;
|
|
564
654
|
}
|
|
@@ -581,3 +671,28 @@ export function poolFillers(m) {
|
|
|
581
671
|
for (const a of m.auctions) fromSplit(a.split, `auction:${a.key}`);
|
|
582
672
|
return fillers;
|
|
583
673
|
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Every place the spec sends money into a farm's rewards vault ("farm:<key>"): a split's share, a schedule that pays it,
|
|
677
|
+
* a craft that pays it, the owner's seed, an early exit's fee into its own vault. [{ farm, by, how }]
|
|
678
|
+
*/
|
|
679
|
+
export function farmFillers(m) {
|
|
680
|
+
const fillers = [];
|
|
681
|
+
const add = (to, by, how) => {
|
|
682
|
+
const d = destination(to ?? '');
|
|
683
|
+
if (d.kind === 'farm') fillers.push({ farm: d.key, by, how });
|
|
684
|
+
};
|
|
685
|
+
const fromSplit = (split, by) => sharesOf(split).forEach((s) => add(s.to, by, `${s.percent}% of each payment`));
|
|
686
|
+
for (const f of m.farms) {
|
|
687
|
+
if (Number(f.seed ?? 0) > 0) fillers.push({ farm: f.key, by: `farm:${f.key}`, how: 'the owner’s seed' });
|
|
688
|
+
if (f.exitFeeTo === 'rewards') fillers.push({ farm: f.key, by: `farm:${f.key}`, how: 'early-exit fees' });
|
|
689
|
+
}
|
|
690
|
+
for (const b of m.allBoxes) fromSplit(b.split, b.shopOf ? `item:${b.shopOf}` : `box:${b.key}`);
|
|
691
|
+
for (const p of m.payments) fromSplit(p.split, `payment:${p.key}`);
|
|
692
|
+
for (const r of m.recipes) for (const i of r.in ?? []) if (i.takes === 'pay') add(i.to, `recipe:${r.key}`, 'a craft pays in');
|
|
693
|
+
for (const s of m.schedules) if (s.does === 'pay' || s.does === 'issue') add(s.to, `schedule:${s.key}`, s.does === 'issue' ? 'mints into it on a timer' : 'pays in on a timer');
|
|
694
|
+
for (const l of m.lotteries) fromSplit(lotterySplit(l), `lottery:${l.key}`);
|
|
695
|
+
for (const c of m.contests) fromSplit(c.split, `contest:${c.key}`);
|
|
696
|
+
for (const a of m.auctions) fromSplit(a.split, `auction:${a.key}`);
|
|
697
|
+
return fillers;
|
|
698
|
+
}
|