@xgamesfun/solana 0.20.1 → 0.25.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 +95 -52
- package/bin/xsolana.mjs +23 -8
- package/examples/blind-forge.json +2 -4
- package/examples/coin-flip-arena.json +4 -12
- package/examples/duel-arena.json +1 -4
- package/examples/elements-set.json +2 -9
- package/examples/expedition-guild.json +1 -8
- package/examples/fair-presale.json +1 -3
- package/examples/guild-missions.json +3 -10
- package/examples/halving-seasons.json +4 -9
- package/examples/hero-trials.json +15 -6
- package/examples/holder-den.json +38 -0
- package/examples/holders-drop.json +105 -0
- package/examples/ideas.json +788 -788
- package/examples/match-predictions.json +24 -0
- package/examples/merge-2048.json +5 -5
- package/examples/nft-raffle.json +1 -0
- package/examples/pack-league.json +108 -0
- package/examples/paper-hands-farm.json +4 -16
- package/examples/partner-vault.json +20 -7
- package/examples/sacrifice-raffle.json +2 -10
- package/examples/shelter-fair.json +1 -0
- package/examples/stakers-draw.json +4 -15
- package/examples/star-fleet.json +1 -8
- package/examples/starforce.json +6 -6
- package/examples/stride-sneakers.json +164 -0
- package/examples/trade-in-shop.json +2 -3
- package/js/approve/details.mjs +122 -52
- 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 +290 -73
- package/js/chain/base58.js +7 -1
- package/js/chain/borsh.js +24 -11
- package/js/chain/bytes.js +11 -8
- package/js/chain/core.js +35 -7
- package/js/chain/curve.js +1 -1
- package/js/chain/lookup.js +124 -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 +44 -16
- 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 +653 -131
- package/js/client/compressed.js +4 -3
- package/js/client/grant.js +267 -83
- package/js/client/idl/loot.js +1 -1
- package/js/client/idl/loot.json +2406 -2176
- package/js/client/index.js +1 -0
- package/js/client/ix.js +3059 -664
- package/js/client/pda.js +39 -33
- package/js/client/policy.js +52 -24
- package/js/client/random.js +70 -0
- package/js/client/sizes.js +414 -16
- package/js/client/traits.js +14 -5
- package/js/connect/connect.mjs +59 -17
- package/js/economy/check.mjs +4153 -725
- package/js/economy/cost.mjs +403 -64
- package/js/economy/examples.mjs +176 -35
- package/js/economy/explain.mjs +466 -111
- package/js/economy/guide.mjs +296 -72
- package/js/economy/legal.mjs +12 -6
- package/js/economy/live.mjs +83 -19
- package/js/economy/model.mjs +462 -124
- package/js/economy/plan.mjs +1306 -219
- package/js/economy/schema.json +390 -484
- package/js/economy/secrets.mjs +17 -7
- package/js/economy/simulate.mjs +883 -223
- package/js/economy/txsize.mjs +15 -3
- package/js/economy/validate.mjs +61 -24
- package/js/mcp/server.mjs +42 -14
- package/js/mcp/tools.mjs +307 -88
- package/js/play/index.js +2864 -619
- package/js/play/keys.js +193 -0
- package/js/results/index.js +223 -30
- 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 +39 -21
- package/skills/xsolana-economy/references/after-launch.md +58 -38
- package/skills/xsolana-economy/references/apps.md +97 -50
- package/skills/xsolana-economy/references/sentence-to-spec.md +148 -121
- package/skills/xsolana-economy/references/telling-the-person.md +61 -17
- package/types/chain/core.d.ts +4 -2
- package/types/chain/lookup.d.ts +5 -2
- package/types/chain/rpc.d.ts +3 -1
- package/types/client/accounts.d.ts +217 -56
- package/types/client/grant.d.ts +72 -27
- package/types/client/idl/loot.d.ts +602 -549
- package/types/client/index.d.ts +1 -0
- package/types/client/ix.d.ts +3884 -247
- package/types/client/pda.d.ts +22 -16
- 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/guide.d.mts +5 -0
- package/types/economy/live.d.mts +16 -1
- package/types/economy/model.d.mts +151 -38
- package/types/economy/plan.d.mts +237 -194
- package/types/economy/simulate.d.mts +21 -13
- package/types/mcp/tools.d.mts +21 -13
- package/types/play/index.d.ts +453 -53
- package/types/play/keys.d.ts +51 -0
- package/types/results/index.d.ts +7 -6
- package/types/results/server.d.ts +126 -0
- package/types/results/wire.d.ts +1 -1
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;
|
|
@@ -30,7 +31,7 @@ export const ACCOUNT_OVERHEAD = 128n;
|
|
|
30
31
|
* devnet and mainnet lowered it (a local validator and LiteSVM still charge it). An RPC that asks more is not believed.
|
|
31
32
|
*/
|
|
32
33
|
export const MOST_RENT_PER_BYTE = 6_960n;
|
|
33
|
-
export const rent =
|
|
34
|
+
export const rent = bytes => (BigInt(bytes) + ACCOUNT_OVERHEAD) * RENT_PER_BYTE;
|
|
34
35
|
/**
|
|
35
36
|
* A collectible's Core account, paid from the SOL its order holds (or by whoever gives it): its rent (a Core asset of
|
|
36
37
|
* about 190 bytes) plus Metaplex Core's creation fee of 0.0015 SOL, which the account keeps. The full simulation
|
|
@@ -41,10 +42,38 @@ 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,
|
|
50
|
+
tiers: 256,
|
|
51
|
+
collectiblesPerCraft: 255,
|
|
52
|
+
vouchers: 3,
|
|
53
|
+
compressedNameBytes: 32,
|
|
54
|
+
compressedUriBytes: 200,
|
|
55
|
+
maxBoostBps: 1_000_000,
|
|
56
|
+
maxTierBps: 110_000,
|
|
57
|
+
// 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.
|
|
58
|
+
boxEntries: 128,
|
|
59
|
+
boxSlots: 32,
|
|
60
|
+
boxOdds: 32,
|
|
61
|
+
boxFates: 64,
|
|
62
|
+
entryPrizes: 14,
|
|
63
|
+
itemTraits: 64,
|
|
64
|
+
traitKeyBytes: 64,
|
|
65
|
+
recipeLines: 48,
|
|
66
|
+
farmRules: 32,
|
|
67
|
+
growthSteps: 128,
|
|
68
|
+
contestSides: 1024,
|
|
69
|
+
deciders: 32,
|
|
70
|
+
collections: 32,
|
|
71
|
+
lotteryRuns: 64,
|
|
72
|
+
farmEnds: 100_000,
|
|
73
|
+
});
|
|
74
|
+
/** Attribute names the program writes itself, which a recipe cannot copy and `Set` cannot change (craft.rs, make.rs),
|
|
75
|
+
* and the two a collectible's life keeps (`lifeLines`: its uses and its next use). */
|
|
76
|
+
export const RESERVED_ATTRIBUTES = Object.freeze(['made', 'used', 'next_use', 'item_id', 'rarity', 'serial', 'origin']);
|
|
48
77
|
export const RARITIES = Object.freeze(['Common', 'Uncommon', 'Rare', 'Epic', 'Legendary', 'Mythic']);
|
|
49
78
|
|
|
50
79
|
// ---------------------------------------------------------------- units
|
|
@@ -61,7 +90,8 @@ export class SpecError extends Error {
|
|
|
61
90
|
/** A plain decimal string for an amount written as a number or text (no exponent). */
|
|
62
91
|
export function decimalText(amount) {
|
|
63
92
|
if (typeof amount === 'string') return amount;
|
|
64
|
-
if (typeof amount !== 'number' || !Number.isFinite(amount) || amount < 0)
|
|
93
|
+
if (typeof amount !== 'number' || !Number.isFinite(amount) || amount < 0)
|
|
94
|
+
throw new SpecError('E-AMOUNT', `${amount} is not an amount.`, 'Write a number like 0.1, or text like "0.1".');
|
|
65
95
|
const text = String(amount);
|
|
66
96
|
if (!/e/i.test(text)) return text;
|
|
67
97
|
return amount.toFixed(20).replace(/\.?0+$/, '');
|
|
@@ -70,12 +100,19 @@ export function decimalText(amount) {
|
|
|
70
100
|
/** "0.1" of a currency with 9 decimals → 100000000n. Refuses more decimals than the currency has. */
|
|
71
101
|
export function toUnits(amount, decimals) {
|
|
72
102
|
const text = decimalText(amount ?? 0);
|
|
73
|
-
if (!/^\d{1,20}(\.\d+)?$/.test(text))
|
|
103
|
+
if (!/^\d{1,20}(\.\d+)?$/.test(text))
|
|
104
|
+
throw new SpecError('E-AMOUNT', `${text} is not an amount.`, 'Write a number like 0.1, or text like "0.1".');
|
|
74
105
|
const [whole, fraction = ''] = text.split('.');
|
|
75
106
|
const trimmed = fraction.replace(/0+$/, '');
|
|
76
|
-
if (trimmed.length > decimals)
|
|
107
|
+
if (trimmed.length > decimals)
|
|
108
|
+
throw new SpecError(
|
|
109
|
+
'E-AMOUNT',
|
|
110
|
+
`${text} has more decimals than its currency (${decimals}).`,
|
|
111
|
+
`Round it to ${decimals} decimals.`,
|
|
112
|
+
);
|
|
77
113
|
const units = BigInt(whole + trimmed.padEnd(decimals, '0'));
|
|
78
|
-
if (units >= 2n ** 64n)
|
|
114
|
+
if (units >= 2n ** 64n)
|
|
115
|
+
throw new SpecError('E-AMOUNT', `${text} is too large for the program.`, 'Use a smaller amount.');
|
|
79
116
|
return units;
|
|
80
117
|
}
|
|
81
118
|
|
|
@@ -90,7 +127,7 @@ export function fromUnits(units, decimals) {
|
|
|
90
127
|
|
|
91
128
|
/** A count written as an amount (a contest of NFTs counts NFTs): 2 or "2" → 2n. Null when it is not a whole number. */
|
|
92
129
|
export function wholeCount(value) {
|
|
93
|
-
const text = typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? String(value) : value ?? '0';
|
|
130
|
+
const text = typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? String(value) : (value ?? '0');
|
|
94
131
|
return typeof text === 'string' && /^\d{1,19}$/.test(text) && BigInt(text) < 2n ** 64n ? BigInt(text) : null;
|
|
95
132
|
}
|
|
96
133
|
|
|
@@ -106,15 +143,16 @@ export function toSeconds(duration) {
|
|
|
106
143
|
if (duration === undefined || duration === null) return 0;
|
|
107
144
|
if (typeof duration === 'number') return duration;
|
|
108
145
|
const m = /^(\d+(?:\.\d+)?)(s|m|h|d|w)$/.exec(duration);
|
|
109
|
-
if (!m)
|
|
146
|
+
if (!m)
|
|
147
|
+
throw new SpecError('E-TIME', `${duration} is not a duration.`, 'Write it like "45s", "30m", "6h", "7d" or "2w".');
|
|
110
148
|
return Math.round(Number(m[1]) * UNIT[m[2]]);
|
|
111
149
|
}
|
|
112
150
|
|
|
113
151
|
/**
|
|
114
|
-
* How long what lands in a farm's rewards
|
|
115
|
-
*
|
|
152
|
+
* How long what lands in a farm's rewards vault takes to be released to its seats, in seconds (a farm that shares what
|
|
153
|
+
* lands, `farm.rs` `release_span`): its longest lock tier, and a day at least.
|
|
116
154
|
*/
|
|
117
|
-
export const releaseSeconds =
|
|
155
|
+
export const releaseSeconds = f => Math.max(86_400, ...(f.tiers ?? []).map(t => toSeconds(t.lock ?? 0)));
|
|
118
156
|
|
|
119
157
|
/** A farm's release span in words: "a day", "30 days", "36 hours". */
|
|
120
158
|
export function releaseWords(f) {
|
|
@@ -123,16 +161,68 @@ export function releaseWords(f) {
|
|
|
123
161
|
return s % 3_600 === 0 ? `${s / 3_600} hours` : `${s} seconds`;
|
|
124
162
|
}
|
|
125
163
|
|
|
164
|
+
/**
|
|
165
|
+
* What a run sent late does, in words (schedule.rs `catches_up`): a sealed schedule's fixed amount pays every run due;
|
|
166
|
+
* any other run pays once, for the latest run due, and the runs before it lapse.
|
|
167
|
+
*/
|
|
168
|
+
export const lateRunWords = s =>
|
|
169
|
+
s.sealed && s.percent === undefined
|
|
170
|
+
? 'a run sent late pays every run due since'
|
|
171
|
+
: 'a run missed is missed: one sent late pays once, for the latest run due, and the runs before it lapse';
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Whether a farm's boost reads a number its own growth writes (`refresh_seat` writes it): each level counts from when it is
|
|
175
|
+
* written, so a page or a keeper should refresh seats.
|
|
176
|
+
*/
|
|
177
|
+
export const boostReadsGrowth = f =>
|
|
178
|
+
(f.boosts ?? []).some(
|
|
179
|
+
b =>
|
|
180
|
+
b.when?.attribute &&
|
|
181
|
+
listOf(f.growth).some(
|
|
182
|
+
g =>
|
|
183
|
+
b.when.attribute === (g.attribute ?? 'xp') ||
|
|
184
|
+
((g.levels ?? []).length > 0 && b.when.attribute === (g.level ?? 'level')),
|
|
185
|
+
),
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
/** A box's roll against what is in stock, in words (order.rs): the odds stay as written, whatever is left. */
|
|
189
|
+
export const STOCK_WORDS =
|
|
190
|
+
'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';
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* What a box that rolls does once an outcome can't be given (`whenOut`, order.rs `WhenOut`), in words. `sold`: it is
|
|
194
|
+
* sold (an open checks the stock); a box only other routes roll (a prize, a mission) never stops a sale.
|
|
195
|
+
*/
|
|
196
|
+
export function whenOutWords(b, { sold = b.active !== false } = {}) {
|
|
197
|
+
if (b.whenOut === 'drawLeft' && !sold)
|
|
198
|
+
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';
|
|
199
|
+
if (b.whenOut === 'drawLeft')
|
|
200
|
+
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)';
|
|
201
|
+
if (!sold)
|
|
202
|
+
return `${STOCK_WORDS}. A free roll (a prize, a recipe’s, a mission’s chest) is on the odds as written too: a draw on an outcome that is out misses and gives nothing back (a pack keeps what its other draws won; a roll that drew nothing waits to be asked again), so emptying a shared source before asking steers nothing. A pack that should draw among what is left needs "whenOut": "drawLeft"`;
|
|
203
|
+
return 'When an outcome runs out, the box stops selling: it isn’t sold while anything an outcome takes (an item’s units left, a pool’s tokens, a stash’s NFTs) couldn’t cover, at the most any outcome sharing it takes in one draw, this purchase’s draws and those of purchases still waiting for the oracle, 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';
|
|
204
|
+
}
|
|
205
|
+
|
|
126
206
|
/**
|
|
127
207
|
* 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
|
-
*
|
|
208
|
+
* 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
|
|
209
|
+
* a seat forfeits after the end.
|
|
130
210
|
*/
|
|
131
211
|
export function farmEndWords(f) {
|
|
132
212
|
if (!f.runs || f.rate) return null;
|
|
133
213
|
const span = releaseWords(f);
|
|
134
|
-
|
|
135
|
-
|
|
214
|
+
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)`;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* The token a farm's rewards are in (`rewards`, a token key): by default its staked token, or for a farm of
|
|
219
|
+
* collectibles the project's currency. One that names a pool (as farms did before they had a vault of their own) is
|
|
220
|
+
* read as that pool's token, and the checker refuses it.
|
|
221
|
+
*/
|
|
222
|
+
export function farmRewardsToken(f, tokens, currencyKey, poolCurrency = new Map()) {
|
|
223
|
+
if (f.rewards && tokens.has(f.rewards)) return f.rewards;
|
|
224
|
+
if (f.rewards && poolCurrency.has(f.rewards)) return poolCurrency.get(f.rewards);
|
|
225
|
+
return f.stakes && f.stakes !== 'collectibles' ? f.stakes : currencyKey;
|
|
136
226
|
}
|
|
137
227
|
|
|
138
228
|
/** A raise's or a shift's `by`: a number, or for a time a duration, which may be negative ("-1d"). */
|
|
@@ -147,21 +237,27 @@ export function toTime(time, now) {
|
|
|
147
237
|
const m = /^publish\+(.+)$/.exec(time);
|
|
148
238
|
if (m) return now + toSeconds(m[1]);
|
|
149
239
|
const at = Date.parse(time);
|
|
150
|
-
if (!Number.isFinite(at))
|
|
240
|
+
if (!Number.isFinite(at))
|
|
241
|
+
throw new SpecError(
|
|
242
|
+
'E-TIME',
|
|
243
|
+
`${time} is not a time.`,
|
|
244
|
+
'Write "publish", "publish+7d" or a date like "2026-11-01T18:00:00Z".',
|
|
245
|
+
);
|
|
151
246
|
return Math.floor(at / 1000);
|
|
152
247
|
}
|
|
153
248
|
|
|
154
|
-
export const rarityNumber =
|
|
155
|
-
|
|
156
|
-
export const
|
|
249
|
+
export const rarityNumber = rarity =>
|
|
250
|
+
typeof rarity === 'number' ? rarity : Math.max(0, RARITIES.indexOf(rarity ?? 'Common'));
|
|
251
|
+
export const utf8Bytes = s => new TextEncoder().encode(s ?? '').length;
|
|
252
|
+
export const isAddress = s => typeof s === 'string' && /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(s);
|
|
157
253
|
|
|
158
254
|
// ---------------------------------------------------------------- the model
|
|
159
255
|
|
|
160
256
|
/** Gives every part without one the next free program id (pools from 0, the rest from 1). */
|
|
161
257
|
function assignIds(list, field, first) {
|
|
162
|
-
const used = new Set(list.map(
|
|
258
|
+
const used = new Set(list.map(x => x[field]).filter(v => Number.isInteger(v)));
|
|
163
259
|
let next = first;
|
|
164
|
-
return list.map(
|
|
260
|
+
return list.map(x => {
|
|
165
261
|
if (Number.isInteger(x[field])) return x;
|
|
166
262
|
while (used.has(next)) next += 1;
|
|
167
263
|
used.add(next);
|
|
@@ -181,7 +277,17 @@ export const spendOf = (then, m) => {
|
|
|
181
277
|
function tokenOf(given) {
|
|
182
278
|
// "project" is the spec's word for a token the project mints; "game" is the older spelling of it.
|
|
183
279
|
const t = given.source === 'project' ? { ...given, source: 'game' } : given;
|
|
184
|
-
if (t.source === 'sol')
|
|
280
|
+
if (t.source === 'sol')
|
|
281
|
+
return {
|
|
282
|
+
...t,
|
|
283
|
+
name: t.name ?? 'SOL',
|
|
284
|
+
symbol: t.symbol ?? 'SOL',
|
|
285
|
+
decimals: 9,
|
|
286
|
+
program: 'spl-token',
|
|
287
|
+
mint: WSOL,
|
|
288
|
+
wsol: true,
|
|
289
|
+
gameMints: false,
|
|
290
|
+
};
|
|
185
291
|
const decimals = t.decimals ?? (t.source === 'game' ? 0 : undefined);
|
|
186
292
|
return {
|
|
187
293
|
...t,
|
|
@@ -203,29 +309,37 @@ function tokenOf(given) {
|
|
|
203
309
|
*/
|
|
204
310
|
export function model(economy, { now = 0 } = {}) {
|
|
205
311
|
const e = economy ?? {};
|
|
206
|
-
const tokens = new Map((e.tokens ?? []).map(
|
|
312
|
+
const tokens = new Map((e.tokens ?? []).map(t => [t.key, tokenOf(t)]));
|
|
207
313
|
const currencyKey = e.currency ?? (tokens.size === 1 ? [...tokens.keys()][0] : undefined);
|
|
208
314
|
const currency = tokens.get(currencyKey);
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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]));
|
|
221
|
-
const assigned = assignIds(order.map((i) => listed[i]), 'index', 0);
|
|
315
|
+
// Numbers are given in this order: the pools that have one, then the rest in the spec's order.
|
|
316
|
+
const listed = e.pools ?? [];
|
|
317
|
+
const order = [...listed.keys()].sort(
|
|
318
|
+
(a, b) => Number(!Number.isInteger(listed[a].index)) - Number(!Number.isInteger(listed[b].index)),
|
|
319
|
+
);
|
|
320
|
+
const assigned = assignIds(
|
|
321
|
+
order.map(i => listed[i]),
|
|
322
|
+
'index',
|
|
323
|
+
0,
|
|
324
|
+
);
|
|
222
325
|
const pools = listed.map((_, i) => assigned[order.indexOf(i)]);
|
|
223
|
-
|
|
326
|
+
// A farm's rewards land in a vault of its own: `rewardsToken` is their token.
|
|
327
|
+
const poolCurrency = new Map(pools.map(p => [p.key, p.currency]));
|
|
328
|
+
const farms = (e.farms ?? []).map(f => ({
|
|
329
|
+
...f,
|
|
330
|
+
rewardsToken: farmRewardsToken(f, tokens, currencyKey, poolCurrency),
|
|
331
|
+
}));
|
|
224
332
|
// A raffle: its NFT is first place's prize, held in the lottery's own stash.
|
|
225
|
-
const lotteries = (e.lotteries ?? []).map(
|
|
333
|
+
const lotteries = (e.lotteries ?? []).map(l => {
|
|
226
334
|
if (!l.raffle) return l;
|
|
227
335
|
const { nft, collection } = typeof l.raffle === 'string' ? { nft: l.raffle } : l.raffle;
|
|
228
|
-
return {
|
|
336
|
+
return {
|
|
337
|
+
...l,
|
|
338
|
+
places: [
|
|
339
|
+
{ percent: 0, prize: { gives: 'nft', nft, held: true, ...(collection ? { collection } : {}) } },
|
|
340
|
+
...(l.places ?? []),
|
|
341
|
+
],
|
|
342
|
+
};
|
|
229
343
|
});
|
|
230
344
|
const m = {
|
|
231
345
|
now,
|
|
@@ -237,7 +351,15 @@ export function model(economy, { now = 0 } = {}) {
|
|
|
237
351
|
currencyKey,
|
|
238
352
|
currency,
|
|
239
353
|
/** The collection the setup makes (null when the spec says `"collection": false`: a project of tokens only). */
|
|
240
|
-
collection:
|
|
354
|
+
collection:
|
|
355
|
+
e.collection === false
|
|
356
|
+
? null
|
|
357
|
+
: {
|
|
358
|
+
name: e.collection?.name ?? (e.name ?? 'Project').slice(0, 32),
|
|
359
|
+
royaltyPercent: e.collection?.royaltyPercent ?? 5,
|
|
360
|
+
uri: e.collection?.uri ?? '',
|
|
361
|
+
...e.collection,
|
|
362
|
+
},
|
|
241
363
|
/** Core collections the owner already has that the project also takes (its update authority is the owner). */
|
|
242
364
|
collections: e.collections ?? [],
|
|
243
365
|
/** How many compressed NFTs the project's tree holds (`compressedTree`), when a prize gives one. */
|
|
@@ -247,6 +369,7 @@ export function model(economy, { now = 0 } = {}) {
|
|
|
247
369
|
items: assignIds(e.items ?? [], 'id', 1),
|
|
248
370
|
pools,
|
|
249
371
|
stashes: assignIds(e.stashes ?? [], 'id', 1),
|
|
372
|
+
tables: assignIds(e.tables ?? [], 'id', 1),
|
|
250
373
|
boxes: [],
|
|
251
374
|
payments: e.payments ?? [],
|
|
252
375
|
recipes: assignIds(e.recipes ?? [], 'id', 1),
|
|
@@ -265,8 +388,8 @@ export function model(economy, { now = 0 } = {}) {
|
|
|
265
388
|
// A shop item is a box of one entry (the program sells items only through boxes). Its box takes the item's price,
|
|
266
389
|
// rise, fall, split and gate; it is numbered with the boxes, after the ones the spec numbered itself.
|
|
267
390
|
const shop = m.items
|
|
268
|
-
.filter(
|
|
269
|
-
.map(
|
|
391
|
+
.filter(it => Number(it.price ?? 0) > 0 || it.free)
|
|
392
|
+
.map(it => ({
|
|
270
393
|
key: `shop-${it.key}`,
|
|
271
394
|
...(Number.isInteger(it.shopBox) ? { id: it.shopBox } : {}),
|
|
272
395
|
name: it.name,
|
|
@@ -276,20 +399,23 @@ export function model(economy, { now = 0 } = {}) {
|
|
|
276
399
|
...(it.split ? { split: it.split } : {}),
|
|
277
400
|
...(it.gate ? { gate: it.gate } : {}),
|
|
278
401
|
// When it sells, and how many one wallet may buy.
|
|
279
|
-
...Object.fromEntries(
|
|
402
|
+
...Object.fromEntries(
|
|
403
|
+
['opens', 'closes', 'perWallet', 'perWalletEach'].filter(k => it[k] !== undefined).map(k => [k, it[k]]),
|
|
404
|
+
),
|
|
280
405
|
active: it.active !== false,
|
|
281
406
|
outcomes: [{ gives: 'item', item: it.key, weight: 1 }],
|
|
282
407
|
shopOf: it.key,
|
|
283
408
|
}));
|
|
284
409
|
const numbered = assignIds([...(e.boxes ?? []), ...shop], 'id', 1);
|
|
285
|
-
m.boxes = numbered.filter(
|
|
286
|
-
m.shop = numbered.filter(
|
|
410
|
+
m.boxes = numbered.filter(b => !b.shopOf);
|
|
411
|
+
m.shop = numbered.filter(b => b.shopOf);
|
|
287
412
|
/** Every box the program holds: the spec's own and the shop's. */
|
|
288
413
|
m.allBoxes = numbered;
|
|
289
|
-
const index =
|
|
414
|
+
const index = list => new Map(list.map(x => [x.key, x]));
|
|
290
415
|
m.item = index(m.items);
|
|
291
416
|
m.pool = index(m.pools);
|
|
292
417
|
m.stash = index(m.stashes);
|
|
418
|
+
m.table = index(m.tables);
|
|
293
419
|
m.box = index(m.allBoxes);
|
|
294
420
|
m.recipe = index(m.recipes);
|
|
295
421
|
m.farm = index(m.farms);
|
|
@@ -299,9 +425,9 @@ export function model(economy, { now = 0 } = {}) {
|
|
|
299
425
|
m.auction = index(m.auctions);
|
|
300
426
|
m.hold = index(m.holds);
|
|
301
427
|
/** The token a pool holds. */
|
|
302
|
-
m.poolToken =
|
|
428
|
+
m.poolToken = key => tokens.get(m.pool.get(key)?.currency);
|
|
303
429
|
/** The token a part is priced in (its own `currency`, else the game's). */
|
|
304
|
-
m.priceToken =
|
|
430
|
+
m.priceToken = part => tokens.get(part?.currency ?? currencyKey);
|
|
305
431
|
return m;
|
|
306
432
|
}
|
|
307
433
|
|
|
@@ -311,17 +437,25 @@ export function model(economy, { now = 0 } = {}) {
|
|
|
311
437
|
* NFT (`compressed`, its asset id).
|
|
312
438
|
*/
|
|
313
439
|
export function stashNfts(s) {
|
|
314
|
-
return (s.nfts ?? []).map(
|
|
315
|
-
if (typeof n === 'string') return { kind: 'core', asset: n, collection: s.collection ?? null };
|
|
316
|
-
|
|
317
|
-
if (n.
|
|
318
|
-
|
|
440
|
+
return (s.nfts ?? []).map(n => {
|
|
441
|
+
if (typeof n === 'string') return { kind: 'core', asset: n, collection: s.collection ?? null, weight: 1 };
|
|
442
|
+
const weight = n.weight ?? 1;
|
|
443
|
+
if (n.mint)
|
|
444
|
+
return {
|
|
445
|
+
kind: 'token-metadata',
|
|
446
|
+
asset: n.mint,
|
|
447
|
+
programmable: Boolean(n.programmable),
|
|
448
|
+
ruleSet: n.ruleSet ?? null,
|
|
449
|
+
weight,
|
|
450
|
+
};
|
|
451
|
+
if (n.compressed) return { kind: 'compressed', asset: n.compressed, weight };
|
|
452
|
+
return { kind: 'core', asset: n.asset, collection: n.collection ?? s.collection ?? null, weight };
|
|
319
453
|
});
|
|
320
454
|
}
|
|
321
455
|
|
|
322
456
|
/**
|
|
323
457
|
* 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>"),
|
|
458
|
+
* "item" (`item`), "tokens" (`amount`, `from` "mint" with `currency`, or "pool:<key>"),
|
|
325
459
|
* "nft" (`nft`, an address the stash `stash` lists) or "roll" (a roll of `box`; in a recipe, on the collectible kept
|
|
326
460
|
* at `on`). A grant's older `gives` ({ item }, { pool, amount }, { roll }) reads as its line. Null for none.
|
|
327
461
|
*/
|
|
@@ -334,11 +468,12 @@ export function prizeLine(p) {
|
|
|
334
468
|
return null;
|
|
335
469
|
}
|
|
336
470
|
|
|
337
|
-
/** The pool a prize line pays from:
|
|
338
|
-
export const prizePool =
|
|
471
|
+
/** The pool a prize line pays from: tokens' "pool:<key>". */
|
|
472
|
+
export const prizePool = p => (p?.gives === 'tokens' && p.from?.startsWith('pool:') ? p.from.slice(5) : null);
|
|
339
473
|
|
|
340
474
|
/** The token a prize line pays in: its pool's, or the one it mints. */
|
|
341
|
-
export const prizeToken = (p, m) =>
|
|
475
|
+
export const prizeToken = (p, m) =>
|
|
476
|
+
prizePool(p) ? m.poolToken(prizePool(p)) : p?.gives === 'tokens' ? m.tokens.get(p.currency ?? m.currencyKey) : null;
|
|
342
477
|
|
|
343
478
|
/**
|
|
344
479
|
* Every prize the spec gives (prize.rs): a grant's, a lottery place's, a contest's winners', a mission tier's and a
|
|
@@ -348,12 +483,40 @@ export const prizeToken = (p, m) => (prizePool(p) ? m.poolToken(prizePool(p)) :
|
|
|
348
483
|
*/
|
|
349
484
|
export function prizes(m) {
|
|
350
485
|
const list = [];
|
|
351
|
-
const add = (p, where, by, route, strict, once = false) =>
|
|
352
|
-
|
|
353
|
-
for (const
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
486
|
+
const add = (p, where, by, route, strict, once = false) =>
|
|
487
|
+
p && list.push({ prize: p, where, by, route, strict, once });
|
|
488
|
+
for (const g of m.grants)
|
|
489
|
+
add(
|
|
490
|
+
prizeLine(g.prize ?? g.gives),
|
|
491
|
+
`economy.grants.${g.key}.${g.prize ? 'prize' : 'gives'}`,
|
|
492
|
+
`grant:${g.key}`,
|
|
493
|
+
'grant',
|
|
494
|
+
true,
|
|
495
|
+
Number(g.limits?.total ?? 0) === 1,
|
|
496
|
+
);
|
|
497
|
+
for (const l of m.lotteries)
|
|
498
|
+
(l.places ?? []).forEach((p, i) =>
|
|
499
|
+
add(
|
|
500
|
+
placeOf(p).prize,
|
|
501
|
+
`economy.lotteries.${l.key}.places[${i}].prize`,
|
|
502
|
+
`lottery:${l.key}`,
|
|
503
|
+
`place ${i + 1}`,
|
|
504
|
+
false,
|
|
505
|
+
!l.every,
|
|
506
|
+
),
|
|
507
|
+
);
|
|
508
|
+
for (const c of m.contests)
|
|
509
|
+
add(prizeLine(c.prize), `economy.contests.${c.key}.prize`, `contest:${c.key}`, 'contest', false);
|
|
510
|
+
for (const f of m.farms)
|
|
511
|
+
(f.tiers ?? []).forEach((t, i) =>
|
|
512
|
+
add(prizeLine(t.prize), `economy.farms.${f.key}.tiers[${i}].prize`, `farm:${f.key}`, 'mission', false),
|
|
513
|
+
);
|
|
514
|
+
for (const r of m.recipes)
|
|
515
|
+
(r.out ?? []).forEach((o, i) =>
|
|
516
|
+
o.gives !== 'roll' || o.on === undefined
|
|
517
|
+
? add(o, `economy.recipes.${r.key}.out[${i}]`, `recipe:${r.key}`, 'recipe', true)
|
|
518
|
+
: null,
|
|
519
|
+
);
|
|
357
520
|
return list;
|
|
358
521
|
}
|
|
359
522
|
|
|
@@ -364,7 +527,10 @@ export function prizes(m) {
|
|
|
364
527
|
* canopy, bytes }
|
|
365
528
|
*/
|
|
366
529
|
export function compressedTree(m) {
|
|
367
|
-
|
|
530
|
+
const boxGives = m.allBoxes.some(b =>
|
|
531
|
+
(b.outcomes ?? []).flatMap(outcomeParts).some(o => o.gives === 'item' && o.compressed),
|
|
532
|
+
);
|
|
533
|
+
if (!boxGives && !prizes(m).some(p => p.prize.gives === 'item' && p.prize.compressed)) return null;
|
|
368
534
|
const depth = Math.min(20, Math.max(14, Math.ceil(Math.log2(Math.max(1, m.treeHolds)))));
|
|
369
535
|
const canopy = Math.min(depth - 10, 8);
|
|
370
536
|
return { depth, buffer: 64, canopy, bytes: treeSize(depth, 64, canopy) };
|
|
@@ -375,11 +541,17 @@ export function compressedTree(m) {
|
|
|
375
541
|
* mission's roll on the NFT leaving it and a recipe's roll on a collectible it keeps. [{ box, by, route }]
|
|
376
542
|
*/
|
|
377
543
|
export function prizeRolls(m) {
|
|
378
|
-
const rolls = prizes(m)
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
for (const
|
|
382
|
-
|
|
544
|
+
const rolls = prizes(m)
|
|
545
|
+
.filter(p => p.prize.gives === 'roll')
|
|
546
|
+
.map(p => ({ box: p.prize.box, by: p.by, route: p.route }));
|
|
547
|
+
for (const a of m.auctions)
|
|
548
|
+
if (a.lot?.roll) rolls.push({ box: a.lot.roll, by: `auction:${a.key}`, route: 'auction' });
|
|
549
|
+
for (const f of m.farms)
|
|
550
|
+
for (const t of f.tiers ?? []) if (t.roll) rolls.push({ box: t.roll, by: `farm:${f.key}`, route: 'mission' });
|
|
551
|
+
for (const r of m.recipes)
|
|
552
|
+
for (const o of r.out ?? [])
|
|
553
|
+
if (o.gives === 'roll' && o.on !== undefined) rolls.push({ box: o.box, by: `recipe:${r.key}`, route: 'recipe' });
|
|
554
|
+
return rolls.filter(r => r.box);
|
|
383
555
|
}
|
|
384
556
|
|
|
385
557
|
/**
|
|
@@ -390,9 +562,21 @@ export function prizeRolls(m) {
|
|
|
390
562
|
*/
|
|
391
563
|
export function rollsOf(m) {
|
|
392
564
|
const rolls = [...prizeRolls(m)];
|
|
393
|
-
for (const b of m.allBoxes)
|
|
394
|
-
|
|
395
|
-
|
|
565
|
+
for (const b of m.allBoxes)
|
|
566
|
+
for (const o of b.outcomes ?? [])
|
|
567
|
+
for (const x of outcomeParts(o))
|
|
568
|
+
if (x.gives === 'roll' && x.box) rolls.push({ box: x.box, by: `box:${b.key}`, route: 'box' });
|
|
569
|
+
const seen = new Set(rolls.map(r => `${r.box}|${r.by}|${r.route}`));
|
|
570
|
+
const rollsFrom = key => [
|
|
571
|
+
...new Set(
|
|
572
|
+
m.box.get(key)
|
|
573
|
+
? (m.box.get(key).outcomes ?? [])
|
|
574
|
+
.flatMap(outcomeParts)
|
|
575
|
+
.filter(x => x.gives === 'roll' && x.box)
|
|
576
|
+
.map(x => x.box)
|
|
577
|
+
: [],
|
|
578
|
+
),
|
|
579
|
+
];
|
|
396
580
|
// A breadth-first walk: each roll of a box is also a roll of every box it rolls, once for each route.
|
|
397
581
|
for (let i = 0; i < rolls.length; i++) {
|
|
398
582
|
const r = rolls[i];
|
|
@@ -406,46 +590,127 @@ export function rollsOf(m) {
|
|
|
406
590
|
return rolls;
|
|
407
591
|
}
|
|
408
592
|
|
|
409
|
-
/** A prize line in words: "one Badge", "100 GOLD from “rewards”", "100 GOLD, minted", "
|
|
593
|
+
/** 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
594
|
export function prizeWords(p, m) {
|
|
411
595
|
const line = prizeLine(p);
|
|
412
596
|
if (!line) return 'nothing';
|
|
413
|
-
const sym =
|
|
414
|
-
if (line.gives === 'item')
|
|
415
|
-
|
|
416
|
-
if (line.gives === '
|
|
417
|
-
if (line.gives === 'nft'
|
|
597
|
+
const sym = key => m.tokens.get(key)?.symbol ?? key;
|
|
598
|
+
if (line.gives === 'item')
|
|
599
|
+
return `one ${m.item.get(line.item)?.name ?? line.item}${line.compressed ? ' (compressed)' : ''}`;
|
|
600
|
+
if (line.gives === 'raffled') return 'an NFT drawn at random from the lottery’s own stash';
|
|
601
|
+
if (line.gives === 'nft' && line.held)
|
|
602
|
+
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)`;
|
|
603
|
+
if (line.gives === 'nft')
|
|
604
|
+
return `the NFT ${String(line.nft).slice(0, 4)}…${String(line.nft).slice(-4)} of “${m.stash.get(line.stash)?.name ?? line.stash}”`;
|
|
418
605
|
if (line.gives === 'roll') return `a roll of “${m.box.get(line.box)?.name ?? line.box}”`;
|
|
419
606
|
const pool = prizePool(line);
|
|
420
607
|
const split = line.split ? ', split by what each winner put in' : '';
|
|
421
|
-
return pool
|
|
608
|
+
return pool
|
|
609
|
+
? `${line.amount} ${sym(m.pool.get(pool)?.currency)} from “${pool}”${split}`
|
|
610
|
+
: `${line.amount} ${sym(line.currency ?? m.currencyKey)}, minted${split}`;
|
|
422
611
|
}
|
|
423
612
|
|
|
424
613
|
/**
|
|
425
|
-
* What one box outcome gives (catalog.rs
|
|
426
|
-
* "
|
|
614
|
+
* What one box outcome gives (catalog.rs), each in the box's own words: "item", "pay" (a fixed amount from a pool),
|
|
615
|
+
* "stash" (an NFT of a stash at random), "mint"
|
|
427
616
|
* (tokens minted), "nft" (one NFT a stash lists) or "roll" (a roll of another box). A bundle gives its `prizes`; "burn"
|
|
428
617
|
* and "nothing" give none.
|
|
429
618
|
*/
|
|
430
619
|
export function outcomeParts(o) {
|
|
431
620
|
if (!o) return [];
|
|
432
621
|
if (o.gives === 'bundle') return (o.prizes ?? []).flatMap(outcomeParts);
|
|
433
|
-
if (o.gives === '
|
|
434
|
-
if (o.gives === 'tokens')
|
|
622
|
+
if (o.gives === 'nothing') return [];
|
|
623
|
+
if (o.gives === 'tokens')
|
|
624
|
+
return [
|
|
625
|
+
o.from?.startsWith('pool:')
|
|
626
|
+
? { gives: 'pay', pool: o.from.slice(5), amount: o.amount }
|
|
627
|
+
: { gives: 'mint', currency: o.currency, amount: o.amount },
|
|
628
|
+
];
|
|
435
629
|
return [o];
|
|
436
630
|
}
|
|
437
631
|
|
|
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
632
|
/** Whether a box leaves its outcome to the oracle (catalog.rs `rolls`): more than one outcome, or an NFT at random. */
|
|
442
|
-
|
|
633
|
+
/** How many draws one open of a box makes: its slots' counts, or one. */
|
|
634
|
+
export const drawsOf = b => ((b.slots ?? []).length ? b.slots.reduce((n, s) => n + Number(s.count), 0) : 1);
|
|
635
|
+
|
|
636
|
+
/** Whether a box's prize costs its settle a collectible's rent: an item minted as a Core NFT, or a fresh compressed one
|
|
637
|
+
* (its traits' Sheet). A plain compressed NFT costs no rent. */
|
|
638
|
+
export const holdsRent = (p, m) =>
|
|
639
|
+
p.gives === 'item' && (!p.compressed || (m?.item.get(p.item)?.drawn ?? []).length > 0);
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* The units of `SETTLE_RENT_UNIT` a settle of box `b`'s order may spend (`loot.settleRent`), which js/play funds its
|
|
643
|
+
* throwaway settle key with and gets back unspent: for each draw, each part of the richest outcome that opens an
|
|
644
|
+
* account (a collectible, a fresh compressed NFT's Sheet, a stash's NFT); once an order, each source of tokens it may
|
|
645
|
+
* pay, and the fate of the NFT it rolls on.
|
|
646
|
+
*/
|
|
647
|
+
export function settleUnits(b, m) {
|
|
648
|
+
const opens = p => holdsRent(p, m) || p.gives === 'stash' || p.gives === 'nft';
|
|
649
|
+
const parts = (b.outcomes ?? []).map(o => outcomeParts(o));
|
|
650
|
+
const most = Math.max(0, ...parts.map(ps => ps.filter(opens).length));
|
|
651
|
+
const sources = new Set(
|
|
652
|
+
parts
|
|
653
|
+
.flat()
|
|
654
|
+
.filter(p => ['tokens', 'pay', 'mint'].includes(p.gives))
|
|
655
|
+
.map(p => prizePool(p) ?? `mint:${p.currency ?? ''}`),
|
|
656
|
+
);
|
|
657
|
+
return drawsOf(b) * most + sources.size + (b.rollsOn ? 1 : 0);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
export const boxRolls = (b, m) =>
|
|
661
|
+
(b.outcomes ?? []).length > 1 ||
|
|
662
|
+
(b.outcomes ?? [])
|
|
663
|
+
.flatMap(outcomeParts)
|
|
664
|
+
.some(p => p.gives === 'stash' || (p.gives === 'item' && (m?.item.get(p.item)?.drawn ?? []).length > 0));
|
|
665
|
+
|
|
666
|
+
/** Whether a box sells with `buy`, in one transaction (buy.rs): it doesn't roll and rolls on no NFT, any number of draws
|
|
667
|
+
* a sale (a fixed pack: its prizes given once for each draw). */
|
|
668
|
+
export const boxBuys = (b, m) => !boxRolls(b, m) && !b.rollsOn;
|
|
669
|
+
|
|
670
|
+
/** 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. */
|
|
671
|
+
const RENEW_BY = -2_147_483_647;
|
|
443
672
|
|
|
444
|
-
/**
|
|
445
|
-
|
|
673
|
+
/**
|
|
674
|
+
* What a recipe's lines owe the life of each collectible they take (an item's `life`), as the program's needs and
|
|
675
|
+
* raises (craft.rs `Need`, `Raise`), at each collectible's place in the recipe (each burned unit, then each kept one, in
|
|
676
|
+
* line order; a pay line takes none): ready after `readyAfter` (`made` older than it), for `lasts` (`made` within it),
|
|
677
|
+
* on every line that takes it; `uses` times (`used` raised by 1 up to it) and a `cooldown` (`next_use` passed, then
|
|
678
|
+
* 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.
|
|
679
|
+
* { needs, raises }, in the program's shapes.
|
|
680
|
+
*/
|
|
681
|
+
export function lifeLines(r, m) {
|
|
682
|
+
const places = (r.in ?? []).flatMap(l =>
|
|
683
|
+
l.takes === 'pay' ? [] : Array(l.takes === 'burn' ? (l.count ?? 1) : 1).fill(l),
|
|
684
|
+
);
|
|
685
|
+
const needs = [];
|
|
686
|
+
const raises = [];
|
|
687
|
+
places.forEach((l, place) => {
|
|
688
|
+
const it = l.item ? m.item.get(l.item) : null;
|
|
689
|
+
if (!it?.life || it.kind === 'normal') return;
|
|
690
|
+
const [ready, lasts, uses, cooldown] = [
|
|
691
|
+
toSeconds(it.life.readyAfter ?? 0),
|
|
692
|
+
toSeconds(it.life.lasts ?? 0),
|
|
693
|
+
it.life.uses ?? 0,
|
|
694
|
+
toSeconds(it.life.cooldown ?? 0),
|
|
695
|
+
];
|
|
696
|
+
if (ready) needs.push({ place, key: 'made', test: { Older: [ready] } });
|
|
697
|
+
if (lasts) needs.push({ place, key: 'made', test: { Within: [lasts] } });
|
|
698
|
+
if (l.takes === 'renew') raises.push({ place, shift: { key: 'used', by: RENEW_BY, most: 0, from_now: false } });
|
|
699
|
+
if (l.takes !== 'use' && l.takes !== 'staked') return;
|
|
700
|
+
if (uses) raises.push({ place, shift: { key: 'used', by: 1, most: uses, from_now: false } });
|
|
701
|
+
if (cooldown) {
|
|
702
|
+
needs.push({ place, key: 'next_use', test: 'Passed' });
|
|
703
|
+
raises.push({ place, shift: { key: 'next_use', by: cooldown, most: cooldown, from_now: true } });
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
return { needs, raises };
|
|
707
|
+
}
|
|
446
708
|
|
|
447
|
-
/** A lottery's place as { percent, prize }: a number is a
|
|
448
|
-
export const placeOf =
|
|
709
|
+
/** A lottery's place as { percent, prize }: a number is a percent of what the round pays from, and no prize. */
|
|
710
|
+
export const placeOf = p =>
|
|
711
|
+
typeof p === 'object' && p !== null
|
|
712
|
+
? { percent: p.percent ?? 0, prize: prizeLine(p.prize), times: p.times ?? 1 }
|
|
713
|
+
: { percent: p, prize: null, times: 1 };
|
|
449
714
|
|
|
450
715
|
/** "pool:house" → { kind: 'pool', key: 'house' }; "burn" → { kind: 'burn' }. */
|
|
451
716
|
export function destination(to) {
|
|
@@ -457,13 +722,15 @@ export function destination(to) {
|
|
|
457
722
|
}
|
|
458
723
|
|
|
459
724
|
/** The share with its basis points, or null bps when the percent is not whole hundredths. */
|
|
460
|
-
export const sharesOf =
|
|
725
|
+
export const sharesOf = split =>
|
|
726
|
+
(split ?? []).map(s => ({ ...destination(s.to), to: s.to, percent: s.percent, bps: toBps(s.percent) }));
|
|
461
727
|
|
|
462
728
|
/**
|
|
463
729
|
* The program's `To` for a destination, with pool indexes resolved (the IDL's own shape). A wallet ("wallet:<address>")
|
|
464
730
|
* 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
731
|
* (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
|
|
732
|
+
* ("@pot:<lottery id>:<pot id>") and "farm:<key>" that farm's rewards vault ("@rewards:<id>"), which the plan resolves once the game is
|
|
733
|
+
* known.
|
|
467
734
|
*/
|
|
468
735
|
export function toEnum(to, m) {
|
|
469
736
|
const d = destination(to);
|
|
@@ -472,7 +739,9 @@ export function toEnum(to, m) {
|
|
|
472
739
|
if (d.kind === 'pool') return { Pool: [m.pool.get(d.key)?.index ?? 0] };
|
|
473
740
|
if (d.kind === 'account') return { Account: [d.key] };
|
|
474
741
|
if (d.kind === 'wallet') return { Wallet: [d.key] };
|
|
475
|
-
if (d.kind === 'lottery')
|
|
742
|
+
if (d.kind === 'lottery')
|
|
743
|
+
return { Account: [`@pot:${m.lottery.get(d.key)?.id ?? 0}:${m.lottery.get(d.key)?.pot ?? 0}`] };
|
|
744
|
+
if (d.kind === 'farm') return { Account: [`@rewards:${m.farm.get(d.key)?.id ?? 0}`] };
|
|
476
745
|
if (d.kind === 'chosen') return { Chosen: { item: d.key ? (m.item.get(d.key)?.id ?? 0) : null } };
|
|
477
746
|
return { Holder: [d.key] };
|
|
478
747
|
}
|
|
@@ -484,13 +753,28 @@ export function toEnum(to, m) {
|
|
|
484
753
|
export function ruleOf(rule, m) {
|
|
485
754
|
if (!rule) return null;
|
|
486
755
|
const time = { ahead: 'Ahead', passed: 'Passed' }[rule.time];
|
|
487
|
-
|
|
488
|
-
|
|
756
|
+
// Any of several items: the game's own collection, its `item_id` one of theirs.
|
|
757
|
+
if (rule.items)
|
|
758
|
+
return {
|
|
759
|
+
of: { Collection: ['@collection'] },
|
|
760
|
+
key: 'item_id',
|
|
761
|
+
test: { OneOf: { texts: rule.items.map(k => String(m.item.get(k)?.id ?? 0)) } },
|
|
762
|
+
};
|
|
763
|
+
const texts = rule.oneOf ?? (rule.equals !== undefined ? [rule.equals] : null);
|
|
764
|
+
const test =
|
|
765
|
+
rule.atLeast !== undefined
|
|
766
|
+
? { AtLeast: [rule.atLeast] }
|
|
767
|
+
: texts
|
|
768
|
+
? { OneOf: { texts: texts.map(String) } }
|
|
769
|
+
: (time ?? 'Any');
|
|
770
|
+
const of = rule.item
|
|
771
|
+
? { Item: [m.item.get(rule.item)?.id ?? 0] }
|
|
772
|
+
: { Collection: [rule.collection ?? '@collection'] };
|
|
489
773
|
return { of, key: rule.attribute ?? '', test };
|
|
490
774
|
}
|
|
491
775
|
|
|
492
776
|
/** One thing or a list of them, as a list: a farm's `gate` and `growth` take either. */
|
|
493
|
-
export const listOf =
|
|
777
|
+
export const listOf = x => (x == null ? [] : Array.isArray(x) ? x : [x]);
|
|
494
778
|
|
|
495
779
|
/**
|
|
496
780
|
* The program's `Gate` for a spec gate: a few wallets (`wallets`), a list of any size (`list`: an allowlist, its root and
|
|
@@ -502,7 +786,12 @@ export const gateOf = (gate, m) => {
|
|
|
502
786
|
if (gate.wallets) return { Wallets: [gate.wallets] };
|
|
503
787
|
if (gate.staked) {
|
|
504
788
|
const farm = m.farm.get(gate.staked.farm);
|
|
505
|
-
return {
|
|
789
|
+
return {
|
|
790
|
+
Staked: {
|
|
791
|
+
farm: `@farm:${farm?.id ?? 0}`,
|
|
792
|
+
at_least: toUnits(gate.staked.atLeast ?? 0, m.tokens.get(farm?.stakes)?.decimals ?? 0),
|
|
793
|
+
},
|
|
794
|
+
};
|
|
506
795
|
}
|
|
507
796
|
if (gate.list) {
|
|
508
797
|
const { root, depth } = gateList(gate.list);
|
|
@@ -512,40 +801,56 @@ export const gateOf = (gate, m) => {
|
|
|
512
801
|
};
|
|
513
802
|
|
|
514
803
|
/** The program's `PerWallet` for a part's "perWallet" (in all) and "perWalletEach" ({ count, every }). */
|
|
515
|
-
export const perWalletOf =
|
|
804
|
+
export const perWalletOf = part => ({
|
|
516
805
|
total: part?.perWallet ?? 0,
|
|
517
806
|
per_window: part?.perWalletEach?.count ?? 0,
|
|
518
807
|
window: part?.perWalletEach ? toSeconds(part.perWalletEach.every ?? '1d') : 0,
|
|
519
808
|
});
|
|
520
809
|
|
|
521
810
|
/** The program's `Window` for a part's "opens" and "closes" (times; 0 leaves a side open). */
|
|
522
|
-
export const windowOf = (part, now) => ({
|
|
811
|
+
export const windowOf = (part, now) => ({
|
|
812
|
+
opens_at: BigInt(part?.opens ? toTime(part.opens, now) : 0),
|
|
813
|
+
closes_at: BigInt(part?.closes ? toTime(part.closes, now) : 0),
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
/** Whether lottery `l` pays its places from a pot it keeps (`"pot"`, `PaysFrom::Pot`); false when each round pays from
|
|
817
|
+
* its own ticket money and sends what its places leave to `"rest"` (`PaysFrom::Round`). */
|
|
818
|
+
export const keepsPot = l => Number.isInteger(l?.pot);
|
|
819
|
+
|
|
820
|
+
/** The program's `PaysFrom` for a lottery: `{ Pot: [id] }` or `{ Round: { rest } }` (its `rest` as `toEnum` gives it). */
|
|
821
|
+
export const paysFromOf = (l, m) => (keepsPot(l) ? { Pot: [l.pot] } : { Round: { rest: toEnum(l.rest, m) } });
|
|
523
822
|
|
|
524
823
|
/** A lottery's split of ticket money: its own, and "cutPercent" as a share to the treasury. */
|
|
525
|
-
export const lotterySplit =
|
|
824
|
+
export const lotterySplit = l => [
|
|
825
|
+
...(l.split ?? []),
|
|
826
|
+
...(l.cutPercent ? [{ to: 'treasury', percent: l.cutPercent }] : []),
|
|
827
|
+
];
|
|
526
828
|
|
|
527
829
|
/** The program's `Effect` for a spec effect: a flat share, or a share per point of the attribute. */
|
|
528
830
|
export function effectOf(effect) {
|
|
529
|
-
if (effect?.perPointPercent !== undefined)
|
|
831
|
+
if (effect?.perPointPercent !== undefined)
|
|
832
|
+
return { PerPoint: { bps: Math.round(effect.perPointPercent * 100), most: effect.upTo ?? 4_294_967_295 } };
|
|
530
833
|
return { Flat: [Math.round((effect?.addPercent ?? 0) * 100)] };
|
|
531
834
|
}
|
|
532
835
|
|
|
533
836
|
/** A box's outcomes with their program weights: chances become whole weights (hundredths of a basis point). */
|
|
534
837
|
export function weights(box) {
|
|
535
838
|
const outcomes = box.outcomes ?? [];
|
|
536
|
-
if (outcomes.every(
|
|
537
|
-
const raw = outcomes.map(
|
|
839
|
+
if (outcomes.every(o => Number.isInteger(o.weight))) return outcomes.map(o => o.weight);
|
|
840
|
+
const raw = outcomes.map(o => Math.round((o.chance ?? 0) * 10_000));
|
|
538
841
|
const gcd = (a, b) => (b ? gcd(b, a % b) : a);
|
|
539
842
|
const g = raw.reduce((a, b) => gcd(a, b), 0) || 1;
|
|
540
|
-
return raw.map(
|
|
843
|
+
return raw.map(w => w / g);
|
|
541
844
|
}
|
|
542
845
|
|
|
543
846
|
/** Every place the spec names a pool as somewhere money leaves from, with what takes it. */
|
|
544
847
|
export function poolTakers(m) {
|
|
545
848
|
const takers = [];
|
|
546
849
|
const add = (pool, by, how) => pool && takers.push({ pool, by, how });
|
|
547
|
-
for (const b of m.allBoxes)
|
|
548
|
-
|
|
850
|
+
for (const b of m.allBoxes)
|
|
851
|
+
for (const o of (b.outcomes ?? []).flatMap(outcomeParts))
|
|
852
|
+
if (o.gives === 'pay') add(o.pool, `box:${b.key}`, 'pays prizes');
|
|
853
|
+
for (const p of prizes(m)) add(prizePool(p.prize), p.by, `pays prizes (${p.route})`);
|
|
549
854
|
for (const s of m.schedules) {
|
|
550
855
|
if (s.does === 'burn') add(s.pool, `schedule:${s.key}`, 'burns a share');
|
|
551
856
|
if (s.does === 'move') add(s.from, `schedule:${s.key}`, 'moves a share out');
|
|
@@ -553,13 +858,13 @@ export function poolTakers(m) {
|
|
|
553
858
|
// from a vault of its own and names no pool.
|
|
554
859
|
if (s.does === 'pay') add(s.from, `schedule:${s.key}`, 'pays out on a timer');
|
|
555
860
|
}
|
|
556
|
-
for (const
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
861
|
+
for (const d of m.decisions)
|
|
862
|
+
if (d.does === 'payout' || d.does === 'burn')
|
|
863
|
+
add(
|
|
864
|
+
d.pool,
|
|
865
|
+
`decision:${d.key}`,
|
|
866
|
+
d.does === 'payout' ? 'pays out on the authority’s word' : 'burns on the authority’s word',
|
|
867
|
+
);
|
|
563
868
|
return takers;
|
|
564
869
|
}
|
|
565
870
|
|
|
@@ -567,11 +872,16 @@ export function poolTakers(m) {
|
|
|
567
872
|
export function poolFillers(m) {
|
|
568
873
|
const fillers = [];
|
|
569
874
|
const add = (pool, by, how) => pool && fillers.push({ pool, by, how });
|
|
570
|
-
const fromSplit = (split, by) =>
|
|
571
|
-
|
|
875
|
+
const fromSplit = (split, by) =>
|
|
876
|
+
sharesOf(split).forEach(s => s.kind === 'pool' && add(s.key, by, `${s.percent}% of each payment`));
|
|
877
|
+
for (const p of m.pools)
|
|
878
|
+
if (p.seed !== undefined && Number(p.seed) > 0) add(p.key, `pool:${p.key}`, 'the owner’s seed');
|
|
572
879
|
for (const b of m.allBoxes) fromSplit(b.split, b.shopOf ? `item:${b.shopOf}` : `box:${b.key}`);
|
|
573
880
|
for (const p of m.payments) fromSplit(p.split, `payment:${p.key}`);
|
|
574
|
-
for (const r of m.recipes)
|
|
881
|
+
for (const r of m.recipes)
|
|
882
|
+
for (const i of r.in ?? [])
|
|
883
|
+
if (i.takes === 'pay' && destination(i.to).kind === 'pool')
|
|
884
|
+
add(destination(i.to).key, `recipe:${r.key}`, 'a craft pays in');
|
|
575
885
|
for (const s of m.schedules) {
|
|
576
886
|
if (s.does === 'issue') add(s.pool, `schedule:${s.key}`, 'mints into it');
|
|
577
887
|
if (s.does === 'move') add(s.to, `schedule:${s.key}`, 'moves a share in');
|
|
@@ -581,3 +891,31 @@ export function poolFillers(m) {
|
|
|
581
891
|
for (const a of m.auctions) fromSplit(a.split, `auction:${a.key}`);
|
|
582
892
|
return fillers;
|
|
583
893
|
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Every place the spec sends money into a farm's rewards vault ("farm:<key>"): a split's share, a schedule that pays it,
|
|
897
|
+
* a craft that pays it, the owner's seed, an early exit's fee into its own vault. [{ farm, by, how }]
|
|
898
|
+
*/
|
|
899
|
+
export function farmFillers(m) {
|
|
900
|
+
const fillers = [];
|
|
901
|
+
const add = (to, by, how) => {
|
|
902
|
+
const d = destination(to ?? '');
|
|
903
|
+
if (d.kind === 'farm') fillers.push({ farm: d.key, by, how });
|
|
904
|
+
};
|
|
905
|
+
const fromSplit = (split, by) => sharesOf(split).forEach(s => add(s.to, by, `${s.percent}% of each payment`));
|
|
906
|
+
for (const f of m.farms) {
|
|
907
|
+
if (Number(f.seed ?? 0) > 0) fillers.push({ farm: f.key, by: `farm:${f.key}`, how: 'the owner’s seed' });
|
|
908
|
+
if (f.exitFeeTo === 'rewards') fillers.push({ farm: f.key, by: `farm:${f.key}`, how: 'early-exit fees' });
|
|
909
|
+
}
|
|
910
|
+
for (const b of m.allBoxes) fromSplit(b.split, b.shopOf ? `item:${b.shopOf}` : `box:${b.key}`);
|
|
911
|
+
for (const p of m.payments) fromSplit(p.split, `payment:${p.key}`);
|
|
912
|
+
for (const r of m.recipes)
|
|
913
|
+
for (const i of r.in ?? []) if (i.takes === 'pay') add(i.to, `recipe:${r.key}`, 'a craft pays in');
|
|
914
|
+
for (const s of m.schedules)
|
|
915
|
+
if (s.does === 'pay' || s.does === 'issue')
|
|
916
|
+
add(s.to, `schedule:${s.key}`, s.does === 'issue' ? 'mints into it on a timer' : 'pays in on a timer');
|
|
917
|
+
for (const l of m.lotteries) fromSplit(lotterySplit(l), `lottery:${l.key}`);
|
|
918
|
+
for (const c of m.contests) fromSplit(c.split, `contest:${c.key}`);
|
|
919
|
+
for (const a of m.auctions) fromSplit(a.split, `auction:${a.key}`);
|
|
920
|
+
return fillers;
|
|
921
|
+
}
|