@xgamesfun/solana 0.26.0 → 0.26.2
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 +8 -8
- package/js/chain/metadata.js +76 -0
- package/js/client/idl/loot.js +1 -1
- package/js/client/idl/loot.json +4 -3
- package/js/client/sizes.js +180 -66
- package/js/economy/check.mjs +64 -19
- package/js/economy/cost.mjs +17 -3
- package/js/economy/explain.mjs +14 -0
- package/js/economy/guide.mjs +2 -2
- package/js/economy/plan.mjs +62 -12
- package/js/economy/schema.json +4 -3
- package/js/play/index.js +15 -7
- package/package.json +1 -1
- package/release.json +5 -5
- package/skills/xsolana-economy/SKILL.md +3 -3
- package/skills/xsolana-economy/references/telling-the-person.md +9 -1
- package/types/chain/metadata.d.ts +40 -0
- package/types/client/sizes.d.ts +42 -8
- package/types/economy/check.d.mts +6 -2
- package/types/economy/cost.d.mts +3 -0
- package/types/economy/live.d.mts +2 -0
- package/types/economy/plan.d.mts +23 -15
package/js/client/idl/loot.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"address": "9cwqM1TXytPnFZsLfmUyv2dg4uW6Ch9C6UJVPns7NFQ",
|
|
3
3
|
"metadata": {
|
|
4
4
|
"name": "loot",
|
|
5
|
-
"version": "0.26.
|
|
5
|
+
"version": "0.26.1",
|
|
6
6
|
"spec": "0.1.0",
|
|
7
7
|
"description": "Economy for Anything: one program every coin of a project passes through",
|
|
8
8
|
"repository": "https://github.com/gamesfun/loot"
|
|
@@ -9551,7 +9551,7 @@
|
|
|
9551
9551
|
{
|
|
9552
9552
|
"code": 6141,
|
|
9553
9553
|
"name": "PackTooLarge",
|
|
9554
|
-
"msg": "A box's draws (one, or each open's times the opens bought together) must all fit one answer of the oracle's: fewer entries, prizes an entry or
|
|
9554
|
+
"msg": "A box's draws (one, or each open's times the opens bought together) must all fit one answer of the oracle's, and the sales one buy adds up one transaction: fewer entries, prizes an entry, draws or opens"
|
|
9555
9555
|
},
|
|
9556
9556
|
{
|
|
9557
9557
|
"code": 6142,
|
|
@@ -11575,7 +11575,8 @@
|
|
|
11575
11575
|
"docs": [
|
|
11576
11576
|
"The carry: rounding of a share over the total weight, times `PRECISION`. The next move before",
|
|
11577
11577
|
"the farm's end credits it to the seats there once it adds up to a share; a move that leaves no",
|
|
11578
|
-
"weight, or any move once the farm is over, frees its whole tokens (`FarmSide::settle`)
|
|
11578
|
+
"weight, or any move once the farm is over, frees its whole tokens (`FarmSide::settle`), and so",
|
|
11579
|
+
"does a walk that begins with no weight or where the weight reaches 0 (`Farm::walk`)."
|
|
11579
11580
|
],
|
|
11580
11581
|
"type": "u128"
|
|
11581
11582
|
},
|
package/js/client/sizes.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// Rent sizes from the current IDL. Fixed allocations reserve the largest enum and option forms,
|
|
2
2
|
// matching Anchor InitSpace. Dynamic records use their actual encoded fields.
|
|
3
|
+
import { addressBytes } from '../chain/base58.js';
|
|
3
4
|
import { encodeAccount, encodeBorsh } from '../chain/borsh.js';
|
|
5
|
+
import { uintLE, utf8 } from '../chain/bytes.js';
|
|
4
6
|
import IDL from './idl/loot.js';
|
|
7
|
+
import { lootAddress } from './pda.js';
|
|
5
8
|
|
|
6
9
|
const scalar = { bool: 1, u8: 1, u16: 2, u32: 4, i32: 4, u64: 8, i64: 8, u128: 16, pubkey: 32 };
|
|
7
10
|
// What a fixed-size account reserves for its lists and strings: a roll's fate (`Fate.shifts`, each key at most 32 bytes).
|
|
@@ -170,7 +173,7 @@ export function orderBytes(lootBox, shares = 0, opens = 1, items) {
|
|
|
170
173
|
|
|
171
174
|
// What one oracle answer's roll spends, counted at no less than each part costs (`order.rs`'s `RollCost`, measured by
|
|
172
175
|
// the program's `tests/roll_costs.rs` and `tests/roll_heap.rs`), against the compute and the heap the oracle gives its
|
|
173
|
-
// answer, less a margin.
|
|
176
|
+
// answer, less a margin. Every number of the program's count is here, and only here: a change of one is one edit.
|
|
174
177
|
const ROLL_BUDGET = 300_000n;
|
|
175
178
|
const ROLL_HEAP = 32n * 1024n - 512n;
|
|
176
179
|
const COST = {
|
|
@@ -184,7 +187,14 @@ const COST = {
|
|
|
184
187
|
tokens: 6_000n,
|
|
185
188
|
draw: 9_000n,
|
|
186
189
|
group: 250n,
|
|
190
|
+
row: 10n,
|
|
191
|
+
// One try of an address search, what the runtime charges (`COST_TRY`).
|
|
192
|
+
try: 1_500n,
|
|
193
|
+
// Reading a prize of an entry after its first (`COST_PRIZE`).
|
|
194
|
+
prize: 300n,
|
|
187
195
|
reread: 1_000n,
|
|
196
|
+
// A draw of an entry that gives nothing, beyond its weighing (`COST_NOTHING`).
|
|
197
|
+
nothing: 1_100n,
|
|
188
198
|
finish: 15_000n,
|
|
189
199
|
won: 150n,
|
|
190
200
|
drawn: 50n,
|
|
@@ -195,6 +205,22 @@ const COST = {
|
|
|
195
205
|
slotEntry: 350n,
|
|
196
206
|
openSlot: 150n,
|
|
197
207
|
};
|
|
208
|
+
/** The most draws one `open_box` takes in all, its count times the box's draws: what one answer could weigh, each draw
|
|
209
|
+
* at its weighing at least (`ROLL_BUDGET / COST_WEIGH`, 300). More is refused before any price is added up
|
|
210
|
+
* (`PackTooLarge`). */
|
|
211
|
+
export const MOST_DRAWS_A_PURCHASE = Number(ROLL_BUDGET / COST.weigh);
|
|
212
|
+
/** The compute one transaction has, and what adding up one sale's price costs at most (`catalog.rs` `COST_SALE`). */
|
|
213
|
+
const MAX_TX_CU = 1_400_000;
|
|
214
|
+
const COST_SALE = 55;
|
|
215
|
+
/** The most sales one `buy` takes (`count`): what one transaction can price (`MAX_TX_CU / COST_SALE`, rounded down:
|
|
216
|
+
* 25,454). More is refused before any price is added up (`PackTooLarge`). */
|
|
217
|
+
export const MOST_SALES_A_BUY = Math.floor(MAX_TX_CU / COST_SALE);
|
|
218
|
+
/**
|
|
219
|
+
* The tries counted for finding a pool's or a stash's address when the project is not known yet (a spec checked before
|
|
220
|
+
* any wallet is named): 2, what an address takes on average (half are found at the first try, a quarter at the second,
|
|
221
|
+
* and so on). With the project known, the count is exact: `256 - bump` tries an address.
|
|
222
|
+
*/
|
|
223
|
+
export const ASSUMED_TRIES = 2;
|
|
198
224
|
const BYTES = {
|
|
199
225
|
start: 6_000n,
|
|
200
226
|
prizeRead: 600n,
|
|
@@ -310,88 +336,152 @@ const accountOf = p => {
|
|
|
310
336
|
};
|
|
311
337
|
|
|
312
338
|
/**
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
* `shares` and `fates` counted, and `bytes` (`boxBytes`) of heap reading the box takes beyond its entries. `slots` may
|
|
316
|
-
* be a number: that many draws among every entry. The answer's heap counts for a box that rolls, or one sold through
|
|
317
|
-
* the oracle (`oracle`: a box that doesn't roll but one `buy` can't carry, `buyFits`). The program refuses a box whose
|
|
318
|
-
* one open doesn't fit at its upsert, and a purchase that doesn't at `open_box`, where it also reads what the accounts
|
|
319
|
-
* hold (`PackTooLarge`).
|
|
339
|
+
* The tries finding a pool's (`pool` true) or a stash's address takes (`order::searched`: `256 - bump`), for `project`'s
|
|
340
|
+
* pool or stash `id`. With no project, `ASSUMED_TRIES`.
|
|
320
341
|
*/
|
|
321
|
-
export function
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
342
|
+
export function searchTries(project, pool, id) {
|
|
343
|
+
if (!project) return ASSUMED_TRIES;
|
|
344
|
+
const [, bump] = pool ? lootAddress(pdaSeeds.pool(project, id)) : lootAddress(pdaSeeds.stash(project, id));
|
|
345
|
+
return 256 - bump;
|
|
346
|
+
}
|
|
347
|
+
const pdaSeeds = {
|
|
348
|
+
pool: (project, index) => [utf8('pool'), addressBytes(project), uintLE(index, 2)],
|
|
349
|
+
stash: (project, id) => [utf8('stash'), addressBytes(project), uintLE(id, 4)],
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* What a roll of `entries` spends, as the program's `RollCost::new` counts it from the box alone (no table rows, a
|
|
354
|
+
* stash of one group, a named NFT's stash of one row): `fixed` (reading the box, every entry's accounts, finding each
|
|
355
|
+
* pool's and stash's address), each entry's draw (`prizes`), its stock read again (`reread`), the prizes it gives, its
|
|
356
|
+
* heap, and the heap reading the box leaves used (`start`, the box's own `bytes` included). `project`: whose pools and
|
|
357
|
+
* stashes these are, to count each search exactly; without it, at `ASSUMED_TRIES` tries an address.
|
|
358
|
+
*/
|
|
359
|
+
function rollCost(entries, { odds = 0, shares = 0, fates = 0, bytes = 0n, project = null }) {
|
|
332
360
|
const n = BigInt(entries.length);
|
|
361
|
+
// Each address is found by what finding it takes, once a prize names it (`draws_fit`'s `reads`): what a search cost is
|
|
362
|
+
// kept by the address's kind and id; once past the answer's budget nothing more is searched for (the box is refused).
|
|
363
|
+
const found = new Map();
|
|
364
|
+
let searched = 0n;
|
|
365
|
+
const reading = p => {
|
|
366
|
+
const pool = p.Tokens?.from?.Pool;
|
|
367
|
+
const stash = p.Stash ?? p.Nft?.stash;
|
|
368
|
+
if (pool == null && stash == null) return 0n;
|
|
369
|
+
const key = pool != null ? `pool ${pool[0] ?? pool}` : `stash ${stash[0] ?? stash}`;
|
|
370
|
+
let cost = found.get(key);
|
|
371
|
+
if (cost === undefined) {
|
|
372
|
+
if (searched > ROLL_BUDGET) cost = 0n;
|
|
373
|
+
else {
|
|
374
|
+
const id = Number(pool != null ? (pool[0] ?? pool) : (stash[0] ?? stash));
|
|
375
|
+
cost = COST.try * BigInt(searchTries(project, pool != null, id));
|
|
376
|
+
found.set(key, cost);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
searched += cost;
|
|
380
|
+
return cost;
|
|
381
|
+
};
|
|
382
|
+
let fixed = COST.fixed + COST.odds * BigInt(odds) + COST.share * BigInt(shares) + COST.fate * BigInt(fates);
|
|
383
|
+
let start = BYTES.start + BigInt(outgrown(entries.length, ENTRY_BYTES)) + BigInt(bytes);
|
|
384
|
+
const prizes = [];
|
|
385
|
+
const reread = [];
|
|
386
|
+
const heap = [];
|
|
387
|
+
for (const e of entries) {
|
|
388
|
+
let [set, again, used] = [0n, 0n, BYTES.draw];
|
|
389
|
+
fixed += e.prizes.length ? COST.entry : COST.blank;
|
|
390
|
+
start += BYTES.entryRead;
|
|
391
|
+
e.prizes.forEach((p, k) => {
|
|
392
|
+
fixed += reading(p) + (k > 0 ? COST.prize : 0n);
|
|
393
|
+
start += BYTES.prizeRead;
|
|
394
|
+
if (p.Item != null || p.Compressed) [set, again] = [set + COST.item, again + COST.reread];
|
|
395
|
+
else if (p.Tokens) [set, again] = [set + COST.tokens, again + (p.Tokens.from.Pool ? COST.reread : 0n)];
|
|
396
|
+
else if (p.Stash != null) [set, again] = [set + COST.draw + COST.group, again + COST.reread];
|
|
397
|
+
else if (p.Nft) {
|
|
398
|
+
// A named NFT is searched for row by row: when its stock is read, again, and when it is taken (one row here).
|
|
399
|
+
fixed += COST.row;
|
|
400
|
+
[set, again] = [set + COST.item + COST.row, again + COST.reread + COST.row];
|
|
401
|
+
} else set += COST.item;
|
|
402
|
+
used += p.Tokens ? (p.Tokens.from.Pool ? BYTES.pooled : BYTES.minted) : BYTES.prize;
|
|
403
|
+
});
|
|
404
|
+
// A draw that gives nothing still draws, and keeps its draw's lists.
|
|
405
|
+
prizes.push(e.prizes.length ? set : COST.nothing);
|
|
406
|
+
reread.push(again);
|
|
407
|
+
heap.push(used);
|
|
408
|
+
}
|
|
333
409
|
const weigh = COST.weigh + COST.weighEntry * n;
|
|
334
|
-
const reads = entries.map(e => e.prizes.map(accountOf));
|
|
335
|
-
const cost = entries.map(e =>
|
|
336
|
-
e.prizes.reduce(
|
|
337
|
-
(c, p) => {
|
|
338
|
-
if (p.Item != null || p.Compressed || p.Nft)
|
|
339
|
-
return { set: c.set + COST.item, again: c.again + COST.reread, bytes: c.bytes + BYTES.prize };
|
|
340
|
-
if (p.Tokens)
|
|
341
|
-
return {
|
|
342
|
-
set: c.set + COST.tokens,
|
|
343
|
-
again: c.again + (p.Tokens.from.Pool ? COST.reread : 0n),
|
|
344
|
-
bytes: c.bytes + (p.Tokens.from.Pool ? BYTES.pooled : BYTES.minted),
|
|
345
|
-
};
|
|
346
|
-
if (p.Stash != null)
|
|
347
|
-
return { set: c.set + COST.draw + COST.group, again: c.again + COST.reread, bytes: c.bytes + BYTES.prize };
|
|
348
|
-
return { set: c.set + COST.item, again: c.again, bytes: c.bytes + BYTES.prize };
|
|
349
|
-
},
|
|
350
|
-
{ set: 0n, again: 0n, bytes: BYTES.draw },
|
|
351
|
-
),
|
|
352
|
-
);
|
|
353
|
-
const next = entries.map(
|
|
354
|
-
(_, i) =>
|
|
355
|
-
weigh +
|
|
356
|
-
cost[i].set +
|
|
357
|
-
cost.reduce((sum, c, j) => (j === i || reads[i].some(a => a && reads[j].includes(a)) ? sum + c.again : sum), 0n),
|
|
358
|
-
);
|
|
359
410
|
const most = BigInt(Math.max(1, ...entries.map(e => e.prizes.length)));
|
|
411
|
+
return { fixed, start, weigh, prizes, reread, heap, knowing: COST.pair * n * n * most * most };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** The entries whose stock a draw of entry `i` reads again: itself, and each naming an account it names. */
|
|
415
|
+
const sharing = (entries, i) => {
|
|
416
|
+
const reads = entries.map(e => e.prizes.map(accountOf));
|
|
417
|
+
return entries.map((_, j) => j).filter(j => j === i || reads[i].some(a => a && reads[j].includes(a)));
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Whether `opens` opens by `list` (slots, never empty) fit one answer (`RollCost::fits`): each draw at the dearest its
|
|
422
|
+
* slot could make, which entries share accounts found first when there are several draws. `next`: each entry's draw.
|
|
423
|
+
* `heap`: the answer's heap counts too.
|
|
424
|
+
*/
|
|
425
|
+
function fitsOne(cost, entries, list, opens, next, heap) {
|
|
426
|
+
const n = BigInt(entries.length);
|
|
427
|
+
const draws = list.reduce((d, s) => d + BigInt(s.count), 0n) * BigInt(opens);
|
|
360
428
|
let spent =
|
|
361
|
-
|
|
362
|
-
COST.
|
|
363
|
-
entries.reduce((c, e) => c + (e.prizes.length ? COST.entry : COST.blank), 0n);
|
|
364
|
-
let used =
|
|
365
|
-
BYTES.start +
|
|
366
|
-
BigInt(outgrown(entries.length, ENTRY_BYTES)) +
|
|
367
|
-
BigInt(bytes) +
|
|
368
|
-
BYTES.entryRead * n +
|
|
369
|
-
BYTES.prizeRead * BigInt(entries.reduce((c, e) => c + e.prizes.length, 0));
|
|
370
|
-
let won = 0n;
|
|
371
|
-
spent +=
|
|
372
|
-
(COST.slot + COST.slotEntry * n) * BigInt(Math.max(1, list.length)) +
|
|
429
|
+
cost.fixed +
|
|
430
|
+
(COST.slot + COST.slotEntry * n) * BigInt(list.length) +
|
|
373
431
|
COST.openSlot * BigInt(list.length) * BigInt(opens) +
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
432
|
+
(draws > 1n ? cost.knowing : 0n);
|
|
433
|
+
let used = cost.start;
|
|
434
|
+
let won = 0n;
|
|
377
435
|
for (const s of list) {
|
|
378
436
|
const count = BigInt(s.count) * BigInt(opens);
|
|
379
437
|
const among = entries.map((_, i) => i).filter(i => !(s.only ?? []).length || (s.only ?? []).includes(i));
|
|
380
438
|
const dearest = f => among.reduce((m, i) => (f(i) > m ? f(i) : m), 0n);
|
|
381
439
|
spent += count * dearest(i => next[i]);
|
|
382
|
-
used += count * dearest(i => cost[i]
|
|
440
|
+
used += count * dearest(i => cost.heap[i]);
|
|
383
441
|
won += count * dearest(i => BigInt(entries[i].prizes.length));
|
|
384
442
|
}
|
|
385
|
-
const rolls = oracle || entries.length > 1 || entries.some(e => e.prizes.some(p => p.Stash != null));
|
|
386
443
|
return (
|
|
387
444
|
spent + COST.finish + COST.won * won + COST.drawn * draws <= ROLL_BUDGET &&
|
|
388
|
-
(!
|
|
445
|
+
(!heap || used + BYTES.finish + BYTES.won * won + draws <= ROLL_HEAP)
|
|
389
446
|
);
|
|
390
447
|
}
|
|
391
448
|
|
|
449
|
+
/**
|
|
450
|
+
* Whether all of `opens` opens of `entries` by `slots` fit one oracle answer (`order::draws_fit`), each draw at the
|
|
451
|
+
* dearest its slot could make, with what is known from the box alone (no table rows, a stash of one group, a named
|
|
452
|
+
* NFT's stash of one row), its `odds`, `shares` and `fates` counted, and `bytes` (`boxBytes`) of heap reading the box
|
|
453
|
+
* takes beyond its entries. `slots` may be a number: that many draws among every entry. The answer's heap counts for a
|
|
454
|
+
* box that rolls, or one sold through the oracle (`oracle`: a box that doesn't roll but one `buy` can't carry,
|
|
455
|
+
* `buyFits`). `project`: the project the box is in, whose pools' and stashes' addresses the write searches for (each
|
|
456
|
+
* search counted at `256 - bump` tries of 1,500 units); without it, at `ASSUMED_TRIES` tries an address. The program
|
|
457
|
+
* refuses a box whose one open doesn't fit at its upsert (`PackTooLarge`). `purchase`: counted as `open_box` counts a
|
|
458
|
+
* purchase of `opens` instead (the answer's heap always; a draw's own stock read again only when there are several; at
|
|
459
|
+
* most `MOST_DRAWS_A_PURCHASE` draws in all), from what the box alone tells: the program reads the accounts too.
|
|
460
|
+
*/
|
|
461
|
+
export function drawsFit(
|
|
462
|
+
entries,
|
|
463
|
+
slots,
|
|
464
|
+
opens = 1,
|
|
465
|
+
{ odds = 0, shares = 0, fates = 0, bytes = 0n, oracle = false, project = null, purchase = false } = {},
|
|
466
|
+
) {
|
|
467
|
+
const given = typeof slots === 'number' || typeof slots === 'bigint' ? [{ count: slots, only: [] }] : (slots ?? []);
|
|
468
|
+
// A box with no slots draws once among every entry.
|
|
469
|
+
const list = given.length ? given : [{ count: 1, only: [] }];
|
|
470
|
+
const draws = list.reduce((d, s) => d + BigInt(s.count), 0n) * BigInt(opens);
|
|
471
|
+
if (purchase && draws > BigInt(MOST_DRAWS_A_PURCHASE)) return false;
|
|
472
|
+
const cost = rollCost(entries, { odds, shares, fates, bytes, project });
|
|
473
|
+
const heap = purchase || oracle || entries.length > 1 || entries.some(e => e.prizes.some(p => p.Stash != null));
|
|
474
|
+
// Reading the box alone already past the answer: refused before its entries are compared.
|
|
475
|
+
if (cost.fixed > ROLL_BUDGET || (heap && cost.start > ROLL_HEAP)) return false;
|
|
476
|
+
// Each entry's draw reads again the stock of the entries sharing its accounts; a purchase of one draw, its own alone.
|
|
477
|
+
const again = i => (purchase && draws <= 1n ? [] : sharing(entries, i)).reduce((s, j) => s + cost.reread[j], 0n);
|
|
478
|
+
const next = entries.map((_, i) => cost.weigh + cost.prizes[i] + again(i));
|
|
479
|
+
return fitsOne(cost, entries, list, opens, next, heap);
|
|
480
|
+
}
|
|
481
|
+
|
|
392
482
|
// What one `buy` takes, as the program counts it at the box's write (`catalog::buy_fits`): a transaction holds 64
|
|
393
483
|
// accounts, 64 instructions and 1,400,000 compute units.
|
|
394
|
-
const BUY = { accounts: 15, calls: 7, cu: 30_000, most: { accounts: 64, calls: 64, cu:
|
|
484
|
+
const BUY = { accounts: 15, calls: 7, cu: 30_000, most: { accounts: 64, calls: 64, cu: MAX_TX_CU } };
|
|
395
485
|
const sameJson = (a, b) =>
|
|
396
486
|
JSON.stringify(a, (_, v) => (typeof v === 'bigint' ? String(v) : v)) ===
|
|
397
487
|
JSON.stringify(b, (_, v) => (typeof v === 'bigint' ? String(v) : v));
|
|
@@ -444,6 +534,28 @@ export function buyFits(box, draws = box.slots?.length ? box.slots.reduce((n, s)
|
|
|
444
534
|
return accounts <= BUY.most.accounts && calls <= BUY.most.calls && cu <= BUY.most.cu;
|
|
445
535
|
}
|
|
446
536
|
|
|
537
|
+
/**
|
|
538
|
+
* Whether the program writes `box` (its args, as `upsertBoxIx` takes them, or a box read from the chain) as far as what
|
|
539
|
+
* it costs goes (`catalog::loot_box`, `PackTooLarge`): a box that doesn't roll sells by `buy` when one transaction carries
|
|
540
|
+
* the whole purchase (`buyFits`), else through the oracle, whose heap then counts and which can't give a roll; and every
|
|
541
|
+
* draw of one open fits one answer (`drawsFit`). `project`: whose pools and stashes the box names (a box read from the
|
|
542
|
+
* chain says); without it, each search at `ASSUMED_TRIES` tries.
|
|
543
|
+
*/
|
|
544
|
+
export function boxFits(box, { project = box.project ?? null } = {}) {
|
|
545
|
+
const entries = box.entries ?? [];
|
|
546
|
+
const rolls = entries.length > 1 || entries.some(e => e.prizes.some(p => p.Stash != null));
|
|
547
|
+
const byBuy = rolls || box.subject != null || buyFits(box);
|
|
548
|
+
if (!byBuy && entries.some(e => e.prizes.some(p => p.Roll != null))) return false;
|
|
549
|
+
return drawsFit(entries, box.slots?.length ? box.slots : 1, 1, {
|
|
550
|
+
odds: (box.odds ?? []).length,
|
|
551
|
+
shares: (box.shares ?? []).length,
|
|
552
|
+
fates: (box.fates ?? []).length,
|
|
553
|
+
bytes: boxBytes(box),
|
|
554
|
+
oracle: !byBuy,
|
|
555
|
+
project,
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
447
559
|
/** The most program rent and fee one order takes from its player, in lamports (0.01 SOL): a client cap (`opensAnOrder`). */
|
|
448
560
|
export const ORDER_COST_CAP = 10_000_000n;
|
|
449
561
|
/** Rent a byte, as devnet and mainnet-beta charge it ((bytes + 128) × 5,080 lamports). */
|
|
@@ -452,7 +564,8 @@ const RENT_PER_BYTE = 5_080n;
|
|
|
452
564
|
const PLAYER_FEE = 2_000_000n;
|
|
453
565
|
|
|
454
566
|
/**
|
|
455
|
-
* The most opens of `lootBox` a client puts in one order: what one oracle answer draws (`drawsFit`
|
|
567
|
+
* The most opens of `lootBox` a client puts in one order: what one oracle answer draws (`drawsFit` as a purchase counts
|
|
568
|
+
* it, the box's project's searches counted when the box names it: a box read from the chain does), and what keeps the
|
|
456
569
|
* order's rent and the player's fee within `ORDER_COST_CAP` (`orderBytes` with `items`, so with the box's shares). At
|
|
457
570
|
* least one: a box whose one open is over the cap is still bought one open an order. A purchase of more is several
|
|
458
571
|
* orders, each its own answer.
|
|
@@ -467,10 +580,11 @@ export function opensAnOrder(
|
|
|
467
580
|
shares,
|
|
468
581
|
fates: (lootBox.fates ?? []).length,
|
|
469
582
|
bytes: boxBytes(lootBox),
|
|
470
|
-
|
|
583
|
+
project: lootBox.project ?? null,
|
|
584
|
+
purchase: true,
|
|
471
585
|
}) && (BigInt(orderBytes(lootBox, shares, n, items)) + 128n) * rentPerByte + PLAYER_FEE <= ORDER_COST_CAP;
|
|
472
586
|
let n = 1;
|
|
473
|
-
while (n <
|
|
587
|
+
while (n < MOST_DRAWS_A_PURCHASE && fits(n + 1)) n += 1;
|
|
474
588
|
return n;
|
|
475
589
|
}
|
|
476
590
|
|
package/js/economy/check.mjs
CHANGED
|
@@ -9,11 +9,12 @@ import { gateList } from '../client/grant.js';
|
|
|
9
9
|
import { MAX_BID_NFTS, MAX_SHARES } from '../client/ix.js';
|
|
10
10
|
import { MAX_POTS } from '../client/accounts.js';
|
|
11
11
|
import { vault } from '../client/pda.js';
|
|
12
|
-
import { boxBytes, buyFits, drawsFit, MAX_ORDER_BYTES, tableBudget } from '../client/sizes.js';
|
|
12
|
+
import { ASSUMED_TRIES, boxBytes, buyFits, drawsFit, MAX_ORDER_BYTES, tableBudget } from '../client/sizes.js';
|
|
13
13
|
import { TOKEN_2022_PROGRAM, TOKEN_PROGRAM } from '../chain/pda.js';
|
|
14
14
|
import { addressBytes, encodeBase58 } from '../chain/base58.js';
|
|
15
15
|
import { isOnCurve } from '../chain/curve.js';
|
|
16
16
|
import { utf8 } from '../chain/bytes.js';
|
|
17
|
+
import { METADATA_LIMITS } from '../chain/metadata.js';
|
|
17
18
|
import { sha256 } from '../chain/sha256.js';
|
|
18
19
|
import {
|
|
19
20
|
FEES,
|
|
@@ -59,7 +60,7 @@ import {
|
|
|
59
60
|
weights,
|
|
60
61
|
} from './model.mjs';
|
|
61
62
|
// plan.mjs imports check() in turn: each module only calls the other's functions, never at load time.
|
|
62
|
-
import { boxArgs, boxOrderBytes, placeholderContext, recipeArgs, setupSteps,
|
|
63
|
+
import { boxArgs, boxOrderBytes, placeholderContext, recipeArgs, setupSteps, stepAloneBytes } from './plan.mjs';
|
|
63
64
|
import { findSecrets } from './secrets.mjs';
|
|
64
65
|
import { simulate } from './simulate.mjs';
|
|
65
66
|
import { BUDGET, MAX_TRANSACTION_BYTES, transactionLocks, transactionSize, WALLET_ROOM } from './txsize.mjs';
|
|
@@ -685,10 +686,16 @@ export function dateWords(seconds) {
|
|
|
685
686
|
}
|
|
686
687
|
|
|
687
688
|
/**
|
|
688
|
-
* Checks an economy spec. `now` (Unix seconds) dates "publish".
|
|
689
|
+
* Checks an economy spec. `now` (Unix seconds) dates "publish". `project`: the project's address, once a wallet is named
|
|
690
|
+
* (the plan for a real setup passes it; a live economy's file names it): a box's write searches for each of its pools'
|
|
691
|
+
* and stashes' addresses, and the checker counts each search exactly with it, else at an assumed 2 tries an address
|
|
692
|
+
* (`ASSUMED_TRIES`).
|
|
689
693
|
* Returns { ok, refusals: [{ code, where, message, fix }], warnings: [{ code, where, message, fix? }] }.
|
|
690
694
|
*/
|
|
691
|
-
export function check(
|
|
695
|
+
export function check(
|
|
696
|
+
economy,
|
|
697
|
+
{ network = 'devnet', now = Math.floor(Date.now() / 1000), project = economy?.live?.project ?? null } = {},
|
|
698
|
+
) {
|
|
692
699
|
const refusals = [];
|
|
693
700
|
const warnings = [];
|
|
694
701
|
const refuse = (code, where, message, fix) => refusals.push({ code, where, message, fix });
|
|
@@ -1007,6 +1014,33 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
1007
1014
|
`“${t.key}” is a token the project makes when the economy goes live, but it names the mint ${t.mint}: the setup would never make it, and would mint from an address the project has no say over.`,
|
|
1008
1015
|
'Remove "mint" for a token the setup makes, or make it an existing token (source "existing", with its "decimals").',
|
|
1009
1016
|
);
|
|
1017
|
+
// A token the setup makes is named on chain as it is made (its Token Metadata record), and the record takes so
|
|
1018
|
+
// many bytes of each, not characters: a longer one would be refused on chain, in the middle of going live. One
|
|
1019
|
+
// with no name or ticker at all would be written blank, and show as an unknown token for the record's price. A
|
|
1020
|
+
// token that is live already is not named again: its words here stay labels, as they were.
|
|
1021
|
+
if (source === 'game' && !economy.live?.mints?.[t.key])
|
|
1022
|
+
for (const [field, what] of [
|
|
1023
|
+
['name', 'name'],
|
|
1024
|
+
['symbol', 'ticker'],
|
|
1025
|
+
['uri', 'link'],
|
|
1026
|
+
]) {
|
|
1027
|
+
const value = String(tok[field] ?? '');
|
|
1028
|
+
const bytes = utf8(value).length;
|
|
1029
|
+
if (bytes > METADATA_LIMITS[field])
|
|
1030
|
+
refuse(
|
|
1031
|
+
'E-TOKEN',
|
|
1032
|
+
`${where}.${field}`,
|
|
1033
|
+
`The ${what} of “${t.key}” is ${bytes} bytes, and a token’s ${what} on chain is at most ${METADATA_LIMITS[field]} (a letter outside plain English takes two or more).`,
|
|
1034
|
+
`Shorten it to ${METADATA_LIMITS[field]} bytes${field === 'uri' ? ', for instance with a shorter path or host' : ''}.`,
|
|
1035
|
+
);
|
|
1036
|
+
else if (field !== 'uri' && !value.trim())
|
|
1037
|
+
refuse(
|
|
1038
|
+
'E-TOKEN',
|
|
1039
|
+
`${where}.${field}`,
|
|
1040
|
+
`“${t.key}” has an empty ${what}: going live writes it on chain as it is, and wallets would show a token with no ${what}.`,
|
|
1041
|
+
`Give it a ${what}, or leave "${field}" out (${field === 'symbol' ? 'its key in capitals is used' : 'its ticker is used'}).`,
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1010
1044
|
if (t.supply !== undefined && source !== 'game')
|
|
1011
1045
|
refuse(
|
|
1012
1046
|
'E-TOKEN',
|
|
@@ -2398,7 +2432,9 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
2398
2432
|
// rolls, or one outcome of items, with no outcome that burns the payment and no NFT to roll on (BadEntries).
|
|
2399
2433
|
const slots = b.slots ?? [];
|
|
2400
2434
|
const outs = b.outcomes ?? [];
|
|
2435
|
+
const heldBefore = refusals.length;
|
|
2401
2436
|
answerHolds(b, where);
|
|
2437
|
+
const answerRefused = refusals.length > heldBefore;
|
|
2402
2438
|
for (const [i, o] of outs.entries()) {
|
|
2403
2439
|
const given = outcomeParts(o);
|
|
2404
2440
|
tooMany(
|
|
@@ -2555,8 +2591,10 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
2555
2591
|
// Every draw of one open fits one answer of the oracle's (PackTooLarge): a pack past it could only draw its first
|
|
2556
2592
|
// cards, so the program refuses the box. A box that doesn't roll sells by `buy`, the whole purchase in one
|
|
2557
2593
|
// transaction, or through the oracle when one `buy` can't carry it (`buyFits`): then the answer's heap counts, and it
|
|
2558
|
-
// can't give a roll, which only `buy` gives (catalog.rs). The heap counts the box's own bytes too (`boxBytes`)
|
|
2559
|
-
|
|
2594
|
+
// can't give a roll, which only `buy` gives (catalog.rs). The heap counts the box's own bytes too (`boxBytes`), and
|
|
2595
|
+
// the compute finding each pool's and stash's address its prizes name (exact with the project, else assumed).
|
|
2596
|
+
// A box of one draw is held to the same count: past it the program refuses the box when it is written.
|
|
2597
|
+
if (outs.length && (cards > 1 || !answerRefused)) {
|
|
2560
2598
|
const args = (() => {
|
|
2561
2599
|
try {
|
|
2562
2600
|
return boxArgs(b, m);
|
|
@@ -2566,38 +2604,45 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
2566
2604
|
})();
|
|
2567
2605
|
const entries = args?.entries;
|
|
2568
2606
|
if (entries) {
|
|
2569
|
-
const byBuy = boxRolls(b, m) || Boolean(b.rollsOn) || buyFits(args, cards);
|
|
2607
|
+
const byBuy = boxRolls(b, m) || Boolean(b.rollsOn) || buyFits(args, Math.max(1, cards));
|
|
2570
2608
|
const extras = {
|
|
2571
2609
|
odds: args.odds.length,
|
|
2572
2610
|
shares: args.shares.length,
|
|
2573
2611
|
fates: args.fates.length,
|
|
2574
2612
|
bytes: boxBytes(args),
|
|
2575
2613
|
oracle: !byBuy,
|
|
2614
|
+
project,
|
|
2576
2615
|
};
|
|
2616
|
+
const searches = entries.some(e => e.prizes.some(p => p.Tokens?.from?.Pool || p.Stash != null || p.Nft));
|
|
2617
|
+
const searched = searches
|
|
2618
|
+
? project
|
|
2619
|
+
? ' (finding the address of each pool and stash it names counted for this project)'
|
|
2620
|
+
: ` (finding the address of each pool and stash it names counted at ${ASSUMED_TRIES} tries each, as no wallet is named yet: the check with your wallet counts it exactly)`
|
|
2621
|
+
: '';
|
|
2622
|
+
const drawn = slots.length ? slots.map(s => ({ count: s.count, only: s.from ?? [] })) : 1;
|
|
2577
2623
|
if (!byBuy && entries.some(e => e.prizes.some(p => p.Roll != null)))
|
|
2578
2624
|
refuse(
|
|
2579
2625
|
'E-ODDS',
|
|
2580
2626
|
`${where}.slots`,
|
|
2581
|
-
`${label(b)} gives a roll, which only a sale in one step gives, but ${cards} of what it gives don’t fit one transaction (its accounts, its instructions or its compute), so the program refuses the box (PackTooLarge).`,
|
|
2627
|
+
`${label(b)} gives a roll, which only a sale in one step gives, but ${Math.max(1, cards)} of what it gives don’t fit one transaction (its accounts, its instructions or its compute), so the program refuses the box (PackTooLarge).`,
|
|
2582
2628
|
'Fewer cards an open, fewer prizes in its outcome, or fewer shares in its split.',
|
|
2583
2629
|
);
|
|
2584
|
-
else if (
|
|
2585
|
-
!drawsFit(
|
|
2586
|
-
entries,
|
|
2587
|
-
slots.map(s => ({ count: s.count, only: s.from ?? [] })),
|
|
2588
|
-
1,
|
|
2589
|
-
extras,
|
|
2590
|
-
)
|
|
2591
|
-
) {
|
|
2630
|
+
else if (cards > 1 && !drawsFit(entries, drawn, 1, extras)) {
|
|
2592
2631
|
let most = cards - 1;
|
|
2593
2632
|
while (most > 1 && !drawsFit(entries, most, 1, extras)) most -= 1;
|
|
2594
2633
|
refuse(
|
|
2595
2634
|
'E-ODDS',
|
|
2596
2635
|
`${where}.slots`,
|
|
2597
|
-
`${label(b)} draws ${cards} cards an open among ${outs.length} outcome${outs.length === 1 ? '' : 's'}: more than one answer of the oracle’s can draw (about ${most} here), so the program refuses the box (PackTooLarge).`,
|
|
2636
|
+
`${label(b)} draws ${cards} cards an open among ${outs.length} outcome${outs.length === 1 ? '' : 's'}: more than one answer of the oracle’s can draw (about ${most} here${searched}), so the program refuses the box (PackTooLarge).`,
|
|
2598
2637
|
'Fewer cards an open (sell two packs instead of one), fewer outcomes, fewer prizes in an outcome, or a smaller box (fewer odds, shares, slots or outcomes that act on the NFT).',
|
|
2599
2638
|
);
|
|
2600
|
-
}
|
|
2639
|
+
} else if (cards <= 1 && !drawsFit(entries, drawn, 1, extras))
|
|
2640
|
+
refuse(
|
|
2641
|
+
'E-ODDS',
|
|
2642
|
+
`${where}.outcomes`,
|
|
2643
|
+
`${label(b)} is too long for one answer of the oracle’s to draw it once${searched}, so the program refuses the box when it is written (PackTooLarge).`,
|
|
2644
|
+
'Fewer outcomes, fewer prizes in an outcome, fewer pools or stashes named, or a smaller box (fewer odds, shares or slots). Put a long weighted list in a table or a stash, drawn by one outcome.',
|
|
2645
|
+
);
|
|
2601
2646
|
}
|
|
2602
2647
|
}
|
|
2603
2648
|
if (cards > 1 && b.rollsOn)
|
|
@@ -5700,7 +5745,7 @@ export function check(economy, { network = 'devnet', now = Math.floor(Date.now()
|
|
|
5700
5745
|
for (const step of setup?.steps ?? []) {
|
|
5701
5746
|
let bytes;
|
|
5702
5747
|
try {
|
|
5703
|
-
bytes =
|
|
5748
|
+
bytes = stepAloneBytes(step, ctx);
|
|
5704
5749
|
} catch {
|
|
5705
5750
|
continue;
|
|
5706
5751
|
}
|
package/js/economy/cost.mjs
CHANGED
|
@@ -47,11 +47,19 @@ export function setupCost(plan, economy) {
|
|
|
47
47
|
const m = model(economy);
|
|
48
48
|
// An account that grows (a live stash taking more NFTs) pays rent for its new bytes only.
|
|
49
49
|
const rentOf = c => (c.grows ? BigInt(c.bytes) * RENT_PER_BYTE : rent(c.bytes));
|
|
50
|
+
// `fee`: what the program that makes an account keeps in it on top of its rent (Token Metadata's, for a token's name
|
|
51
|
+
// on chain). It is not rent: it never comes back.
|
|
50
52
|
const accounts = plan.transactions
|
|
51
53
|
.flatMap(t => t.steps)
|
|
52
54
|
.flatMap(s => s.creates ?? [])
|
|
53
|
-
.map(c => ({
|
|
55
|
+
.map(c => ({
|
|
56
|
+
...c,
|
|
57
|
+
lamports: rentOf(c).toString(),
|
|
58
|
+
sol: sol(rentOf(c)),
|
|
59
|
+
...(c.fee ? { feeSol: sol(BigInt(c.fee)) } : {}),
|
|
60
|
+
}));
|
|
54
61
|
const rentLamports = accounts.reduce((n, a) => n + BigInt(a.lamports), 0n);
|
|
62
|
+
const metadataLamports = accounts.reduce((n, a) => n + BigInt(a.fee ?? 0), 0n);
|
|
55
63
|
const charged = plan.transactions.flatMap(t => t.steps).filter(s => !NO_FEE_STEP.test(s.id));
|
|
56
64
|
const feeLamports = BigInt(charged.length) * FEES.project;
|
|
57
65
|
const networkLamports = plan.transactions.reduce((n, t) => n + BigInt(t.signers.length) * SIGNATURE_FEE, 0n);
|
|
@@ -76,7 +84,7 @@ export function setupCost(plan, economy) {
|
|
|
76
84
|
for (const l of m.lotteries) if (steps.has(`rent:lottery:${l.key}`)) seedLamports += roundRentOf(l, m);
|
|
77
85
|
for (const sc of m.schedules)
|
|
78
86
|
if (sc.sealed && (sc.fundFrom ?? 'wallet') === 'wallet') putIn(sc.currency, sc.fund, `fund:${sc.key}`);
|
|
79
|
-
const total = rentLamports + feeLamports + networkLamports + seedLamports;
|
|
87
|
+
const total = rentLamports + metadataLamports + feeLamports + networkLamports + seedLamports;
|
|
80
88
|
const notes = [];
|
|
81
89
|
if (accounts.some(a => a.approx))
|
|
82
90
|
notes.push('Accounts marked “about” are sized roughly (a Core collection or an external token account).');
|
|
@@ -96,6 +104,7 @@ export function setupCost(plan, economy) {
|
|
|
96
104
|
return {
|
|
97
105
|
accounts,
|
|
98
106
|
rentSol: sol(rentLamports),
|
|
107
|
+
metadataFeesSol: sol(metadataLamports),
|
|
99
108
|
programFeesSol: sol(feeLamports),
|
|
100
109
|
networkFeesSol: sol(networkLamports),
|
|
101
110
|
seeds,
|
|
@@ -483,6 +492,8 @@ export function explainCost(c) {
|
|
|
483
492
|
];
|
|
484
493
|
lines.push(
|
|
485
494
|
` rent ${s.rentSol} SOL for ${s.accounts.length} accounts; program fees ${s.programFeesSol} SOL; network fees ${s.networkFeesSol} SOL${
|
|
495
|
+
s.metadataFeesSol ? `; Token Metadata’s fees ${s.metadataFeesSol} SOL (a token’s name on chain)` : ''
|
|
496
|
+
}${
|
|
486
497
|
Object.keys(s.seeds).length
|
|
487
498
|
? `; seeds ${Object.entries(s.seeds)
|
|
488
499
|
.map(([k, v]) => `${v} ${k}`)
|
|
@@ -490,7 +501,10 @@ export function explainCost(c) {
|
|
|
490
501
|
: ''
|
|
491
502
|
}.`,
|
|
492
503
|
);
|
|
493
|
-
for (const a of s.accounts)
|
|
504
|
+
for (const a of s.accounts)
|
|
505
|
+
lines.push(
|
|
506
|
+
` ${a.approx ? 'about ' : ''}${a.sol} SOL: ${a.what} (${a.bytes} bytes)${a.feeSol ? `, and ${a.feeSol} SOL Token Metadata keeps for it` : ''}`,
|
|
507
|
+
);
|
|
494
508
|
lines.push('While it runs (for a player, quote the “in all” number: it is what leaves their wallet for good):');
|
|
495
509
|
for (const r of c.actions) {
|
|
496
510
|
const deposits = r.deposits.map(d => `${d.sol} SOL for ${d.what} (back: ${d.back})`).join('; ');
|
package/js/economy/explain.mjs
CHANGED
|
@@ -204,6 +204,18 @@ export function explainEconomy(economy, { now = Math.floor(Date.now() / 1000) }
|
|
|
204
204
|
);
|
|
205
205
|
if (economy.prompt) lines.push(`Asked for: “${economy.prompt}”`);
|
|
206
206
|
|
|
207
|
+
// A token the setup makes is named on chain as it is made (plan.mjs): what wallets will show, and that a picture
|
|
208
|
+
// needs its link before going live. One that is live already keeps what it has.
|
|
209
|
+
section(
|
|
210
|
+
'The tokens going live makes',
|
|
211
|
+
[...m.tokens.values()]
|
|
212
|
+
.filter(t => t.source === 'game' && !economy.live?.mints?.[t.key])
|
|
213
|
+
.map(
|
|
214
|
+
t =>
|
|
215
|
+
`${t.symbol}${t.name !== t.symbol ? ` (“${t.name}”)` : ''}: ${t.fixedSupply ? `a fixed supply of ${num(t.supply)}, all of it to your wallet, and no more ever` : 'minted by the project as its rules say'}. It is named on chain as it is made, so wallets show ${t.uri ? 'its name, its ticker and the picture its link gives' : 'its name and ticker and no picture: for one, give it a `uri` (a link to a JSON file that names its `image`) before going live'}. Only your wallet can change the name, ticker and link later, and these tools don’t change a live token’s yet: get them right first.`,
|
|
216
|
+
),
|
|
217
|
+
);
|
|
218
|
+
|
|
207
219
|
const fee = num(sim.fees.playerSol);
|
|
208
220
|
const actions = [];
|
|
209
221
|
for (const it of sim.items) {
|
|
@@ -559,6 +571,8 @@ export function explainEconomy(economy, { now = Math.floor(Date.now() / 1000) }
|
|
|
559
571
|
lines.push('', 'What setting it up costs');
|
|
560
572
|
lines.push(
|
|
561
573
|
`- About ${num(s.totalSol)} SOL in all, in ${plural(s.transactions, 'transaction')} (${plural(s.approvals, 'wallet approval')}): rent ${num(s.rentSol)} SOL for ${plural(s.accounts.length, 'account')}, program fees ${num(s.programFeesSol)} SOL, network fees ${num(s.networkFeesSol)} SOL${
|
|
574
|
+
s.metadataFeesSol ? `, Token Metadata’s fees ${num(s.metadataFeesSol)} SOL (a token’s name on chain)` : ''
|
|
575
|
+
}${
|
|
562
576
|
Object.keys(s.seeds).length
|
|
563
577
|
? `, seeds ${Object.entries(s.seeds)
|
|
564
578
|
.map(([k, v]) => `${num(v)} ${k}`)
|
package/js/economy/guide.mjs
CHANGED
|
@@ -10,7 +10,7 @@ const sol = lamports => Number(lamports) / Number(LAMPORTS_PER_SOL);
|
|
|
10
10
|
export const PARTS = {
|
|
11
11
|
tokens: {
|
|
12
12
|
def: 'token',
|
|
13
|
-
says: 'The currencies: SOL, any existing mint by address (classic or Token-2022, coins from a token launchpad or an AMM included), or a token the game makes when it goes live: one the game mints as its rules say (points, tickets, shards, an emission), or one with a fixed `supply` minted once to the owner with minting turned off for good (a community token you keep). loot does not put a token on a market: for trading, launch it on a token launchpad or an AMM, then name the mint.',
|
|
13
|
+
says: 'The currencies: SOL, any existing mint by address (classic or Token-2022, coins from a token launchpad or an AMM included), or a token the game makes when it goes live: one the game mints as its rules say (points, tickets, shards, an emission), or one with a fixed `supply` minted once to the owner with minting turned off for good (a community token you keep). A token the game makes is named on chain as it is made (`name`, `symbol`, and `uri`: a link to a JSON file naming its picture), which is what wallets show; only the owner’s wallet can change that later, and changing them in a spec that is already live changes nothing on chain yet. loot does not put a token on a market: for trading, launch it on a token launchpad or an AMM, then name the mint.',
|
|
14
14
|
acts: 'nobody: a token is a fact',
|
|
15
15
|
},
|
|
16
16
|
currency: {
|
|
@@ -39,7 +39,7 @@ export const PARTS = {
|
|
|
39
39
|
boxes: {
|
|
40
40
|
def: 'box',
|
|
41
41
|
nested: ['outcome', 'odds', 'gate', 'rule', 'effect', 'rise', 'fall', 'split', 'perWalletEach'],
|
|
42
|
-
says: 'A paid (or free) roll on chain with MagicBlock’s verifiable randomness, optionally on an NFT (`rollsOn`: an enchant, a mission’s result, a risky forge), whose outcomes then move its numbers (`shifts`) and send it back, burn it or give it away (`then`; a box that can burn or give it away holds it until the roll settles): each outcome gives an item, a fixed prize from a pool, tokens minted (`tokens`), an NFT of a stash (picked at random, or a named one: `nft`), a roll of another box (`roll`), several of these together (`bundle`: { "gives": "bundle", "prizes": [...] }, a pack; it gives all it names or isn’t given, so it names a pool once, a stash’s random NFT beside nothing else of that stash, and two of an item need two left), or nothing. A box of one outcome with nothing random and no NFT it rolls on is bought in one step (`buy`): the price split and what it gives handed over at once, with no order and no oracle. A box takes an NFT as its price through `rollsOn` (which NFTs) and `then` (where the one paid goes: burned, given, or into a stash, a trade-in that becomes another player’s prize). A box is also the prize any route gives: a grant (`gives: { "roll": box }`), a lottery’s place (`{ "percent", "roll" }`), a contest’s winners (`roll`), an auction’s lot (`{ "roll": box }`), a mission’s tier and a recipe roll it for a winner, who gets whatever it gives; such a box is left `active: false`. How long a box can be is what one answer of the oracle’s holds (measured: 128 outcomes that give nothing, 29 of token amounts, 18 each of its own item; 41 slots; 72 odds rules; 16 token prizes or 18 collectibles in one outcome; 340 texts in a gate): a longer weighted list goes in a table or a stash, drawn by one outcome. A roll is on its written odds whatever is in stock, and `whenOut` says what a draw on an outcome that is out does: "stopSales" (the default) stops selling while one couldn’t be covered, a paid miss gives that draw’s worth back at the settle (with no draw made a paid open fails and is refunded), and a free roll goes back to waiting and is asked again until it hits; "drawLeft" draws among what is left; "giveNothing" keeps selling and a miss gives nothing, paid or free. Odds rules (`odds`) add any percent (99,900 makes an outcome a thousand times likelier; a weight stops at the largest, 4,294,967,295) and count on every route, read when a roll is first asked. A sale in one step sells only while its pools hold every fixed prize. A prize won is set aside for its winner at once. An edit of a box’s roll fails only a purchase still waiting (for its refund); any other roll waiting rolls on the box as edited. A gate (a collectible shown, a few wallets, or a list of any size: `{ "list": [address, …] }`, an allowlist whose proof each player passes) and odds can read what a player shows; `opens`/`closes` time a drop; `perWallet`/`perWalletEach` limit each wallet (one ticket per wallet). A split share "chosen" lets the buyer name a referrer; "chosen:<item>" the holder of a collectible they name (plot rent).',
|
|
42
|
+
says: 'A paid (or free) roll on chain with MagicBlock’s verifiable randomness, optionally on an NFT (`rollsOn`: an enchant, a mission’s result, a risky forge), whose outcomes then move its numbers (`shifts`) and send it back, burn it or give it away (`then`; a box that can burn or give it away holds it until the roll settles): each outcome gives an item, a fixed prize from a pool, tokens minted (`tokens`), an NFT of a stash (picked at random, or a named one: `nft`), a roll of another box (`roll`), several of these together (`bundle`: { "gives": "bundle", "prizes": [...] }, a pack; it gives all it names or isn’t given, so it names a pool once, a stash’s random NFT beside nothing else of that stash, and two of an item need two left), or nothing. A box of one outcome with nothing random and no NFT it rolls on is bought in one step (`buy`): the price split and what it gives handed over at once, with no order and no oracle. A box takes an NFT as its price through `rollsOn` (which NFTs) and `then` (where the one paid goes: burned, given, or into a stash, a trade-in that becomes another player’s prize). A box is also the prize any route gives: a grant (`gives: { "roll": box }`), a lottery’s place (`{ "percent", "roll" }`), a contest’s winners (`roll`), an auction’s lot (`{ "roll": box }`), a mission’s tier and a recipe roll it for a winner, who gets whatever it gives; such a box is left `active: false`. How long a box can be is what one answer of the oracle’s holds (measured: 128 outcomes that give nothing, 29 of token amounts, 18 each of its own item; 41 slots; 72 odds rules; 16 token prizes or 18 collectibles in one outcome; 340 texts in a gate; fewer where its outcomes name pools or stashes, each search for one’s address counted: sixteen stashes fit 9 draws an open where finding them takes 36 tries): a longer weighted list goes in a table or a stash, drawn by one outcome. A roll is on its written odds whatever is in stock, and `whenOut` says what a draw on an outcome that is out does: "stopSales" (the default) stops selling while one couldn’t be covered, a paid miss gives that draw’s worth back at the settle (with no draw made a paid open fails and is refunded), and a free roll goes back to waiting and is asked again until it hits; "drawLeft" draws among what is left; "giveNothing" keeps selling and a miss gives nothing, paid or free. Odds rules (`odds`) add any percent (99,900 makes an outcome a thousand times likelier; a weight stops at the largest, 4,294,967,295) and count on every route, read when a roll is first asked. A sale in one step sells only while its pools hold every fixed prize. A prize won is set aside for its winner at once. An edit of a box’s roll fails only a purchase still waiting (for its refund); any other roll waiting rolls on the box as edited. A gate (a collectible shown, a few wallets, or a list of any size: `{ "list": [address, …] }`, an allowlist whose proof each player passes) and odds can read what a player shows; `opens`/`closes` time a drop; `perWallet`/`perWalletEach` limit each wallet (one ticket per wallet). A split share "chosen" lets the buyer name a referrer; "chosen:<item>" the holder of a collectible they name (plot rent).',
|
|
43
43
|
acts: `the player opens (${sol(FEES.player)} SOL + ${sol(ORACLE_FEE)} SOL to the oracle); its player, a key they named or the project’s keys settle. Chance that pays money is flagged`,
|
|
44
44
|
},
|
|
45
45
|
payments: {
|