@xgamesfun/solana 0.0.0-stage → 0.20.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 +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 +108 -0
- package/examples/duel-arena.json +137 -0
- package/examples/elements-set.json +281 -0
- package/examples/event-badges.json +67 -0
- package/examples/expedition-guild.json +161 -0
- package/examples/fair-presale.json +48 -0
- package/examples/falling-drop.json +90 -0
- package/examples/guild-missions.json +144 -0
- package/examples/halving-seasons.json +112 -0
- package/examples/hero-trials.json +96 -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 +56 -0
- package/examples/partner-vault.json +115 -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 +125 -0
- package/examples/star-fleet.json +157 -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 +262 -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 +711 -0
- package/js/chain/base58.js +73 -0
- package/js/chain/borsh.js +261 -0
- package/js/chain/bytes.js +141 -0
- package/js/chain/core.js +169 -0
- package/js/chain/curve.js +52 -0
- package/js/chain/lookup.js +273 -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 +217 -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 +347 -0
- package/js/client/compressed.js +46 -0
- package/js/client/grant.js +240 -0
- package/js/client/idl/loot.js +2 -0
- package/js/client/idl/loot.json +17064 -0
- package/js/client/index.js +9 -0
- package/js/client/ix.js +2156 -0
- package/js/client/pda.js +110 -0
- package/js/client/policy.js +48 -0
- package/js/client/sizes.js +54 -0
- package/js/client/traits.js +44 -0
- package/js/connect/connect.mjs +182 -0
- package/js/economy/check.mjs +1991 -0
- package/js/economy/cost.mjs +170 -0
- package/js/economy/examples.mjs +114 -0
- package/js/economy/explain.mjs +235 -0
- package/js/economy/guide.mjs +231 -0
- package/js/economy/legal.mjs +15 -0
- package/js/economy/live.mjs +107 -0
- package/js/economy/model.mjs +583 -0
- package/js/economy/plan.mjs +887 -0
- package/js/economy/schema.json +2923 -0
- package/js/economy/secrets.mjs +60 -0
- package/js/economy/simulate.mjs +952 -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 +479 -0
- package/js/play/index.js +2109 -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 +105 -0
- package/skills/xsolana-economy/references/after-launch.md +114 -0
- package/skills/xsolana-economy/references/apps.md +157 -0
- package/skills/xsolana-economy/references/sentence-to-spec.md +172 -0
- package/skills/xsolana-economy/references/telling-the-person.md +93 -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 +50 -0
- package/types/chain/curve.d.ts +1 -0
- package/types/chain/lookup.d.ts +191 -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 +66 -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 +178 -0
- package/types/client/compressed.d.ts +29 -0
- package/types/client/grant.d.ts +125 -0
- package/types/client/idl/loot.d.ts +4467 -0
- package/types/client/index.d.ts +8 -0
- package/types/client/ix.d.ts +1687 -0
- package/types/client/pda.d.ts +91 -0
- package/types/client/policy.d.ts +23 -0
- package/types/client/sizes.d.ts +13 -0
- package/types/client/traits.d.ts +12 -0
- package/types/economy/check.d.mts +105 -0
- package/types/economy/cost.d.mts +68 -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 +109 -0
- package/types/economy/model.d.mts +363 -0
- package/types/economy/plan.d.mts +1013 -0
- package/types/economy/secrets.d.mts +4 -0
- package/types/economy/simulate.d.mts +156 -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 +627 -0
- package/types/play/index.d.ts +469 -0
- package/types/results/index.d.ts +155 -0
- package/types/results/wire.d.ts +122 -0
|
@@ -0,0 +1,887 @@
|
|
|
1
|
+
// The setup plan: every instruction that puts an economy live on loot, in order, built with the client and
|
|
2
|
+
// packed into transactions that fit. Nothing here signs or talks to the chain. Pure.
|
|
3
|
+
//
|
|
4
|
+
// A step is { id, part, says, signers, creates: [{ what, bytes }], build(ctx) → instructions }. `ctx` names the
|
|
5
|
+
// addresses a step needs: { owner, authority, project, collection, tree, treasury, mints: { tokenKey: mint }, keys: {
|
|
6
|
+
// name: address } } where `keys` are the fresh keys this setup makes ("new:collection", "new:tree", "new:mint/points"), each of
|
|
7
|
+
// which signs the step that creates it. `placeholderContext` fills every address with a stand-in, so a plan can be
|
|
8
|
+
// sized and shown before anyone connects a wallet.
|
|
9
|
+
import { encodeBase58 } from '../chain/base58.js';
|
|
10
|
+
import { createCollectionInstruction } from '../chain/core.js';
|
|
11
|
+
import { NATIVE_MINT, TOKEN_2022_PROGRAM, TOKEN_PROGRAM } from '../chain/pda.js';
|
|
12
|
+
import { createAssociatedTokenAccountIdempotent, syncNative, transferLamports } from '../chain/programs.js';
|
|
13
|
+
import { createAccountIx, initializeMint2Ix, MINT_BYTES, mintToIx, setAuthorityIx, transferCheckedIx } from '../chain/token.js';
|
|
14
|
+
import { compileMessage, transactionSize } from '../chain/tx.js';
|
|
15
|
+
import { sha256 } from '../chain/sha256.js';
|
|
16
|
+
import { utf8 } from '../chain/bytes.js';
|
|
17
|
+
import * as loot from '../client/index.js';
|
|
18
|
+
import { check } from './check.mjs';
|
|
19
|
+
import { ACCOUNT_OVERHEAD, boxBuys, boxRolls, compressedTree, destination, effectOf, gateOf, LIMITS, listOf, lotterySplit, model, MOST_RENT_PER_BYTE, outcomeBurns, outcomeParts, perWalletOf, placeOf, prizeLine, prizePool, prizeRolls, prizeToken, rarityNumber, ruleOf, sharesOf, spendOf, stashNfts, toBy, toEnum, toSeconds, toTime, toUnits, weights, wholeCount, windowOf } from './model.mjs';
|
|
20
|
+
import { BUDGET } from './txsize.mjs';
|
|
21
|
+
|
|
22
|
+
const owner = 'owner';
|
|
23
|
+
/** Most transactions one wallet approval carries. */
|
|
24
|
+
export const PER_APPROVAL = 12;
|
|
25
|
+
|
|
26
|
+
const amount = (value, t) => toUnits(value ?? 0, t?.decimals ?? 0);
|
|
27
|
+
const shares = (split, m) => sharesOf(split).map((s) => ({ to: toEnum(s.to, m), bps: s.bps ?? 0 }));
|
|
28
|
+
const rise = (r, t) => (r ? { step: amount(r.step ?? 0, t), growth_bps: Math.round((r.growthPercent ?? 0) * 100) } : null);
|
|
29
|
+
const fall = (f, t, now) => (f ? { step: amount(f.step, t), every: toSeconds(f.every), floor: amount(f.floor ?? 0, t), since: BigInt(toTime(f.start, now)), shared: f.clock === 'shared' } : null);
|
|
30
|
+
const tokenProgramOf = (t) => (t?.program === 'token-2022' ? TOKEN_2022_PROGRAM : TOKEN_PROGRAM);
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------- spec parts → the program's args
|
|
33
|
+
|
|
34
|
+
export function itemArgs(it) {
|
|
35
|
+
const kind = it.kind === 'normal' ? 'Normal' : 'Collectible';
|
|
36
|
+
// A collectible is minted into its own `collection`, or the one the setup makes ("@collection" until it exists).
|
|
37
|
+
const life = it.life && kind !== 'Normal' ? { ready_after: toSeconds(it.life.readyAfter), lasts: toSeconds(it.life.lasts), uses: it.life.uses ?? 0, cooldown: toSeconds(it.life.cooldown) } : null;
|
|
38
|
+
return {
|
|
39
|
+
kind,
|
|
40
|
+
rarity: rarityNumber(it.rarity),
|
|
41
|
+
soulbound: !!it.soulbound,
|
|
42
|
+
burnable: !!it.burnable,
|
|
43
|
+
name: it.name,
|
|
44
|
+
uri: it.uri ?? '',
|
|
45
|
+
max_supply: it.supply ?? 0,
|
|
46
|
+
life,
|
|
47
|
+
active: it.active !== false,
|
|
48
|
+
collection: kind === 'Normal' ? NOBODY : it.collection ?? '@collection',
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The program's `Prize` for one part of a box outcome (`model.outcomeParts`). */
|
|
53
|
+
export function partPrize(o, m) {
|
|
54
|
+
if (o.gives === 'item') return { Item: [m.item.get(o.item)?.id ?? 0] };
|
|
55
|
+
if (o.gives === 'stash') return { Stash: [m.stash.get(o.stash)?.id ?? 0] };
|
|
56
|
+
if (o.gives === 'pay') return { Tokens: { from: { Pool: [m.pool.get(o.pool)?.index ?? 0] }, amount: { Fixed: [amount(o.amount, m.poolToken(o.pool))] } } };
|
|
57
|
+
if (o.gives === 'share') return { Tokens: { from: { Pool: [m.pool.get(o.pool)?.index ?? 0] }, amount: { Bps: [Math.round((o.percent ?? 0) * 100)] } } };
|
|
58
|
+
if (o.gives === 'mint') return { Tokens: { from: { Mint: [`@mint:${o.currency ?? m.currencyKey}`] }, amount: { Fixed: [amount(o.amount, m.tokens.get(o.currency ?? m.currencyKey))] } } };
|
|
59
|
+
return prizeArgs(o, m);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function boxArgs(b, m) {
|
|
63
|
+
const t = m.priceToken(b);
|
|
64
|
+
const w = weights(b);
|
|
65
|
+
return {
|
|
66
|
+
price: amount(b.price, t),
|
|
67
|
+
currency: `@mint:${b.currency ?? m.currencyKey}`,
|
|
68
|
+
rise: rise(b.rise, t),
|
|
69
|
+
fall: fall(b.fall, t, m.now),
|
|
70
|
+
stock_odds: !!b.stockOdds,
|
|
71
|
+
active: b.active !== false,
|
|
72
|
+
shares: shares(b.split, m),
|
|
73
|
+
// An entry gives its prizes (a bundle up to three), and may burn the payment in place of the split.
|
|
74
|
+
entries: (b.outcomes ?? []).map((o, i) => ({ prizes: outcomeParts(o).map((part) => partPrize(part, m)), burns: outcomeBurns(o), weight: w[i] })),
|
|
75
|
+
gate: gateOf(b.gate, m),
|
|
76
|
+
odds: (b.odds ?? []).map((o) => ({ entry: o.outcome, boost: { rule: ruleOf(o.when, m), effect: effectOf(o.add) } })),
|
|
77
|
+
window: windowOf(b, m.now),
|
|
78
|
+
per_wallet: perWalletOf(b),
|
|
79
|
+
// A box that rolls on an NFT: the rule it must pass, and what each outcome does to it.
|
|
80
|
+
subject: b.rollsOn ? ruleOf(b.rollsOn, m) : null,
|
|
81
|
+
fates: (b.outcomes ?? []).flatMap((o, i) => {
|
|
82
|
+
const shifts = (o.shifts ?? []).map((x) => ({ key: x.attribute, by: toBy(x.by), most: toSeconds(x.upTo ?? 0), from_now: Boolean(x.fromNow) }));
|
|
83
|
+
// An outcome's own `then`, or the box's: an NFT taken as its price goes there whatever the roll gives.
|
|
84
|
+
const then = thenOf(o.then ?? b.then, m);
|
|
85
|
+
return shifts.length || then !== 'Back' ? [{ entry: i, shifts, then }] : [];
|
|
86
|
+
}),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Where an outcome sends the NFT a box rolls on: back, burned, given (the treasury's wallet is filled in when the economy goes live), or into a stash. */
|
|
91
|
+
export const thenOf = (then, m) => {
|
|
92
|
+
if (!then || then === 'back') return 'Back';
|
|
93
|
+
if (then === 'burn') return 'Burn';
|
|
94
|
+
if (then.stash) return { Stash: [m?.stash.get(then.stash)?.id ?? 0] };
|
|
95
|
+
return { Give: [then === 'treasury' ? '@treasury' : then.giveTo] };
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** The most a raise names (a u32): a farm’s time moved as written, with no cap of its own until 2106. */
|
|
99
|
+
const NO_CAP = 4_294_967_295;
|
|
100
|
+
/** Whether `key` is a time a farm of the spec keeps on what it stakes (its lock, its upkeep). */
|
|
101
|
+
const farmTime = (key, m) => m.farms.some((f) => f.lockAttribute === key || f.upkeepAttribute === key);
|
|
102
|
+
|
|
103
|
+
export function recipeArgs(r, m) {
|
|
104
|
+
const tok = (key) => m.tokens.get(key ?? m.currencyKey);
|
|
105
|
+
const inputs = (r.in ?? []).map((l) => {
|
|
106
|
+
if (l.takes === 'burn') return { Burn: { item: m.item.get(l.item)?.id ?? 0, count: l.count ?? 1 } };
|
|
107
|
+
if (l.takes === 'use') return { Use: { item: m.item.get(l.item)?.id ?? 0 } };
|
|
108
|
+
// A staked one names the farm it sits in (craft.rs): the craft takes it only from its seat there.
|
|
109
|
+
if (l.takes === 'staked') return { Staked: { item: m.item.get(l.item)?.id ?? 0, farm: m.farm.get(l.farm)?.id ?? 0 } };
|
|
110
|
+
if (l.takes === 'renew') return { Renew: { item: m.item.get(l.item)?.id ?? 0 } };
|
|
111
|
+
if (l.takes === 'show') return { Show: { collection: l.collection ?? '@collection' } };
|
|
112
|
+
// A trade-in: one NFT of `collection`, any kind, burned, given on, or put in a stash.
|
|
113
|
+
if (l.takes === 'trade') return { Trade: { collection: l.collection ?? '@collection', then: spendOf(l.then, m) } };
|
|
114
|
+
return { Pay: { mint: `@mint:${l.currency ?? m.currencyKey}`, amount: amount(l.amount, tok(l.currency)), to: toEnum(l.to, m) } };
|
|
115
|
+
});
|
|
116
|
+
// A line is a prize (prize.rs), or a roll on a collectible it keeps (`on`).
|
|
117
|
+
const outputs = (r.out ?? []).map((l) => (l.gives === 'roll' && l.on !== undefined ? { RollOn: { box_id: m.box.get(l.box)?.id ?? 0, place: l.on } } : { Prize: [prizeArgs(l, m)] }));
|
|
118
|
+
return {
|
|
119
|
+
active: r.active !== false,
|
|
120
|
+
inputs,
|
|
121
|
+
outputs,
|
|
122
|
+
needs: (r.needs ?? []).map((n) => ({ place: n.place, key: n.key, at_least: n.atLeast })),
|
|
123
|
+
keeps: (r.keeps ?? []).map((k) => ({ key: k.key, from: k.from, to: k.to })),
|
|
124
|
+
raises: (r.raises ?? []).map((x) => ({ place: x.place, key: x.key, by: toBy(x.by), most: x.upTo === undefined && farmTime(x.key, m) && !x.fromNow ? NO_CAP : toSeconds(x.upTo ?? 0), from_now: Boolean(x.fromNow) })),
|
|
125
|
+
window: windowOf(r, m.now),
|
|
126
|
+
per_wallet: perWalletOf(r),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function scheduleArgs(s, m) {
|
|
131
|
+
const every = toSeconds(s.every);
|
|
132
|
+
const poolOf = (key) => m.pool.get(key)?.index ?? 0;
|
|
133
|
+
let action;
|
|
134
|
+
if (s.does === 'issue') {
|
|
135
|
+
const t = m.tokens.get(s.currency ?? m.pool.get(s.pool)?.currency);
|
|
136
|
+
action = { from: { Mint: [`@mint:${t?.key}`] }, to: { Pool: [poolOf(s.pool)] }, amount: { Fixed: [amount(s.amount, t)] }, halve_every: s.halveEvery ?? 0 };
|
|
137
|
+
} else if (s.sealed) {
|
|
138
|
+
// A sealed schedule pays from its own vault, in `currency`.
|
|
139
|
+
const t = m.tokens.get(s.currency ?? m.currencyKey);
|
|
140
|
+
const how = s.percent !== undefined ? { Bps: [Math.round(s.percent * 100)] } : { Fixed: [amount(s.amount, t)] };
|
|
141
|
+
action = { from: { Vest: [`@mint:${t?.key}`] }, to: toEnum(s.to, m), amount: how, halve_every: s.halveEvery ?? 0 };
|
|
142
|
+
} else {
|
|
143
|
+
const from = s.does === 'burn' ? s.pool : s.from;
|
|
144
|
+
const t = m.poolToken(from);
|
|
145
|
+
const to = s.does === 'burn' ? 'Burn' : s.does === 'move' ? { Pool: [poolOf(s.to)] } : toEnum(s.to, m);
|
|
146
|
+
const how = s.percent !== undefined ? { Bps: [Math.round(s.percent * 100)] } : { Fixed: [amount(s.amount, t)] };
|
|
147
|
+
action = { from: { Pool: [poolOf(from)] }, to, amount: how, halve_every: s.halveEvery ?? 0 };
|
|
148
|
+
}
|
|
149
|
+
return { active: s.active !== false, action, every: BigInt(every), next_at: BigInt(s.start ? toTime(s.start, m.now) : m.now + every), max_runs: s.maxRuns ?? 0 };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The program's `Prize` for a prize line (`model.prizeLine`): tokens minted or from a pool (a fixed amount, or a share
|
|
154
|
+
* of the pool), an item (a compressed NFT of it into the project's tree, "@tree"), one NFT of a stash, or a roll of a box.
|
|
155
|
+
*/
|
|
156
|
+
export function prizeArgs(p, m) {
|
|
157
|
+
const line = prizeLine(p);
|
|
158
|
+
if (line.gives === 'item' && line.compressed) return { Compressed: { item: m.item.get(line.item)?.id ?? 0, tree: '@tree' } };
|
|
159
|
+
if (line.gives === 'item') return { Item: [m.item.get(line.item)?.id ?? 0] };
|
|
160
|
+
// An NFT a lottery's place holds in the lottery's own stash (a raffle), or one of a project stash.
|
|
161
|
+
if (line.gives === 'nft' && line.held) return { Held: [line.nft] };
|
|
162
|
+
if (line.gives === 'nft') return { Nft: { stash: m.stash.get(line.stash)?.id ?? 0, nft: line.nft } };
|
|
163
|
+
if (line.gives === 'roll') return { Roll: [m.box.get(line.box)?.id ?? 0] };
|
|
164
|
+
const pool = prizePool(line);
|
|
165
|
+
const from = pool ? { Pool: [m.pool.get(pool)?.index ?? 0] } : { Mint: [`@mint:${line.currency ?? m.currencyKey}`] };
|
|
166
|
+
if (line.gives === 'share') return { Tokens: { from, amount: { Bps: [Math.round((line.percent ?? 0) * 100)] } } };
|
|
167
|
+
return { Tokens: { from, amount: { Fixed: [amount(line.amount, prizeToken(line, m))] } } };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* A grant list's leaves: a plain address is the grant's amount in round 0; an entry { wallet, amount, round } names
|
|
172
|
+
* its own amount (whole units of `t`, the grant's token) and round, so one wallet listed under several rounds claims
|
|
173
|
+
* each once (monthly vesting, a season's airdrop).
|
|
174
|
+
*/
|
|
175
|
+
export const listLeaves = (list, t) => list.map((e) => (typeof e === 'string' ? loot.listLeaf(e, null) : loot.listLeaf(e.wallet, null, loot.listRound(e.round ?? 0), e.amount !== undefined ? amount(e.amount, t) : 0n)));
|
|
176
|
+
|
|
177
|
+
/** A prize line's `Prize`, or null for none. */
|
|
178
|
+
const prizeOrNull = (p, m) => (prizeLine(p) ? prizeArgs(p, m) : null);
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* A lottery's args: its own pot in `currency` (the project's by default), ticket money split by `split` (and
|
|
182
|
+
* `cutPercent` to the treasury) with the rest into the pot. No `every` draws once, at `firstDraw`.
|
|
183
|
+
*/
|
|
184
|
+
export function lotteryArgs(l, m) {
|
|
185
|
+
const potKey = l.currency ?? m.currencyKey;
|
|
186
|
+
const potToken = m.tokens.get(potKey);
|
|
187
|
+
const ticket = l.ticketCurrency ? m.tokens.get(l.ticketCurrency) : potToken;
|
|
188
|
+
const every = toSeconds(l.every);
|
|
189
|
+
return {
|
|
190
|
+
price: l.ticketNft ? 0n : amount(l.price, ticket),
|
|
191
|
+
mint: `@mint:${potKey}`,
|
|
192
|
+
shares: shares(lotterySplit(l), m),
|
|
193
|
+
// A place takes a share of the pot, a prize for its winner, or both.
|
|
194
|
+
places: (l.places ?? []).map(placeOf).map((p) => ({ bps: Math.round(p.percent * 100), prize: prizeOrNull(p.prize, m) })),
|
|
195
|
+
ticket_mint: l.ticketCurrency ? `@mint:${l.ticketCurrency}` : '11111111111111111111111111111111',
|
|
196
|
+
every: BigInt(every),
|
|
197
|
+
draws_at: BigInt(l.firstDraw ? toTime(l.firstDraw, m.now) : m.now + every),
|
|
198
|
+
gate: gateOf(l.gate, m),
|
|
199
|
+
per_wallet: l.perWallet ?? 0,
|
|
200
|
+
// A ticket paid with one NFT that passes the rule, in place of tokens.
|
|
201
|
+
nft_ticket: l.ticketNft ? { rule: ruleOf(l.ticketNft.rule, m), then: spendOf(l.ticketNft.then, m) } : null,
|
|
202
|
+
// No loss: every ticket goes back to its buyer once the round is drawn; the winners take the pot alone.
|
|
203
|
+
returns: Boolean(l.noLoss),
|
|
204
|
+
// A least sale: a round that sold fewer tickets is cancelled at its draw and refunded.
|
|
205
|
+
min_sold: BigInt(l.minSold ?? 0),
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* The args the authority opens a contest with, `at` (Unix seconds) its opening: its sides, when entries close and the
|
|
211
|
+
* result is due, its split, its gate (a list of wallets for a duel), the keys that may decide it, the least pot (an
|
|
212
|
+
* all-or-nothing crowdfund) and the stake every entry puts in.
|
|
213
|
+
*/
|
|
214
|
+
export function contestArgs(c, m, at = m.now) {
|
|
215
|
+
const t = m.priceToken(c);
|
|
216
|
+
// A decider's verdict is taken once entries close, so with deciders entries close before the result is due (the
|
|
217
|
+
// program refuses them at once): with no "entriesClose", an hour before the result is due (halfway for a contest
|
|
218
|
+
// shorter than two hours); with no "resultDue", an hour after entries close.
|
|
219
|
+
const deciders = (c.deciders ?? []).length > 0;
|
|
220
|
+
const due = c.resultDue === undefined && c.entriesClose !== undefined && deciders ? toSeconds(c.entriesClose) + 3600 : toSeconds(c.resultDue ?? c.entriesClose ?? '1d');
|
|
221
|
+
const closes = c.entriesClose !== undefined ? toSeconds(c.entriesClose) : deciders ? due - Math.min(3600, Math.floor(due / 2)) : due;
|
|
222
|
+
return {
|
|
223
|
+
sides: c.sides,
|
|
224
|
+
closes_at: BigInt(at + closes),
|
|
225
|
+
result_by: BigInt(at + due),
|
|
226
|
+
shares: shares(c.split, m),
|
|
227
|
+
gate: gateOf(c.gate, m),
|
|
228
|
+
deciders: c.deciders ?? [],
|
|
229
|
+
// A contest of NFTs counts its pot in NFTs.
|
|
230
|
+
min_pot: c.stakeNft ? wholeCount(c.minPot) ?? 0n : amount(c.minPot ?? 0, t),
|
|
231
|
+
stake: amount(c.stake ?? 0, t),
|
|
232
|
+
prize: prizeOrNull(c.prize, m),
|
|
233
|
+
// A token prize set aside whole at the decision and split among the winners by what each put in.
|
|
234
|
+
prize_split: Boolean(c.prize?.split),
|
|
235
|
+
// Each entry stakes one NFT that passes this rule, and the NFTs are the pot.
|
|
236
|
+
nft: c.stakeNft ? ruleOf(c.stakeNft, m) : null,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function farmArgs(f, m) {
|
|
241
|
+
const tiers = (f.tiers ?? [{ lock: 0, multiplier: 1 }]).map((t) => ({
|
|
242
|
+
lock: toSeconds(t.lock),
|
|
243
|
+
bps: Math.round((t.multiplier ?? 1) * 10_000),
|
|
244
|
+
prize: prizeOrNull(t.prize, m),
|
|
245
|
+
roll: t.roll ? m.box.get(t.roll)?.id ?? 0 : null,
|
|
246
|
+
// A token seat may leave before this tier's lock by leaving this share of what it takes out.
|
|
247
|
+
exit_bps: Math.round((t.exitFee ?? 0) * 100),
|
|
248
|
+
}));
|
|
249
|
+
// Every rule of its gate; with none, a farm of one item's collectibles takes that item.
|
|
250
|
+
const named = listOf(f.gate);
|
|
251
|
+
const gates = f.stakes !== 'collectibles' ? [] : (named.length ? named : f.item ? [{ item: f.item, attribute: 'item_id' }] : []).map((r) => ruleOf(r, m));
|
|
252
|
+
// A rate is per NFT_UNIT of weight: one collectible, or 1,000,000 base units of a staked token.
|
|
253
|
+
const rewards = m.poolToken(f.rewards);
|
|
254
|
+
const staked = f.stakes === 'collectibles' ? null : m.tokens.get(f.stakes);
|
|
255
|
+
const perDay = f.rate ? amount(f.rate.perDay, rewards) : 0n;
|
|
256
|
+
return {
|
|
257
|
+
custody: { freeze: 'Freeze', watch: 'Watch' }[f.custody] ?? 'Escrow',
|
|
258
|
+
rewards_pool: m.pool.get(f.rewards)?.index ?? 0,
|
|
259
|
+
tiers,
|
|
260
|
+
boosts: (f.boosts ?? []).map((b) => ({ rule: ruleOf(b.when, m), effect: effectOf(b.add) })),
|
|
261
|
+
max_boost_bps: Math.round((f.maxBoostPercent ?? 0) * 100),
|
|
262
|
+
gates,
|
|
263
|
+
rate: f.rate ? { per_day: staked ? (perDay * 1_000_000n) / 10n ** BigInt(staked.decimals ?? 0) : perDay, mints: Boolean(f.rate.minted) } : null,
|
|
264
|
+
growth: listOf(f.growth).map((g) => ({ key: g.attribute ?? 'xp', per_day: g.perDay, level: (g.levels ?? []).length ? g.level ?? 'level' : '', steps: g.levels ?? [], streak: Boolean(g.streak) })),
|
|
265
|
+
// A farm that runs for a time ends that long after it goes live.
|
|
266
|
+
ends_at: f.runs ? BigInt(m.now + toSeconds(f.runs)) : 0n,
|
|
267
|
+
// The times it keeps on what is staked: its lock, and an upkeep.
|
|
268
|
+
lock_key: f.lockAttribute ?? '',
|
|
269
|
+
upkeep_key: f.upkeepAttribute ?? '',
|
|
270
|
+
// Where an early exit's fee goes, how long a stake waits on its way out, and a toll on every payout of rewards.
|
|
271
|
+
exit_to: f.exitFeeTo ? sinkOf(f.exitFeeTo, m) : null,
|
|
272
|
+
unbond: f.unbond ? toSeconds(f.unbond) : 0,
|
|
273
|
+
toll: f.toll ? { bps: Math.round(f.toll.percent * 100), to: sinkOf(f.toll.to, m) } : null,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** A fee's sink (`Sink`): "burn", or "pool:<key>", one of the project's pools. */
|
|
278
|
+
export const sinkOf = (to, m) => (to === 'burn' ? 'Burn' : { Pool: [m.pool.get(String(to).replace(/^pool:/, ''))?.index ?? 0] });
|
|
279
|
+
|
|
280
|
+
/** An auction's args: its lot, reserve, least raise, times and split. "start" is when it goes live unless it says. */
|
|
281
|
+
export function auctionArgs(a, m) {
|
|
282
|
+
const t = m.priceToken(a);
|
|
283
|
+
const startsAt = toTime(a.start, m.now);
|
|
284
|
+
return {
|
|
285
|
+
lot: a.lot?.asset ? { Asset: [a.lot.asset] } : a.lot?.roll ? { Roll: [rollOf(a.lot.roll, m)] } : { Item: [m.item.get(a.lot?.item)?.id ?? 0] },
|
|
286
|
+
// With NFT bids, the reserve counts NFTs.
|
|
287
|
+
reserve: a.bidNfts ? BigInt(a.reserve ?? 1) : amount(a.reserve ?? 0, t),
|
|
288
|
+
min_raise_bps: Math.round((a.minRaise ?? 5) * 100),
|
|
289
|
+
starts_at: BigInt(startsAt),
|
|
290
|
+
ends_at: BigInt(startsAt + toSeconds(a.runs)),
|
|
291
|
+
extend: toSeconds(a.extendBy ?? '10m'),
|
|
292
|
+
shares: shares(a.split, m),
|
|
293
|
+
gate: gateOf(a.gate, m),
|
|
294
|
+
// A bid is a bundle of NFTs that pass this rule, in place of tokens.
|
|
295
|
+
nft_bids: a.bidNfts ? ruleOf(a.bidNfts, m) : null,
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const NOBODY = '11111111111111111111111111111111';
|
|
300
|
+
/** The program id of the box a prize rolls (a box key), or null for none. */
|
|
301
|
+
const rollOf = (box, m) => (box ? m.box.get(box)?.id ?? 0 : null);
|
|
302
|
+
|
|
303
|
+
export function grantArgs(g, m) {
|
|
304
|
+
const gives = prizeArgs(g.prize ?? g.gives, m);
|
|
305
|
+
const vouchers = [...(g.vouchers ?? []), NOBODY, NOBODY, NOBODY].slice(0, 3);
|
|
306
|
+
const list = g.list ?? [];
|
|
307
|
+
const keys = g.keys ?? [];
|
|
308
|
+
const l = g.limits ?? {};
|
|
309
|
+
return {
|
|
310
|
+
gives,
|
|
311
|
+
vouchers,
|
|
312
|
+
list_root: list.length ? loot.merkleRoot(listLeaves(list, prizeToken(prizeLine(g.prize ?? g.gives), m))) : new Uint8Array(32),
|
|
313
|
+
key_root: keys.length ? loot.merkleRoot(keys.map((k) => loot.keyLeaf(k))) : new Uint8Array(32),
|
|
314
|
+
// A grant that names no voucher key and no list is one any wallet may claim, within its limits.
|
|
315
|
+
open: !(g.vouchers ?? []).length && !list.length && !keys.length,
|
|
316
|
+
limits: {
|
|
317
|
+
wallet: perWalletOf(l),
|
|
318
|
+
per_person: l.perPerson ?? 0,
|
|
319
|
+
per_key: l.perKey ?? 0,
|
|
320
|
+
per_day: l.perDay ?? 0,
|
|
321
|
+
total: l.total ?? 0,
|
|
322
|
+
window: windowOf({ opens: l.opens, closes: l.until }, m.now),
|
|
323
|
+
},
|
|
324
|
+
active: g.active !== false,
|
|
325
|
+
gate: gateOf(g.gate, m),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// ---------------------------------------------------------------- resolving "@" names at build time
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Replaces "@mint:<key>" strings anywhere in `value` with the mint `ctx` names, "@ata:<wallet>:<key>" with that
|
|
333
|
+
* wallet's token account for it, "@pot:<id>" with a lottery's pot and "@farm:<id>" with a farm's address.
|
|
334
|
+
*/
|
|
335
|
+
function resolve(value, ctx) {
|
|
336
|
+
if (value === '@treasury') {
|
|
337
|
+
if (!ctx.treasury) throw new Error('No treasury yet for a roll that gives an NFT to it.');
|
|
338
|
+
return ctx.treasury;
|
|
339
|
+
}
|
|
340
|
+
if (value === '@collection') {
|
|
341
|
+
if (!ctx.collection) throw new Error('No collection yet for a rule or an item about the project’s collection.');
|
|
342
|
+
return ctx.collection;
|
|
343
|
+
}
|
|
344
|
+
if (value === '@tree') {
|
|
345
|
+
if (!ctx.tree) throw new Error('No tree yet for a compressed prize.');
|
|
346
|
+
return ctx.tree;
|
|
347
|
+
}
|
|
348
|
+
if (typeof value === 'string' && value.startsWith('@mint:')) {
|
|
349
|
+
const key = value.slice(6);
|
|
350
|
+
const mint = ctx.mints[key];
|
|
351
|
+
if (!mint) throw new Error(`No mint yet for the token “${key}”.`);
|
|
352
|
+
return mint;
|
|
353
|
+
}
|
|
354
|
+
if (typeof value === 'string' && value.startsWith('@farm:')) {
|
|
355
|
+
if (!ctx.project) throw new Error('No project yet for a stake gate’s farm.');
|
|
356
|
+
return loot.pda.farm(ctx.project, Number(value.slice(6)));
|
|
357
|
+
}
|
|
358
|
+
if (typeof value === 'string' && value.startsWith('@pot:')) {
|
|
359
|
+
if (!ctx.project) throw new Error('No project yet for a lottery’s pot.');
|
|
360
|
+
return loot.pda.pot(loot.pda.lottery(ctx.project, Number(value.slice(5))));
|
|
361
|
+
}
|
|
362
|
+
if (typeof value === 'string' && value.startsWith('@ata:')) {
|
|
363
|
+
const [, wallet, key, program] = value.split(':');
|
|
364
|
+
const mint = ctx.mints[key];
|
|
365
|
+
if (!mint) throw new Error(`No mint yet for the token “${key}”.`);
|
|
366
|
+
return loot.ata(wallet, mint, program === 'token-2022' ? TOKEN_2022_PROGRAM : TOKEN_PROGRAM);
|
|
367
|
+
}
|
|
368
|
+
if (Array.isArray(value)) return value.map((v) => resolve(v, ctx));
|
|
369
|
+
if (value instanceof Uint8Array || typeof value !== 'object' || value === null) return value;
|
|
370
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, resolve(v, ctx)]));
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// ---------------------------------------------------------------- the steps
|
|
374
|
+
|
|
375
|
+
const encodedSize = loot.accountBytes;
|
|
376
|
+
/** Bubblegum's config of a tree. */
|
|
377
|
+
const TREE_CONFIG_BYTES = 96;
|
|
378
|
+
const placeholderKey = (name) => encodeBase58(sha256(utf8(`loot-plan:${name}`)));
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Every step that sets the economy up, in order: the tokens the project mints, the collection, the project, the pools, items, boxes (the shop's too), recipes, schedules, lotteries, grants,
|
|
382
|
+
* farms, auctions, then the seeds and the grants' funds. `only` limits it to these part ids (a change to a live economy).
|
|
383
|
+
*
|
|
384
|
+
* A farm opens before the seed of its rewards pool lands, and anyone may stake in between. That is safe: a sharing
|
|
385
|
+
* farm credits nothing at once, and releases what lands in its pool over its longest lock (a day at least) to the
|
|
386
|
+
* seats there as it is released, its clock stopped while nobody is in. A seed that lands
|
|
387
|
+
* before the first stake or after it is released the same way from that stake on, so seeding first would change nothing.
|
|
388
|
+
*/
|
|
389
|
+
export function setupSteps(economy, { now = 0, only = null, live: given = null } = {}) {
|
|
390
|
+
const m = model(economy, { now });
|
|
391
|
+
const live = madeSoFar(given);
|
|
392
|
+
const steps = [];
|
|
393
|
+
const want = (id) => !only || only.has(id);
|
|
394
|
+
const landed = new Set(live?.landed ?? []);
|
|
395
|
+
const add = (step) => want(step.part) && !landed.has(step.id) && steps.push({ creates: [], signers: [owner], ...step });
|
|
396
|
+
const project = (ctx) => ctx.project;
|
|
397
|
+
const fresh = !live?.project;
|
|
398
|
+
|
|
399
|
+
// Tokens the project mints: a classic mint with the owner as its authority, handed to the project right after.
|
|
400
|
+
for (const t of m.tokens.values()) {
|
|
401
|
+
if (t.source !== 'game' || live?.mints?.[t.key] || t.mint && !t.mint.startsWith('@')) continue;
|
|
402
|
+
const key = `new:mint/${t.key}`;
|
|
403
|
+
if (t.fixedSupply) {
|
|
404
|
+
const supply = amount(t.supply, t);
|
|
405
|
+
add({
|
|
406
|
+
id: `token:${t.key}`,
|
|
407
|
+
part: `token:${t.key}`,
|
|
408
|
+
says: `Make the token ${t.symbol} (${t.decimals} decimals) with a fixed supply of ${t.supply}, all of it to your wallet, then turn its minting off for good`,
|
|
409
|
+
signers: [owner, key],
|
|
410
|
+
creates: [{ what: `the mint of ${t.symbol}`, bytes: MINT_BYTES }, { what: `your ${t.symbol} account`, bytes: 165 }],
|
|
411
|
+
build: (ctx) => [
|
|
412
|
+
createAccountIx({ from: ctx.owner, address: ctx.keys[key], lamports: ctx.rent(MINT_BYTES), space: MINT_BYTES, owner: TOKEN_PROGRAM }),
|
|
413
|
+
initializeMint2Ix({ mint: ctx.keys[key], decimals: t.decimals, authority: ctx.owner }),
|
|
414
|
+
createAssociatedTokenAccountIdempotent(ctx.owner, ctx.owner, ctx.keys[key]),
|
|
415
|
+
mintToIx({ mint: ctx.keys[key], to: loot.ata(ctx.owner, ctx.keys[key]), authority: ctx.owner, amount: supply }),
|
|
416
|
+
setAuthorityIx({ account: ctx.keys[key], current: ctx.owner, newAuthority: null }),
|
|
417
|
+
],
|
|
418
|
+
});
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
add({
|
|
422
|
+
id: `token:${t.key}`,
|
|
423
|
+
part: `token:${t.key}`,
|
|
424
|
+
says: `Make the token ${t.symbol} (${t.decimals} decimals), then hand its minting to the project`,
|
|
425
|
+
signers: [owner, key],
|
|
426
|
+
creates: [{ what: `the mint of ${t.symbol}`, bytes: MINT_BYTES }],
|
|
427
|
+
build: (ctx) => [
|
|
428
|
+
createAccountIx({ from: ctx.owner, address: ctx.keys[key], lamports: ctx.rent(MINT_BYTES), space: MINT_BYTES, owner: TOKEN_PROGRAM }),
|
|
429
|
+
initializeMint2Ix({ mint: ctx.keys[key], decimals: t.decimals, authority: ctx.owner }),
|
|
430
|
+
setAuthorityIx({ account: ctx.keys[key], current: ctx.owner, newAuthority: ctx.project }),
|
|
431
|
+
],
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// The collection and the project. A spec that gives compressed NFTs makes its collection with Bubblegum's plugin,
|
|
436
|
+
// which only a new collection can carry.
|
|
437
|
+
const tree = compressedTree(m);
|
|
438
|
+
if (fresh) {
|
|
439
|
+
const c = m.collection;
|
|
440
|
+
if (c)
|
|
441
|
+
add({
|
|
442
|
+
id: 'collection',
|
|
443
|
+
part: 'project',
|
|
444
|
+
says: `Make the collection “${c.name}”, with ${c.royaltyPercent ?? 5}% resale royalty to you; the project may mint into it${tree ? ', as Core NFTs or compressed ones' : ''}`,
|
|
445
|
+
signers: [owner, 'new:collection'],
|
|
446
|
+
creates: [{ what: 'the collection', bytes: 1 + 32 + 4 + c.name.length + 4 + (c.uri ?? '').length + 140 + (tree ? 3 : 0), approx: true }],
|
|
447
|
+
build: (ctx) => [createCollectionInstruction({ collection: ctx.keys['new:collection'], creator: ctx.owner, project: ctx.project, name: c.name, uri: c.uri ?? '', royaltyBps: Math.round((c.royaltyPercent ?? 5) * 100), compressed: Boolean(tree) })],
|
|
448
|
+
});
|
|
449
|
+
// The treasury is a wallet: the remainder of every split goes to its account of that mint, which the program
|
|
450
|
+
// opens when it is not there. The project's vaults, and every account a share pays, open the same way when first
|
|
451
|
+
// paid, so the setup opens none of them.
|
|
452
|
+
const collections = (ctx) => [...(ctx.collection ? [ctx.collection] : []), ...m.collections];
|
|
453
|
+
const count = (c ? 1 : 0) + m.collections.length;
|
|
454
|
+
add({
|
|
455
|
+
id: 'project',
|
|
456
|
+
part: 'project',
|
|
457
|
+
says: `Open the project on the loot program${count ? ` with ${count === 1 ? 'its collection' : `its ${count} collections`}` : ', with no collection'}; ${m.authority === 'owner' ? 'your wallet' : m.authority} signs its decisions, and ${m.treasuryWallet ?? 'your wallet'} is its treasury`,
|
|
458
|
+
creates: [{ what: 'the project', bytes: loot.projectBytes(count) }, { what: 'the project’s fee box', bytes: 0 }],
|
|
459
|
+
build: (ctx) => [loot.createProjectIx({ owner: ctx.owner, id: m.projectNumber, collections: collections(ctx), treasury: ctx.treasury, authority: ctx.authority })],
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// The tree compressed prizes are minted into: made empty by the owner, then opened with the project as its creator.
|
|
464
|
+
if (tree && !live?.tree)
|
|
465
|
+
add({
|
|
466
|
+
id: 'tree',
|
|
467
|
+
part: 'tree',
|
|
468
|
+
says: `Open the project’s tree of compressed NFTs, for ${(2 ** tree.depth).toLocaleString('en-US')} of them in all`,
|
|
469
|
+
signers: [owner, 'new:tree'],
|
|
470
|
+
creates: [{ what: 'the tree of compressed NFTs', bytes: tree.bytes }, { what: 'the tree’s config (Bubblegum’s)', bytes: TREE_CONFIG_BYTES }],
|
|
471
|
+
build: (ctx) => [
|
|
472
|
+
createAccountIx({ from: ctx.owner, address: ctx.keys['new:tree'], lamports: ctx.rent(tree.bytes), space: tree.bytes, owner: loot.COMPRESSION_PROGRAM }),
|
|
473
|
+
loot.openTreeIx({ signer: ctx.owner, project: project(ctx), tree: ctx.keys['new:tree'], maxDepth: tree.depth, maxBufferSize: tree.buffer }),
|
|
474
|
+
],
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
for (const p of m.pools) {
|
|
478
|
+
const t = m.tokens.get(p.currency);
|
|
479
|
+
add({ id: `pool:${p.key}`, part: `pool:${p.key}`, says: `Open the pool “${p.key}” for ${t?.symbol}`, creates: [{ what: `the pool “${p.key}”`, bytes: 165 }], build: (ctx) => [loot.openPoolIx({ owner: ctx.owner, project: project(ctx), index: p.index, mint: ctx.mints[p.currency] })] });
|
|
480
|
+
}
|
|
481
|
+
const record = (part, def, says, kind, what, options = () => ({})) => {
|
|
482
|
+
const [variant] = Object.keys(def);
|
|
483
|
+
const bytes = recordBytesOf(kind, def[variant]);
|
|
484
|
+
// A lottery's first write opens its pot: a token account as well as the record.
|
|
485
|
+
const pot = kind === 'Lottery' ? [{ what: `${what}’s pot`, bytes: 165 }, { what: `${what}’s ticket escrow`, bytes: 165 }] : def.Schedule?.args.action.from.Vest ? [{ what: `${what}’s sealed vault`, bytes: 165 }] : [];
|
|
486
|
+
add({ id: `${part}:${kind}`, part, says, creates: [{ what, bytes }, ...pot], build: (ctx) => [loot.upsertIx({ owner: ctx.owner, project: project(ctx), def: resolve(def, ctx), ...options(ctx) })] });
|
|
487
|
+
};
|
|
488
|
+
for (const it of m.items) record(`item:${it.key}`, { Item: { id: it.id, args: itemArgs(it) } }, `${it.active === false ? 'Switch off' : 'Write'} the item “${it.name}”`, 'Item', `the item “${it.name}”`);
|
|
489
|
+
// A box's line names its odds, so the owner sees what they approve (above all when a live box's odds change).
|
|
490
|
+
const odds = (b) => {
|
|
491
|
+
const ws = weights(b);
|
|
492
|
+
const total = ws.reduce((n, w) => n + w, 0) || 1;
|
|
493
|
+
const part = (o) => (o.gives === 'item' ? m.item.get(o.item)?.name ?? o.item : o.gives === 'stash' ? `an NFT of “${m.stash.get(o.stash)?.name ?? o.stash}”` : o.gives === 'pay' ? `${o.amount} ${m.poolToken(o.pool)?.symbol ?? ''}`.trim() : o.gives === 'share' ? `${o.percent}% of “${o.pool}”` : o.gives === 'mint' ? `${o.amount} ${m.tokens.get(o.currency ?? m.currencyKey)?.symbol ?? ''}`.trim() : o.gives === 'nft' ? `the NFT ${String(o.nft).slice(0, 4)}…` : o.gives === 'roll' ? `a roll of “${m.box.get(o.box)?.name ?? o.box}”` : '');
|
|
494
|
+
const what = (o) => outcomeParts(o).map(part).join(' + ') || (outcomeBurns(o) ? 'a burn' : 'nothing');
|
|
495
|
+
return (b.outcomes ?? []).map((o, i) => `${what(o)} ${Math.round((ws[i] / total) * 10_000) / 100}%`).join(', ');
|
|
496
|
+
};
|
|
497
|
+
const wasLive = new Set((live?.published?.boxes ?? []).map((b) => b.key));
|
|
498
|
+
for (const b of m.allBoxes) {
|
|
499
|
+
const says = b.shopOf ? `Put “${b.name}” in the shop at ${b.price} ${m.priceToken(b)?.symbol}` : `${b.active === false ? 'Switch off' : wasLive.has(b.key) ? 'Change' : 'Put on sale'} the box “${b.name}” at ${b.price} ${m.priceToken(b)?.symbol}${b.active === false ? '' : `: ${odds(b)}`}`;
|
|
500
|
+
record(b.shopOf ? `item:${b.shopOf}` : `box:${b.key}`, { Box: { id: b.id, args: boxArgs(b, m) } }, says, 'LootBox', b.shopOf ? `the shop listing of “${b.name}”` : `the box “${b.name}”`);
|
|
501
|
+
}
|
|
502
|
+
for (const r of m.recipes) record(`recipe:${r.key}`, { Recipe: { id: r.id, args: recipeArgs(r, m) } }, `Write the recipe “${r.name}”`, 'Recipe', `the recipe “${r.name}”`);
|
|
503
|
+
for (const s of m.schedules) {
|
|
504
|
+
// A sealed schedule that pays a lottery's pot names the lottery when it is written (the pot is the project's own).
|
|
505
|
+
const pays = destination(s.to ?? '');
|
|
506
|
+
const lottery = s.sealed && pays.kind === 'lottery' ? m.lottery.get(pays.key) : null;
|
|
507
|
+
record(`schedule:${s.key}`, { Schedule: { id: s.id, args: scheduleArgs(s, m) } }, `${s.active === false ? 'Stop' : 'Start'} the ${s.sealed ? 'sealed ' : ''}schedule “${s.name ?? s.key}”`, 'Schedule', `the schedule “${s.name ?? s.key}”`, (ctx) => (lottery ? { lottery: loot.pda.lottery(project(ctx), lottery.id) } : {}));
|
|
508
|
+
// A sealed schedule's vault is funded once, at go-live: from the owner's wallet, minted, or from a pool.
|
|
509
|
+
const id = `fund:${s.key}`;
|
|
510
|
+
if (!s.sealed || !Number(s.fund ?? 0) || (live?.landed ?? []).includes(id) || live?.published?.schedules?.some((x) => x.key === s.key)) continue;
|
|
511
|
+
const t = m.tokens.get(s.currency ?? m.currencyKey);
|
|
512
|
+
const from = s.fundFrom ?? 'wallet';
|
|
513
|
+
add({
|
|
514
|
+
id,
|
|
515
|
+
part: `schedule:${s.key}`,
|
|
516
|
+
signers: from === 'wallet' || m.authority === 'owner' ? [owner] : ['authority'],
|
|
517
|
+
says: `Put ${s.fund} ${t?.symbol ?? ''} in the sealed schedule “${s.name ?? s.key}”${from === 'wallet' ? ' from your wallet' : from === 'mint' ? ', minted' : ` from “${from.slice(5)}”`}: nobody can take it back`,
|
|
518
|
+
creates: [],
|
|
519
|
+
build: (ctx) => {
|
|
520
|
+
const mint = ctx.mints[t.key];
|
|
521
|
+
const vest = loot.pda.vest(loot.pda.schedule(project(ctx), s.id));
|
|
522
|
+
const value = amount(s.fund, t);
|
|
523
|
+
if (from === 'wallet') {
|
|
524
|
+
const tokenProgram = programsOf(ctx)[mint] ?? TOKEN_PROGRAM;
|
|
525
|
+
// SOL is wrapped first, as a seed is: the owner's wallet holds it as plain SOL, the vault takes wrapped SOL.
|
|
526
|
+
const wrap = t.wsol ? [createAssociatedTokenAccountIdempotent(ctx.owner, ctx.owner, NATIVE_MINT), transferLamports(ctx.owner, loot.ata(ctx.owner, NATIVE_MINT), value), syncNative(loot.ata(ctx.owner, NATIVE_MINT))] : [];
|
|
527
|
+
return [...wrap, transferCheckedIx({ from: loot.ata(ctx.owner, mint, tokenProgram), mint, to: vest, owner: ctx.owner, amount: value, decimals: t.decimals ?? 0, tokenProgram })];
|
|
528
|
+
}
|
|
529
|
+
const source = from === 'mint' ? { Mint: [mint] } : { Pool: [m.pool.get(from.slice(5))?.index ?? 0] };
|
|
530
|
+
return [loot.moveTokensIx({ authority: ctx.authority, project: project(ctx), mint, from: source, to: { Account: [vest] }, amount: { Fixed: [value] }, programs: programsOf(ctx) })];
|
|
531
|
+
},
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
for (const l of m.lotteries) {
|
|
535
|
+
// A raffle's NFT goes into the lottery's own stash first: the lottery is written naming an NFT already there.
|
|
536
|
+
for (const p of (l.places ?? []).map(placeOf).filter((p) => p.prize?.held)) {
|
|
537
|
+
const id = `stash:lottery-${l.key}:${p.prize.nft}`;
|
|
538
|
+
if ((live?.landed ?? []).includes(id) || live?.published?.lotteries?.some((x) => x.key === l.key)) continue;
|
|
539
|
+
add({
|
|
540
|
+
id,
|
|
541
|
+
part: `lottery:${l.key}`,
|
|
542
|
+
says: `Put your NFT ${p.prize.nft} in the lottery “${l.name}”: it stays there until its winner takes it, and nobody, you included, can take it out before`,
|
|
543
|
+
creates: [{ what: `the lottery “${l.name}”’s stash`, bytes: STASH_HEAD_BYTES + 32 }],
|
|
544
|
+
build: (ctx) => [loot.stashIx({ signer: ctx.owner, project: project(ctx), lottery: l.id, asset: p.prize.nft, collection: p.prize.collection ?? ctx.collection })],
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
record(`lottery:${l.key}`, { Lottery: { id: l.id, args: lotteryArgs(l, m) } }, `Start the ${l.raffle ? 'raffle' : 'lottery'} “${l.name}”`, 'Lottery', `the lottery “${l.name}”`);
|
|
548
|
+
}
|
|
549
|
+
for (const g of m.grants) record(`grant:${g.key}`, { Grant: { id: g.id, args: grantArgs(g, m) } }, `Write the grant “${g.name}”`, 'Grant', `the grant “${g.name}”`);
|
|
550
|
+
for (const f of m.farms) {
|
|
551
|
+
if (live?.farms?.includes(f.key)) continue;
|
|
552
|
+
const rewards = m.poolToken(f.rewards);
|
|
553
|
+
add({
|
|
554
|
+
id: `farm:${f.key}`,
|
|
555
|
+
part: `farm:${f.key}`,
|
|
556
|
+
says: `Open the farm “${f.name}”: stake ${f.stakes === 'collectibles' ? 'collectibles' : m.tokens.get(f.stakes)?.symbol}, earn ${rewards?.symbol} from “${f.rewards}”`,
|
|
557
|
+
creates: [{ what: `the farm “${f.name}”`, bytes: encodedSize('Farm', { project: NOBODY, id: 0, farmed: f.stakes === 'collectibles' ? 'Collectibles' : { Tokens: { mint: NOBODY } }, ...placeholders(farmArgs(f, m)), rewards_mint: NOBODY, rate_at: 0n, rate_carry: 0n, total_weight: 0n, reward_per_weight: 0n, reward_remainder: 0n, given: 0n, unreleased: 0n, released_at: 0n, release_ends: 0n, release_from: 0n, release_base: 0n }) }, { what: 'its rewards vault', bytes: 165 }, ...(f.stakes === 'collectibles' ? [] : [{ what: 'its stake vault', bytes: 165 }])],
|
|
558
|
+
build: (ctx) => [loot.openFarmIx({ owner: ctx.owner, project: project(ctx), id: f.id, args: resolve(farmArgs(f, m), ctx), rewardsMint: ctx.mints[rewards.key], stakedMint: f.stakes === 'collectibles' ? null : ctx.mints[f.stakes] })],
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Traits the project vouches for from NFTs' off-chain metadata: one Merkle root per collection, signed once.
|
|
563
|
+
const vouched = new Map((live?.published?.traits ?? []).map((t) => [t.collection, t.root]));
|
|
564
|
+
for (const t of m.traits) {
|
|
565
|
+
if (vouched.get(t.collection) === t.root) continue;
|
|
566
|
+
add({
|
|
567
|
+
id: `traits:${t.collection}`,
|
|
568
|
+
part: `traits:${t.collection}`,
|
|
569
|
+
says: `Vouch for the traits of the NFTs of ${t.collection} (a root of their off-chain metadata)`,
|
|
570
|
+
creates: [{ what: `the traits of ${t.collection}`, bytes: 1 + 32 * 3 }],
|
|
571
|
+
build: (ctx) => [loot.setTraitsIx({ signer: ctx.owner, project: project(ctx), key: t.collection, root: Uint8Array.from(t.root.match(/../g).map((h) => parseInt(h, 16))) })],
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Stashes: the owner's wallet hands each NFT it lists to the project, one instruction each (`stash`). A compressed NFT
|
|
576
|
+
// needs a proof fetched when it is signed, so the checker leaves those to `loot.stashIx` after go-live.
|
|
577
|
+
// What is in a live stash already: what its last publish listed, and the steps of this one that landed.
|
|
578
|
+
const stashed = new Set([
|
|
579
|
+
...(live?.published?.stashes ?? []).flatMap((s) => stashNfts(s).map((n) => `${s.key}:${n.asset}`)),
|
|
580
|
+
...(live?.landed ?? []).filter((id) => id.startsWith('stash:')).map((id) => id.slice(6)),
|
|
581
|
+
]);
|
|
582
|
+
for (const s of m.stashes) {
|
|
583
|
+
const nfts = stashNfts(s);
|
|
584
|
+
const putting = nfts.filter((n) => n.kind !== 'compressed' && !stashed.has(`${s.key}:${n.asset}`));
|
|
585
|
+
// The first NFT this plan puts in names the stash's rent: all of it for a new stash, what it grows by for one live already.
|
|
586
|
+
const fresh = !nfts.some((n) => stashed.has(`${s.key}:${n.asset}`));
|
|
587
|
+
for (const [i, n] of putting.entries()) {
|
|
588
|
+
add({
|
|
589
|
+
id: `stash:${s.key}:${n.asset}`,
|
|
590
|
+
part: `stash:${s.key}`,
|
|
591
|
+
says: `Put your NFT ${n.asset} in the stash “${s.name}”: the project holds it until a prize or a roll gives it away`,
|
|
592
|
+
creates: i > 0 ? [] : fresh ? [{ what: `the stash “${s.name}”, with ${putting.length} NFT${putting.length === 1 ? '' : 's'}`, bytes: STASH_HEAD_BYTES + 32 * putting.length }] : [{ what: `room for ${putting.length} more NFT${putting.length === 1 ? '' : 's'} in “${s.name}”`, bytes: 32 * putting.length, grows: true }],
|
|
593
|
+
build: (ctx) => [
|
|
594
|
+
loot.stashIx({
|
|
595
|
+
signer: ctx.owner,
|
|
596
|
+
project: project(ctx),
|
|
597
|
+
id: s.id,
|
|
598
|
+
asset: n.asset,
|
|
599
|
+
collection: n.kind === 'core' ? n.collection ?? ctx.collection : undefined,
|
|
600
|
+
tokenMetadata: n.kind === 'token-metadata' ? { programmable: n.programmable, ruleSet: n.ruleSet ?? undefined } : undefined,
|
|
601
|
+
}),
|
|
602
|
+
],
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Auctions, each opened by its seller alone: a new unit of an item by the authority, a collectible by the owner who
|
|
608
|
+
// holds it. Its address names the seller (`auctionSeller`).
|
|
609
|
+
for (const a of m.auctions) {
|
|
610
|
+
const t = m.priceToken(a);
|
|
611
|
+
const args = auctionArgs(a, m);
|
|
612
|
+
const asset = Boolean(a.lot?.asset);
|
|
613
|
+
const signer = asset || m.authority === 'owner' ? owner : 'authority';
|
|
614
|
+
const terms = `bids in ${t?.symbol} from ${a.reserve ?? 'any amount'}, for ${a.runs}`;
|
|
615
|
+
add({
|
|
616
|
+
id: `auction:${a.key}`,
|
|
617
|
+
part: `auction:${a.key}`,
|
|
618
|
+
signers: [signer],
|
|
619
|
+
says: asset
|
|
620
|
+
? `Put your NFT up for auction in “${a.name}”: it stays with the project until someone wins it or the auction ends (${terms})`
|
|
621
|
+
: a.lot?.roll
|
|
622
|
+
? `Open the auction “${a.name}” of a roll of the box “${m.box.get(a.lot.roll)?.name ?? a.lot.roll}”, made for its winner at the end (${terms})`
|
|
623
|
+
: `Open the auction “${a.name}” of one new “${m.item.get(a.lot?.item)?.name ?? a.lot?.item}” (${terms})`,
|
|
624
|
+
creates: [{ what: `the auction “${a.name}” (back when it ends)`, bytes: encodedSize('Auction', { project: NOBODY, id: 0, seller: NOBODY, mint: NOBODY, lot: args.lot, ours: false, serial: 0, reserve: 0n, min_raise_bps: 0, starts_at: 0n, ends_at: 0n, extend: 0, high_bidder: NOBODY, high: 0n, last_bid_at: 0n, shares: args.shares, gate: args.gate, nft_bids: placeholders(args).nft_bids, held: [] }) }],
|
|
625
|
+
build: (ctx) => {
|
|
626
|
+
const seller = auctionSeller(a, ctx);
|
|
627
|
+
return [loot.openAuctionIx({ seller, project: project(ctx), collection: a.lot?.collection ?? ctx.collection, id: a.id, mint: ctx.mints[t.key], args: resolve(args, ctx) })];
|
|
628
|
+
},
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Seeds: what the owner puts in each pool. SOL is wrapped into the owner's account first.
|
|
633
|
+
for (const p of m.pools) {
|
|
634
|
+
if (!(Number(p.seed ?? 0) > 0) || live?.seeded?.includes(p.key)) continue;
|
|
635
|
+
const t = m.tokens.get(p.currency);
|
|
636
|
+
const units = amount(p.seed, t);
|
|
637
|
+
if (t.gameMints) {
|
|
638
|
+
const signer = m.authority === 'owner' ? owner : 'authority';
|
|
639
|
+
add({
|
|
640
|
+
id: `seed:${p.key}`,
|
|
641
|
+
part: `seed:${p.key}`,
|
|
642
|
+
signers: [signer],
|
|
643
|
+
says: `Mint ${p.seed} ${t.symbol} into “${p.key}”`,
|
|
644
|
+
build: (ctx) => [loot.moveTokensIx({ authority: ctx.authority, project: project(ctx), mint: ctx.mints[t.key], from: { Mint: [ctx.mints[t.key]] }, to: { Pool: [p.index] }, amount: { Fixed: [units] }, reference: `seed ${p.key}` })],
|
|
645
|
+
});
|
|
646
|
+
} else {
|
|
647
|
+
add({
|
|
648
|
+
id: `seed:${p.key}`,
|
|
649
|
+
part: `seed:${p.key}`,
|
|
650
|
+
says: `Put ${p.seed} ${t.symbol} into “${p.key}”`,
|
|
651
|
+
build: (ctx) => [
|
|
652
|
+
...(t.wsol ? [createAssociatedTokenAccountIdempotent(ctx.owner, ctx.owner, NATIVE_MINT), transferLamports(ctx.owner, loot.ata(ctx.owner, NATIVE_MINT), units), syncNative(loot.ata(ctx.owner, NATIVE_MINT))] : []),
|
|
653
|
+
loot.payIx({ player: ctx.owner, project: project(ctx), mint: ctx.mints[t.key], treasury: ctx.treasury, amount: units, shares: [{ to: { Pool: [p.index] }, bps: 10_000 }], reference: `seed ${p.key}`, programs: { [ctx.mints[t.key]]: tokenProgramOf(t) } }),
|
|
654
|
+
],
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
// A lottery's seed: the owner puts it straight into the lottery's pot.
|
|
659
|
+
for (const l of m.lotteries) {
|
|
660
|
+
if (!(Number(l.seed ?? 0) > 0) || live?.seeded?.includes(`lottery:${l.key}`)) continue;
|
|
661
|
+
const t = m.tokens.get(l.currency ?? m.currencyKey);
|
|
662
|
+
const units = amount(l.seed, t);
|
|
663
|
+
add({
|
|
664
|
+
id: `seed:lottery:${l.key}`,
|
|
665
|
+
part: `lottery:${l.key}`,
|
|
666
|
+
says: `Put ${l.seed} ${t.symbol} into the pot of “${l.name}”`,
|
|
667
|
+
build: (ctx) => {
|
|
668
|
+
const pot = loot.pda.pot(loot.pda.lottery(project(ctx), l.id));
|
|
669
|
+
return [
|
|
670
|
+
...(t.wsol ? [createAssociatedTokenAccountIdempotent(ctx.owner, ctx.owner, NATIVE_MINT), transferLamports(ctx.owner, loot.ata(ctx.owner, NATIVE_MINT), units), syncNative(loot.ata(ctx.owner, NATIVE_MINT))] : []),
|
|
671
|
+
loot.payIx({ player: ctx.owner, project: project(ctx), mint: ctx.mints[t.key], treasury: ctx.treasury, amount: units, shares: [{ to: { Account: [pot] }, bps: 10_000 }], reference: `seed ${l.key}`, programs: programsOf(ctx) }),
|
|
672
|
+
];
|
|
673
|
+
},
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
for (const g of m.grants) {
|
|
677
|
+
if (!(Number(g.fund ?? 0) > 0) || live?.funded?.includes(g.key)) continue;
|
|
678
|
+
const lamports = toUnits(g.fund, 9);
|
|
679
|
+
add({ id: `fund:${g.key}`, part: `fund:${g.key}`, says: `Put ${g.fund} SOL in the funds of the grant “${g.name}” (they pay each claim’s rent)`, build: (ctx) => [transferLamports(ctx.owner, loot.pda.grant(project(ctx), g.id), lamports)] });
|
|
680
|
+
}
|
|
681
|
+
return { steps, model: m };
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/** A stash's bytes before its NFTs: its tag, its project, its id and its count (`stash.rs`); each NFT adds 32. */
|
|
685
|
+
export const STASH_HEAD_BYTES = 41;
|
|
686
|
+
|
|
687
|
+
/** Who puts a spec's auction up, and so is named in its address: the owner for a collectible it holds, the authority for a new unit of an item. */
|
|
688
|
+
export const auctionSeller = (a, ctx) => (a.lot?.asset ? ctx.owner : ctx.authority);
|
|
689
|
+
|
|
690
|
+
export const recordBytesOf = (kind, body) => {
|
|
691
|
+
const args = body.args;
|
|
692
|
+
const base = {
|
|
693
|
+
Item: () => ({ project: NOBODY, item_id: 0, ...placeholders(args), issued: 0, voided: 0 }),
|
|
694
|
+
LootBox: () => ({ project: NOBODY, box_id: 0, ...placeholders(args), version: 0 }),
|
|
695
|
+
Recipe: () => ({ project: NOBODY, id: 0, version: 0, ...placeholders(args) }),
|
|
696
|
+
Schedule: () => ({ project: NOBODY, id: 0, ...placeholders(args), runs: 0 }),
|
|
697
|
+
Lottery: () => ({ project: NOBODY, id: 0, pot_bump: 0, funds_bump: 0, incarnation: 0n, revision: 0, pending: 0, ...placeholders(args), places: [...(args.places ?? []).map((p) => (typeof p === 'object' ? { prize: null, ...p } : { bps: p, prize: null })), ...Array(LIMITS.places).fill({ bps: 0, prize: null })].slice(0, LIMITS.places), round: 0, status: 'Open', sold: 0n, asked_first: 0n, asked_slot: 0n, first_ticket: 0n, paid: 0, pot_at_payout: 0n, given: 0 }),
|
|
698
|
+
Grant: () => ({ project: NOBODY, id: 0, incarnation: 0n, ...placeholders(args), counters: { claimed: 0, today: 0, day: 0n } }),
|
|
699
|
+
}[kind];
|
|
700
|
+
return encodedSize(kind, base());
|
|
701
|
+
};
|
|
702
|
+
/** Exact initial contest record size for its configured shares, gate, sides and deciders. */
|
|
703
|
+
export const contestBytes = (contest, m) => encodedSize('Contest', { project: NOBODY, id: 0, incarnation: 0n, opener: NOBODY, mint: NOBODY, ...placeholders(contestArgs(contest, m)), verdict: 'Open', winner: 0, entries: 0, unclaimed: 0n, sides: Array(contest.sides).fill(0n), losers: 0 });
|
|
704
|
+
/** Args with "@mint:" names standing in as addresses, for sizing. */
|
|
705
|
+
const placeholders = (args) => resolve(args, { project: placeholderKey('project'), collection: placeholderKey('new:collection'), tree: placeholderKey('new:tree'), treasury: placeholderKey('treasury'), mints: new Proxy({}, { get: (_, k) => placeholderKey(`mint/${String(k)}`) }) });
|
|
706
|
+
/** The token program of each Token-2022 mint a context names (the program's account lists name it). */
|
|
707
|
+
const programsOf = (ctx) => ctx.tokenPrograms ?? {};
|
|
708
|
+
|
|
709
|
+
// ---------------------------------------------------------------- contexts
|
|
710
|
+
|
|
711
|
+
/** Stand-in addresses for every name a plan uses, so it can be built and sized without a wallet. */
|
|
712
|
+
export function placeholderContext(m, { rent = (bytes) => (BigInt(bytes) + ACCOUNT_OVERHEAD) * MOST_RENT_PER_BYTE } = {}) {
|
|
713
|
+
const ownerKey = placeholderKey('owner');
|
|
714
|
+
const collection = m.collection ? placeholderKey('new:collection') : null;
|
|
715
|
+
const tree = compressedTree(m) ? placeholderKey('new:tree') : null;
|
|
716
|
+
const mints = {};
|
|
717
|
+
for (const t of m.tokens.values()) mints[t.key] = t.mint?.startsWith('@') ? placeholderKey(`new:mint/${t.key}`) : t.mint;
|
|
718
|
+
return {
|
|
719
|
+
owner: ownerKey,
|
|
720
|
+
authority: m.authority === 'owner' ? ownerKey : m.authority,
|
|
721
|
+
collection,
|
|
722
|
+
tree,
|
|
723
|
+
project: loot.pda.project(ownerKey, m.projectNumber),
|
|
724
|
+
treasury: m.treasuryWallet ?? ownerKey,
|
|
725
|
+
mints,
|
|
726
|
+
keys: { ...(collection ? { 'new:collection': collection } : {}), ...(tree ? { 'new:tree': tree } : {}), ...Object.fromEntries([...m.tokens.values()].map((t) => [`new:mint/${t.key}`, placeholderKey(`new:mint/${t.key}`)])) },
|
|
727
|
+
rent,
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* What of `live` is made. While a setup is landing (`partial`), the project, its collection, its tree and each mint the
|
|
733
|
+
* setup makes count only once the step that makes it landed (`landed`): a setup cut short makes the rest again, with
|
|
734
|
+
* this session's fresh keys, instead of naming an account that was never made. What was live before a change, and
|
|
735
|
+
* that the change does not make, counts as it is. The treasury and authority count only with the project. The approval
|
|
736
|
+
* page holds what is left against the chain before it builds on it (`js/approve/live-check.mjs`).
|
|
737
|
+
*/
|
|
738
|
+
export function madeSoFar(live) {
|
|
739
|
+
if (!live?.partial) return live;
|
|
740
|
+
const landed = new Set(live.landed ?? []);
|
|
741
|
+
const changing = new Set(live.changing ?? []);
|
|
742
|
+
// A new setup makes everything; a change makes only the parts it names.
|
|
743
|
+
const made = (part, id) => landed.has(id) || (Boolean(live.published) && !changing.has(part));
|
|
744
|
+
const out = { ...live };
|
|
745
|
+
// The treasury and authority are the project's: named only once it is made, never from the file before then.
|
|
746
|
+
if (!made('project', 'project')) {
|
|
747
|
+
delete out.project;
|
|
748
|
+
delete out.treasury;
|
|
749
|
+
delete out.authority;
|
|
750
|
+
}
|
|
751
|
+
if (!made('project', 'collection')) delete out.collection;
|
|
752
|
+
if (!made('tree', 'tree')) delete out.tree;
|
|
753
|
+
if (live.mints) out.mints = Object.fromEntries(Object.entries(live.mints).filter(([key]) => made(`token:${key}`, `token:${key}`)));
|
|
754
|
+
return out;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/** The context of a real setup: the owner's wallet, fresh keys for what it creates, and what is live already. */
|
|
758
|
+
export function setupContext(m, { owner: ownerKey, keys, live: given = {}, rent }) {
|
|
759
|
+
const live = madeSoFar(given) ?? {};
|
|
760
|
+
// A setup goes on only in the owner's own project: never in one a file names for them.
|
|
761
|
+
const own = loot.pda.project(ownerKey, m.projectNumber);
|
|
762
|
+
if (live.project && live.project !== own) throw new Error(`The live section names the project ${live.project}, which is not the project ${own} of the wallet ${ownerKey}.`);
|
|
763
|
+
const collection = live.collection ?? keys['new:collection'] ?? null;
|
|
764
|
+
const tree = live.tree ?? keys['new:tree'] ?? null;
|
|
765
|
+
const mints = {};
|
|
766
|
+
// A mint the spec names by its address is that one, whatever the live section says; one the setup makes is the
|
|
767
|
+
// live section's once it landed (checked against the chain by the approval page), else this setup's fresh key.
|
|
768
|
+
for (const t of m.tokens.values()) mints[t.key] = t.mint && !t.mint.startsWith('@') ? t.mint : live.mints?.[t.key] ?? keys[`new:mint/${t.key}`];
|
|
769
|
+
return {
|
|
770
|
+
owner: ownerKey,
|
|
771
|
+
authority: m.authority === 'owner' ? ownerKey : m.authority,
|
|
772
|
+
collection,
|
|
773
|
+
tree,
|
|
774
|
+
project: own,
|
|
775
|
+
treasury: live.treasury ?? m.treasuryWallet ?? ownerKey,
|
|
776
|
+
mints,
|
|
777
|
+
keys,
|
|
778
|
+
rent,
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// ---------------------------------------------------------------- packing
|
|
783
|
+
|
|
784
|
+
/** The bytes of a transaction of `instructions` the owner pays for: what `pack` holds against the budget. */
|
|
785
|
+
export const stepBytes = (instructions, ctx) => transactionSize(compileMessage({ payer: ctx.owner, blockhash: placeholderKey('blockhash'), instructions }));
|
|
786
|
+
|
|
787
|
+
/** Packs steps into transactions under the size budget, one signer set at a time; each step whole. */
|
|
788
|
+
export function pack(steps, ctx) {
|
|
789
|
+
const transactions = [];
|
|
790
|
+
let current = null;
|
|
791
|
+
const sizeOf = (instructions) => stepBytes(instructions, ctx);
|
|
792
|
+
for (const step of steps) {
|
|
793
|
+
const instructions = step.build(ctx);
|
|
794
|
+
const signerKind = step.signers.includes('authority') ? 'authority' : 'owner';
|
|
795
|
+
if (current && current.signer === signerKind) {
|
|
796
|
+
const next = [...current.instructions, ...instructions];
|
|
797
|
+
if (sizeOf(next) <= BUDGET) {
|
|
798
|
+
current.instructions = next;
|
|
799
|
+
current.steps.push(step);
|
|
800
|
+
current.built.push(instructions);
|
|
801
|
+
current.signers = [...new Set([...current.signers, ...step.signers])];
|
|
802
|
+
continue;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
const size = sizeOf(instructions);
|
|
806
|
+
if (size > BUDGET) throw new Error(`The step “${step.says}” is ${size} bytes: too big for one transaction.`);
|
|
807
|
+
// `built`: each step's own instructions, in the order of `steps`.
|
|
808
|
+
current = { signer: signerKind, steps: [step], built: [instructions], instructions, signers: [...step.signers] };
|
|
809
|
+
transactions.push(current);
|
|
810
|
+
}
|
|
811
|
+
for (const t of transactions) t.bytes = sizeOf(t.instructions);
|
|
812
|
+
return transactions;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* The plan for a spec: refusals, or every step packed into transactions. With `ctx` the transactions carry the
|
|
817
|
+
* real instructions; without it they are built on stand-in addresses (for sizing, cost and showing).
|
|
818
|
+
*/
|
|
819
|
+
export function plan(economy, { now = Math.floor(Date.now() / 1000), ctx = null, only = null, live = null } = {}) {
|
|
820
|
+
const checked = check(economy, { now });
|
|
821
|
+
if (!checked.ok) return { ok: false, refusals: checked.refusals, warnings: checked.warnings };
|
|
822
|
+
const { steps, model: m } = setupSteps(economy, { now, only, live });
|
|
823
|
+
if (ctx) ctx.tokenPrograms ??= Object.fromEntries([...m.tokens.values()].filter((t) => t.program === 'token-2022' && ctx.mints[t.key]).map((t) => [ctx.mints[t.key], TOKEN_2022_PROGRAM]));
|
|
824
|
+
const context = ctx ?? placeholderContext(m);
|
|
825
|
+
const transactions = pack(steps, context);
|
|
826
|
+
return {
|
|
827
|
+
ok: true,
|
|
828
|
+
warnings: checked.warnings,
|
|
829
|
+
steps,
|
|
830
|
+
transactions,
|
|
831
|
+
approvals: Math.max(1, Math.ceil(transactions.filter((t) => t.signer === 'owner').length / PER_APPROVAL)) + (transactions.some((t) => t.signer === 'authority') ? 1 : 0),
|
|
832
|
+
freshKeys: [...new Set(steps.flatMap((s) => s.signers.filter((x) => x.startsWith('new:'))))],
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/** What each part needs while the economy runs: the instruction, and who signs it. */
|
|
837
|
+
export function playSteps(economy) {
|
|
838
|
+
const m = model(economy);
|
|
839
|
+
const rows = [];
|
|
840
|
+
const add = (part, ix, signer, says) => rows.push({ part, ix, signer, says });
|
|
841
|
+
// A shop item, and any box with one outcome and nothing random, sells in one instruction: price split, prize given.
|
|
842
|
+
for (const b of m.shop) {
|
|
843
|
+
const it = m.item.get(b.shopOf);
|
|
844
|
+
add(`item:${it.key}`, 'buy', 'player', it.kind === 'normal' ? `buy “${it.name}”: the project’s backend delivers it from the receipt` : `buy “${it.name}”: minted to them at once`);
|
|
845
|
+
}
|
|
846
|
+
for (const b of m.boxes) {
|
|
847
|
+
if (boxBuys(b)) {
|
|
848
|
+
add(`box:${b.key}`, 'buy', 'player', `buy “${b.name}”: what it gives, at once`);
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
add(`box:${b.key}`, 'open_box', 'player', `open “${b.name}”`);
|
|
852
|
+
if (boxRolls(b)) add(`box:${b.key}`, 'callback_roll', 'the oracle', 'roll it');
|
|
853
|
+
add(`box:${b.key}`, 'settle', 'anyone', 'carry out what it rolled');
|
|
854
|
+
}
|
|
855
|
+
for (const p of m.payments) add(`payment:${p.key}`, 'pay', 'player', `pay “${p.name}”`);
|
|
856
|
+
add('payments', 'pay_nft', 'player', 'pay with an NFT (burned, or given to a wallet): the receipt is the record');
|
|
857
|
+
for (const s of m.stashes) add(`stash:${s.key}`, 'stash', 'the owner or the authority', `put an NFT it holds in “${s.name}”, or take one out to any wallet`);
|
|
858
|
+
for (const r of prizeRolls(m).filter((r) => !['mission', 'recipe'].includes(r.route))) {
|
|
859
|
+
add(r.by, 'ask_roll', 'anyone, who pays the oracle', `ask for the roll of “${m.box.get(r.box)?.name ?? r.box}” a winner got (${r.route})`);
|
|
860
|
+
add(r.by, 'settle', 'anyone', 'carry out what it rolled');
|
|
861
|
+
}
|
|
862
|
+
for (const r of m.recipes) add(`recipe:${r.key}`, 'craft', 'player', `craft “${r.name}”`);
|
|
863
|
+
for (const s of m.schedules) add(`schedule:${s.key}`, 'run_schedule', 'anyone', `run “${s.name ?? s.key}” when it is due`);
|
|
864
|
+
for (const l of m.lotteries) add(`lottery:${l.key}`, 'buy_tickets', 'player', 'buy tickets'), add(`lottery:${l.key}`, 'draw_lottery', 'anyone', 'ask for the draw when it is due'), add(`lottery:${l.key}`, 'finish_lottery_contribution', 'anyone', 'finish each purchase, then pay each winner');
|
|
865
|
+
for (const f of m.farms) {
|
|
866
|
+
if (f.stakes === 'collectibles') add(`farm:${f.key}`, 'stake_nft', 'player', 'stake a collectible'), add(`farm:${f.key}`, 'unstake_nft', 'player', 'take it out once its lock has run');
|
|
867
|
+
else add(`farm:${f.key}`, 'stake_tokens', 'player', 'stake'), add(`farm:${f.key}`, 'unstake_tokens', 'player', 'unstake or collect'), add(`farm:${f.key}`, 'stake_nft', 'player', 'put a collectible on as a booster');
|
|
868
|
+
add(`farm:${f.key}`, 'harvest', 'player', 'collect rewards');
|
|
869
|
+
add(`farm:${f.key}`, 'refresh_seat', 'anyone', 'read a seat’s boosts again');
|
|
870
|
+
}
|
|
871
|
+
for (const c of m.contests) {
|
|
872
|
+
add(`contest:${c.key}`, 'open_contest', 'authority', `open “${c.name}”`);
|
|
873
|
+
add(`contest:${c.key}`, 'enter', 'player', 'back a side');
|
|
874
|
+
add(`contest:${c.key}`, 'decide', 'authority (or anyone, to call it off once overdue)', 'name the side that won');
|
|
875
|
+
add(`contest:${c.key}`, 'claim', 'anyone', 'pay an entry');
|
|
876
|
+
}
|
|
877
|
+
for (const a of m.auctions) {
|
|
878
|
+
add(`auction:${a.key}`, 'open_auction', a.lot?.asset ? 'the owner, alone (a collectible of theirs)' : 'the authority, alone (a new unit of an item)', `put the lot up (at go-live)`);
|
|
879
|
+
add(`auction:${a.key}`, 'bid', 'player', 'bid: the bid it tops goes back at once');
|
|
880
|
+
add(`auction:${a.key}`, 'end_auction', 'anyone once it is over; its seller, to call off one with no bid', 'sell to the high bidder, or send the lot home');
|
|
881
|
+
}
|
|
882
|
+
for (const h of m.holds) add(`hold:${h.key}`, 'hold', 'player', `hand in a “${h.item}” until ${h.until ?? `${h.for ?? 'the time they choose'} from then`}`), add(`hold:${h.key}`, 'release', 'authority while it runs, anyone after', 'say where it goes, or send it home');
|
|
883
|
+
for (const g of m.grants) add(`grant:${g.key}`, 'claim_grant', 'the winning wallet, which signs it (with a voucher, a list proof, or an open grant); anyone may pay its fees', `claim “${g.name}”`);
|
|
884
|
+
for (const d of m.decisions) add(`decision:${d.key}`, { payout: 'move_tokens', burn: 'move_tokens', issue: 'move_tokens', make: 'collectible', unmake: 'collectible', set: 'collectible', award: 'release', decide: 'decide', roll: 'roll', open_contest: 'open_contest' }[d.does], 'authority', d.when);
|
|
885
|
+
return rows;
|
|
886
|
+
}
|
|
887
|
+
|