@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/model.mjs
CHANGED
|
@@ -31,7 +31,7 @@ export const ACCOUNT_OVERHEAD = 128n;
|
|
|
31
31
|
* devnet and mainnet lowered it (a local validator and LiteSVM still charge it). An RPC that asks more is not believed.
|
|
32
32
|
*/
|
|
33
33
|
export const MOST_RENT_PER_BYTE = 6_960n;
|
|
34
|
-
export const rent =
|
|
34
|
+
export const rent = bytes => (BigInt(bytes) + ACCOUNT_OVERHEAD) * RENT_PER_BYTE;
|
|
35
35
|
/**
|
|
36
36
|
* A collectible's Core account, paid from the SOL its order holds (or by whoever gives it): its rent (a Core asset of
|
|
37
37
|
* about 190 bytes) plus Metaplex Core's creation fee of 0.0015 SOL, which the account keeps. The full simulation
|
|
@@ -46,9 +46,30 @@ export const WSOL = 'So11111111111111111111111111111111111111112';
|
|
|
46
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
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
48
|
export const LIMITS = Object.freeze({
|
|
49
|
-
callbackAccounts: 21,
|
|
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,
|
|
50
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.
|
|
51
|
-
boxEntries: 128,
|
|
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,
|
|
52
73
|
});
|
|
53
74
|
/** Attribute names the program writes itself, which a recipe cannot copy and `Set` cannot change (craft.rs, make.rs),
|
|
54
75
|
* and the two a collectible's life keeps (`lifeLines`: its uses and its next use). */
|
|
@@ -69,7 +90,8 @@ export class SpecError extends Error {
|
|
|
69
90
|
/** A plain decimal string for an amount written as a number or text (no exponent). */
|
|
70
91
|
export function decimalText(amount) {
|
|
71
92
|
if (typeof amount === 'string') return amount;
|
|
72
|
-
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".');
|
|
73
95
|
const text = String(amount);
|
|
74
96
|
if (!/e/i.test(text)) return text;
|
|
75
97
|
return amount.toFixed(20).replace(/\.?0+$/, '');
|
|
@@ -78,12 +100,19 @@ export function decimalText(amount) {
|
|
|
78
100
|
/** "0.1" of a currency with 9 decimals → 100000000n. Refuses more decimals than the currency has. */
|
|
79
101
|
export function toUnits(amount, decimals) {
|
|
80
102
|
const text = decimalText(amount ?? 0);
|
|
81
|
-
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".');
|
|
82
105
|
const [whole, fraction = ''] = text.split('.');
|
|
83
106
|
const trimmed = fraction.replace(/0+$/, '');
|
|
84
|
-
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
|
+
);
|
|
85
113
|
const units = BigInt(whole + trimmed.padEnd(decimals, '0'));
|
|
86
|
-
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.');
|
|
87
116
|
return units;
|
|
88
117
|
}
|
|
89
118
|
|
|
@@ -98,7 +127,7 @@ export function fromUnits(units, decimals) {
|
|
|
98
127
|
|
|
99
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. */
|
|
100
129
|
export function wholeCount(value) {
|
|
101
|
-
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');
|
|
102
131
|
return typeof text === 'string' && /^\d{1,19}$/.test(text) && BigInt(text) < 2n ** 64n ? BigInt(text) : null;
|
|
103
132
|
}
|
|
104
133
|
|
|
@@ -114,7 +143,8 @@ export function toSeconds(duration) {
|
|
|
114
143
|
if (duration === undefined || duration === null) return 0;
|
|
115
144
|
if (typeof duration === 'number') return duration;
|
|
116
145
|
const m = /^(\d+(?:\.\d+)?)(s|m|h|d|w)$/.exec(duration);
|
|
117
|
-
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".');
|
|
118
148
|
return Math.round(Number(m[1]) * UNIT[m[2]]);
|
|
119
149
|
}
|
|
120
150
|
|
|
@@ -122,7 +152,7 @@ export function toSeconds(duration) {
|
|
|
122
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
|
|
123
153
|
* lands, `farm.rs` `release_span`): its longest lock tier, and a day at least.
|
|
124
154
|
*/
|
|
125
|
-
export const releaseSeconds =
|
|
155
|
+
export const releaseSeconds = f => Math.max(86_400, ...(f.tiers ?? []).map(t => toSeconds(t.lock ?? 0)));
|
|
126
156
|
|
|
127
157
|
/** A farm's release span in words: "a day", "30 days", "36 hours". */
|
|
128
158
|
export function releaseWords(f) {
|
|
@@ -135,16 +165,29 @@ export function releaseWords(f) {
|
|
|
135
165
|
* What a run sent late does, in words (schedule.rs `catches_up`): a sealed schedule's fixed amount pays every run due;
|
|
136
166
|
* any other run pays once, for the latest run due, and the runs before it lapse.
|
|
137
167
|
*/
|
|
138
|
-
export const lateRunWords =
|
|
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';
|
|
139
172
|
|
|
140
173
|
/**
|
|
141
174
|
* Whether a farm's boost reads a number its own growth writes (`refresh_seat` writes it): each level counts from when it is
|
|
142
175
|
* written, so a page or a keeper should refresh seats.
|
|
143
176
|
*/
|
|
144
|
-
export const boostReadsGrowth =
|
|
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
|
+
);
|
|
145
187
|
|
|
146
188
|
/** 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 =
|
|
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';
|
|
148
191
|
|
|
149
192
|
/**
|
|
150
193
|
* What a box that rolls does once an outcome can't be given (`whenOut`, order.rs `WhenOut`), in words. `sold`: it is
|
|
@@ -155,8 +198,9 @@ export function whenOutWords(b, { sold = b.active !== false } = {}) {
|
|
|
155
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';
|
|
156
199
|
if (b.whenOut === 'drawLeft')
|
|
157
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)';
|
|
158
|
-
if (!sold)
|
|
159
|
-
|
|
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';
|
|
160
204
|
}
|
|
161
205
|
|
|
162
206
|
/**
|
|
@@ -193,21 +237,27 @@ export function toTime(time, now) {
|
|
|
193
237
|
const m = /^publish\+(.+)$/.exec(time);
|
|
194
238
|
if (m) return now + toSeconds(m[1]);
|
|
195
239
|
const at = Date.parse(time);
|
|
196
|
-
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
|
+
);
|
|
197
246
|
return Math.floor(at / 1000);
|
|
198
247
|
}
|
|
199
248
|
|
|
200
|
-
export const rarityNumber =
|
|
201
|
-
|
|
202
|
-
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);
|
|
203
253
|
|
|
204
254
|
// ---------------------------------------------------------------- the model
|
|
205
255
|
|
|
206
256
|
/** Gives every part without one the next free program id (pools from 0, the rest from 1). */
|
|
207
257
|
function assignIds(list, field, first) {
|
|
208
|
-
const used = new Set(list.map(
|
|
258
|
+
const used = new Set(list.map(x => x[field]).filter(v => Number.isInteger(v)));
|
|
209
259
|
let next = first;
|
|
210
|
-
return list.map(
|
|
260
|
+
return list.map(x => {
|
|
211
261
|
if (Number.isInteger(x[field])) return x;
|
|
212
262
|
while (used.has(next)) next += 1;
|
|
213
263
|
used.add(next);
|
|
@@ -227,7 +277,17 @@ export const spendOf = (then, m) => {
|
|
|
227
277
|
function tokenOf(given) {
|
|
228
278
|
// "project" is the spec's word for a token the project mints; "game" is the older spelling of it.
|
|
229
279
|
const t = given.source === 'project' ? { ...given, source: 'game' } : given;
|
|
230
|
-
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
|
+
};
|
|
231
291
|
const decimals = t.decimals ?? (t.source === 'game' ? 0 : undefined);
|
|
232
292
|
return {
|
|
233
293
|
...t,
|
|
@@ -249,22 +309,37 @@ function tokenOf(given) {
|
|
|
249
309
|
*/
|
|
250
310
|
export function model(economy, { now = 0 } = {}) {
|
|
251
311
|
const e = economy ?? {};
|
|
252
|
-
const tokens = new Map((e.tokens ?? []).map(
|
|
312
|
+
const tokens = new Map((e.tokens ?? []).map(t => [t.key, tokenOf(t)]));
|
|
253
313
|
const currencyKey = e.currency ?? (tokens.size === 1 ? [...tokens.keys()][0] : undefined);
|
|
254
314
|
const currency = tokens.get(currencyKey);
|
|
255
315
|
// Numbers are given in this order: the pools that have one, then the rest in the spec's order.
|
|
256
316
|
const listed = e.pools ?? [];
|
|
257
|
-
const order = [...listed.keys()].sort(
|
|
258
|
-
|
|
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
|
+
);
|
|
259
325
|
const pools = listed.map((_, i) => assigned[order.indexOf(i)]);
|
|
260
326
|
// A farm's rewards land in a vault of its own: `rewardsToken` is their token.
|
|
261
|
-
const poolCurrency = new Map(pools.map(
|
|
262
|
-
const farms = (e.farms ?? []).map(
|
|
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
|
+
}));
|
|
263
332
|
// A raffle: its NFT is first place's prize, held in the lottery's own stash.
|
|
264
|
-
const lotteries = (e.lotteries ?? []).map(
|
|
333
|
+
const lotteries = (e.lotteries ?? []).map(l => {
|
|
265
334
|
if (!l.raffle) return l;
|
|
266
335
|
const { nft, collection } = typeof l.raffle === 'string' ? { nft: l.raffle } : l.raffle;
|
|
267
|
-
return {
|
|
336
|
+
return {
|
|
337
|
+
...l,
|
|
338
|
+
places: [
|
|
339
|
+
{ percent: 0, prize: { gives: 'nft', nft, held: true, ...(collection ? { collection } : {}) } },
|
|
340
|
+
...(l.places ?? []),
|
|
341
|
+
],
|
|
342
|
+
};
|
|
268
343
|
});
|
|
269
344
|
const m = {
|
|
270
345
|
now,
|
|
@@ -276,7 +351,15 @@ export function model(economy, { now = 0 } = {}) {
|
|
|
276
351
|
currencyKey,
|
|
277
352
|
currency,
|
|
278
353
|
/** The collection the setup makes (null when the spec says `"collection": false`: a project of tokens only). */
|
|
279
|
-
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
|
+
},
|
|
280
363
|
/** Core collections the owner already has that the project also takes (its update authority is the owner). */
|
|
281
364
|
collections: e.collections ?? [],
|
|
282
365
|
/** How many compressed NFTs the project's tree holds (`compressedTree`), when a prize gives one. */
|
|
@@ -305,8 +388,8 @@ export function model(economy, { now = 0 } = {}) {
|
|
|
305
388
|
// A shop item is a box of one entry (the program sells items only through boxes). Its box takes the item's price,
|
|
306
389
|
// rise, fall, split and gate; it is numbered with the boxes, after the ones the spec numbered itself.
|
|
307
390
|
const shop = m.items
|
|
308
|
-
.filter(
|
|
309
|
-
.map(
|
|
391
|
+
.filter(it => Number(it.price ?? 0) > 0 || it.free)
|
|
392
|
+
.map(it => ({
|
|
310
393
|
key: `shop-${it.key}`,
|
|
311
394
|
...(Number.isInteger(it.shopBox) ? { id: it.shopBox } : {}),
|
|
312
395
|
name: it.name,
|
|
@@ -316,17 +399,19 @@ export function model(economy, { now = 0 } = {}) {
|
|
|
316
399
|
...(it.split ? { split: it.split } : {}),
|
|
317
400
|
...(it.gate ? { gate: it.gate } : {}),
|
|
318
401
|
// When it sells, and how many one wallet may buy.
|
|
319
|
-
...Object.fromEntries(
|
|
402
|
+
...Object.fromEntries(
|
|
403
|
+
['opens', 'closes', 'perWallet', 'perWalletEach'].filter(k => it[k] !== undefined).map(k => [k, it[k]]),
|
|
404
|
+
),
|
|
320
405
|
active: it.active !== false,
|
|
321
406
|
outcomes: [{ gives: 'item', item: it.key, weight: 1 }],
|
|
322
407
|
shopOf: it.key,
|
|
323
408
|
}));
|
|
324
409
|
const numbered = assignIds([...(e.boxes ?? []), ...shop], 'id', 1);
|
|
325
|
-
m.boxes = numbered.filter(
|
|
326
|
-
m.shop = numbered.filter(
|
|
410
|
+
m.boxes = numbered.filter(b => !b.shopOf);
|
|
411
|
+
m.shop = numbered.filter(b => b.shopOf);
|
|
327
412
|
/** Every box the program holds: the spec's own and the shop's. */
|
|
328
413
|
m.allBoxes = numbered;
|
|
329
|
-
const index =
|
|
414
|
+
const index = list => new Map(list.map(x => [x.key, x]));
|
|
330
415
|
m.item = index(m.items);
|
|
331
416
|
m.pool = index(m.pools);
|
|
332
417
|
m.stash = index(m.stashes);
|
|
@@ -340,9 +425,9 @@ export function model(economy, { now = 0 } = {}) {
|
|
|
340
425
|
m.auction = index(m.auctions);
|
|
341
426
|
m.hold = index(m.holds);
|
|
342
427
|
/** The token a pool holds. */
|
|
343
|
-
m.poolToken =
|
|
428
|
+
m.poolToken = key => tokens.get(m.pool.get(key)?.currency);
|
|
344
429
|
/** The token a part is priced in (its own `currency`, else the game's). */
|
|
345
|
-
m.priceToken =
|
|
430
|
+
m.priceToken = part => tokens.get(part?.currency ?? currencyKey);
|
|
346
431
|
return m;
|
|
347
432
|
}
|
|
348
433
|
|
|
@@ -352,10 +437,17 @@ export function model(economy, { now = 0 } = {}) {
|
|
|
352
437
|
* NFT (`compressed`, its asset id).
|
|
353
438
|
*/
|
|
354
439
|
export function stashNfts(s) {
|
|
355
|
-
return (s.nfts ?? []).map(
|
|
440
|
+
return (s.nfts ?? []).map(n => {
|
|
356
441
|
if (typeof n === 'string') return { kind: 'core', asset: n, collection: s.collection ?? null, weight: 1 };
|
|
357
442
|
const weight = n.weight ?? 1;
|
|
358
|
-
if (n.mint)
|
|
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
|
+
};
|
|
359
451
|
if (n.compressed) return { kind: 'compressed', asset: n.compressed, weight };
|
|
360
452
|
return { kind: 'core', asset: n.asset, collection: n.collection ?? s.collection ?? null, weight };
|
|
361
453
|
});
|
|
@@ -377,10 +469,11 @@ export function prizeLine(p) {
|
|
|
377
469
|
}
|
|
378
470
|
|
|
379
471
|
/** The pool a prize line pays from: tokens' "pool:<key>". */
|
|
380
|
-
export const prizePool =
|
|
472
|
+
export const prizePool = p => (p?.gives === 'tokens' && p.from?.startsWith('pool:') ? p.from.slice(5) : null);
|
|
381
473
|
|
|
382
474
|
/** The token a prize line pays in: its pool's, or the one it mints. */
|
|
383
|
-
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;
|
|
384
477
|
|
|
385
478
|
/**
|
|
386
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
|
|
@@ -390,12 +483,40 @@ export const prizeToken = (p, m) => (prizePool(p) ? m.poolToken(prizePool(p)) :
|
|
|
390
483
|
*/
|
|
391
484
|
export function prizes(m) {
|
|
392
485
|
const list = [];
|
|
393
|
-
const add = (p, where, by, route, strict, once = false) =>
|
|
394
|
-
|
|
395
|
-
for (const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
+
);
|
|
399
520
|
return list;
|
|
400
521
|
}
|
|
401
522
|
|
|
@@ -406,8 +527,10 @@ export function prizes(m) {
|
|
|
406
527
|
* canopy, bytes }
|
|
407
528
|
*/
|
|
408
529
|
export function compressedTree(m) {
|
|
409
|
-
const boxGives = m.allBoxes.some(
|
|
410
|
-
|
|
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;
|
|
411
534
|
const depth = Math.min(20, Math.max(14, Math.ceil(Math.log2(Math.max(1, m.treeHolds)))));
|
|
412
535
|
const canopy = Math.min(depth - 10, 8);
|
|
413
536
|
return { depth, buffer: 64, canopy, bytes: treeSize(depth, 64, canopy) };
|
|
@@ -418,11 +541,17 @@ export function compressedTree(m) {
|
|
|
418
541
|
* mission's roll on the NFT leaving it and a recipe's roll on a collectible it keeps. [{ box, by, route }]
|
|
419
542
|
*/
|
|
420
543
|
export function prizeRolls(m) {
|
|
421
|
-
const rolls = prizes(m)
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
for (const
|
|
425
|
-
|
|
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);
|
|
426
555
|
}
|
|
427
556
|
|
|
428
557
|
/**
|
|
@@ -433,9 +562,21 @@ export function prizeRolls(m) {
|
|
|
433
562
|
*/
|
|
434
563
|
export function rollsOf(m) {
|
|
435
564
|
const rolls = [...prizeRolls(m)];
|
|
436
|
-
for (const b of m.allBoxes)
|
|
437
|
-
|
|
438
|
-
|
|
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
|
+
];
|
|
439
580
|
// A breadth-first walk: each roll of a box is also a roll of every box it rolls, once for each route.
|
|
440
581
|
for (let i = 0; i < rolls.length; i++) {
|
|
441
582
|
const r = rolls[i];
|
|
@@ -453,15 +594,20 @@ export function rollsOf(m) {
|
|
|
453
594
|
export function prizeWords(p, m) {
|
|
454
595
|
const line = prizeLine(p);
|
|
455
596
|
if (!line) return 'nothing';
|
|
456
|
-
const sym =
|
|
457
|
-
if (line.gives === 'item')
|
|
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)' : ''}`;
|
|
458
600
|
if (line.gives === 'raffled') return 'an NFT drawn at random from the lottery’s own stash';
|
|
459
|
-
if (line.gives === 'nft' && line.held)
|
|
460
|
-
|
|
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}”`;
|
|
461
605
|
if (line.gives === 'roll') return `a roll of “${m.box.get(line.box)?.name ?? line.box}”`;
|
|
462
606
|
const pool = prizePool(line);
|
|
463
607
|
const split = line.split ? ', split by what each winner put in' : '';
|
|
464
|
-
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}`;
|
|
465
611
|
}
|
|
466
612
|
|
|
467
613
|
/**
|
|
@@ -474,17 +620,23 @@ export function outcomeParts(o) {
|
|
|
474
620
|
if (!o) return [];
|
|
475
621
|
if (o.gives === 'bundle') return (o.prizes ?? []).flatMap(outcomeParts);
|
|
476
622
|
if (o.gives === 'nothing') return [];
|
|
477
|
-
if (o.gives === 'tokens')
|
|
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
|
+
];
|
|
478
629
|
return [o];
|
|
479
630
|
}
|
|
480
631
|
|
|
481
632
|
/** Whether a box leaves its outcome to the oracle (catalog.rs `rolls`): more than one outcome, or an NFT at random. */
|
|
482
633
|
/** How many draws one open of a box makes: its slots' counts, or one. */
|
|
483
|
-
export const drawsOf =
|
|
634
|
+
export const drawsOf = b => ((b.slots ?? []).length ? b.slots.reduce((n, s) => n + Number(s.count), 0) : 1);
|
|
484
635
|
|
|
485
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
|
|
486
637
|
* (its traits' Sheet). A plain compressed NFT costs no rent. */
|
|
487
|
-
export const holdsRent = (p, m) =>
|
|
638
|
+
export const holdsRent = (p, m) =>
|
|
639
|
+
p.gives === 'item' && (!p.compressed || (m?.item.get(p.item)?.drawn ?? []).length > 0);
|
|
488
640
|
|
|
489
641
|
/**
|
|
490
642
|
* The units of `SETTLE_RENT_UNIT` a settle of box `b`'s order may spend (`loot.settleRent`), which js/play funds its
|
|
@@ -493,14 +645,23 @@ export const holdsRent = (p, m) => p.gives === 'item' && (!p.compressed || (m?.i
|
|
|
493
645
|
* pay, and the fate of the NFT it rolls on.
|
|
494
646
|
*/
|
|
495
647
|
export function settleUnits(b, m) {
|
|
496
|
-
const opens =
|
|
497
|
-
const parts = (b.outcomes ?? []).map(
|
|
498
|
-
const most = Math.max(0, ...parts.map(
|
|
499
|
-
const sources = new Set(
|
|
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
|
+
);
|
|
500
657
|
return drawsOf(b) * most + sources.size + (b.rollsOn ? 1 : 0);
|
|
501
658
|
}
|
|
502
659
|
|
|
503
|
-
export const boxRolls = (b, m) =>
|
|
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));
|
|
504
665
|
|
|
505
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
|
|
506
667
|
* a sale (a fixed pack: its prizes given once for each draw). */
|
|
@@ -518,13 +679,20 @@ const RENEW_BY = -2_147_483_647;
|
|
|
518
679
|
* { needs, raises }, in the program's shapes.
|
|
519
680
|
*/
|
|
520
681
|
export function lifeLines(r, m) {
|
|
521
|
-
const places = (r.in ?? []).flatMap(
|
|
682
|
+
const places = (r.in ?? []).flatMap(l =>
|
|
683
|
+
l.takes === 'pay' ? [] : Array(l.takes === 'burn' ? (l.count ?? 1) : 1).fill(l),
|
|
684
|
+
);
|
|
522
685
|
const needs = [];
|
|
523
686
|
const raises = [];
|
|
524
687
|
places.forEach((l, place) => {
|
|
525
688
|
const it = l.item ? m.item.get(l.item) : null;
|
|
526
689
|
if (!it?.life || it.kind === 'normal') return;
|
|
527
|
-
const [ready, lasts, uses, cooldown] = [
|
|
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
|
+
];
|
|
528
696
|
if (ready) needs.push({ place, key: 'made', test: { Older: [ready] } });
|
|
529
697
|
if (lasts) needs.push({ place, key: 'made', test: { Within: [lasts] } });
|
|
530
698
|
if (l.takes === 'renew') raises.push({ place, shift: { key: 'used', by: RENEW_BY, most: 0, from_now: false } });
|
|
@@ -538,8 +706,11 @@ export function lifeLines(r, m) {
|
|
|
538
706
|
return { needs, raises };
|
|
539
707
|
}
|
|
540
708
|
|
|
541
|
-
/** A lottery's place as { percent, prize }: a number is a
|
|
542
|
-
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 };
|
|
543
714
|
|
|
544
715
|
/** "pool:house" → { kind: 'pool', key: 'house' }; "burn" → { kind: 'burn' }. */
|
|
545
716
|
export function destination(to) {
|
|
@@ -551,13 +722,14 @@ export function destination(to) {
|
|
|
551
722
|
}
|
|
552
723
|
|
|
553
724
|
/** The share with its basis points, or null bps when the percent is not whole hundredths. */
|
|
554
|
-
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) }));
|
|
555
727
|
|
|
556
728
|
/**
|
|
557
729
|
* The program's `To` for a destination, with pool indexes resolved (the IDL's own shape). A wallet ("wallet:<address>")
|
|
558
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"
|
|
559
731
|
* (or "chosen:<item>") is the one the buyer names when they buy; "lottery:<key>" is that lottery's pot
|
|
560
|
-
* ("@pot:<id>") and "farm:<key>" that farm's rewards vault ("@rewards:<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
|
|
561
733
|
* known.
|
|
562
734
|
*/
|
|
563
735
|
export function toEnum(to, m) {
|
|
@@ -567,7 +739,8 @@ export function toEnum(to, m) {
|
|
|
567
739
|
if (d.kind === 'pool') return { Pool: [m.pool.get(d.key)?.index ?? 0] };
|
|
568
740
|
if (d.kind === 'account') return { Account: [d.key] };
|
|
569
741
|
if (d.kind === 'wallet') return { Wallet: [d.key] };
|
|
570
|
-
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}`] };
|
|
571
744
|
if (d.kind === 'farm') return { Account: [`@rewards:${m.farm.get(d.key)?.id ?? 0}`] };
|
|
572
745
|
if (d.kind === 'chosen') return { Chosen: { item: d.key ? (m.item.get(d.key)?.id ?? 0) : null } };
|
|
573
746
|
return { Holder: [d.key] };
|
|
@@ -580,13 +753,28 @@ export function toEnum(to, m) {
|
|
|
580
753
|
export function ruleOf(rule, m) {
|
|
581
754
|
if (!rule) return null;
|
|
582
755
|
const time = { ahead: 'Ahead', passed: 'Passed' }[rule.time];
|
|
583
|
-
|
|
584
|
-
|
|
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'] };
|
|
585
773
|
return { of, key: rule.attribute ?? '', test };
|
|
586
774
|
}
|
|
587
775
|
|
|
588
776
|
/** One thing or a list of them, as a list: a farm's `gate` and `growth` take either. */
|
|
589
|
-
export const listOf =
|
|
777
|
+
export const listOf = x => (x == null ? [] : Array.isArray(x) ? x : [x]);
|
|
590
778
|
|
|
591
779
|
/**
|
|
592
780
|
* The program's `Gate` for a spec gate: a few wallets (`wallets`), a list of any size (`list`: an allowlist, its root and
|
|
@@ -598,7 +786,12 @@ export const gateOf = (gate, m) => {
|
|
|
598
786
|
if (gate.wallets) return { Wallets: [gate.wallets] };
|
|
599
787
|
if (gate.staked) {
|
|
600
788
|
const farm = m.farm.get(gate.staked.farm);
|
|
601
|
-
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
|
+
};
|
|
602
795
|
}
|
|
603
796
|
if (gate.list) {
|
|
604
797
|
const { root, depth } = gateList(gate.list);
|
|
@@ -608,39 +801,55 @@ export const gateOf = (gate, m) => {
|
|
|
608
801
|
};
|
|
609
802
|
|
|
610
803
|
/** The program's `PerWallet` for a part's "perWallet" (in all) and "perWalletEach" ({ count, every }). */
|
|
611
|
-
export const perWalletOf =
|
|
804
|
+
export const perWalletOf = part => ({
|
|
612
805
|
total: part?.perWallet ?? 0,
|
|
613
806
|
per_window: part?.perWalletEach?.count ?? 0,
|
|
614
807
|
window: part?.perWalletEach ? toSeconds(part.perWalletEach.every ?? '1d') : 0,
|
|
615
808
|
});
|
|
616
809
|
|
|
617
810
|
/** The program's `Window` for a part's "opens" and "closes" (times; 0 leaves a side open). */
|
|
618
|
-
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) } });
|
|
619
822
|
|
|
620
823
|
/** A lottery's split of ticket money: its own, and "cutPercent" as a share to the treasury. */
|
|
621
|
-
export const lotterySplit =
|
|
824
|
+
export const lotterySplit = l => [
|
|
825
|
+
...(l.split ?? []),
|
|
826
|
+
...(l.cutPercent ? [{ to: 'treasury', percent: l.cutPercent }] : []),
|
|
827
|
+
];
|
|
622
828
|
|
|
623
829
|
/** The program's `Effect` for a spec effect: a flat share, or a share per point of the attribute. */
|
|
624
830
|
export function effectOf(effect) {
|
|
625
|
-
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 } };
|
|
626
833
|
return { Flat: [Math.round((effect?.addPercent ?? 0) * 100)] };
|
|
627
834
|
}
|
|
628
835
|
|
|
629
836
|
/** A box's outcomes with their program weights: chances become whole weights (hundredths of a basis point). */
|
|
630
837
|
export function weights(box) {
|
|
631
838
|
const outcomes = box.outcomes ?? [];
|
|
632
|
-
if (outcomes.every(
|
|
633
|
-
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));
|
|
634
841
|
const gcd = (a, b) => (b ? gcd(b, a % b) : a);
|
|
635
842
|
const g = raw.reduce((a, b) => gcd(a, b), 0) || 1;
|
|
636
|
-
return raw.map(
|
|
843
|
+
return raw.map(w => w / g);
|
|
637
844
|
}
|
|
638
845
|
|
|
639
846
|
/** Every place the spec names a pool as somewhere money leaves from, with what takes it. */
|
|
640
847
|
export function poolTakers(m) {
|
|
641
848
|
const takers = [];
|
|
642
849
|
const add = (pool, by, how) => pool && takers.push({ pool, by, how });
|
|
643
|
-
for (const b of m.allBoxes)
|
|
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');
|
|
644
853
|
for (const p of prizes(m)) add(prizePool(p.prize), p.by, `pays prizes (${p.route})`);
|
|
645
854
|
for (const s of m.schedules) {
|
|
646
855
|
if (s.does === 'burn') add(s.pool, `schedule:${s.key}`, 'burns a share');
|
|
@@ -649,7 +858,13 @@ export function poolTakers(m) {
|
|
|
649
858
|
// from a vault of its own and names no pool.
|
|
650
859
|
if (s.does === 'pay') add(s.from, `schedule:${s.key}`, 'pays out on a timer');
|
|
651
860
|
}
|
|
652
|
-
for (const d of m.decisions)
|
|
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
|
+
);
|
|
653
868
|
return takers;
|
|
654
869
|
}
|
|
655
870
|
|
|
@@ -657,11 +872,16 @@ export function poolTakers(m) {
|
|
|
657
872
|
export function poolFillers(m) {
|
|
658
873
|
const fillers = [];
|
|
659
874
|
const add = (pool, by, how) => pool && fillers.push({ pool, by, how });
|
|
660
|
-
const fromSplit = (split, by) =>
|
|
661
|
-
|
|
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');
|
|
662
879
|
for (const b of m.allBoxes) fromSplit(b.split, b.shopOf ? `item:${b.shopOf}` : `box:${b.key}`);
|
|
663
880
|
for (const p of m.payments) fromSplit(p.split, `payment:${p.key}`);
|
|
664
|
-
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');
|
|
665
885
|
for (const s of m.schedules) {
|
|
666
886
|
if (s.does === 'issue') add(s.pool, `schedule:${s.key}`, 'mints into it');
|
|
667
887
|
if (s.does === 'move') add(s.to, `schedule:${s.key}`, 'moves a share in');
|
|
@@ -682,15 +902,18 @@ export function farmFillers(m) {
|
|
|
682
902
|
const d = destination(to ?? '');
|
|
683
903
|
if (d.kind === 'farm') fillers.push({ farm: d.key, by, how });
|
|
684
904
|
};
|
|
685
|
-
const fromSplit = (split, by) => sharesOf(split).forEach(
|
|
905
|
+
const fromSplit = (split, by) => sharesOf(split).forEach(s => add(s.to, by, `${s.percent}% of each payment`));
|
|
686
906
|
for (const f of m.farms) {
|
|
687
907
|
if (Number(f.seed ?? 0) > 0) fillers.push({ farm: f.key, by: `farm:${f.key}`, how: 'the owner’s seed' });
|
|
688
908
|
if (f.exitFeeTo === 'rewards') fillers.push({ farm: f.key, by: `farm:${f.key}`, how: 'early-exit fees' });
|
|
689
909
|
}
|
|
690
910
|
for (const b of m.allBoxes) fromSplit(b.split, b.shopOf ? `item:${b.shopOf}` : `box:${b.key}`);
|
|
691
911
|
for (const p of m.payments) fromSplit(p.split, `payment:${p.key}`);
|
|
692
|
-
for (const r of m.recipes)
|
|
693
|
-
|
|
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');
|
|
694
917
|
for (const l of m.lotteries) fromSplit(lotterySplit(l), `lottery:${l.key}`);
|
|
695
918
|
for (const c of m.contests) fromSplit(c.split, `contest:${c.key}`);
|
|
696
919
|
for (const a of m.auctions) fromSplit(a.split, `auction:${a.key}`);
|