@xgamesfun/solana 0.0.0-stage → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DISCLAIMER.md +41 -0
- package/LICENSE +202 -0
- package/NOTICE +13 -0
- package/README.md +172 -2
- package/bin/xsolana.mjs +189 -0
- package/examples/artwork-auction.json +51 -0
- package/examples/blind-forge.json +128 -0
- package/examples/coin-flip-arena.json +103 -0
- package/examples/duel-arena.json +137 -0
- package/examples/elements-set.json +274 -0
- package/examples/event-badges.json +67 -0
- package/examples/expedition-guild.json +154 -0
- package/examples/fair-presale.json +48 -0
- package/examples/falling-drop.json +90 -0
- package/examples/guild-missions.json +137 -0
- package/examples/halving-seasons.json +107 -0
- package/examples/hero-trials.json +93 -0
- package/examples/ideas.json +789 -0
- package/examples/level-ladder.json +65 -0
- package/examples/merge-2048.json +128 -0
- package/examples/nft-raffle.json +33 -0
- package/examples/paper-hands-farm.json +44 -0
- package/examples/partner-vault.json +112 -0
- package/examples/sacrifice-raffle.json +118 -0
- package/examples/scorched-treasury.json +84 -0
- package/examples/shelter-fair.json +164 -0
- package/examples/spark-pity.json +153 -0
- package/examples/stakers-draw.json +118 -0
- package/examples/star-fleet.json +150 -0
- package/examples/starforce.json +116 -0
- package/examples/stud-farm.json +134 -0
- package/examples/team-vesting.json +70 -0
- package/examples/trade-in-shop.json +168 -0
- package/js/approve/details.mjs +263 -0
- package/js/approve/ed25519.mjs +36 -0
- package/js/approve/live-check.mjs +144 -0
- package/js/approve/page.html +86 -0
- package/js/approve/page.js +203 -0
- package/js/approve/server.mjs +726 -0
- package/js/chain/base58.js +73 -0
- package/js/chain/borsh.js +270 -0
- package/js/chain/bytes.js +141 -0
- package/js/chain/core.js +181 -0
- package/js/chain/curve.js +52 -0
- package/js/chain/lookup.js +279 -0
- package/js/chain/mint.js +96 -0
- package/js/chain/pda.js +66 -0
- package/js/chain/programs.js +102 -0
- package/js/chain/rpc.js +219 -0
- package/js/chain/sha256.js +68 -0
- package/js/chain/token.js +85 -0
- package/js/chain/token2022.js +247 -0
- package/js/chain/tx.js +224 -0
- package/js/client/accounts.js +679 -0
- package/js/client/compressed.js +46 -0
- package/js/client/grant.js +299 -0
- package/js/client/idl/loot.js +2 -0
- package/js/client/idl/loot.json +17206 -0
- package/js/client/index.js +10 -0
- package/js/client/ix.js +2887 -0
- package/js/client/pda.js +114 -0
- package/js/client/policy.js +69 -0
- package/js/client/random.js +70 -0
- package/js/client/sizes.js +297 -0
- package/js/client/traits.js +44 -0
- package/js/connect/connect.mjs +182 -0
- package/js/economy/check.mjs +2236 -0
- package/js/economy/cost.mjs +185 -0
- package/js/economy/examples.mjs +114 -0
- package/js/economy/explain.mjs +239 -0
- package/js/economy/guide.mjs +232 -0
- package/js/economy/legal.mjs +15 -0
- package/js/economy/live.mjs +130 -0
- package/js/economy/model.mjs +698 -0
- package/js/economy/plan.mjs +1155 -0
- package/js/economy/schema.json +3071 -0
- package/js/economy/secrets.mjs +60 -0
- package/js/economy/simulate.mjs +1019 -0
- package/js/economy/txsize.mjs +45 -0
- package/js/economy/validate.mjs +85 -0
- package/js/index.js +10 -0
- package/js/mcp/server.mjs +113 -0
- package/js/mcp/tools.mjs +485 -0
- package/js/play/index.js +2855 -0
- package/js/play/keys.js +167 -0
- package/js/results/index.js +184 -0
- package/js/results/wire.js +90 -0
- package/package.json +100 -4
- package/release.json +8 -0
- package/skills/xsolana-economy/SKILL.md +109 -0
- package/skills/xsolana-economy/references/after-launch.md +124 -0
- package/skills/xsolana-economy/references/apps.md +193 -0
- package/skills/xsolana-economy/references/sentence-to-spec.md +180 -0
- package/skills/xsolana-economy/references/telling-the-person.md +134 -0
- package/types/chain/base58.d.ts +10 -0
- package/types/chain/borsh.d.ts +23 -0
- package/types/chain/bytes.d.ts +45 -0
- package/types/chain/core.d.ts +52 -0
- package/types/chain/curve.d.ts +1 -0
- package/types/chain/lookup.d.ts +194 -0
- package/types/chain/mint.d.ts +25 -0
- package/types/chain/pda.d.ts +25 -0
- package/types/chain/programs.d.ts +65 -0
- package/types/chain/rpc.d.ts +68 -0
- package/types/chain/sha256.d.ts +1 -0
- package/types/chain/token.d.ts +141 -0
- package/types/chain/token2022.d.ts +53 -0
- package/types/chain/tx.d.ts +60 -0
- package/types/client/accounts.d.ts +318 -0
- package/types/client/compressed.d.ts +29 -0
- package/types/client/grant.d.ts +170 -0
- package/types/client/idl/loot.d.ts +4498 -0
- package/types/client/index.d.ts +9 -0
- package/types/client/ix.d.ts +5315 -0
- package/types/client/pda.d.ts +95 -0
- package/types/client/policy.d.ts +48 -0
- package/types/client/random.d.ts +26 -0
- package/types/client/sizes.d.ts +101 -0
- package/types/client/traits.d.ts +12 -0
- package/types/economy/check.d.mts +100 -0
- package/types/economy/cost.d.mts +69 -0
- package/types/economy/examples.d.mts +27 -0
- package/types/economy/explain.d.mts +19 -0
- package/types/economy/guide.d.mts +140 -0
- package/types/economy/legal.d.mts +12 -0
- package/types/economy/live.d.mts +124 -0
- package/types/economy/model.d.mts +431 -0
- package/types/economy/plan.d.mts +1014 -0
- package/types/economy/secrets.d.mts +4 -0
- package/types/economy/simulate.d.mts +164 -0
- package/types/economy/txsize.d.mts +17 -0
- package/types/economy/validate.d.mts +3 -0
- package/types/index.d.ts +9 -0
- package/types/mcp/tools.d.mts +635 -0
- package/types/play/index.d.ts +880 -0
- package/types/play/keys.d.ts +51 -0
- package/types/results/index.d.ts +155 -0
- package/types/results/wire.d.ts +122 -0
package/js/client/pda.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Every address the loot program derives, with the seeds the program uses. Each takes base58
|
|
2
|
+
// addresses and numbers and returns a base58 address. Browser-safe.
|
|
3
|
+
import { addressBytes } from '../chain/base58.js';
|
|
4
|
+
import { intLE, utf8, uintLE } from '../chain/bytes.js';
|
|
5
|
+
import { associatedTokenAddress, findProgramAddress, MPL_CORE_PROGRAM, TOKEN_PROGRAM } from '../chain/pda.js';
|
|
6
|
+
import IDL from './idl/loot.js';
|
|
7
|
+
|
|
8
|
+
export const LOOT_PROGRAM = IDL.address;
|
|
9
|
+
/** MagicBlock's VRF program and its default queue, the only queue the program accepts. */
|
|
10
|
+
export const VRF_PROGRAM = 'Vrf1RNUjXmQGjmQrQLvJHs9SNkvDJEsRVFPkfSQUwGz';
|
|
11
|
+
export const VRF_QUEUE = 'Cuj97ggrhhidhbu39TijNVqE74xvKJ69gDervRUXAxGh';
|
|
12
|
+
export { MPL_CORE_PROGRAM };
|
|
13
|
+
export const BPF_LOADER_UPGRADEABLE = 'BPFLoaderUpgradeab1e11111111111111111111111';
|
|
14
|
+
/** Compressed NFTs: Bubblegum, and the account compression program and noop of its v2 trees. */
|
|
15
|
+
export const BUBBLEGUM_PROGRAM = 'BGUMAp9Gq7iTEuizy4pqaxsTyUCBK68MDfK752saRPUY';
|
|
16
|
+
export const COMPRESSION_PROGRAM = 'mcmt6YrQEMKw8Mw43FmpRLmf7BqRnFMKmAcbxE3xkAW';
|
|
17
|
+
export const NOOP_PROGRAM = 'mnoopTCrg4p8ry25e4bcWA9XZjbNjMTfgYVGGEdRsf3';
|
|
18
|
+
|
|
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
|
+
|
|
27
|
+
/** The PDA of these seeds under the loot program, with its bump. */
|
|
28
|
+
export const lootAddress = (seeds, program = LOOT_PROGRAM) => findProgramAddress(seeds, program);
|
|
29
|
+
const at = (...seeds) => lootAddress(seeds)[0];
|
|
30
|
+
|
|
31
|
+
export const pda = {
|
|
32
|
+
platform: () => at(seed('platform')),
|
|
33
|
+
/** A project: any wallet (its creator) opens as many as it likes, each with a number of its own (`id`, a u64). A
|
|
34
|
+
* new owner keeps the address. */
|
|
35
|
+
project: (creator, id = 0) => at(seed('project'), key(creator), u64(id)),
|
|
36
|
+
/** The project's fee box: where the platform's fees on its transactions wait. */
|
|
37
|
+
fees: (project) => at(seed('fees'), key(project)),
|
|
38
|
+
pool: (project, index) => at(seed('pool'), key(project), u16(index)),
|
|
39
|
+
item: (project, id) => at(seed('item'), key(project), u32(id)),
|
|
40
|
+
box: (project, id) => at(seed('box'), key(project), u32(id)),
|
|
41
|
+
/** NFTs the project holds to give away as prizes: a box entry of a stash gives one of them at random. */
|
|
42
|
+
stash: (project, id) => at(seed('stash'), key(project), u32(id)),
|
|
43
|
+
/** A table of weighted rows the project writes and seals (a card among thousands, a trait's values), or commits by a Merkle root. */
|
|
44
|
+
table: (project, id) => at(seed('table'), key(project), u32(id)),
|
|
45
|
+
/** What the project keeps about a wallet (its player's profile: XP, level, rank). */
|
|
46
|
+
profile: (project, wallet) => at(seed('profile'), key(project), key(wallet)),
|
|
47
|
+
schedule: (project, id) => at(seed('schedule'), key(project), u32(id)),
|
|
48
|
+
lottery: (project, id) => at(seed('lottery'), key(project), u32(id)),
|
|
49
|
+
recipe: (project, id) => at(seed('recipe'), key(project), u32(id)),
|
|
50
|
+
contest: (project, id) => at(seed('contest'), key(project), u32(id)),
|
|
51
|
+
/** An auction: an id is its seller's own. Its bids wait in the project's vault for its mint (`vault`). */
|
|
52
|
+
auction: (project, seller, id) => at(seed('auction'), key(project), key(seller), u32(id)),
|
|
53
|
+
farm: (project, id) => at(seed('farm'), key(project), u32(id)),
|
|
54
|
+
grant: (project, id) => at(seed('grant'), key(project), u16(id)),
|
|
55
|
+
order: (project, player, nonce) => at(seed('order'), key(project), key(player), u64(nonce)),
|
|
56
|
+
/** Every collectible the program mints of its own accord (an order's, a prize's, an auction's lot): its address
|
|
57
|
+
* names its item and its serial, minted once at most. */
|
|
58
|
+
itemAsset: (item, serial) => at(seed('asset'), key(item), u32(serial)),
|
|
59
|
+
/** A contest's pot, or a lottery's: `["pot", contest or lottery]`. */
|
|
60
|
+
pot: (record) => at(seed('pot'), key(record)),
|
|
61
|
+
/** One round of a lottery: its record, opened by the round's first purchase (`["round", lottery, incarnation, round]`). */
|
|
62
|
+
round: (lottery, incarnation, round) => at(seed('round'), key(lottery), u64(incarnation), u32(round)),
|
|
63
|
+
/** A lottery round's own token account of `mint`, which the round's record owns. */
|
|
64
|
+
roundTokens: (round, mint) => at(seed('round-tokens'), key(round), key(mint)),
|
|
65
|
+
/** A contest's list of entries, one record a player (page 0). */
|
|
66
|
+
entries: (contest, incarnation, page = 0) => at(seed('entries'), key(contest), u64(incarnation), u8(page)),
|
|
67
|
+
/** A hold names its NFT and its end (Unix seconds): a release sent late never finds a later hold of the NFT. */
|
|
68
|
+
hold: (asset, freeAt) => at(seed('hold'), key(asset), i64(freeAt)),
|
|
69
|
+
/** An offer: an id is its maker's own. Its money waits in the project's vault for its mint (`vault`). */
|
|
70
|
+
offer: (project, maker, id) => at(seed('offer'), key(project), key(maker), u32(id)),
|
|
71
|
+
/** A lottery's own stash: the NFTs its places promise (`Prize::Held`), which leave only as its prizes. */
|
|
72
|
+
lotteryStash: (lottery) => at(seed('lottery_stash'), key(lottery)),
|
|
73
|
+
/** A contest's prize split by stake, set aside when it is decided: `["prize", contest]`. */
|
|
74
|
+
prizeVault: (contest) => at(seed('prize'), key(contest)),
|
|
75
|
+
/** A sealed schedule's own vault, which only its runs pay out of: `["vest", schedule]`. */
|
|
76
|
+
vest: (schedule) => at(seed('vest'), key(schedule)),
|
|
77
|
+
/** A listing of an NFT for sale or for rent: one per NFT a project holds, so its address names the NFT. */
|
|
78
|
+
listing: (project, asset) => at(seed('listing'), key(project), key(asset)),
|
|
79
|
+
staked: (farm) => at(seed('staked'), key(farm)),
|
|
80
|
+
rewards: (farm) => at(seed('rewards'), key(farm)),
|
|
81
|
+
/** A farm's record of when its seats stop earning, for a farm that keeps one (`Farm.keeps_ends`). */
|
|
82
|
+
ends: (farm) => at(seed('ends'), key(farm)),
|
|
83
|
+
/** A seat: a player's in a farm of tokens, a collectible's (its asset) in a farm of collectibles. */
|
|
84
|
+
seat: (farm, by) => at(seed('seat'), key(farm), key(by)),
|
|
85
|
+
/** 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
|
+
sheet: (project, nft) => at(seed('sheet'), key(project), key(nft)),
|
|
87
|
+
/** 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: (asset) => at(seed('leaf'), key(asset)),
|
|
89
|
+
/** The root of the traits the project vouches for, under its own `key` (a collection's address, by convention). */
|
|
90
|
+
traits: (project, key_) => at(seed('traits'), key(project), key(key_)),
|
|
91
|
+
/** Bubblegum's config of a tree: `[tree]`. */
|
|
92
|
+
treeConfig: (tree) => findProgramAddress([key(tree)], BUBBLEGUM_PROGRAM)[0],
|
|
93
|
+
/** A compressed NFT's asset id: Bubblegum's `["asset", tree, nonce]`, `nonce` the count minted before it. */
|
|
94
|
+
compressedAsset: (tree, nonce) => findProgramAddress([seed('asset'), key(tree), u64(nonce)], BUBBLEGUM_PROGRAM)[0],
|
|
95
|
+
/** A wallet's counts in a project: how many times it bought from each box, crafted each recipe or held tickets in
|
|
96
|
+
* a lottery's round that limits it (`decodeCounts`). */
|
|
97
|
+
counts: (project, wallet) => at(seed('counts'), key(project), key(wallet)),
|
|
98
|
+
/** Shard `shard` of a grant's limit records: what each wallet, person, badge and voucher key claimed. */
|
|
99
|
+
claims: (grant, incarnation, shard) => at(seed('claims'), key(grant), u64(incarnation), u8(shard)),
|
|
100
|
+
/** Shard `shard` of the rounds a grant paid. */
|
|
101
|
+
rounds: (grant, incarnation, shard) => at(seed('rounds'), key(grant), u64(incarnation), u8(shard)),
|
|
102
|
+
/** The program's identity, which signs its requests to the oracle. */
|
|
103
|
+
identity: () => at(seed('identity')),
|
|
104
|
+
/** The identity the oracle signs callbacks with: the VRF program's PDA for this program. */
|
|
105
|
+
oracleIdentity: () => findProgramAddress([seed('identity'), key(LOOT_PROGRAM)], VRF_PROGRAM)[0],
|
|
106
|
+
/** The program's ProgramData account (its upgrade authority signs `init_platform`). */
|
|
107
|
+
programData: () => findProgramAddress([key(LOOT_PROGRAM)], BPF_LOADER_UPGRADEABLE)[0],
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** `owner`'s associated token account of `mint` (owner may be a PDA: a project's vault, a pool is not one). */
|
|
111
|
+
export const ata = (owner, mint, tokenProgram = TOKEN_PROGRAM) => associatedTokenAddress(owner, mint, tokenProgram);
|
|
112
|
+
|
|
113
|
+
/** A project's vault for `mint`: its associated token account, where every open order's payment is held. */
|
|
114
|
+
export const vault = (project, mint, tokenProgram = TOKEN_PROGRAM) => ata(project, mint, tokenProgram);
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Lifecycle rules shared by browsers and optional keepers, read from the decoded accounts. The program still checks
|
|
2
|
+
// every transition; these say when one is worth sending.
|
|
3
|
+
import IDL from './idl/loot.js';
|
|
4
|
+
|
|
5
|
+
const constant = name => BigInt(IDL.constants.find(value => value.name === name).value);
|
|
6
|
+
/** How long the oracle has to answer before a roll or a draw may be asked again, and a pending order refunded (seconds). */
|
|
7
|
+
export const STALE_SECONDS = constant('STALE_SECONDS');
|
|
8
|
+
/** How long after a lottery's first ask the next ask cancels its round, and an asked order holding a payment or an NFT
|
|
9
|
+
* may be refunded (seconds). */
|
|
10
|
+
export const LOTTERY_TIMEOUT_SECONDS = constant('LOTTERY_TIMEOUT_SECONDS');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The earliest an order can be refunded, as `{ slot, at }` (the program wants a later slot than `slot` and a later
|
|
14
|
+
* second than `at`), or null when it never can be as it stands: a failed order from the next slot; a paid order the
|
|
15
|
+
* oracle never answered 10 minutes after it was asked (`STALE_SECONDS`); an asked roll holding a payment or an NFT 7 days
|
|
16
|
+
* after its first ask. A rolled order has an outcome to carry out, and is settled. A waiting roll, or an asked one holding
|
|
17
|
+
* nothing, is asked (again): `rollAskable`.
|
|
18
|
+
*/
|
|
19
|
+
export function orderRefundAt(order) {
|
|
20
|
+
const slot = BigInt(order.requested_slot);
|
|
21
|
+
if (order.status === 'Failed') return { slot, at: null };
|
|
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)) return { slot: null, at: BigInt(order.first_asked_at) + LOTTERY_TIMEOUT_SECONDS };
|
|
24
|
+
if (['Rolled', 'Waiting', 'Asked'].includes(order.status)) return null;
|
|
25
|
+
throw new Error('Unknown order status.');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Whether an order can be refunded at `slot` and `now` (Unix seconds, the chain's clock): `orderRefundAt`. */
|
|
29
|
+
export function orderRefundable(order, slot, now) {
|
|
30
|
+
const due = orderRefundAt(order);
|
|
31
|
+
if (!due) return false;
|
|
32
|
+
return (due.slot === null || BigInt(slot) > due.slot) && (due.at === null || BigInt(now) > due.at);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Whether its player or the project's keys may ask the oracle for a mission's or a recipe's roll at `slot` and `now`
|
|
37
|
+
* (Unix seconds, the chain's clock): never asked, or never answered within `STALE_SECONDS`. Once asked, a later ask comes
|
|
38
|
+
* in a later slot than the last (`SameSlot`: never the same seed twice), so a roll that found nothing in stock and waits
|
|
39
|
+
* again is asked from the next slot on.
|
|
40
|
+
*/
|
|
41
|
+
export const rollAskable = (order, slot, now) =>
|
|
42
|
+
(order.status === 'Waiting' && (BigInt(order.asked_first ?? 0) === 0n || BigInt(slot) > BigInt(order.requested_slot))) ||
|
|
43
|
+
(order.status === 'Asked' && BigInt(now) > BigInt(order.asked_at) + STALE_SECONDS);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Whether a contest is off at `now` (Unix seconds), as the program reads its verdict: called off, its result
|
|
47
|
+
* overdue, or short of its least pot once entries closed. Every entry of one that is off takes its money back.
|
|
48
|
+
*/
|
|
49
|
+
export function contestOff(contest, now) {
|
|
50
|
+
if (contest.verdict === 'Off') return true;
|
|
51
|
+
if (contest.verdict !== 'Open') return false;
|
|
52
|
+
const total = contest.sides.reduce((sum, side) => sum + BigInt(side), 0n);
|
|
53
|
+
return BigInt(now) > BigInt(contest.result_by) || (BigInt(now) >= BigInt(contest.closes_at) && total < BigInt(contest.min_pot ?? 0));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Whether a lottery round's draw (`round`: its decoded record) was asked for and the oracle has not answered within
|
|
57
|
+
* `STALE_SECONDS` by `now` (Unix seconds, the chain's clock): anyone may ask again. */
|
|
58
|
+
export const lotteryDrawStuck = (round, now) => round?.status === 'Drawing' && BigInt(now) > BigInt(round.asked_at) + STALE_SECONDS;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Whether a lottery round's draw (`round`: its decoded record) was asked for and the oracle has not answered within
|
|
62
|
+
* `LOTTERY_TIMEOUT_SECONDS` of the first ask (asking again doesn't move it) by `now` (Unix seconds, the chain's clock):
|
|
63
|
+
* the next ask of that round (`drawLotteryIx` with its address) cancels it, and every entry is refunded. A round nobody
|
|
64
|
+
* asked to draw is never cancelled.
|
|
65
|
+
*/
|
|
66
|
+
export const lotteryCancellable = (round, now) => round?.status === 'Drawing' && BigInt(now) > BigInt(round.first_asked_at) + LOTTERY_TIMEOUT_SECONDS;
|
|
67
|
+
|
|
68
|
+
/** A farm seat's `earns_until` when nothing ends its earning (i64::MAX): no tier term and no fuel it reads. */
|
|
69
|
+
export const NO_END = (1n << 63n) - 1n;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// The loot program's rolling module (`src/random`), mirrored: every random choice it makes is drawn from a stream of
|
|
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
|
+
// 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 gamesfun-loot's `tests/fixtures`).
|
|
5
|
+
import { concat, uintLE } from '../chain/bytes.js';
|
|
6
|
+
import { sha256 } from '../chain/sha256.js';
|
|
7
|
+
|
|
8
|
+
/** What a path's first word names (`random::tag`): an entry of a box, a stash's NFT, a lottery's ticket, a fresh trait. */
|
|
9
|
+
export const TAG = Object.freeze({ ENTRY: 1, STASH: 2, LOTTERY: 3, TRAIT: 4 });
|
|
10
|
+
/** The most words a path holds. */
|
|
11
|
+
export const MAX_PATH = 8;
|
|
12
|
+
const U64 = (1n << 64n) - 1n;
|
|
13
|
+
const PREFIX = new TextEncoder().encode('loot-roll-v1');
|
|
14
|
+
|
|
15
|
+
/** A stream of the oracle's 32-byte `answer` at `path` (whole numbers): seed = sha256("loot-roll-v1" | answer | path as
|
|
16
|
+
* u32 little-endian words); its nth number the first 8 bytes, little-endian, of sha256(seed | n as u64 little-endian). */
|
|
17
|
+
export function stream(answer, path = []) {
|
|
18
|
+
const words = path.slice(0, MAX_PATH).map((w) => uintLE(w, 4));
|
|
19
|
+
const seed = sha256(concat(PREFIX, Uint8Array.from(answer), ...words));
|
|
20
|
+
let counter = 0n;
|
|
21
|
+
return {
|
|
22
|
+
next() {
|
|
23
|
+
const block = sha256(concat(seed, uintLE(counter, 8)));
|
|
24
|
+
counter += 1n;
|
|
25
|
+
return block.subarray(0, 8).reduceRight((n, b) => (n << 8n) | BigInt(b), 0n);
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A number from 0 to `n` - 1, every one as likely: a draw past the last whole multiple of `n` is drawn again. null for
|
|
31
|
+
* `n` 0. */
|
|
32
|
+
export function uniform(s, n) {
|
|
33
|
+
n = BigInt(n);
|
|
34
|
+
if (n === 0n) return null;
|
|
35
|
+
const limit = U64 - (((1n << 64n) - n) % n);
|
|
36
|
+
for (;;) {
|
|
37
|
+
const roll = s.next();
|
|
38
|
+
if (roll <= limit) return roll % n;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The row `at` lands on, the weights walked in order. null past the last. */
|
|
43
|
+
export function walk(weights, at) {
|
|
44
|
+
let left = BigInt(at);
|
|
45
|
+
for (const [i, w] of weights.entries()) {
|
|
46
|
+
if (left < BigInt(w)) return i;
|
|
47
|
+
left -= BigInt(w);
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A row by weight (a weight of 0 never drawn); null when none weighs anything, or they sum past 64 bits. */
|
|
53
|
+
export function weighted(s, weights) {
|
|
54
|
+
const total = weights.reduce((n, w) => n + BigInt(w), 0n);
|
|
55
|
+
if (total > U64) return null;
|
|
56
|
+
const at = uniform(s, total);
|
|
57
|
+
return at === null ? null : walk(weights, at);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A number from `min` to `max`, both included, every one as likely (either order). */
|
|
61
|
+
export function range(s, min, max) {
|
|
62
|
+
let [lo, hi] = [BigInt(min), BigInt(max)];
|
|
63
|
+
if (lo > hi) [lo, hi] = [hi, lo];
|
|
64
|
+
const n = hi - lo + 1n;
|
|
65
|
+
return n > U64 ? s.next() : lo + uniform(s, n);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A fresh unit's range trait `t` (its place among the item's traits) as the program draws it at the mint: from the
|
|
69
|
+
* order's `answer`, `at` the unit's place in the order's `won`. */
|
|
70
|
+
export const rangeTrait = (answer, at, t, min, max) => range(stream(answer, [TAG.TRAIT, at, t]), min, max);
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
// Rent sizes from the current IDL. Fixed allocations reserve the largest enum and option forms,
|
|
2
|
+
// matching Anchor InitSpace. Dynamic records use their actual encoded fields.
|
|
3
|
+
import { encodeAccount, encodeBorsh } from '../chain/borsh.js';
|
|
4
|
+
import IDL from './idl/loot.js';
|
|
5
|
+
|
|
6
|
+
const scalar = { bool: 1, u8: 1, u16: 2, u32: 4, i32: 4, u64: 8, i64: 8, u128: 16, pubkey: 32 };
|
|
7
|
+
// What a fixed-size account reserves for its lists and strings: a roll's fate (`Fate.shifts`, each key at most 32 bytes).
|
|
8
|
+
const bounds = { 'Fate.shifts': 2 };
|
|
9
|
+
const strings = { 'Shift.key': 32 };
|
|
10
|
+
function maximum(type, path) {
|
|
11
|
+
if (type === 'string') {
|
|
12
|
+
if (!(path in strings)) throw new Error(`No allocation bound for ${path}`);
|
|
13
|
+
return 4 + strings[path];
|
|
14
|
+
}
|
|
15
|
+
if (typeof type === 'string') {
|
|
16
|
+
if (!(type in scalar)) throw new Error(`Unbounded IDL type ${path}: ${type}`);
|
|
17
|
+
return scalar[type];
|
|
18
|
+
}
|
|
19
|
+
if (type.array) return type.array[1] * maximum(type.array[0], path);
|
|
20
|
+
if (type.option) return 1 + maximum(type.option, path);
|
|
21
|
+
if (type.vec) {
|
|
22
|
+
if (!(path in bounds)) throw new Error(`No allocation bound for ${path}`);
|
|
23
|
+
return 4 + bounds[path] * maximum(type.vec, path);
|
|
24
|
+
}
|
|
25
|
+
const name = type.defined.name;
|
|
26
|
+
const definition = IDL.types.find((entry) => entry.name === name)?.type;
|
|
27
|
+
if (!definition) throw new Error(`Missing current IDL type ${name}`);
|
|
28
|
+
const fields = (list) => (list ?? []).reduce((n, field) => n + maximum(field.type ?? field, `${name}.${field.name ?? ''}`), 0);
|
|
29
|
+
return definition.kind === 'enum' ? 1 + Math.max(...definition.variants.map((variant) => fields(variant.fields))) : fields(definition.fields);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The space reserved by a fixed-size account, including its discriminator. */
|
|
33
|
+
export function allocationBytes(name) {
|
|
34
|
+
const account = IDL.accounts.find((entry) => entry.name === name);
|
|
35
|
+
if (!account) throw new Error(`Missing current IDL account ${name}`);
|
|
36
|
+
return account.discriminator.length + maximum({ defined: { name } }, name);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The exact bytes of a dynamically allocated account. */
|
|
40
|
+
export const accountBytes = (name, value) => encodeAccount(IDL, name, value).length;
|
|
41
|
+
/** A seat; a loan's hold (with room for its debt); a hold a player asks for, made without one (`Hold::PLAIN`). */
|
|
42
|
+
export const ACCOUNT_BYTES = Object.freeze({
|
|
43
|
+
Seat: allocationBytes('Seat'),
|
|
44
|
+
Loan: allocationBytes('Hold'),
|
|
45
|
+
Hold: allocationBytes('Hold') - maximum({ defined: { name: 'Debt' } }, 'Debt'),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/** A project's bytes with `collections` collections, as the program sizes the account. */
|
|
49
|
+
export const projectBytes = (collections = 0) => 153 + 32 * collections;
|
|
50
|
+
|
|
51
|
+
/** The most accounts the oracle's answer to a roll carries (`MAX_CALLBACK_ACCOUNTS`). */
|
|
52
|
+
export const MAX_CALLBACK_ACCOUNTS = 21;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The most accounts a roll of `entries` hands the oracle (`catalog::callback_accounts`): the order, the box and the
|
|
56
|
+
* ticket; one for each prize that needs one at the roll (an item, a pool, a stash); and with tokens, the two token
|
|
57
|
+
* programs, the project, and a mint and a vault for each source of tokens.
|
|
58
|
+
*/
|
|
59
|
+
export function callbackAccounts(entries) {
|
|
60
|
+
const sources = new Set();
|
|
61
|
+
let named = 0;
|
|
62
|
+
for (const prize of entries.flatMap((e) => e.prizes)) {
|
|
63
|
+
if (prize.Item || prize.Compressed || prize.Nft || prize.Stash) named += 1;
|
|
64
|
+
else if (prize.Tokens) {
|
|
65
|
+
if (prize.Tokens.from.Pool) named += 1;
|
|
66
|
+
sources.add(JSON.stringify(prize.Tokens.from));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return 3 + named + (sources.size ? 3 + 2 * sources.size : 0);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* How many fresh items' table accounts the oracle's answer to a roll of `entries` can carry (`order::table_budget`):
|
|
74
|
+
* what the box's own accounts leave of `MAX_CALLBACK_ACCOUNTS`. Negative: the box can't be rolled at all.
|
|
75
|
+
*/
|
|
76
|
+
export const tableBudget = (entries) => MAX_CALLBACK_ACCOUNTS - callbackAccounts(entries);
|
|
77
|
+
|
|
78
|
+
const draws = (lootBox) => (lootBox.slots?.length ? lootBox.slots.reduce((n, s) => n + Number(s.count), 0) : 1);
|
|
79
|
+
const ZERO = '11111111111111111111111111111111';
|
|
80
|
+
|
|
81
|
+
/** 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
|
|
82
|
+
* at its upsert (`BadEntries`). In practice a pack is bounded first by what one oracle answer draws (`drawsFit`). */
|
|
83
|
+
export const MAX_ORDER_BYTES = 10_240;
|
|
84
|
+
|
|
85
|
+
/** 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
|
+
* a roll, a held NFT or a raffled place nothing. */
|
|
87
|
+
const wonBytes = (p) => (p.Item || p.Compressed ? 14 : p.Tokens || p.Nft || p.Stash ? 41 : 0);
|
|
88
|
+
const tableCount = (item) => (item?.traits ?? []).filter((t) => t.from?.Table).length;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The rows of fresh items' tables a draw of each entry writes into its order (`order::rows_of`), walked with the box's
|
|
92
|
+
* table budget as the roll's accounts are (`entryAccounts`: an item whose tables don't fit what is left draws none).
|
|
93
|
+
* `items` maps an item's id to its decoded Item; one left out is taken as not fresh.
|
|
94
|
+
*/
|
|
95
|
+
export function entryRows(entries, items = {}) {
|
|
96
|
+
let budget = Math.max(0, tableBudget(entries));
|
|
97
|
+
return entries.map(({ prizes }) => prizes.reduce((n, p) => {
|
|
98
|
+
const id = p.Item?.[0] ?? p.Compressed?.item;
|
|
99
|
+
const t = id === undefined ? 0 : tableCount(items[id]);
|
|
100
|
+
if (t > budget) return n;
|
|
101
|
+
budget -= t;
|
|
102
|
+
return n + t;
|
|
103
|
+
}, 0));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The bytes of an order of `lootBox` with `shares` shares and `opens` opens. With `items` (an item's id to its decoded
|
|
108
|
+
* Item), as `open_box` sizes it (`Order::sized`): each draw at its dearest entry's prizes as the `Won` they become (an
|
|
109
|
+
* item 14 bytes, tokens or an NFT 41, a roll none) and 4 bytes for each table row its fresh items draw now
|
|
110
|
+
* (`entryRows`); its odds and its largest fate. Without `items`, as a roll made inside another call sizes it (a craft,
|
|
111
|
+
* a grant's claim, a mission, a roll lot: `Order::space_for`): the box's whole table budget a draw for a box that
|
|
112
|
+
* gives items. A pack's order grows with its cards.
|
|
113
|
+
*/
|
|
114
|
+
export function orderBytes(lootBox, shares = 0, opens = 1, items) {
|
|
115
|
+
const drawn = draws(lootBox) * Math.max(1, opens);
|
|
116
|
+
const aDraw = Math.max(0, ...lootBox.entries.map((e) => e.prizes.reduce((n, p) => n + wonBytes(p), 0)));
|
|
117
|
+
const gives = lootBox.entries.some((e) => e.prizes.some((p) => p.Item || p.Compressed));
|
|
118
|
+
const rows = items ? Math.max(0, ...entryRows(lootBox.entries, items)) : gives ? Math.max(0, tableBudget(lootBox.entries)) : 0;
|
|
119
|
+
const fates = lootBox.fates ?? [];
|
|
120
|
+
const fate = fates.reduce((a, f) => (a && encodeBorsh(IDL, { defined: { name: 'Fate' } }, a).length >= encodeBorsh(IDL, { defined: { name: 'Fate' } }, f).length ? a : f), null);
|
|
121
|
+
const empty = {
|
|
122
|
+
project: ZERO, player: ZERO, mint: ZERO, loot_box: ZERO, box_version: 0,
|
|
123
|
+
odds_bps: new Array(lootBox.odds?.length ?? 0).fill(0),
|
|
124
|
+
status: 'Waiting',
|
|
125
|
+
won: [],
|
|
126
|
+
paid: 0n, fee: 0n,
|
|
127
|
+
shares: new Array(shares).fill({ to: { Account: [ZERO] }, bps: 0 }),
|
|
128
|
+
opens: 0, settled: 0, undrawn: 0, back: 0n, settler: ZERO, drew: 0, answer: new Uint8Array(32), drawn: [], reserved: 0n,
|
|
129
|
+
requested_slot: 0n, asked_first: 0n, asked_at: 0n, first_asked_at: 0n, counted: 0, subject: lootBox.subject ? ZERO : null, held: false, fate,
|
|
130
|
+
};
|
|
131
|
+
return encodeAccount(IDL, 'Order', empty).length + drawn * (aDraw + rows * 4);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// What one oracle answer's roll spends, counted at no less than each part costs (`order.rs`'s `RollCost`, measured by
|
|
135
|
+
// the program's `tests/roll_costs.rs` and `tests/roll_heap.rs`), against the compute and the heap the oracle gives its
|
|
136
|
+
// answer, less a margin.
|
|
137
|
+
const ROLL_BUDGET = 300_000n;
|
|
138
|
+
const ROLL_HEAP = 32n * 1024n - 512n;
|
|
139
|
+
const COST = { fixed: 20_000n, entry: 3_500n, blank: 1_400n, weigh: 1_000n, weighEntry: 40n, pair: 150n, item: 1_500n, tokens: 6_000n, draw: 9_000n, group: 250n, reread: 1_000n, finish: 15_000n, won: 150n, drawn: 50n, odds: 1_000n, share: 1_000n, fate: 1_000n, slot: 500n, slotEntry: 350n, openSlot: 150n };
|
|
140
|
+
const BYTES = { start: 6_000n, prizeRead: 600n, entryRead: 180n, minted: 700n, pooled: 1_000n, prize: 200n, draw: 40n, finish: 2_000n, won: 120n, boxByte: 2n, gateByte: 3n, odds: 100n, fate: 60n, slot: 64n };
|
|
141
|
+
|
|
142
|
+
/** The Borsh length of `value` as IDL type `type`, an address counted as 32 bytes whatever stands in for it (a spec's
|
|
143
|
+
* `@mint:…` before it goes live). */
|
|
144
|
+
function borshLength(type, value) {
|
|
145
|
+
if (typeof type === 'string') return type === 'string' ? 4 + new TextEncoder().encode(String(value)).length : type === 'bytes' ? 4 + value.length : scalar[type];
|
|
146
|
+
if (type.array) return type.array[1] * (type.array[0] === 'u8' ? 1 : borshLength(type.array[0], value?.[0]));
|
|
147
|
+
if (type.vec) return 4 + (value ?? []).reduce((n, v) => n + borshLength(type.vec, v), 0);
|
|
148
|
+
if (type.option) return value == null ? 1 : 1 + borshLength(type.option, value);
|
|
149
|
+
const definition = IDL.types.find((entry) => entry.name === type.defined.name).type;
|
|
150
|
+
if (definition.kind === 'enum') {
|
|
151
|
+
const name = typeof value === 'string' ? value : Object.keys(value)[0];
|
|
152
|
+
const fields = definition.variants.find((v) => v.name === name)?.fields ?? [];
|
|
153
|
+
const payload = typeof value === 'string' ? [] : value[name];
|
|
154
|
+
return 1 + fields.reduce((n, f, i) => n + (typeof f === 'object' && 'name' in f ? borshLength(f.type, payload[f.name]) : borshLength(f, payload[i])), 0);
|
|
155
|
+
}
|
|
156
|
+
return definition.fields.reduce((n, f) => n + borshLength(f.type, value[f.name]), 0);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const defined = (name) => ({ defined: { name } });
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The heap reading a box takes beyond its entries (`order::box_bytes`): its odds, gate, shares, subject, slots and
|
|
163
|
+
* fates, read whole with it, by their bytes and their number. `box`: the box's args, as `upsertBoxIx` takes them.
|
|
164
|
+
*/
|
|
165
|
+
export function boxBytes({ odds = [], gate = null, shares = [], subject = null, slots = [], fates = [] }) {
|
|
166
|
+
const slotsWritten = slots.map((s) => ({ only: [], distinct: false, share: 0, ...s }));
|
|
167
|
+
const fatesWritten = fates.map((f) => ({ then: null, ...f, shifts: (f.shifts ?? []).map((x) => ({ from_now: false, ...x })) }));
|
|
168
|
+
const bytes = borshLength({ vec: defined('Odds') }, odds) + borshLength({ vec: defined('Share') }, shares) + borshLength({ option: defined('Rule') }, subject)
|
|
169
|
+
+ borshLength({ vec: defined('Slot') }, slotsWritten) + borshLength({ vec: defined('Fate') }, fatesWritten);
|
|
170
|
+
return BYTES.boxByte * BigInt(bytes) + BYTES.gateByte * BigInt(borshLength({ option: defined('Gate') }, gate)) + BYTES.odds * BigInt(odds.length) + BYTES.fate * BigInt(fates.length) + BYTES.slot * BigInt(slots.length);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** 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 = (p) => {
|
|
175
|
+
if (p.Item != null) return `item ${p.Item[0] ?? p.Item}`;
|
|
176
|
+
if (p.Compressed) return `item ${p.Compressed.item}`;
|
|
177
|
+
if (p.Tokens?.from?.Pool) return `pool ${p.Tokens.from.Pool[0] ?? p.Tokens.from.Pool}`;
|
|
178
|
+
if (p.Stash != null) return `stash ${p.Stash[0] ?? p.Stash}`;
|
|
179
|
+
if (p.Nft) return `stash ${p.Nft.stash}`;
|
|
180
|
+
return null;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Whether all of `opens` opens of `entries` by `slots` fit one oracle answer (`order::draws_fit`), each draw at the
|
|
185
|
+
* dearest its slot could make, with what is known from the box alone (no table rows, a stash of one group), its `odds`,
|
|
186
|
+
* `shares` and `fates` counted, and `bytes` (`boxBytes`) of heap reading the box takes beyond its entries. `slots` may
|
|
187
|
+
* be a number: that many draws among every entry. The answer's heap counts for a box that rolls, or one sold through
|
|
188
|
+
* the oracle (`oracle`: a box that doesn't roll but one `buy` can't carry, `buyFits`). The program refuses a box whose
|
|
189
|
+
* 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
|
+
* hold (`PackTooLarge`).
|
|
191
|
+
*/
|
|
192
|
+
export function drawsFit(entries, slots, opens = 1, { odds = 0, shares = 0, fates = 0, bytes = 0n, oracle = false } = {}) {
|
|
193
|
+
const list = typeof slots === 'number' || typeof slots === 'bigint' ? [{ count: slots, only: [] }] : (slots ?? []);
|
|
194
|
+
const draws = list.reduce((n, s) => n + BigInt(s.count), 0n) * BigInt(opens);
|
|
195
|
+
if (draws <= 1n) return true;
|
|
196
|
+
const n = BigInt(entries.length);
|
|
197
|
+
const weigh = COST.weigh + COST.weighEntry * n;
|
|
198
|
+
const reads = entries.map((e) => e.prizes.map(accountOf));
|
|
199
|
+
const cost = entries.map((e) => e.prizes.reduce((c, p) => {
|
|
200
|
+
if (p.Item != null || p.Compressed || p.Nft) return { set: c.set + COST.item, again: c.again + COST.reread, bytes: c.bytes + BYTES.prize };
|
|
201
|
+
if (p.Tokens) return { set: c.set + COST.tokens, again: c.again + (p.Tokens.from.Pool ? COST.reread : 0n), bytes: c.bytes + (p.Tokens.from.Pool ? BYTES.pooled : BYTES.minted) };
|
|
202
|
+
if (p.Stash != null) return { set: c.set + COST.draw + COST.group, again: c.again + COST.reread, bytes: c.bytes + BYTES.prize };
|
|
203
|
+
return { set: c.set + COST.item, again: c.again, bytes: c.bytes + BYTES.prize };
|
|
204
|
+
}, { set: 0n, again: 0n, bytes: BYTES.draw }));
|
|
205
|
+
const next = entries.map((_, i) => weigh + cost[i].set + cost.reduce((sum, c, j) => (j === i || reads[i].some((a) => a && reads[j].includes(a)) ? sum + c.again : sum), 0n));
|
|
206
|
+
const most = BigInt(Math.max(1, ...entries.map((e) => e.prizes.length)));
|
|
207
|
+
let spent = COST.fixed + COST.pair * n * n * most * most + entries.reduce((c, e) => c + (e.prizes.length ? COST.entry : COST.blank), 0n);
|
|
208
|
+
let used = BYTES.start + BigInt(bytes) + BYTES.entryRead * n + BYTES.prizeRead * BigInt(entries.reduce((c, e) => c + e.prizes.length, 0));
|
|
209
|
+
let won = 0n;
|
|
210
|
+
spent += (COST.slot + COST.slotEntry * n) * BigInt(Math.max(1, list.length)) + COST.openSlot * BigInt(list.length) * BigInt(opens) + COST.odds * BigInt(odds) + COST.share * BigInt(shares) + COST.fate * BigInt(fates);
|
|
211
|
+
for (const s of list) {
|
|
212
|
+
const count = BigInt(s.count) * BigInt(opens);
|
|
213
|
+
const among = entries.map((_, i) => i).filter((i) => !(s.only ?? []).length || (s.only ?? []).includes(i));
|
|
214
|
+
const dearest = (f) => among.reduce((m, i) => (f(i) > m ? f(i) : m), 0n);
|
|
215
|
+
spent += count * dearest((i) => next[i]);
|
|
216
|
+
used += count * dearest((i) => cost[i].bytes);
|
|
217
|
+
won += count * dearest((i) => BigInt(entries[i].prizes.length));
|
|
218
|
+
}
|
|
219
|
+
const rolls = oracle || entries.length > 1 || entries.some((e) => e.prizes.some((p) => p.Stash != null));
|
|
220
|
+
return spent + COST.finish + COST.won * won + COST.drawn * draws <= ROLL_BUDGET && (!rolls || used + BYTES.finish + BYTES.won * won + draws <= ROLL_HEAP);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// What one `buy` takes, as the program counts it at the box's write (`catalog::buy_fits`): a transaction holds 64
|
|
224
|
+
// accounts, 64 instructions and 1,400,000 compute units.
|
|
225
|
+
const BUY = { accounts: 15, calls: 7, cu: 30_000, most: { accounts: 64, calls: 64, cu: 1_400_000 } };
|
|
226
|
+
const sameJson = (a, b) => JSON.stringify(a, (_, v) => (typeof v === 'bigint' ? String(v) : v)) === JSON.stringify(b, (_, v) => (typeof v === 'bigint' ? String(v) : v));
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Whether one `buy` of a box that doesn't roll (one entry, given `draws` times: its slots' counts) fits one transaction
|
|
230
|
+
* (`catalog::buy_fits`): its accounts, its instructions and its compute, each prize's at the most it takes. A named NFT
|
|
231
|
+
* is given once at most. A box that doesn't fit sells through the oracle (`open_box`), its answer's heap counted
|
|
232
|
+
* (`drawsFit` with `oracle`), and can't give a roll (`PackTooLarge`). `box`: the box's args.
|
|
233
|
+
*/
|
|
234
|
+
export function buyFits(box, draws = (box.slots?.length ? box.slots.reduce((n, s) => n + Number(s.count), 0) : 1)) {
|
|
235
|
+
const entry = box.entries?.[0];
|
|
236
|
+
if (!entry) return true;
|
|
237
|
+
let { accounts, calls, cu } = BUY;
|
|
238
|
+
for (const share of box.shares ?? []) {
|
|
239
|
+
const to = typeof share.to === 'string' ? share.to : Object.keys(share.to)[0];
|
|
240
|
+
const [a, c] = to === 'Burn' ? [0, 1] : to === 'Pool' || to === 'Account' ? [1, 1] : to === 'Holder' ? [3, 6] : [2, 6];
|
|
241
|
+
accounts += a;
|
|
242
|
+
calls += c;
|
|
243
|
+
cu += 30_000;
|
|
244
|
+
}
|
|
245
|
+
const gate = box.gate ? Object.keys(box.gate)[0] : null;
|
|
246
|
+
accounts += gate === 'List' ? Number(box.gate.List.depth) : gate === 'Staked' ? 2 : gate === 'Holds' ? 5 : 0;
|
|
247
|
+
const limit = box.per_wallet ?? {};
|
|
248
|
+
if (Number(limit.total ?? 0) > 0 || Number(limit.per_window ?? 0) > 0) calls += 1;
|
|
249
|
+
// A collectible's collection and Metaplex Core, named once for all of them.
|
|
250
|
+
if (entry.prizes.some((p) => p.Item != null)) accounts += 2;
|
|
251
|
+
for (const [i, p] of entry.prizes.entries()) {
|
|
252
|
+
const first = !entry.prizes.slice(0, i).some((q) => (accountOf(q) && accountOf(q) === accountOf(p)) || sameJson(q, p));
|
|
253
|
+
const gives = p.Tokens ? 1 : draws;
|
|
254
|
+
let once = 0, each = 0, c = 0, units = 0;
|
|
255
|
+
if (p.Tokens) [once, c, units] = [p.Tokens.from.Pool ? 3 : 2, 5, 30_000];
|
|
256
|
+
else if (p.Item != null) [once, each, c, units] = [1, 1, 4, 40_000];
|
|
257
|
+
else if (p.Compressed) [once, c, units] = [9, 6, 150_000];
|
|
258
|
+
else if (p.Roll != null) [once, each, c, units] = [1, 1, 1, 20_000];
|
|
259
|
+
else if (p.Nft) {
|
|
260
|
+
if (draws > 1) return false;
|
|
261
|
+
[once, c, units] = [8, 6, 60_000];
|
|
262
|
+
}
|
|
263
|
+
accounts += (first ? once : 0) + each * gives;
|
|
264
|
+
calls += c * gives;
|
|
265
|
+
cu += units * gives;
|
|
266
|
+
}
|
|
267
|
+
return accounts <= BUY.most.accounts && calls <= BUY.most.calls && cu <= BUY.most.cu;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** The most program rent and fee one order takes from its player, in lamports (0.01 SOL): a client cap (`opensAnOrder`). */
|
|
271
|
+
export const ORDER_COST_CAP = 10_000_000n;
|
|
272
|
+
/** Rent a byte, as devnet and mainnet-beta charge it ((bytes + 128) × 5,080 lamports). */
|
|
273
|
+
const RENT_PER_BYTE = 5_080n;
|
|
274
|
+
/** The player's fee for one order (`fee::PLAYER_FEE`, 0.002 SOL). */
|
|
275
|
+
const PLAYER_FEE = 2_000_000n;
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* The most opens of `lootBox` a client puts in one order: what one oracle answer draws (`drawsFit`), and what keeps the
|
|
279
|
+
* order's rent and the player's fee within `ORDER_COST_CAP` (`orderBytes` with `items`, so with the box's shares). At
|
|
280
|
+
* 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
|
+
* orders, each its own answer.
|
|
282
|
+
*/
|
|
283
|
+
export function opensAnOrder(lootBox, { shares = (lootBox.shares ?? []).length, items = {}, rentPerByte = RENT_PER_BYTE } = {}) {
|
|
284
|
+
const fits = (n) => drawsFit(lootBox.entries, lootBox.slots?.length ? lootBox.slots : 1, n, { odds: (lootBox.odds ?? []).length, shares, fates: (lootBox.fates ?? []).length, bytes: boxBytes(lootBox), oracle: true }) && (BigInt(orderBytes(lootBox, shares, n, items)) + 128n) * rentPerByte + PLAYER_FEE <= ORDER_COST_CAP;
|
|
285
|
+
let n = 1;
|
|
286
|
+
while (n < 4096 && fits(n + 1)) n += 1;
|
|
287
|
+
return n;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Lists of records (tag 26, `list.rs`): a tag byte and a 6-byte header, then fixed-width records.
|
|
291
|
+
export const LIST_HEAD_BYTES = 7;
|
|
292
|
+
/** A limit record (a wallet's count of a box, a recipe or a lottery round; a grant's claim count): key, count, window, in it. */
|
|
293
|
+
export const LIMIT_RECORD_BYTES = 28;
|
|
294
|
+
/** A round a grant paid a wallet: its key alone. */
|
|
295
|
+
export const ROUND_RECORD_BYTES = 16;
|
|
296
|
+
/** A contest's entry: of tokens, and of NFTs (the NFT, its place on its side, the picks taken). */
|
|
297
|
+
export const CONTEST_ENTRY_BYTES = Object.freeze({ tokens: 43, nfts: 83 });
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// The traits a project vouches for (`set_traits`, `prove_traits`): a Merkle tree of what NFTs' off-chain metadata says
|
|
2
|
+
// of them (a background, a class), whose root the project's owner or authority signs once. Anyone then writes an NFT's
|
|
3
|
+
// traits into its Sheet with its proof. Leaves and nodes are hashed exactly as the program hashes them. Browser-safe.
|
|
4
|
+
import { addressBytes } from '../chain/base58.js';
|
|
5
|
+
import { compareBytes, concat, uintLE, utf8 } from '../chain/bytes.js';
|
|
6
|
+
import { sha256 } from '../chain/sha256.js';
|
|
7
|
+
|
|
8
|
+
const text = (s) => concat(uintLE(utf8(s).length, 4), utf8(s));
|
|
9
|
+
|
|
10
|
+
/** A leaf: an NFT's address and its traits ([{ key, value }], in order). */
|
|
11
|
+
export const traitsLeaf = (nft, attributes) =>
|
|
12
|
+
sha256(concat(Uint8Array.of(0), addressBytes(nft), uintLE(attributes.length, 4), ...attributes.map((a) => concat(text(a.key), text(a.value)))));
|
|
13
|
+
|
|
14
|
+
/** Two nodes joined, the smaller first, so a proof needs no left or right. */
|
|
15
|
+
export const traitsNode = (a, b) => (compareBytes(a, b) <= 0 ? sha256(concat(Uint8Array.of(1), a, b)) : sha256(concat(Uint8Array.of(1), b, a)));
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The tree of `entries` ([{ nft, attributes }]): { root, proof(nft) → [node, …] bottom up }. A node left without a pair
|
|
19
|
+
* at a level goes up as it is.
|
|
20
|
+
*/
|
|
21
|
+
export function traitsTree(entries) {
|
|
22
|
+
const levels = [entries.map((e) => traitsLeaf(e.nft, e.attributes))];
|
|
23
|
+
while (levels.at(-1).length > 1) {
|
|
24
|
+
const below = levels.at(-1);
|
|
25
|
+
const up = [];
|
|
26
|
+
for (let i = 0; i < below.length; i += 2) up.push(i + 1 < below.length ? traitsNode(below[i], below[i + 1]) : below[i]);
|
|
27
|
+
levels.push(up);
|
|
28
|
+
}
|
|
29
|
+
const at = new Map(entries.map((e, i) => [e.nft, i]));
|
|
30
|
+
return {
|
|
31
|
+
root: levels.at(-1)[0],
|
|
32
|
+
proof(nft) {
|
|
33
|
+
let i = at.get(nft);
|
|
34
|
+
if (i === undefined) throw new Error(`${nft} is not in this traits tree.`);
|
|
35
|
+
const out = [];
|
|
36
|
+
for (const level of levels.slice(0, -1)) {
|
|
37
|
+
const sibling = i ^ 1;
|
|
38
|
+
if (sibling < level.length) out.push(level[sibling]);
|
|
39
|
+
i >>= 1;
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|