@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/client/pda.js
CHANGED
|
@@ -16,13 +16,13 @@ export const BUBBLEGUM_PROGRAM = 'BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY';
|
|
|
16
16
|
export const COMPRESSION_PROGRAM = 'mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW';
|
|
17
17
|
export const NOOP_PROGRAM = 'mnoopTCrg4p8ry25e4bcWA9XZjbNjMTfgYVGGEdRsf3';
|
|
18
18
|
|
|
19
|
-
const seed =
|
|
20
|
-
const key =
|
|
21
|
-
const u8 =
|
|
22
|
-
const u16 =
|
|
23
|
-
const u32 =
|
|
24
|
-
const u64 =
|
|
25
|
-
const i64 =
|
|
19
|
+
const seed = s => utf8(s);
|
|
20
|
+
const key = a => addressBytes(a);
|
|
21
|
+
const u8 = n => uintLE(n, 1);
|
|
22
|
+
const u16 = n => uintLE(n, 2);
|
|
23
|
+
const u32 = n => uintLE(n, 4);
|
|
24
|
+
const u64 = n => uintLE(n, 8);
|
|
25
|
+
const i64 = n => intLE(n, 8);
|
|
26
26
|
|
|
27
27
|
/** The PDA of these seeds under the loot program, with its bump. */
|
|
28
28
|
export const lootAddress = (seeds, program = LOOT_PROGRAM) => findProgramAddress(seeds, program);
|
|
@@ -34,7 +34,7 @@ export const pda = {
|
|
|
34
34
|
* new owner keeps the address. */
|
|
35
35
|
project: (creator, id = 0) => at(seed('project'), key(creator), u64(id)),
|
|
36
36
|
/** The project's fee box: where the platform's fees on its transactions wait. */
|
|
37
|
-
fees:
|
|
37
|
+
fees: project => at(seed('fees'), key(project)),
|
|
38
38
|
pool: (project, index) => at(seed('pool'), key(project), u16(index)),
|
|
39
39
|
item: (project, id) => at(seed('item'), key(project), u32(id)),
|
|
40
40
|
box: (project, id) => at(seed('box'), key(project), u32(id)),
|
|
@@ -56,8 +56,10 @@ export const pda = {
|
|
|
56
56
|
/** Every collectible the program mints of its own accord (an order's, a prize's, an auction's lot): its address
|
|
57
57
|
* names its item and its serial, minted once at most. */
|
|
58
58
|
itemAsset: (item, serial) => at(seed('asset'), key(item), u32(serial)),
|
|
59
|
-
/** A contest's pot
|
|
60
|
-
pot:
|
|
59
|
+
/** A contest's pot: `["pot", contest]`. */
|
|
60
|
+
pot: record => at(seed('pot'), key(record)),
|
|
61
|
+
/** A lottery's pot `id` (below `MAX_POTS`), kept from round to round: `["pot", lottery, id]`. */
|
|
62
|
+
lotteryPot: (lottery, id) => at(seed('pot'), key(lottery), u8(id)),
|
|
61
63
|
/** One round of a lottery: its record, opened by the round's first purchase (`["round", lottery, incarnation, round]`). */
|
|
62
64
|
round: (lottery, incarnation, round) => at(seed('round'), key(lottery), u64(incarnation), u32(round)),
|
|
63
65
|
/** A lottery round's own token account of `mint`, which the round's record owns. */
|
|
@@ -69,27 +71,27 @@ export const pda = {
|
|
|
69
71
|
/** An offer: an id is its maker's own. Its money waits in the project's vault for its mint (`vault`). */
|
|
70
72
|
offer: (project, maker, id) => at(seed('offer'), key(project), key(maker), u32(id)),
|
|
71
73
|
/** A lottery's own stash: the NFTs its places promise (`Prize::Held`), which leave only as its prizes. */
|
|
72
|
-
lotteryStash:
|
|
74
|
+
lotteryStash: lottery => at(seed('lottery_stash'), key(lottery)),
|
|
73
75
|
/** A contest's prize split by stake, set aside when it is decided: `["prize", contest]`. */
|
|
74
|
-
prizeVault:
|
|
76
|
+
prizeVault: contest => at(seed('prize'), key(contest)),
|
|
75
77
|
/** A sealed schedule's own vault, which only its runs pay out of: `["vest", schedule]`. */
|
|
76
|
-
vest:
|
|
78
|
+
vest: schedule => at(seed('vest'), key(schedule)),
|
|
77
79
|
/** A listing of an NFT for sale or for rent: one per NFT a project holds, so its address names the NFT. */
|
|
78
80
|
listing: (project, asset) => at(seed('listing'), key(project), key(asset)),
|
|
79
|
-
staked:
|
|
80
|
-
rewards:
|
|
81
|
+
staked: farm => at(seed('staked'), key(farm)),
|
|
82
|
+
rewards: farm => at(seed('rewards'), key(farm)),
|
|
81
83
|
/** A farm's record of when its seats stop earning, for a farm that keeps one (`Farm.keeps_ends`). */
|
|
82
|
-
ends:
|
|
84
|
+
ends: farm => at(seed('ends'), key(farm)),
|
|
83
85
|
/** A seat: a player's in a farm of tokens, a collectible's (its asset) in a farm of collectibles. */
|
|
84
86
|
seat: (farm, by) => at(seed('seat'), key(farm), key(by)),
|
|
85
87
|
/** What the project writes about an NFT it has no authority over (its Sheet): `nft` is a Core asset, a Token Metadata mint or a compressed NFT's asset id. */
|
|
86
88
|
sheet: (project, nft) => at(seed('sheet'), key(project), key(nft)),
|
|
87
89
|
/** A compressed NFT as last proven (`prove_leaf`), read while the leaf is unchanged and its root is still in the tree's change log. */
|
|
88
|
-
leaf:
|
|
90
|
+
leaf: asset => at(seed('leaf'), key(asset)),
|
|
89
91
|
/** The root of the traits the project vouches for, under its own `key` (a collection's address, by convention). */
|
|
90
92
|
traits: (project, key_) => at(seed('traits'), key(project), key(key_)),
|
|
91
93
|
/** Bubblegum's config of a tree: `[tree]`. */
|
|
92
|
-
treeConfig:
|
|
94
|
+
treeConfig: tree => findProgramAddress([key(tree)], BUBBLEGUM_PROGRAM)[0],
|
|
93
95
|
/** A compressed NFT's asset id: Bubblegum's `["asset", tree, nonce]`, `nonce` the count minted before it. */
|
|
94
96
|
compressedAsset: (tree, nonce) => findProgramAddress([seed('asset'), key(tree), u64(nonce)], BUBBLEGUM_PROGRAM)[0],
|
|
95
97
|
/** A wallet's counts in a project: how many times it bought from each box, crafted each recipe or held tickets in
|
package/js/client/policy.js
CHANGED
|
@@ -20,7 +20,8 @@ export function orderRefundAt(order) {
|
|
|
20
20
|
const slot = BigInt(order.requested_slot);
|
|
21
21
|
if (order.status === 'Failed') return { slot, at: null };
|
|
22
22
|
if (order.status === 'Pending') return { slot, at: BigInt(order.asked_at) + STALE_SECONDS };
|
|
23
|
-
if (order.status === 'Asked' && (BigInt(order.paid ?? 0) > 0n || order.held))
|
|
23
|
+
if (order.status === 'Asked' && (BigInt(order.paid ?? 0) > 0n || order.held))
|
|
24
|
+
return { slot: null, at: BigInt(order.first_asked_at) + LOTTERY_TIMEOUT_SECONDS };
|
|
24
25
|
if (['Rolled', 'Waiting', 'Asked'].includes(order.status)) return null;
|
|
25
26
|
throw new Error('Unknown order status.');
|
|
26
27
|
}
|
|
@@ -39,7 +40,8 @@ export function orderRefundable(order, slot, now) {
|
|
|
39
40
|
* again is asked from the next slot on.
|
|
40
41
|
*/
|
|
41
42
|
export const rollAskable = (order, slot, now) =>
|
|
42
|
-
(order.status === 'Waiting' &&
|
|
43
|
+
(order.status === 'Waiting' &&
|
|
44
|
+
(BigInt(order.asked_first ?? 0) === 0n || BigInt(slot) > BigInt(order.requested_slot))) ||
|
|
43
45
|
(order.status === 'Asked' && BigInt(now) > BigInt(order.asked_at) + STALE_SECONDS);
|
|
44
46
|
|
|
45
47
|
/**
|
|
@@ -50,12 +52,16 @@ export function contestOff(contest, now) {
|
|
|
50
52
|
if (contest.verdict === 'Off') return true;
|
|
51
53
|
if (contest.verdict !== 'Open') return false;
|
|
52
54
|
const total = contest.sides.reduce((sum, side) => sum + BigInt(side), 0n);
|
|
53
|
-
return
|
|
55
|
+
return (
|
|
56
|
+
BigInt(now) > BigInt(contest.result_by) ||
|
|
57
|
+
(BigInt(now) >= BigInt(contest.closes_at) && total < BigInt(contest.min_pot ?? 0))
|
|
58
|
+
);
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
/** Whether a lottery round's draw (`round`: its decoded record) was asked for and the oracle has not answered within
|
|
57
62
|
* `STALE_SECONDS` by `now` (Unix seconds, the chain's clock): anyone may ask again. */
|
|
58
|
-
export const lotteryDrawStuck = (round, now) =>
|
|
63
|
+
export const lotteryDrawStuck = (round, now) =>
|
|
64
|
+
round?.status === 'Drawing' && BigInt(now) > BigInt(round.asked_at) + STALE_SECONDS;
|
|
59
65
|
|
|
60
66
|
/**
|
|
61
67
|
* Whether a lottery round's draw (`round`: its decoded record) was asked for and the oracle has not answered within
|
|
@@ -63,7 +69,8 @@ export const lotteryDrawStuck = (round, now) => round?.status === 'Drawing' && B
|
|
|
63
69
|
* the next ask of that round (`drawLotteryIx` with its address) cancels it, and every entry is refunded. A round nobody
|
|
64
70
|
* asked to draw is never cancelled.
|
|
65
71
|
*/
|
|
66
|
-
export const lotteryCancellable = (round, now) =>
|
|
72
|
+
export const lotteryCancellable = (round, now) =>
|
|
73
|
+
round?.status === 'Drawing' && BigInt(now) > BigInt(round.first_asked_at) + LOTTERY_TIMEOUT_SECONDS;
|
|
67
74
|
|
|
68
75
|
/** A farm seat's `earns_until` when nothing ends its earning (i64::MAX): no tier term and no fuel it reads. */
|
|
69
76
|
export const NO_END = (1n << 63n) - 1n;
|
package/js/client/random.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The loot program's rolling module (`src/random`), mirrored: every random choice it makes is drawn from a stream of
|
|
2
2
|
// the oracle's answer, by a path that names what is drawn, with one unbiased sampler. A page shows from an order's
|
|
3
3
|
// answer what each draw gave (and a fresh unit's range traits) exactly as the program drew them. Held to the
|
|
4
|
-
// program's own vectors (`js/test/fixtures/roll-vectors.json`, from
|
|
4
|
+
// program's own vectors (`js/test/fixtures/roll-vectors.json`, from the program's own tests).
|
|
5
5
|
import { concat, uintLE } from '../chain/bytes.js';
|
|
6
6
|
import { sha256 } from '../chain/sha256.js';
|
|
7
7
|
|
|
@@ -15,7 +15,7 @@ const PREFIX = new TextEncoder().encode('loot-roll-v1');
|
|
|
15
15
|
/** A stream of the oracle's 32-byte `answer` at `path` (whole numbers): seed = sha256("loot-roll-v1" | answer | path as
|
|
16
16
|
* u32 little-endian words); its nth number the first 8 bytes, little-endian, of sha256(seed | n as u64 little-endian). */
|
|
17
17
|
export function stream(answer, path = []) {
|
|
18
|
-
const words = path.slice(0, MAX_PATH).map(
|
|
18
|
+
const words = path.slice(0, MAX_PATH).map(w => uintLE(w, 4));
|
|
19
19
|
const seed = sha256(concat(PREFIX, Uint8Array.from(answer), ...words));
|
|
20
20
|
let counter = 0n;
|
|
21
21
|
return {
|
package/js/client/sizes.js
CHANGED
|
@@ -23,15 +23,18 @@ function maximum(type, path) {
|
|
|
23
23
|
return 4 + bounds[path] * maximum(type.vec, path);
|
|
24
24
|
}
|
|
25
25
|
const name = type.defined.name;
|
|
26
|
-
const definition = IDL.types.find(
|
|
26
|
+
const definition = IDL.types.find(entry => entry.name === name)?.type;
|
|
27
27
|
if (!definition) throw new Error(`Missing current IDL type ${name}`);
|
|
28
|
-
const fields =
|
|
29
|
-
|
|
28
|
+
const fields = list =>
|
|
29
|
+
(list ?? []).reduce((n, field) => n + maximum(field.type ?? field, `${name}.${field.name ?? ''}`), 0);
|
|
30
|
+
return definition.kind === 'enum'
|
|
31
|
+
? 1 + Math.max(...definition.variants.map(variant => fields(variant.fields)))
|
|
32
|
+
: fields(definition.fields);
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
/** The space reserved by a fixed-size account, including its discriminator. */
|
|
33
36
|
export function allocationBytes(name) {
|
|
34
|
-
const account = IDL.accounts.find(
|
|
37
|
+
const account = IDL.accounts.find(entry => entry.name === name);
|
|
35
38
|
if (!account) throw new Error(`Missing current IDL account ${name}`);
|
|
36
39
|
return account.discriminator.length + maximum({ defined: { name } }, name);
|
|
37
40
|
}
|
|
@@ -59,7 +62,7 @@ export const MAX_CALLBACK_ACCOUNTS = 21;
|
|
|
59
62
|
export function callbackAccounts(entries) {
|
|
60
63
|
const sources = new Set();
|
|
61
64
|
let named = 0;
|
|
62
|
-
for (const prize of entries.flatMap(
|
|
65
|
+
for (const prize of entries.flatMap(e => e.prizes)) {
|
|
63
66
|
if (prize.Item || prize.Compressed || prize.Nft || prize.Stash) named += 1;
|
|
64
67
|
else if (prize.Tokens) {
|
|
65
68
|
if (prize.Tokens.from.Pool) named += 1;
|
|
@@ -73,9 +76,9 @@ export function callbackAccounts(entries) {
|
|
|
73
76
|
* How many fresh items' table accounts the oracle's answer to a roll of `entries` can carry (`order::table_budget`):
|
|
74
77
|
* what the box's own accounts leave of `MAX_CALLBACK_ACCOUNTS`. Negative: the box can't be rolled at all.
|
|
75
78
|
*/
|
|
76
|
-
export const tableBudget =
|
|
79
|
+
export const tableBudget = entries => MAX_CALLBACK_ACCOUNTS - callbackAccounts(entries);
|
|
77
80
|
|
|
78
|
-
const draws =
|
|
81
|
+
const draws = lootBox => (lootBox.slots?.length ? lootBox.slots.reduce((n, s) => n + Number(s.count), 0) : 1);
|
|
79
82
|
const ZERO = '11111111111111111111111111111111';
|
|
80
83
|
|
|
81
84
|
/** The most an order can be (`MAX_ORDER_BYTES`, not in the IDL): a box whose one open's order would be more is refused
|
|
@@ -84,8 +87,8 @@ export const MAX_ORDER_BYTES = 10_240;
|
|
|
84
87
|
|
|
85
88
|
/** The bytes a draw sets aside for a prize, as the `Won` it becomes (`Order::sized`): an item 14, tokens or an NFT 41,
|
|
86
89
|
* a roll, a held NFT or a raffled place nothing. */
|
|
87
|
-
const wonBytes =
|
|
88
|
-
const tableCount =
|
|
90
|
+
const wonBytes = p => (p.Item || p.Compressed ? 14 : p.Tokens || p.Nft || p.Stash ? 41 : 0);
|
|
91
|
+
const tableCount = item => (item?.traits ?? []).filter(t => t.from?.Table).length;
|
|
89
92
|
|
|
90
93
|
/**
|
|
91
94
|
* The rows of fresh items' tables a draw of each entry writes into its order (`order::rows_of`), walked with the box's
|
|
@@ -94,13 +97,15 @@ const tableCount = (item) => (item?.traits ?? []).filter((t) => t.from?.Table).l
|
|
|
94
97
|
*/
|
|
95
98
|
export function entryRows(entries, items = {}) {
|
|
96
99
|
let budget = Math.max(0, tableBudget(entries));
|
|
97
|
-
return entries.map(({ prizes }) =>
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
return entries.map(({ prizes }) =>
|
|
101
|
+
prizes.reduce((n, p) => {
|
|
102
|
+
const id = p.Item?.[0] ?? p.Compressed?.item;
|
|
103
|
+
const t = id === undefined ? 0 : tableCount(items[id]);
|
|
104
|
+
if (t > budget) return n;
|
|
105
|
+
budget -= t;
|
|
106
|
+
return n + t;
|
|
107
|
+
}, 0),
|
|
108
|
+
);
|
|
104
109
|
}
|
|
105
110
|
|
|
106
111
|
/**
|
|
@@ -113,20 +118,52 @@ export function entryRows(entries, items = {}) {
|
|
|
113
118
|
*/
|
|
114
119
|
export function orderBytes(lootBox, shares = 0, opens = 1, items) {
|
|
115
120
|
const drawn = draws(lootBox) * Math.max(1, opens);
|
|
116
|
-
const aDraw = Math.max(0, ...lootBox.entries.map(
|
|
117
|
-
const gives = lootBox.entries.some(
|
|
118
|
-
const rows = items
|
|
121
|
+
const aDraw = Math.max(0, ...lootBox.entries.map(e => e.prizes.reduce((n, p) => n + wonBytes(p), 0)));
|
|
122
|
+
const gives = lootBox.entries.some(e => e.prizes.some(p => p.Item || p.Compressed));
|
|
123
|
+
const rows = items
|
|
124
|
+
? Math.max(0, ...entryRows(lootBox.entries, items))
|
|
125
|
+
: gives
|
|
126
|
+
? Math.max(0, tableBudget(lootBox.entries))
|
|
127
|
+
: 0;
|
|
119
128
|
const fates = lootBox.fates ?? [];
|
|
120
|
-
const fate = fates.reduce(
|
|
129
|
+
const fate = fates.reduce(
|
|
130
|
+
(a, f) =>
|
|
131
|
+
a &&
|
|
132
|
+
encodeBorsh(IDL, { defined: { name: 'Fate' } }, a).length >=
|
|
133
|
+
encodeBorsh(IDL, { defined: { name: 'Fate' } }, f).length
|
|
134
|
+
? a
|
|
135
|
+
: f,
|
|
136
|
+
null,
|
|
137
|
+
);
|
|
121
138
|
const empty = {
|
|
122
|
-
project: ZERO,
|
|
139
|
+
project: ZERO,
|
|
140
|
+
player: ZERO,
|
|
141
|
+
mint: ZERO,
|
|
142
|
+
loot_box: ZERO,
|
|
143
|
+
box_version: 0,
|
|
123
144
|
odds_bps: new Array(lootBox.odds?.length ?? 0).fill(0),
|
|
124
145
|
status: 'Waiting',
|
|
125
146
|
won: [],
|
|
126
|
-
paid: 0n,
|
|
147
|
+
paid: 0n,
|
|
148
|
+
fee: 0n,
|
|
127
149
|
shares: new Array(shares).fill({ to: { Account: [ZERO] }, bps: 0 }),
|
|
128
|
-
opens: 0,
|
|
129
|
-
|
|
150
|
+
opens: 0,
|
|
151
|
+
settled: 0,
|
|
152
|
+
undrawn: 0,
|
|
153
|
+
back: 0n,
|
|
154
|
+
settler: ZERO,
|
|
155
|
+
drew: 0,
|
|
156
|
+
answer: new Uint8Array(32),
|
|
157
|
+
drawn: [],
|
|
158
|
+
reserved: 0n,
|
|
159
|
+
requested_slot: 0n,
|
|
160
|
+
asked_first: 0n,
|
|
161
|
+
asked_at: 0n,
|
|
162
|
+
first_asked_at: 0n,
|
|
163
|
+
counted: 0,
|
|
164
|
+
subject: lootBox.subject ? ZERO : null,
|
|
165
|
+
held: false,
|
|
166
|
+
fate,
|
|
130
167
|
};
|
|
131
168
|
return encodeAccount(IDL, 'Order', empty).length + drawn * (aDraw + rows * 4);
|
|
132
169
|
}
|
|
@@ -136,42 +173,105 @@ export function orderBytes(lootBox, shares = 0, opens = 1, items) {
|
|
|
136
173
|
// answer, less a margin.
|
|
137
174
|
const ROLL_BUDGET = 300_000n;
|
|
138
175
|
const ROLL_HEAP = 32n * 1024n - 512n;
|
|
139
|
-
const COST = {
|
|
140
|
-
|
|
176
|
+
const COST = {
|
|
177
|
+
fixed: 20_000n,
|
|
178
|
+
entry: 3_500n,
|
|
179
|
+
blank: 1_400n,
|
|
180
|
+
weigh: 1_000n,
|
|
181
|
+
weighEntry: 40n,
|
|
182
|
+
pair: 150n,
|
|
183
|
+
item: 1_500n,
|
|
184
|
+
tokens: 6_000n,
|
|
185
|
+
draw: 9_000n,
|
|
186
|
+
group: 250n,
|
|
187
|
+
reread: 1_000n,
|
|
188
|
+
finish: 15_000n,
|
|
189
|
+
won: 150n,
|
|
190
|
+
drawn: 50n,
|
|
191
|
+
odds: 1_000n,
|
|
192
|
+
share: 1_000n,
|
|
193
|
+
fate: 1_000n,
|
|
194
|
+
slot: 500n,
|
|
195
|
+
slotEntry: 350n,
|
|
196
|
+
openSlot: 150n,
|
|
197
|
+
};
|
|
198
|
+
const BYTES = {
|
|
199
|
+
start: 6_000n,
|
|
200
|
+
prizeRead: 600n,
|
|
201
|
+
entryRead: 180n,
|
|
202
|
+
minted: 700n,
|
|
203
|
+
pooled: 1_000n,
|
|
204
|
+
prize: 200n,
|
|
205
|
+
draw: 40n,
|
|
206
|
+
finish: 2_000n,
|
|
207
|
+
won: 120n,
|
|
208
|
+
boxByte: 2n,
|
|
209
|
+
gateByte: 3n,
|
|
210
|
+
odds: 100n,
|
|
211
|
+
fate: 60n,
|
|
212
|
+
slot: 64n,
|
|
213
|
+
};
|
|
141
214
|
|
|
142
215
|
/** The Borsh length of `value` as IDL type `type`, an address counted as 32 bytes whatever stands in for it (a spec's
|
|
143
216
|
* `@mint:…` before it goes live). */
|
|
144
217
|
function borshLength(type, value) {
|
|
145
|
-
if (typeof type === 'string')
|
|
218
|
+
if (typeof type === 'string')
|
|
219
|
+
return type === 'string'
|
|
220
|
+
? 4 + new TextEncoder().encode(String(value)).length
|
|
221
|
+
: type === 'bytes'
|
|
222
|
+
? 4 + value.length
|
|
223
|
+
: scalar[type];
|
|
146
224
|
if (type.array) return type.array[1] * (type.array[0] === 'u8' ? 1 : borshLength(type.array[0], value?.[0]));
|
|
147
225
|
if (type.vec) return 4 + (value ?? []).reduce((n, v) => n + borshLength(type.vec, v), 0);
|
|
148
226
|
if (type.option) return value == null ? 1 : 1 + borshLength(type.option, value);
|
|
149
|
-
const definition = IDL.types.find(
|
|
227
|
+
const definition = IDL.types.find(entry => entry.name === type.defined.name).type;
|
|
150
228
|
if (definition.kind === 'enum') {
|
|
151
229
|
const name = typeof value === 'string' ? value : Object.keys(value)[0];
|
|
152
|
-
const fields = definition.variants.find(
|
|
230
|
+
const fields = definition.variants.find(v => v.name === name)?.fields ?? [];
|
|
153
231
|
const payload = typeof value === 'string' ? [] : value[name];
|
|
154
|
-
return
|
|
232
|
+
return (
|
|
233
|
+
1 +
|
|
234
|
+
fields.reduce(
|
|
235
|
+
(n, f, i) =>
|
|
236
|
+
n +
|
|
237
|
+
(typeof f === 'object' && 'name' in f ? borshLength(f.type, payload[f.name]) : borshLength(f, payload[i])),
|
|
238
|
+
0,
|
|
239
|
+
)
|
|
240
|
+
);
|
|
155
241
|
}
|
|
156
242
|
return definition.fields.reduce((n, f) => n + borshLength(f.type, value[f.name]), 0);
|
|
157
243
|
}
|
|
158
244
|
|
|
159
|
-
const defined =
|
|
245
|
+
const defined = name => ({ defined: { name } });
|
|
160
246
|
|
|
161
247
|
/**
|
|
162
248
|
* The heap reading a box takes beyond its entries (`order::box_bytes`): its odds, gate, shares, subject, slots and
|
|
163
249
|
* fates, read whole with it, by their bytes and their number. `box`: the box's args, as `upsertBoxIx` takes them.
|
|
164
250
|
*/
|
|
165
251
|
export function boxBytes({ odds = [], gate = null, shares = [], subject = null, slots = [], fates = [] }) {
|
|
166
|
-
const slotsWritten = slots.map(
|
|
167
|
-
const fatesWritten = fates.map(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
252
|
+
const slotsWritten = slots.map(s => ({ only: [], distinct: false, share: 0, ...s }));
|
|
253
|
+
const fatesWritten = fates.map(f => ({
|
|
254
|
+
then: null,
|
|
255
|
+
...f,
|
|
256
|
+
shifts: (f.shifts ?? []).map(x => ({ from_now: false, ...x })),
|
|
257
|
+
}));
|
|
258
|
+
const bytes =
|
|
259
|
+
borshLength({ vec: defined('Odds') }, odds) +
|
|
260
|
+
borshLength({ vec: defined('Share') }, shares) +
|
|
261
|
+
borshLength({ option: defined('Rule') }, subject) +
|
|
262
|
+
borshLength({ vec: defined('Slot') }, slotsWritten) +
|
|
263
|
+
borshLength({ vec: defined('Fate') }, fatesWritten);
|
|
264
|
+
return (
|
|
265
|
+
BYTES.boxByte * BigInt(bytes) +
|
|
266
|
+
BYTES.gateByte * BigInt(borshLength({ option: defined('Gate') }, gate)) +
|
|
267
|
+
BYTES.odds * BigInt(odds.length) +
|
|
268
|
+
BYTES.fate * BigInt(fates.length) +
|
|
269
|
+
BYTES.slot * BigInt(slots.length)
|
|
270
|
+
);
|
|
171
271
|
}
|
|
172
272
|
|
|
173
273
|
/** The account a prize reads, as a string, when it reads one the box can name: entries naming the same share it. */
|
|
174
|
-
const accountOf =
|
|
274
|
+
const accountOf = p => {
|
|
175
275
|
if (p.Item != null) return `item ${p.Item[0] ?? p.Item}`;
|
|
176
276
|
if (p.Compressed) return `item ${p.Compressed.item}`;
|
|
177
277
|
if (p.Tokens?.from?.Pool) return `pool ${p.Tokens.from.Pool[0] ?? p.Tokens.from.Pool}`;
|
|
@@ -189,41 +289,80 @@ const accountOf = (p) => {
|
|
|
189
289
|
* one open doesn't fit at its upsert, and a purchase that doesn't at `open_box`, where it also reads what the accounts
|
|
190
290
|
* hold (`PackTooLarge`).
|
|
191
291
|
*/
|
|
192
|
-
export function drawsFit(
|
|
292
|
+
export function drawsFit(
|
|
293
|
+
entries,
|
|
294
|
+
slots,
|
|
295
|
+
opens = 1,
|
|
296
|
+
{ odds = 0, shares = 0, fates = 0, bytes = 0n, oracle = false } = {},
|
|
297
|
+
) {
|
|
193
298
|
const list = typeof slots === 'number' || typeof slots === 'bigint' ? [{ count: slots, only: [] }] : (slots ?? []);
|
|
194
299
|
const draws = list.reduce((n, s) => n + BigInt(s.count), 0n) * BigInt(opens);
|
|
195
300
|
if (draws <= 1n) return true;
|
|
196
301
|
const n = BigInt(entries.length);
|
|
197
302
|
const weigh = COST.weigh + COST.weighEntry * n;
|
|
198
|
-
const reads = entries.map(
|
|
199
|
-
const cost = entries.map(
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
303
|
+
const reads = entries.map(e => e.prizes.map(accountOf));
|
|
304
|
+
const cost = entries.map(e =>
|
|
305
|
+
e.prizes.reduce(
|
|
306
|
+
(c, p) => {
|
|
307
|
+
if (p.Item != null || p.Compressed || p.Nft)
|
|
308
|
+
return { set: c.set + COST.item, again: c.again + COST.reread, bytes: c.bytes + BYTES.prize };
|
|
309
|
+
if (p.Tokens)
|
|
310
|
+
return {
|
|
311
|
+
set: c.set + COST.tokens,
|
|
312
|
+
again: c.again + (p.Tokens.from.Pool ? COST.reread : 0n),
|
|
313
|
+
bytes: c.bytes + (p.Tokens.from.Pool ? BYTES.pooled : BYTES.minted),
|
|
314
|
+
};
|
|
315
|
+
if (p.Stash != null)
|
|
316
|
+
return { set: c.set + COST.draw + COST.group, again: c.again + COST.reread, bytes: c.bytes + BYTES.prize };
|
|
317
|
+
return { set: c.set + COST.item, again: c.again, bytes: c.bytes + BYTES.prize };
|
|
318
|
+
},
|
|
319
|
+
{ set: 0n, again: 0n, bytes: BYTES.draw },
|
|
320
|
+
),
|
|
321
|
+
);
|
|
322
|
+
const next = entries.map(
|
|
323
|
+
(_, i) =>
|
|
324
|
+
weigh +
|
|
325
|
+
cost[i].set +
|
|
326
|
+
cost.reduce((sum, c, j) => (j === i || reads[i].some(a => a && reads[j].includes(a)) ? sum + c.again : sum), 0n),
|
|
327
|
+
);
|
|
328
|
+
const most = BigInt(Math.max(1, ...entries.map(e => e.prizes.length)));
|
|
329
|
+
let spent =
|
|
330
|
+
COST.fixed +
|
|
331
|
+
COST.pair * n * n * most * most +
|
|
332
|
+
entries.reduce((c, e) => c + (e.prizes.length ? COST.entry : COST.blank), 0n);
|
|
333
|
+
let used =
|
|
334
|
+
BYTES.start +
|
|
335
|
+
BigInt(bytes) +
|
|
336
|
+
BYTES.entryRead * n +
|
|
337
|
+
BYTES.prizeRead * BigInt(entries.reduce((c, e) => c + e.prizes.length, 0));
|
|
209
338
|
let won = 0n;
|
|
210
|
-
spent +=
|
|
339
|
+
spent +=
|
|
340
|
+
(COST.slot + COST.slotEntry * n) * BigInt(Math.max(1, list.length)) +
|
|
341
|
+
COST.openSlot * BigInt(list.length) * BigInt(opens) +
|
|
342
|
+
COST.odds * BigInt(odds) +
|
|
343
|
+
COST.share * BigInt(shares) +
|
|
344
|
+
COST.fate * BigInt(fates);
|
|
211
345
|
for (const s of list) {
|
|
212
346
|
const count = BigInt(s.count) * BigInt(opens);
|
|
213
|
-
const among = entries.map((_, i) => i).filter(
|
|
214
|
-
const dearest =
|
|
215
|
-
spent += count * dearest(
|
|
216
|
-
used += count * dearest(
|
|
217
|
-
won += count * dearest(
|
|
347
|
+
const among = entries.map((_, i) => i).filter(i => !(s.only ?? []).length || (s.only ?? []).includes(i));
|
|
348
|
+
const dearest = f => among.reduce((m, i) => (f(i) > m ? f(i) : m), 0n);
|
|
349
|
+
spent += count * dearest(i => next[i]);
|
|
350
|
+
used += count * dearest(i => cost[i].bytes);
|
|
351
|
+
won += count * dearest(i => BigInt(entries[i].prizes.length));
|
|
218
352
|
}
|
|
219
|
-
const rolls = oracle || entries.length > 1 || entries.some(
|
|
220
|
-
return
|
|
353
|
+
const rolls = oracle || entries.length > 1 || entries.some(e => e.prizes.some(p => p.Stash != null));
|
|
354
|
+
return (
|
|
355
|
+
spent + COST.finish + COST.won * won + COST.drawn * draws <= ROLL_BUDGET &&
|
|
356
|
+
(!rolls || used + BYTES.finish + BYTES.won * won + draws <= ROLL_HEAP)
|
|
357
|
+
);
|
|
221
358
|
}
|
|
222
359
|
|
|
223
360
|
// What one `buy` takes, as the program counts it at the box's write (`catalog::buy_fits`): a transaction holds 64
|
|
224
361
|
// accounts, 64 instructions and 1,400,000 compute units.
|
|
225
362
|
const BUY = { accounts: 15, calls: 7, cu: 30_000, most: { accounts: 64, calls: 64, cu: 1_400_000 } };
|
|
226
|
-
const sameJson = (a, b) =>
|
|
363
|
+
const sameJson = (a, b) =>
|
|
364
|
+
JSON.stringify(a, (_, v) => (typeof v === 'bigint' ? String(v) : v)) ===
|
|
365
|
+
JSON.stringify(b, (_, v) => (typeof v === 'bigint' ? String(v) : v));
|
|
227
366
|
|
|
228
367
|
/**
|
|
229
368
|
* Whether one `buy` of a box that doesn't roll (one entry, given `draws` times: its slots' counts) fits one transaction
|
|
@@ -231,13 +370,14 @@ const sameJson = (a, b) => JSON.stringify(a, (_, v) => (typeof v === 'bigint' ?
|
|
|
231
370
|
* is given once at most. A box that doesn't fit sells through the oracle (`open_box`), its answer's heap counted
|
|
232
371
|
* (`drawsFit` with `oracle`), and can't give a roll (`PackTooLarge`). `box`: the box's args.
|
|
233
372
|
*/
|
|
234
|
-
export function buyFits(box, draws =
|
|
373
|
+
export function buyFits(box, draws = box.slots?.length ? box.slots.reduce((n, s) => n + Number(s.count), 0) : 1) {
|
|
235
374
|
const entry = box.entries?.[0];
|
|
236
375
|
if (!entry) return true;
|
|
237
376
|
let { accounts, calls, cu } = BUY;
|
|
238
377
|
for (const share of box.shares ?? []) {
|
|
239
378
|
const to = typeof share.to === 'string' ? share.to : Object.keys(share.to)[0];
|
|
240
|
-
const [a, c] =
|
|
379
|
+
const [a, c] =
|
|
380
|
+
to === 'Burn' ? [0, 1] : to === 'Pool' || to === 'Account' ? [1, 1] : to === 'Holder' ? [3, 6] : [2, 6];
|
|
241
381
|
accounts += a;
|
|
242
382
|
calls += c;
|
|
243
383
|
cu += 30_000;
|
|
@@ -247,11 +387,16 @@ export function buyFits(box, draws = (box.slots?.length ? box.slots.reduce((n, s
|
|
|
247
387
|
const limit = box.per_wallet ?? {};
|
|
248
388
|
if (Number(limit.total ?? 0) > 0 || Number(limit.per_window ?? 0) > 0) calls += 1;
|
|
249
389
|
// A collectible's collection and Metaplex Core, named once for all of them.
|
|
250
|
-
if (entry.prizes.some(
|
|
390
|
+
if (entry.prizes.some(p => p.Item != null)) accounts += 2;
|
|
251
391
|
for (const [i, p] of entry.prizes.entries()) {
|
|
252
|
-
const first = !entry.prizes
|
|
392
|
+
const first = !entry.prizes
|
|
393
|
+
.slice(0, i)
|
|
394
|
+
.some(q => (accountOf(q) && accountOf(q) === accountOf(p)) || sameJson(q, p));
|
|
253
395
|
const gives = p.Tokens ? 1 : draws;
|
|
254
|
-
let once = 0,
|
|
396
|
+
let once = 0,
|
|
397
|
+
each = 0,
|
|
398
|
+
c = 0,
|
|
399
|
+
units = 0;
|
|
255
400
|
if (p.Tokens) [once, c, units] = [p.Tokens.from.Pool ? 3 : 2, 5, 30_000];
|
|
256
401
|
else if (p.Item != null) [once, each, c, units] = [1, 1, 4, 40_000];
|
|
257
402
|
else if (p.Compressed) [once, c, units] = [9, 6, 150_000];
|
|
@@ -280,8 +425,18 @@ const PLAYER_FEE = 2_000_000n;
|
|
|
280
425
|
* least one: a box whose one open is over the cap is still bought one open an order. A purchase of more is several
|
|
281
426
|
* orders, each its own answer.
|
|
282
427
|
*/
|
|
283
|
-
export function opensAnOrder(
|
|
284
|
-
|
|
428
|
+
export function opensAnOrder(
|
|
429
|
+
lootBox,
|
|
430
|
+
{ shares = (lootBox.shares ?? []).length, items = {}, rentPerByte = RENT_PER_BYTE } = {},
|
|
431
|
+
) {
|
|
432
|
+
const fits = n =>
|
|
433
|
+
drawsFit(lootBox.entries, lootBox.slots?.length ? lootBox.slots : 1, n, {
|
|
434
|
+
odds: (lootBox.odds ?? []).length,
|
|
435
|
+
shares,
|
|
436
|
+
fates: (lootBox.fates ?? []).length,
|
|
437
|
+
bytes: boxBytes(lootBox),
|
|
438
|
+
oracle: true,
|
|
439
|
+
}) && (BigInt(orderBytes(lootBox, shares, n, items)) + 128n) * rentPerByte + PLAYER_FEE <= ORDER_COST_CAP;
|
|
285
440
|
let n = 1;
|
|
286
441
|
while (n < 4096 && fits(n + 1)) n += 1;
|
|
287
442
|
return n;
|
package/js/client/traits.js
CHANGED
|
@@ -5,25 +5,34 @@ import { addressBytes } from '../chain/base58.js';
|
|
|
5
5
|
import { compareBytes, concat, uintLE, utf8 } from '../chain/bytes.js';
|
|
6
6
|
import { sha256 } from '../chain/sha256.js';
|
|
7
7
|
|
|
8
|
-
const text =
|
|
8
|
+
const text = s => concat(uintLE(utf8(s).length, 4), utf8(s));
|
|
9
9
|
|
|
10
10
|
/** A leaf: an NFT's address and its traits ([{ key, value }], in order). */
|
|
11
11
|
export const traitsLeaf = (nft, attributes) =>
|
|
12
|
-
sha256(
|
|
12
|
+
sha256(
|
|
13
|
+
concat(
|
|
14
|
+
Uint8Array.of(0),
|
|
15
|
+
addressBytes(nft),
|
|
16
|
+
uintLE(attributes.length, 4),
|
|
17
|
+
...attributes.map(a => concat(text(a.key), text(a.value))),
|
|
18
|
+
),
|
|
19
|
+
);
|
|
13
20
|
|
|
14
21
|
/** Two nodes joined, the smaller first, so a proof needs no left or right. */
|
|
15
|
-
export const traitsNode = (a, b) =>
|
|
22
|
+
export const traitsNode = (a, b) =>
|
|
23
|
+
compareBytes(a, b) <= 0 ? sha256(concat(Uint8Array.of(1), a, b)) : sha256(concat(Uint8Array.of(1), b, a));
|
|
16
24
|
|
|
17
25
|
/**
|
|
18
26
|
* The tree of `entries` ([{ nft, attributes }]): { root, proof(nft) → [node, …] bottom up }. A node left without a pair
|
|
19
27
|
* at a level goes up as it is.
|
|
20
28
|
*/
|
|
21
29
|
export function traitsTree(entries) {
|
|
22
|
-
const levels = [entries.map(
|
|
30
|
+
const levels = [entries.map(e => traitsLeaf(e.nft, e.attributes))];
|
|
23
31
|
while (levels.at(-1).length > 1) {
|
|
24
32
|
const below = levels.at(-1);
|
|
25
33
|
const up = [];
|
|
26
|
-
for (let i = 0; i < below.length; i += 2)
|
|
34
|
+
for (let i = 0; i < below.length; i += 2)
|
|
35
|
+
up.push(i + 1 < below.length ? traitsNode(below[i], below[i + 1]) : below[i]);
|
|
27
36
|
levels.push(up);
|
|
28
37
|
}
|
|
29
38
|
const at = new Map(entries.map((e, i) => [e.nft, i]));
|