@xgamesfun/solana 0.20.1 → 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/README.md +10 -10
- package/examples/coin-flip-arena.json +2 -7
- package/examples/elements-set.json +2 -9
- package/examples/expedition-guild.json +1 -8
- package/examples/guild-missions.json +3 -10
- package/examples/halving-seasons.json +4 -9
- package/examples/hero-trials.json +1 -4
- package/examples/merge-2048.json +5 -5
- package/examples/paper-hands-farm.json +4 -16
- package/examples/partner-vault.json +1 -4
- package/examples/stakers-draw.json +1 -8
- package/examples/star-fleet.json +1 -8
- package/examples/starforce.json +6 -6
- package/js/approve/details.mjs +8 -7
- package/js/approve/server.mjs +20 -5
- package/js/chain/borsh.js +9 -0
- package/js/chain/core.js +14 -2
- package/js/chain/lookup.js +8 -2
- package/js/chain/rpc.js +4 -2
- package/js/client/accounts.js +448 -116
- package/js/client/grant.js +125 -66
- package/js/client/idl/loot.js +1 -1
- package/js/client/idl/loot.json +2360 -2218
- package/js/client/index.js +1 -0
- package/js/client/ix.js +1204 -473
- package/js/client/pda.js +20 -16
- package/js/client/policy.js +44 -23
- package/js/client/random.js +70 -0
- package/js/client/sizes.js +255 -12
- package/js/economy/check.mjs +455 -210
- package/js/economy/cost.mjs +45 -30
- package/js/economy/examples.mjs +2 -2
- package/js/economy/explain.mjs +24 -20
- package/js/economy/guide.mjs +31 -30
- package/js/economy/live.mjs +30 -7
- package/js/economy/model.mjs +171 -56
- package/js/economy/plan.mjs +380 -112
- package/js/economy/schema.json +291 -143
- package/js/economy/simulate.mjs +154 -87
- package/js/mcp/tools.mjs +14 -8
- package/js/play/index.js +1121 -375
- package/js/play/keys.js +167 -0
- package/package.json +1 -1
- package/release.json +5 -5
- package/skills/xsolana-economy/SKILL.md +15 -11
- package/skills/xsolana-economy/references/after-launch.md +37 -27
- package/skills/xsolana-economy/references/apps.md +55 -19
- package/skills/xsolana-economy/references/sentence-to-spec.md +30 -22
- package/skills/xsolana-economy/references/telling-the-person.md +52 -11
- package/types/chain/core.d.ts +3 -1
- package/types/chain/lookup.d.ts +5 -2
- package/types/chain/rpc.d.ts +3 -1
- package/types/client/accounts.d.ts +195 -55
- package/types/client/grant.d.ts +72 -27
- package/types/client/idl/loot.d.ts +590 -559
- package/types/client/index.d.ts +1 -0
- package/types/client/ix.d.ts +3862 -234
- package/types/client/pda.d.ts +19 -15
- package/types/client/policy.d.ts +39 -14
- package/types/client/random.d.ts +26 -0
- package/types/client/sizes.d.ts +93 -5
- package/types/economy/check.d.mts +5 -10
- package/types/economy/cost.d.mts +2 -1
- package/types/economy/live.d.mts +16 -1
- package/types/economy/model.d.mts +105 -37
- package/types/economy/plan.d.mts +193 -192
- package/types/economy/simulate.d.mts +21 -13
- package/types/mcp/tools.d.mts +21 -13
- package/types/play/index.d.ts +450 -39
- package/types/play/keys.d.ts +51 -0
package/js/economy/plan.mjs
CHANGED
|
@@ -7,8 +7,12 @@
|
|
|
7
7
|
// which signs the step that creates it. `placeholderContext` fills every address with a stand-in, so a plan can be
|
|
8
8
|
// sized and shown before anyone connects a wallet.
|
|
9
9
|
import { encodeBase58 } from '../chain/base58.js';
|
|
10
|
+
import { encodeBorsh } from '../chain/borsh.js';
|
|
11
|
+
import IDL from '../client/idl/loot.js';
|
|
10
12
|
import { createCollectionInstruction } from '../chain/core.js';
|
|
11
|
-
import { NATIVE_MINT, TOKEN_2022_PROGRAM, TOKEN_PROGRAM } from '../chain/pda.js';
|
|
13
|
+
import { ASSOCIATED_TOKEN_PROGRAM, NATIVE_MINT, SYSTEM_PROGRAM, SYSVAR_INSTRUCTIONS, SYSVAR_SLOT_HASHES, TOKEN_2022_PROGRAM, TOKEN_PROGRAM } from '../chain/pda.js';
|
|
14
|
+
import { createTableIx, extendTableIx } from '../chain/lookup.js';
|
|
15
|
+
import { MEMO_PROGRAM } from '../chain/token2022.js';
|
|
12
16
|
import { createAssociatedTokenAccountIdempotent, syncNative, transferLamports } from '../chain/programs.js';
|
|
13
17
|
import { createAccountIx, initializeMint2Ix, MINT_BYTES, mintToIx, setAuthorityIx, transferCheckedIx } from '../chain/token.js';
|
|
14
18
|
import { compileMessage, transactionSize } from '../chain/tx.js';
|
|
@@ -16,7 +20,7 @@ import { sha256 } from '../chain/sha256.js';
|
|
|
16
20
|
import { utf8 } from '../chain/bytes.js';
|
|
17
21
|
import * as loot from '../client/index.js';
|
|
18
22
|
import { check } from './check.mjs';
|
|
19
|
-
import { ACCOUNT_OVERHEAD, boxBuys, boxRolls, compressedTree, destination, effectOf, gateOf,
|
|
23
|
+
import { ACCOUNT_OVERHEAD, boxBuys, rent, RENT_PER_BYTE, boxRolls, compressedTree, destination, effectOf, gateOf, listOf, lotterySplit, model, MOST_RENT_PER_BYTE, outcomeParts, perWalletOf, placeOf, lifeLines, prizeLine, prizePool, prizeRolls, prizeToken, rarityNumber, ruleOf, sharesOf, spendOf, stashNfts, toBy, toEnum, toSeconds, toTime, toUnits, weights, wholeCount, windowOf } from './model.mjs';
|
|
20
24
|
import { BUDGET } from './txsize.mjs';
|
|
21
25
|
|
|
22
26
|
const owner = 'owner';
|
|
@@ -31,10 +35,10 @@ const tokenProgramOf = (t) => (t?.program === 'token-2022' ? TOKEN_2022_PROGRAM
|
|
|
31
35
|
|
|
32
36
|
// ---------------------------------------------------------------- spec parts → the program's args
|
|
33
37
|
|
|
34
|
-
export function itemArgs(it) {
|
|
38
|
+
export function itemArgs(it, m) {
|
|
35
39
|
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
|
-
|
|
40
|
+
// A collectible is minted into its own `collection`, or the one the setup makes ("@collection" until it exists). Its
|
|
41
|
+
// life is written into each recipe that takes it (`lifeLines`).
|
|
38
42
|
return {
|
|
39
43
|
kind,
|
|
40
44
|
rarity: rarityNumber(it.rarity),
|
|
@@ -43,19 +47,51 @@ export function itemArgs(it) {
|
|
|
43
47
|
name: it.name,
|
|
44
48
|
uri: it.uri ?? '',
|
|
45
49
|
max_supply: it.supply ?? 0,
|
|
46
|
-
life,
|
|
47
50
|
active: it.active !== false,
|
|
48
51
|
collection: kind === 'Normal' ? NOBODY : it.collection ?? '@collection',
|
|
52
|
+
// A fresh collectible's traits, each drawn per unit (a table's row, or a number in a range).
|
|
53
|
+
traits: kind === 'Normal' ? [] : (it.drawn ?? []).map((t) => ({ key: t.attribute, from: t.table !== undefined ? { Table: [m?.table.get(t.table)?.id ?? 0] } : { Range: { min: BigInt(t.min ?? 0), max: BigInt(t.max ?? 0) } } })),
|
|
49
54
|
};
|
|
50
55
|
}
|
|
51
56
|
|
|
57
|
+
/**
|
|
58
|
+
* The bytes of an order of box `b` (`orderBytes`), its spec's names standing in as addresses, with the shares its split
|
|
59
|
+
* stores. `opened`: an open's (`open_box`, `Order::sized`: each draw at its dearest outcome's prizes as they are kept,
|
|
60
|
+
* rows only for the tables its fresh items draw); otherwise a roll made inside another call (a craft, a grant's claim,
|
|
61
|
+
* a mission, a lottery's place, a roll lot: `Order::space_for`, the box's whole table budget a draw).
|
|
62
|
+
*/
|
|
63
|
+
export const boxOrderBytes = (b, m, opens = 1, { opened = false } = {}) => {
|
|
64
|
+
const args = boxArgs(b, m);
|
|
65
|
+
const items = opened ? Object.fromEntries(m.items.map((it) => [it.id, itemArgs(it, m)])) : undefined;
|
|
66
|
+
return loot.orderBytes(placeholders(args), (args.shares ?? []).length, opens, items);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/** The most bytes of rows one table write carries: what a 1,232-byte transaction leaves after the upsert's accounts,
|
|
70
|
+
* its signature and its own bytes. */
|
|
71
|
+
export const TABLE_BATCH_BYTES = 800;
|
|
72
|
+
|
|
73
|
+
/** A spec table's rows as the program takes them ({ weight, left, label }), in batches that fit a transaction each. */
|
|
74
|
+
export function tableBatches(t) {
|
|
75
|
+
const rows = t.rows.map((r) => ({ weight: r.weight, left: r.stock ?? 4294967295, label: t.kind === 'numbers' ? '' : String(r.label ?? '') }));
|
|
76
|
+
const batches = [[]];
|
|
77
|
+
let bytes = 0;
|
|
78
|
+
for (const r of rows) {
|
|
79
|
+
const size = 12 + new TextEncoder().encode(r.label).length;
|
|
80
|
+
if (bytes + size > TABLE_BATCH_BYTES && batches.at(-1).length) batches.push([]), (bytes = 0);
|
|
81
|
+
batches.at(-1).push(r);
|
|
82
|
+
bytes += size;
|
|
83
|
+
}
|
|
84
|
+
return batches;
|
|
85
|
+
}
|
|
86
|
+
|
|
52
87
|
/** The program's `Prize` for one part of a box outcome (`model.outcomeParts`). */
|
|
53
88
|
export function partPrize(o, m) {
|
|
89
|
+
// An item, or a compressed NFT of it, minted at the settle into the project's tree.
|
|
90
|
+
if (o.gives === 'item' && o.compressed) return { Compressed: { item: m.item.get(o.item)?.id ?? 0, tree: '@tree' } };
|
|
54
91
|
if (o.gives === 'item') return { Item: [m.item.get(o.item)?.id ?? 0] };
|
|
55
92
|
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:
|
|
57
|
-
if (o.gives === '
|
|
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))] } } };
|
|
93
|
+
if (o.gives === 'pay') return { Tokens: { from: { Pool: [m.pool.get(o.pool)?.index ?? 0] }, amount: amount(o.amount, m.poolToken(o.pool)) } };
|
|
94
|
+
if (o.gives === 'mint') return { Tokens: { from: { Mint: [`@mint:${o.currency ?? m.currencyKey}`] }, amount: amount(o.amount, m.tokens.get(o.currency ?? m.currencyKey)) } };
|
|
59
95
|
return prizeArgs(o, m);
|
|
60
96
|
}
|
|
61
97
|
|
|
@@ -68,10 +104,14 @@ export function boxArgs(b, m) {
|
|
|
68
104
|
rise: rise(b.rise, t),
|
|
69
105
|
fall: fall(b.fall, t, m.now),
|
|
70
106
|
stock_odds: !!b.stockOdds,
|
|
107
|
+
when_out: b.whenOut === 'drawLeft' ? 'DrawLeft' : 'StopSales',
|
|
71
108
|
active: b.active !== false,
|
|
72
109
|
shares: shares(b.split, m),
|
|
73
|
-
//
|
|
74
|
-
|
|
110
|
+
// What one open draws: each slot so many draws among the outcomes it names (all, when none), no outcome twice
|
|
111
|
+
// when distinct. None is one draw among every outcome.
|
|
112
|
+
slots: (b.slots ?? []).map((s) => ({ count: s.count, only: s.from ?? [], distinct: Boolean(s.distinct), share: Math.round((s.share ?? 0) * 100) })),
|
|
113
|
+
// An entry gives its prizes (a bundle of any number), and may burn the payment in place of the split.
|
|
114
|
+
entries: (b.outcomes ?? []).map((o, i) => ({ prizes: outcomeParts(o).map((part) => partPrize(part, m)), weight: w[i] })),
|
|
75
115
|
gate: gateOf(b.gate, m),
|
|
76
116
|
odds: (b.odds ?? []).map((o) => ({ entry: o.outcome, boost: { rule: ruleOf(o.when, m), effect: effectOf(o.add) } })),
|
|
77
117
|
window: windowOf(b, m.now),
|
|
@@ -82,24 +122,19 @@ export function boxArgs(b, m) {
|
|
|
82
122
|
const shifts = (o.shifts ?? []).map((x) => ({ key: x.attribute, by: toBy(x.by), most: toSeconds(x.upTo ?? 0), from_now: Boolean(x.fromNow) }));
|
|
83
123
|
// An outcome's own `then`, or the box's: an NFT taken as its price goes there whatever the roll gives.
|
|
84
124
|
const then = thenOf(o.then ?? b.then, m);
|
|
85
|
-
return shifts.length || then !==
|
|
125
|
+
return shifts.length || then !== null ? [{ entry: i, shifts, then }] : [];
|
|
86
126
|
}),
|
|
87
127
|
};
|
|
88
128
|
}
|
|
89
129
|
|
|
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. */
|
|
130
|
+
/** Where an outcome sends the NFT a box rolls on: back (null), burned, given (the treasury's wallet is filled in when the economy goes live), or into a stash. */
|
|
91
131
|
export const thenOf = (then, m) => {
|
|
92
|
-
if (!then || then === 'back') return
|
|
132
|
+
if (!then || then === 'back') return null;
|
|
93
133
|
if (then === 'burn') return 'Burn';
|
|
94
134
|
if (then.stash) return { Stash: [m?.stash.get(then.stash)?.id ?? 0] };
|
|
95
135
|
return { Give: [then === 'treasury' ? '@treasury' : then.giveTo] };
|
|
96
136
|
};
|
|
97
137
|
|
|
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
138
|
export function recipeArgs(r, m) {
|
|
104
139
|
const tok = (key) => m.tokens.get(key ?? m.currencyKey);
|
|
105
140
|
const inputs = (r.in ?? []).map((l) => {
|
|
@@ -107,21 +142,24 @@ export function recipeArgs(r, m) {
|
|
|
107
142
|
if (l.takes === 'use') return { Use: { item: m.item.get(l.item)?.id ?? 0 } };
|
|
108
143
|
// A staked one names the farm it sits in (craft.rs): the craft takes it only from its seat there.
|
|
109
144
|
if (l.takes === 'staked') return { Staked: { item: m.item.get(l.item)?.id ?? 0, farm: m.farm.get(l.farm)?.id ?? 0 } };
|
|
110
|
-
|
|
145
|
+
// A renew keeps the collectible as a use does; its life's raise counts its uses from 0 again (`lifeLines`).
|
|
146
|
+
if (l.takes === 'renew') return { Use: { item: m.item.get(l.item)?.id ?? 0 } };
|
|
111
147
|
if (l.takes === 'show') return { Show: { collection: l.collection ?? '@collection' } };
|
|
112
148
|
// A trade-in: one NFT of `collection`, any kind, burned, given on, or put in a stash.
|
|
113
149
|
if (l.takes === 'trade') return { Trade: { collection: l.collection ?? '@collection', then: spendOf(l.then, m) } };
|
|
114
150
|
return { Pay: { mint: `@mint:${l.currency ?? m.currencyKey}`, amount: amount(l.amount, tok(l.currency)), to: toEnum(l.to, m) } };
|
|
115
151
|
});
|
|
152
|
+
const life = lifeLines(r, m);
|
|
116
153
|
// A line is a prize (prize.rs), or a roll on a collectible it keeps (`on`).
|
|
117
154
|
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
155
|
return {
|
|
119
156
|
active: r.active !== false,
|
|
120
157
|
inputs,
|
|
121
158
|
outputs,
|
|
122
|
-
|
|
159
|
+
// The recipe's own needs and raises, then what the life of each collectible it takes adds (`lifeLines`).
|
|
160
|
+
needs: [...(r.needs ?? []).map((n) => ({ place: n.place, key: n.key, test: { AtLeast: [n.atLeast ?? 0] } })), ...life.needs],
|
|
123
161
|
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:
|
|
162
|
+
raises: [...(r.raises ?? []).map((x) => ({ place: x.place, shift: { key: x.key, by: toBy(x.by), most: toSeconds(x.upTo ?? 0), from_now: Boolean(x.fromNow) } })), ...life.raises],
|
|
125
163
|
window: windowOf(r, m.now),
|
|
126
164
|
per_wallet: perWalletOf(r),
|
|
127
165
|
};
|
|
@@ -132,8 +170,10 @@ export function scheduleArgs(s, m) {
|
|
|
132
170
|
const poolOf = (key) => m.pool.get(key)?.index ?? 0;
|
|
133
171
|
let action;
|
|
134
172
|
if (s.does === 'issue') {
|
|
135
|
-
|
|
136
|
-
|
|
173
|
+
// Minted into a pool, or into a farm's rewards vault ("to": "farm:<key>"): an emission its stakers share.
|
|
174
|
+
const farm = destination(s.to ?? '').kind === 'farm' ? m.farm.get(destination(s.to).key) : null;
|
|
175
|
+
const t = m.tokens.get(s.currency ?? (farm ? farm.rewardsToken : m.pool.get(s.pool)?.currency));
|
|
176
|
+
action = { from: { Mint: [`@mint:${t?.key}`] }, to: farm ? toEnum(s.to, m) : { Pool: [poolOf(s.pool)] }, amount: { Fixed: [amount(s.amount, t)] }, halve_every: s.halveEvery ?? 0 };
|
|
137
177
|
} else if (s.sealed) {
|
|
138
178
|
// A sealed schedule pays from its own vault, in `currency`.
|
|
139
179
|
const t = m.tokens.get(s.currency ?? m.currencyKey);
|
|
@@ -159,12 +199,13 @@ export function prizeArgs(p, m) {
|
|
|
159
199
|
if (line.gives === 'item') return { Item: [m.item.get(line.item)?.id ?? 0] };
|
|
160
200
|
// An NFT a lottery's place holds in the lottery's own stash (a raffle), or one of a project stash.
|
|
161
201
|
if (line.gives === 'nft' && line.held) return { Held: [line.nft] };
|
|
202
|
+
// An NFT drawn at random from the lottery's own stash (a raffle of many NFTs).
|
|
203
|
+
if (line.gives === 'raffled') return 'Raffled';
|
|
162
204
|
if (line.gives === 'nft') return { Nft: { stash: m.stash.get(line.stash)?.id ?? 0, nft: line.nft } };
|
|
163
205
|
if (line.gives === 'roll') return { Roll: [m.box.get(line.box)?.id ?? 0] };
|
|
164
206
|
const pool = prizePool(line);
|
|
165
207
|
const from = pool ? { Pool: [m.pool.get(pool)?.index ?? 0] } : { Mint: [`@mint:${line.currency ?? m.currencyKey}`] };
|
|
166
|
-
|
|
167
|
-
return { Tokens: { from, amount: { Fixed: [amount(line.amount, prizeToken(line, m))] } } };
|
|
208
|
+
return { Tokens: { from, amount: amount(line.amount, prizeToken(line, m)) } };
|
|
168
209
|
}
|
|
169
210
|
|
|
170
211
|
/**
|
|
@@ -187,25 +228,82 @@ export function lotteryArgs(l, m) {
|
|
|
187
228
|
const ticket = l.ticketCurrency ? m.tokens.get(l.ticketCurrency) : potToken;
|
|
188
229
|
const every = toSeconds(l.every);
|
|
189
230
|
return {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
231
|
+
// What a round copies at its first purchase.
|
|
232
|
+
terms: {
|
|
233
|
+
price: l.ticketNft ? 0n : amount(l.price, ticket),
|
|
234
|
+
mint: `@mint:${potKey}`,
|
|
235
|
+
shares: shares(lotterySplit(l), m),
|
|
236
|
+
// A place takes a share of the pot, a prize for its winner, or both.
|
|
237
|
+
// A run of `times` places alike: "places 2 to 201 each take 1%" is one.
|
|
238
|
+
places: (l.places ?? []).map(placeOf).map((p) => ({ bps: Math.round(p.percent * 100), prize: prizeOrNull(p.prize, m), times: p.times })),
|
|
239
|
+
ticket_mint: l.ticketCurrency ? `@mint:${l.ticketCurrency}` : '11111111111111111111111111111111',
|
|
240
|
+
draws_at: BigInt(l.firstDraw ? toTime(l.firstDraw, m.now) : m.now + every),
|
|
241
|
+
// A ticket paid with one NFT that passes the rule, in place of tokens.
|
|
242
|
+
nft_ticket: l.ticketNft ? { rule: ruleOf(l.ticketNft.rule, m), then: spendOf(l.ticketNft.then, m) } : null,
|
|
243
|
+
// No loss: every ticket goes back to its buyer once the round is drawn; the winners take the pot alone.
|
|
244
|
+
returns: Boolean(l.noLoss),
|
|
245
|
+
// A least sale: a round that sold fewer tickets is cancelled at its draw and refunded.
|
|
246
|
+
min_sold: BigInt(l.minSold ?? 0),
|
|
247
|
+
},
|
|
196
248
|
every: BigInt(every),
|
|
197
|
-
draws_at: BigInt(l.firstDraw ? toTime(l.firstDraw, m.now) : m.now + every),
|
|
198
249
|
gate: gateOf(l.gate, m),
|
|
199
250
|
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
251
|
};
|
|
207
252
|
}
|
|
208
253
|
|
|
254
|
+
/**
|
|
255
|
+
* A lottery round's header as its first purchase sizes it (`LotteryRound::new`: its `head`), and the token accounts a
|
|
256
|
+
* round may open (`accounts`: the payment mint's, the pot's, one for each run whose prize comes from a pool). The
|
|
257
|
+
* header holds a copy of the terms, a total for each share, two bits a place, a record for each run of tokens and an
|
|
258
|
+
* NFT slot for each place of the lottery's own stash.
|
|
259
|
+
*/
|
|
260
|
+
export function roundSize(l, m) {
|
|
261
|
+
const { terms } = placeholders(lotteryArgs(l, m));
|
|
262
|
+
const places = terms.places.reduce((n, p) => n + Number(p.times ?? 1), 0);
|
|
263
|
+
const tokens = terms.places.filter((p) => p.prize?.Tokens).length;
|
|
264
|
+
const pools = terms.places.filter((p) => p.prize?.Tokens?.from.Pool).length;
|
|
265
|
+
const own = terms.places.filter((p) => p.prize?.Held || p.prize === 'Raffled' || p.prize?.Raffled !== undefined).reduce((n, p) => n + Number(p.times ?? 1), 0);
|
|
266
|
+
const termsBytes = encodeBorsh(IDL, { defined: { name: 'LotteryTerms' } }, terms).length;
|
|
267
|
+
// The tag; project, lottery, id, incarnation, round, bump, opener, lent, head, status, revision; the terms; fee, sold,
|
|
268
|
+
// to_pot; common; owns, owed, the draw's slots and times, answer and first ticket, the pot's four numbers; paid; prizes (59
|
|
269
|
+
// bytes each); accounts (64 each); nfts; assigned.
|
|
270
|
+
const head = 1 + 130 + termsBytes + 17 + (4 + 8 * terms.shares.length) + 112 + (4 + Math.ceil((2 * places) / 8)) + (4 + 59 * tokens) + (4 + 64 * (2 + pools)) + (4 + 32 * own) + 4;
|
|
271
|
+
const payment = l.ticketCurrency ?? l.currency ?? m.currencyKey;
|
|
272
|
+
const accounts = (l.ticketNft ? 1 : payment === (l.currency ?? m.currencyKey) ? 1 : 2) + pools;
|
|
273
|
+
return { head, accounts };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** How many rounds of a lottery are open or still paying out at once, and how many purchases a round takes, as the
|
|
277
|
+
* spec expects them (`expect`): by default two rounds of a lottery that draws again (one selling, one paying out) and
|
|
278
|
+
* one of a lottery that draws once; a round's purchases from the scenario's purchases a day over a round, else 100. */
|
|
279
|
+
export function roundsExpected(l, m) {
|
|
280
|
+
const every = toSeconds(l.every ?? 0);
|
|
281
|
+
const perDay = Number(m.scenario?.perDay?.[`lottery:${l.key}`] ?? 0);
|
|
282
|
+
const days = every > 0 ? every / 86_400 : l.firstDraw ? Math.max(1, (toTime(l.firstDraw, m.now) - m.now) / 86_400) : 1;
|
|
283
|
+
const rounds = l.expect?.rounds ?? (every > 0 ? 2 : 1);
|
|
284
|
+
const entries = l.expect?.entries ?? (perDay > 0 ? Math.ceil(perDay * days) : 100);
|
|
285
|
+
return { rounds, entries };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* The SOL, in lamports, a lottery holds for its rounds' rent: the spec's `roundRent` (0 turns it off), or by default
|
|
290
|
+
* what its expected rounds take at once (`roundsExpected`), each its header, its token accounts and its expected
|
|
291
|
+
* entries' bytes (`LotteryRound.lent`: in a round the lottery opened, its spare SOL pays each entry while it covers
|
|
292
|
+
* them, so buyers pay no program rent for a ticket), at devnet's and mainnet's rent, rounded up to 0.001 SOL. Short
|
|
293
|
+
* of it (a busier round, a cluster that charges more), the first buyer fronts the header and later buyers pay their
|
|
294
|
+
* own entries. Each round gives back what it took when it closes; the lottery gives the rest back to the owner when it closes.
|
|
295
|
+
*/
|
|
296
|
+
export function roundRentOf(l, m) {
|
|
297
|
+
if (l.roundRent !== undefined) return toUnits(l.roundRent, 9);
|
|
298
|
+
const { head, accounts } = roundSize(l, m);
|
|
299
|
+
const { rounds, entries } = roundsExpected(l, m);
|
|
300
|
+
const width = loot.entryWidth(lotterySplit(l).some((s) => String(s.to).startsWith('chosen')), Boolean(l.ticketNft));
|
|
301
|
+
// The header and its entries' list head (6 bytes), each token account, each entry's bytes.
|
|
302
|
+
const one = rent(head + 6) + BigInt(accounts) * rent(165) + BigInt(entries * width) * RENT_PER_BYTE;
|
|
303
|
+
const all = BigInt(rounds) * one;
|
|
304
|
+
return ((all + 999_999n) / 1_000_000n) * 1_000_000n;
|
|
305
|
+
}
|
|
306
|
+
|
|
209
307
|
/**
|
|
210
308
|
* The args the authority opens a contest with, `at` (Unix seconds) its opening: its sides, when entries close and the
|
|
211
309
|
* result is due, its split, its gate (a list of wallets for a duel), the keys that may decide it, the least pot (an
|
|
@@ -232,8 +330,10 @@ export function contestArgs(c, m, at = m.now) {
|
|
|
232
330
|
prize: prizeOrNull(c.prize, m),
|
|
233
331
|
// A token prize set aside whole at the decision and split among the winners by what each put in.
|
|
234
332
|
prize_split: Boolean(c.prize?.split),
|
|
235
|
-
// Each entry stakes one NFT that passes this rule, and the NFTs are the pot
|
|
333
|
+
// Each entry stakes one NFT that passes this rule, and the NFTs are the pot: each winner shares the losers', at most
|
|
334
|
+
// `losersEach` of them (none: no cap).
|
|
236
335
|
nft: c.stakeNft ? ruleOf(c.stakeNft, m) : null,
|
|
336
|
+
cap: c.stakeNft && c.losersEach ? c.losersEach : null,
|
|
237
337
|
};
|
|
238
338
|
}
|
|
239
339
|
|
|
@@ -245,37 +345,42 @@ export function farmArgs(f, m) {
|
|
|
245
345
|
roll: t.roll ? m.box.get(t.roll)?.id ?? 0 : null,
|
|
246
346
|
// A token seat may leave before this tier's lock by leaving this share of what it takes out.
|
|
247
347
|
exit_bps: Math.round((t.exitFee ?? 0) * 100),
|
|
348
|
+
// How long a seat of this tier earns and grows from when its lock started (0: no end): a term.
|
|
349
|
+
earns_for: t.earnsFor ? toSeconds(t.earnsFor) : 0,
|
|
248
350
|
}));
|
|
249
351
|
// Every rule of its gate; with none, a farm of one item's collectibles takes that item.
|
|
250
352
|
const named = listOf(f.gate);
|
|
251
353
|
const gates = f.stakes !== 'collectibles' ? [] : (named.length ? named : f.item ? [{ item: f.item, attribute: 'item_id' }] : []).map((r) => ruleOf(r, m));
|
|
252
354
|
// A rate is per NFT_UNIT of weight: one collectible, or 1,000,000 base units of a staked token.
|
|
253
|
-
const rewards = m.
|
|
355
|
+
const rewards = m.tokens.get(f.rewardsToken);
|
|
254
356
|
const staked = f.stakes === 'collectibles' ? null : m.tokens.get(f.stakes);
|
|
255
357
|
const perDay = f.rate ? amount(f.rate.perDay, rewards) : 0n;
|
|
358
|
+
// A shared rate is the whole farm's a day, shared by weight; any other is for each unit of weight.
|
|
359
|
+
const shared = Boolean(f.rate?.shared);
|
|
256
360
|
return {
|
|
257
361
|
custody: { freeze: 'Freeze', watch: 'Watch' }[f.custody] ?? 'Escrow',
|
|
258
|
-
rewards_pool: m.pool.get(f.rewards)?.index ?? 0,
|
|
259
362
|
tiers,
|
|
260
363
|
boosts: (f.boosts ?? []).map((b) => ({ rule: ruleOf(b.when, m), effect: effectOf(b.add) })),
|
|
261
364
|
max_boost_bps: Math.round((f.maxBoostPercent ?? 0) * 100),
|
|
262
365
|
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) })),
|
|
366
|
+
rate: f.rate ? { per_day: staked && !shared ? (perDay * 1_000_000n) / 10n ** BigInt(staked.decimals ?? 0) : perDay, mints: Boolean(f.rate.minted), shared, halve_every: f.rate.halveEvery ? toSeconds(f.rate.halveEvery) : 0 } : null,
|
|
367
|
+
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), profile: false, at_least: 0n })),
|
|
265
368
|
// A farm that runs for a time ends that long after it goes live.
|
|
266
369
|
ends_at: f.runs ? BigInt(m.now + toSeconds(f.runs)) : 0n,
|
|
267
370
|
// The times it keeps on what is staked: its lock, and an upkeep.
|
|
268
371
|
lock_key: f.lockAttribute ?? '',
|
|
269
372
|
upkeep_key: f.upkeepAttribute ?? '',
|
|
270
|
-
// Where an early exit's fee goes
|
|
373
|
+
// Where an early exit's fee goes.
|
|
271
374
|
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
375
|
};
|
|
275
376
|
}
|
|
276
377
|
|
|
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] });
|
|
378
|
+
/** A fee's sink (`Sink`): "burn", "rewards" (the farm's own vault), or "pool:<key>", one of the project's pools. */
|
|
379
|
+
export const sinkOf = (to, m) => (to === 'burn' ? 'Burn' : to === 'rewards' ? 'Rewards' : { Pool: [m.pool.get(String(to).replace(/^pool:/, ''))?.index ?? 0] });
|
|
380
|
+
|
|
381
|
+
/** Whether a spec's farm keeps a record of when its seats stop earning (`loot.farmKeepsEnds`): a tier's term or an upkeep,
|
|
382
|
+
* and it pays something. Each different time waiting costs whoever writes it `loot.END_BYTES` of rent. */
|
|
383
|
+
export const farmEnds = (f) => ((f.tiers ?? []).some((t) => t.earnsFor) || Boolean(f.upkeepAttribute)) && (!f.rate || Number(f.rate.perDay) > 0);
|
|
279
384
|
|
|
280
385
|
/** An auction's args: its lot, reserve, least raise, times and split. "start" is when it goes live unless it says. */
|
|
281
386
|
export function auctionArgs(a, m) {
|
|
@@ -317,12 +422,14 @@ export function grantArgs(g, m) {
|
|
|
317
422
|
wallet: perWalletOf(l),
|
|
318
423
|
per_person: l.perPerson ?? 0,
|
|
319
424
|
per_key: l.perKey ?? 0,
|
|
320
|
-
|
|
321
|
-
total: l.total ?? 0,
|
|
425
|
+
// Every wallet's claims together: in all, and a day (a window of 86,400 seconds, from the unix epoch).
|
|
426
|
+
all: { total: l.total ?? 0, per_window: l.perDay ?? 0, window: l.perDay ? 86_400 : 0 },
|
|
322
427
|
window: windowOf({ opens: l.opens, closes: l.until }, m.now),
|
|
323
428
|
},
|
|
324
429
|
active: g.active !== false,
|
|
325
430
|
gate: gateOf(g.gate, m),
|
|
431
|
+
// Its records in 2^bits lists, the fewest that keep each under ~100,000 over its life (wallets × rounds).
|
|
432
|
+
shard_bits: loot.shardBitsFor(g.wallets ?? list.length ?? 0, g.rounds ?? 1),
|
|
326
433
|
};
|
|
327
434
|
}
|
|
328
435
|
|
|
@@ -330,7 +437,8 @@ export function grantArgs(g, m) {
|
|
|
330
437
|
|
|
331
438
|
/**
|
|
332
439
|
* 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
|
|
440
|
+
* wallet's token account for it, "@pot:<id>" with a lottery's pot, "@farm:<id>" with a farm's address and "@rewards:<id>"
|
|
441
|
+
* with a farm's rewards vault.
|
|
334
442
|
*/
|
|
335
443
|
function resolve(value, ctx) {
|
|
336
444
|
if (value === '@treasury') {
|
|
@@ -355,6 +463,10 @@ function resolve(value, ctx) {
|
|
|
355
463
|
if (!ctx.project) throw new Error('No project yet for a stake gate’s farm.');
|
|
356
464
|
return loot.pda.farm(ctx.project, Number(value.slice(6)));
|
|
357
465
|
}
|
|
466
|
+
if (typeof value === 'string' && value.startsWith('@rewards:')) {
|
|
467
|
+
if (!ctx.project) throw new Error('No project yet for a farm’s rewards vault.');
|
|
468
|
+
return loot.pda.rewards(loot.pda.farm(ctx.project, Number(value.slice(9))));
|
|
469
|
+
}
|
|
358
470
|
if (typeof value === 'string' && value.startsWith('@pot:')) {
|
|
359
471
|
if (!ctx.project) throw new Error('No project yet for a lottery’s pot.');
|
|
360
472
|
return loot.pda.pot(loot.pda.lottery(ctx.project, Number(value.slice(5))));
|
|
@@ -381,8 +493,8 @@ const placeholderKey = (name) => encodeBase58(sha256(utf8(`loot-plan:${name}`)))
|
|
|
381
493
|
* 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
494
|
* farms, auctions, then the seeds and the grants' funds. `only` limits it to these part ids (a change to a live economy).
|
|
383
495
|
*
|
|
384
|
-
* A farm opens before
|
|
385
|
-
* farm credits nothing at once, and releases what lands in its
|
|
496
|
+
* A farm opens before its seed lands in its rewards vault, and anyone may stake in between. That is safe: a sharing
|
|
497
|
+
* farm credits nothing at once, and releases what lands in its vault over its longest lock (a day at least) to the
|
|
386
498
|
* seats there as it is released, its clock stopped while nobody is in. A seed that lands
|
|
387
499
|
* before the first stake or after it is released the same way from that stake on, so seeding first would change nothing.
|
|
388
500
|
*/
|
|
@@ -474,6 +586,39 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
|
|
|
474
586
|
],
|
|
475
587
|
});
|
|
476
588
|
|
|
589
|
+
// The token accounts players' purchases would otherwise open (the owner pays their rent now, about 0.002 SOL each,
|
|
590
|
+
// so no player's purchase pays another wallet's): the treasury's of each coin the economy takes, the project's vault
|
|
591
|
+
// of each coin a box or an auction holds, and each fixed wallet's a split pays.
|
|
592
|
+
const accounts = goLiveAccounts(m);
|
|
593
|
+
if (fresh && accounts.length)
|
|
594
|
+
add({
|
|
595
|
+
id: 'accounts',
|
|
596
|
+
part: 'project',
|
|
597
|
+
says: `Open ${accounts.length} token account${accounts.length === 1 ? '' : 's'} the economy pays into (the treasury’s, the project’s vaults, the wallets its splits name), so no player’s purchase opens them`,
|
|
598
|
+
creates: accounts.map((x) => ({ what: `${x.who}’s account of ${m.tokens.get(x.token)?.symbol ?? x.token}`, bytes: 165 })),
|
|
599
|
+
build: (ctx) => accounts.map((x) => createAssociatedTokenAccountIdempotent(ctx.owner, x.who === 'the project' ? ctx.project : x.who === 'the treasury' ? ctx.treasury : x.who, ctx.mints[x.token], programsOf(ctx)[ctx.mints[x.token]] ?? TOKEN_PROGRAM)),
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
// The project's standing lookup table (the owner its authority): the addresses its transactions name most, so a
|
|
603
|
+
// transaction too big for a legacy one goes as v0 with it in one approval, never with a table of its own. Made with
|
|
604
|
+
// the first of its addresses, then extended a group a step.
|
|
605
|
+
if (fresh) {
|
|
606
|
+
const named = standingAddresses(m);
|
|
607
|
+
groupsOf(named, TABLE_GROUP).forEach((group, i) =>
|
|
608
|
+
add({
|
|
609
|
+
id: i ? `table:${i}` : 'table',
|
|
610
|
+
part: 'project',
|
|
611
|
+
says: i ? `Add ${group.length} more addresses to the project’s lookup table` : `Make the project’s lookup table of the ${named.length} addresses its transactions name most, so a large one takes one approval`,
|
|
612
|
+
creates: i ? [{ what: `room for ${group.length} addresses in the lookup table`, bytes: 32 * group.length, grows: true }] : [{ what: 'the project’s lookup table', bytes: 56 + 32 * group.length }],
|
|
613
|
+
build: (ctx) => {
|
|
614
|
+
// With no recent slot known (a plan built offline), there is no table to make.
|
|
615
|
+
if (ctx.slot == null) return [];
|
|
616
|
+
const { address: table, instruction } = createTableIx({ authority: ctx.owner, slot: ctx.slot });
|
|
617
|
+
return [...(i ? [] : [instruction]), extendTableIx({ table, authority: ctx.owner, addresses: group.map((at) => at(ctx)).filter(Boolean) })];
|
|
618
|
+
},
|
|
619
|
+
}));
|
|
620
|
+
}
|
|
621
|
+
|
|
477
622
|
for (const p of m.pools) {
|
|
478
623
|
const t = m.tokens.get(p.currency);
|
|
479
624
|
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] })] });
|
|
@@ -481,30 +626,57 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
|
|
|
481
626
|
const record = (part, def, says, kind, what, options = () => ({})) => {
|
|
482
627
|
const [variant] = Object.keys(def);
|
|
483
628
|
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 }
|
|
486
|
-
add({
|
|
629
|
+
// A lottery's first write opens its pot: a token account as well as the record (each round holds its own money).
|
|
630
|
+
const pot = kind === 'Lottery' ? [{ what: `${what}’s pot`, bytes: 165 }] : def.Schedule?.args.action.from.Vest ? [{ what: `${what}’s sealed vault`, bytes: 165 }] : [];
|
|
631
|
+
add({
|
|
632
|
+
id: `${part}:${kind}`, part, says, creates: [{ what, bytes }, ...pot],
|
|
633
|
+
build: (ctx) => [loot.upsertIx({ owner: ctx.owner, project: project(ctx), def: resolve(def, ctx), ...options(ctx) })],
|
|
634
|
+
});
|
|
487
635
|
};
|
|
488
|
-
|
|
636
|
+
// Each table, a batch of rows a transaction (`TABLE_BATCH_BYTES` of rows at most), sealed with its last batch.
|
|
637
|
+
for (const t of m.tables) {
|
|
638
|
+
const batches = tableBatches(t);
|
|
639
|
+
const len = t.kind === 'numbers' ? 8 : 40;
|
|
640
|
+
batches.forEach((rows, i) => {
|
|
641
|
+
const seal = i === batches.length - 1;
|
|
642
|
+
add({
|
|
643
|
+
id: `table:${t.key}:${i}`,
|
|
644
|
+
part: `table:${t.key}`,
|
|
645
|
+
says: `Write ${rows.length} row${rows.length === 1 ? '' : 's'} of the table “${t.key}”${seal ? ', and seal it: its weights never change from now on' : ''}`,
|
|
646
|
+
creates: i === 0 ? [{ what: `the table “${t.key}”, ${t.rows.length} rows`, bytes: loot.tableRowAt(len, t.rows.length - 1) + len }] : [],
|
|
647
|
+
build: (ctx) => [loot.upsertIx({ owner: ctx.owner, project: project(ctx), def: { Table: { id: t.id, args: { payload: t.kind === 'numbers' ? 'Number' : 'Label', rows, seal } } } })],
|
|
648
|
+
});
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
for (const it of m.items) record(`item:${it.key}`, { Item: { id: it.id, args: itemArgs(it, m) } }, `${it.active === false ? 'Switch off' : 'Write'} the item “${it.name}”`, 'Item', `the item “${it.name}”`);
|
|
489
652
|
// A box's line names its odds, so the owner sees what they approve (above all when a live box's odds change).
|
|
490
653
|
const odds = (b) => {
|
|
491
654
|
const ws = weights(b);
|
|
492
655
|
const total = ws.reduce((n, w) => n + w, 0) || 1;
|
|
493
656
|
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(' + ') ||
|
|
657
|
+
const what = (o) => outcomeParts(o).map(part).join(' + ') || 'nothing';
|
|
495
658
|
return (b.outcomes ?? []).map((o, i) => `${what(o)} ${Math.round((ws[i] / total) * 10_000) / 100}%`).join(', ');
|
|
496
659
|
};
|
|
497
660
|
const wasLive = new Set((live?.published?.boxes ?? []).map((b) => b.key));
|
|
498
|
-
|
|
661
|
+
// A box is written before any box that names it to roll (one may name itself): in that order.
|
|
662
|
+
const boxOrder = [];
|
|
663
|
+
const visit = (b, path = new Set()) => {
|
|
664
|
+
if (boxOrder.includes(b) || path.has(b)) return;
|
|
665
|
+
path.add(b);
|
|
666
|
+
for (const id of loot.defRolls({ Box: { args: boxArgs(b, m) } })) {
|
|
667
|
+
const named = m.allBoxes.find((x) => x.id === id);
|
|
668
|
+
if (named && named !== b) visit(named, path);
|
|
669
|
+
}
|
|
670
|
+
boxOrder.push(b);
|
|
671
|
+
};
|
|
672
|
+
for (const b of m.allBoxes) visit(b);
|
|
673
|
+
for (const b of boxOrder) {
|
|
499
674
|
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
675
|
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
676
|
}
|
|
502
677
|
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
678
|
for (const s of m.schedules) {
|
|
504
|
-
|
|
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) } : {}));
|
|
679
|
+
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}”`);
|
|
508
680
|
// A sealed schedule's vault is funded once, at go-live: from the owner's wallet, minted, or from a pool.
|
|
509
681
|
const id = `fund:${s.key}`;
|
|
510
682
|
if (!s.sealed || !Number(s.fund ?? 0) || (live?.landed ?? []).includes(id) || live?.published?.schedules?.some((x) => x.key === s.key)) continue;
|
|
@@ -532,29 +704,32 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
|
|
|
532
704
|
});
|
|
533
705
|
}
|
|
534
706
|
for (const l of m.lotteries) {
|
|
535
|
-
// A raffle's
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
707
|
+
// A raffle's NFTs go into the lottery's own stash first, several a call: the lottery is written naming NFTs already
|
|
708
|
+
// there, or drawing from as many as its places.
|
|
709
|
+
const raffled = [...(l.places ?? []).map(placeOf).filter((p) => p.prize?.held).map((p) => ({ nft: p.prize.nft, collection: p.prize.collection })), ...(l.nfts ?? []).map((n) => (typeof n === 'string' ? { nft: n } : n))];
|
|
710
|
+
const opened = live?.published?.lotteries?.some((x) => x.key === l.key);
|
|
711
|
+
groupsOf(raffled.filter((n) => !(live?.landed ?? []).includes(`stash:lottery-${l.key}:${n.nft}`)), STASH_GROUP).forEach((group, i) => {
|
|
712
|
+
if (opened) return;
|
|
539
713
|
add({
|
|
540
|
-
id,
|
|
714
|
+
id: `stash:lottery-${l.key}:${group.map((n) => n.nft).join(',')}`,
|
|
541
715
|
part: `lottery:${l.key}`,
|
|
542
|
-
says: `Put your NFT ${
|
|
543
|
-
creates: [{ what: `the lottery “${l.name}”’s stash`, bytes:
|
|
544
|
-
build: (ctx) => [loot.stashIx({ signer: ctx.owner, project: project(ctx), lottery: l.id, asset:
|
|
716
|
+
says: `Put your NFT${group.length === 1 ? ` ${group[0].nft}` : `s ${group.map((n) => n.nft).join(', ')}`} in the lottery “${l.name}”: ${group.length === 1 ? 'it stays' : 'they stay'} there until ${group.length === 1 ? 'its winner takes it' : 'their winners take them'}, and nobody, you included, can take ${group.length === 1 ? 'it' : 'them'} out before`,
|
|
717
|
+
creates: i === 0 ? [{ what: `the lottery “${l.name}”’s stash`, bytes: stashBytes(raffled.length) }] : [],
|
|
718
|
+
build: (ctx) => [loot.stashIx({ signer: ctx.owner, project: project(ctx), lottery: l.id, asset: group[0].nft, collection: group[0].collection ?? ctx.collection, more: group.slice(1).map((n) => ({ asset: n.nft, collection: n.collection ?? ctx.collection })) })],
|
|
545
719
|
});
|
|
546
|
-
}
|
|
720
|
+
});
|
|
547
721
|
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
722
|
}
|
|
549
723
|
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}”`);
|
|
724
|
+
/** The boxes a farm's tiers roll, tier by tier (its roll, then a prize that is a roll). */
|
|
550
725
|
for (const f of m.farms) {
|
|
551
726
|
if (live?.farms?.includes(f.key)) continue;
|
|
552
|
-
const rewards = m.
|
|
727
|
+
const rewards = m.tokens.get(f.rewardsToken);
|
|
553
728
|
add({
|
|
554
729
|
id: `farm:${f.key}`,
|
|
555
730
|
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
|
|
557
|
-
creates: [{ what: `the farm “${f.name}”`, bytes:
|
|
731
|
+
says: `Open the farm “${f.name}”: stake ${f.stakes === 'collectibles' ? 'collectibles' : m.tokens.get(f.stakes)?.symbol}, earn ${rewards?.symbol}${f.rate?.minted ? ', minted' : ' from its own rewards vault'}`,
|
|
732
|
+
creates: [{ what: `the farm “${f.name}”`, bytes: farmBytes(f, m) }, { what: 'its rewards vault', bytes: 165 }, ...(f.stakes === 'collectibles' ? [] : [{ what: 'its stake vault', bytes: 165 }]), ...(farmEnds(f) ? [{ what: 'its record of when seats stop earning', bytes: loot.ENDS_HEAD_BYTES }] : [])],
|
|
558
733
|
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
734
|
});
|
|
560
735
|
}
|
|
@@ -577,29 +752,34 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
|
|
|
577
752
|
// What is in a live stash already: what its last publish listed, and the steps of this one that landed.
|
|
578
753
|
const stashed = new Set([
|
|
579
754
|
...(live?.published?.stashes ?? []).flatMap((s) => stashNfts(s).map((n) => `${s.key}:${n.asset}`)),
|
|
580
|
-
|
|
755
|
+
// A step puts in a group of NFTs: `stash:<key>:<asset>,<asset>…`.
|
|
756
|
+
...(live?.landed ?? []).filter((id) => id.startsWith('stash:')).flatMap((id) => {
|
|
757
|
+
const [key, assets = ''] = id.slice(6).split(/:(.*)/s);
|
|
758
|
+
return assets.split(',').map((asset) => `${key}:${asset}`);
|
|
759
|
+
}),
|
|
581
760
|
]);
|
|
582
761
|
for (const s of m.stashes) {
|
|
583
762
|
const nfts = stashNfts(s);
|
|
584
763
|
const putting = nfts.filter((n) => n.kind !== 'compressed' && !stashed.has(`${s.key}:${n.asset}`));
|
|
585
764
|
// 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
765
|
const fresh = !nfts.some((n) => stashed.has(`${s.key}:${n.asset}`));
|
|
587
|
-
|
|
766
|
+
// Several NFTs a call, each with its weight: Core NFTs a group of `STASH_GROUP`, Token Metadata ones two (each takes
|
|
767
|
+
// more accounts).
|
|
768
|
+
const kindOf = (n) => ({ asset: n.asset, weight: n.weight, collection: n.kind === 'core' ? n.collection : undefined, tokenMetadata: n.kind === 'token-metadata' ? { programmable: n.programmable, ruleSet: n.ruleSet ?? undefined } : undefined });
|
|
769
|
+
const groups = [...groupsOf(putting.filter((n) => n.kind === 'core'), STASH_GROUP), ...groupsOf(putting.filter((n) => n.kind !== 'core'), 2)];
|
|
770
|
+
for (const [i, group] of groups.entries()) {
|
|
588
771
|
add({
|
|
589
|
-
id: `stash:${s.key}:${n.asset}`,
|
|
772
|
+
id: `stash:${s.key}:${group.map((n) => n.asset).join(',')}`,
|
|
590
773
|
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:
|
|
593
|
-
build: (ctx) =>
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
tokenMetadata: n.kind === 'token-metadata' ? { programmable: n.programmable, ruleSet: n.ruleSet ?? undefined } : undefined,
|
|
601
|
-
}),
|
|
602
|
-
],
|
|
774
|
+
says: `Put your NFT${group.length === 1 ? ` ${group[0].asset}` : `s ${group.map((n) => n.asset).join(', ')}`} in the stash “${s.name}”: the project holds ${group.length === 1 ? 'it' : 'them'} until a prize or a roll gives ${group.length === 1 ? 'it' : 'them'} away`,
|
|
775
|
+
creates: i > 0 ? [] : fresh ? [{ what: `the stash “${s.name}”, with ${putting.length} NFT${putting.length === 1 ? '' : 's'}`, bytes: stashBytes(putting.length) }] : [{ what: `room for ${putting.length} more NFT${putting.length === 1 ? '' : 's'} in “${s.name}”`, bytes: 36 * putting.length, grows: true }],
|
|
776
|
+
build: (ctx) => {
|
|
777
|
+
const [first, ...more] = group.map((n) => {
|
|
778
|
+
const k = kindOf(n);
|
|
779
|
+
return { ...k, collection: n.kind === 'core' ? k.collection ?? ctx.collection : undefined };
|
|
780
|
+
});
|
|
781
|
+
return [loot.stashIx({ signer: ctx.owner, project: project(ctx), id: s.id, ...first, more })];
|
|
782
|
+
},
|
|
603
783
|
});
|
|
604
784
|
}
|
|
605
785
|
}
|
|
@@ -621,7 +801,8 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
|
|
|
621
801
|
: a.lot?.roll
|
|
622
802
|
? `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
803
|
: `Open the auction “${a.name}” of one new “${m.item.get(a.lot?.item)?.name ?? a.lot?.item}” (${terms})`,
|
|
624
|
-
|
|
804
|
+
// An auction of NFT bids is made with room for the most a bid holds (`MAX_BID_NFTS`), its seller's rent.
|
|
805
|
+
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, pin: 0, reserve: 0n, min_raise_bps: 0, starts_at: 0n, ends_at: 0n, extend: 0, shares: args.shares, gate: args.gate, nft_bids: placeholders(args).nft_bids, high_bidder: NOBODY, high: 0n, last_bid_at: 0n, proceeds_owed: 0n, held: [] }) + (args.nft_bids ? 32 * loot.MAX_BID_NFTS : 0) }],
|
|
625
806
|
build: (ctx) => {
|
|
626
807
|
const seller = auctionSeller(a, ctx);
|
|
627
808
|
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) })];
|
|
@@ -655,6 +836,24 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
|
|
|
655
836
|
});
|
|
656
837
|
}
|
|
657
838
|
}
|
|
839
|
+
// A farm's seed: the owner puts it straight into the farm's rewards vault, once the farm is open.
|
|
840
|
+
for (const f of m.farms) {
|
|
841
|
+
if (!(Number(f.seed ?? 0) > 0) || live?.seeded?.includes(`farm:${f.key}`)) continue;
|
|
842
|
+
const t = m.tokens.get(f.rewardsToken);
|
|
843
|
+
const units = amount(f.seed, t);
|
|
844
|
+
add({
|
|
845
|
+
id: `seed:farm:${f.key}`,
|
|
846
|
+
part: `farm:${f.key}`,
|
|
847
|
+
says: `Put ${f.seed} ${t.symbol} into the rewards of “${f.name}”`,
|
|
848
|
+
build: (ctx) => {
|
|
849
|
+
const vault = loot.pda.rewards(loot.pda.farm(project(ctx), f.id));
|
|
850
|
+
return [
|
|
851
|
+
...(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))] : []),
|
|
852
|
+
loot.payIx({ player: ctx.owner, project: project(ctx), mint: ctx.mints[t.key], treasury: ctx.treasury, amount: units, shares: [{ to: { Account: [vault] }, bps: 10_000 }], reference: `seed ${f.key}`, programs: programsOf(ctx) }),
|
|
853
|
+
];
|
|
854
|
+
},
|
|
855
|
+
});
|
|
856
|
+
}
|
|
658
857
|
// A lottery's seed: the owner puts it straight into the lottery's pot.
|
|
659
858
|
for (const l of m.lotteries) {
|
|
660
859
|
if (!(Number(l.seed ?? 0) > 0) || live?.seeded?.includes(`lottery:${l.key}`)) continue;
|
|
@@ -673,16 +872,72 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
|
|
|
673
872
|
},
|
|
674
873
|
});
|
|
675
874
|
}
|
|
875
|
+
// SOL a lottery holds for its rounds' rent (`roundRentOf`: the spec's `roundRent`, or by default what its expected
|
|
876
|
+
// rounds and their entries take): each round borrows it at its first purchase (its header and payment account), each
|
|
877
|
+
// purchase's entry, and its ask (the pot's and pools' accounts), back at its close. A lottery live already keeps what
|
|
878
|
+
// it holds unless the spec names a `roundRent` to add.
|
|
879
|
+
for (const l of m.lotteries) {
|
|
880
|
+
const lamports = roundRentOf(l, m);
|
|
881
|
+
if (lamports <= 0n || (l.roundRent === undefined && (live?.published?.lotteries ?? []).some((x) => x.key === l.key))) continue;
|
|
882
|
+
const sol = Number(lamports) / 1e9;
|
|
883
|
+
add({ id: `rent:lottery:${l.key}`, part: `lottery:${l.key}`, says: `Put ${sol} SOL in “${l.name}” for its rounds’ rent and its tickets’ entries${l.roundRent === undefined ? ` (sized for ${roundsExpected(l, m).rounds} round${roundsExpected(l, m).rounds === 1 ? '' : 's'} of ${roundsExpected(l, m).entries} purchases at once)` : ''}, so no buyer pays program rent; each round gives it back when it closes`, build: (ctx) => [transferLamports(ctx.owner, loot.pda.lottery(project(ctx), l.id), lamports)] });
|
|
884
|
+
}
|
|
676
885
|
for (const g of m.grants) {
|
|
677
886
|
if (!(Number(g.fund ?? 0) > 0) || live?.funded?.includes(g.key)) continue;
|
|
678
887
|
const lamports = toUnits(g.fund, 9);
|
|
679
888
|
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
889
|
}
|
|
681
|
-
|
|
890
|
+
// The authority's steps go last: the owner approves theirs in one batch, the authority theirs in another.
|
|
891
|
+
return { steps: [...steps.filter((x) => !x.signers.includes('authority')), ...steps.filter((x) => x.signers.includes('authority'))], model: m };
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/** The token accounts going live opens (`setupSteps`): [{ who ('the treasury', 'the project' or a wallet), token }], each
|
|
895
|
+
* once, for the coins the economy takes in. */
|
|
896
|
+
export function goLiveAccounts(m) {
|
|
897
|
+
const out = new Map();
|
|
898
|
+
const add = (who, token) => token && out.set(`${who}|${token}`, { who, token });
|
|
899
|
+
const takes = (part, token = part.currency ?? m.currencyKey) => {
|
|
900
|
+
add('the treasury', token);
|
|
901
|
+
for (const s of sharesOf(part.split)) if (s.kind === 'wallet') add(s.key, token);
|
|
902
|
+
};
|
|
903
|
+
for (const b of m.allBoxes) takes(b), add('the project', b.currency ?? m.currencyKey);
|
|
904
|
+
for (const p of m.payments) takes(p);
|
|
905
|
+
for (const l of m.lotteries) if (!l.ticketNft) takes(l, l.ticketCurrency ?? l.currency ?? m.currencyKey);
|
|
906
|
+
for (const a of m.auctions) if (!a.bidNfts) takes(a), add('the project', a.currency ?? m.currencyKey);
|
|
907
|
+
return [...out.values()];
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/** How many addresses one step adds to the project's lookup table (each extension its own transaction). */
|
|
911
|
+
const TABLE_GROUP = 24;
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* The addresses a project's transactions name most, for its standing lookup table: loot's own fixed accounts (the
|
|
915
|
+
* platform, the oracle's), the project and its fee box, its treasury and collections, each coin's mint, its vault and
|
|
916
|
+
* the treasury's account of it, then each pool, item, box and stash. Each a function of the go-live context.
|
|
917
|
+
*/
|
|
918
|
+
export function standingAddresses(m) {
|
|
919
|
+
const fixed = [loot.LOOT_PROGRAM, loot.pda.platform(), loot.pda.identity(), loot.VRF_PROGRAM, loot.VRF_QUEUE, loot.MPL_CORE_PROGRAM, TOKEN_PROGRAM, TOKEN_2022_PROGRAM, ASSOCIATED_TOKEN_PROGRAM, SYSTEM_PROGRAM, SYSVAR_SLOT_HASHES, SYSVAR_INSTRUCTIONS, MEMO_PROGRAM].map((a) => () => a);
|
|
920
|
+
const programOf = (ctx, key) => programsOf(ctx)[ctx.mints[key]] ?? TOKEN_PROGRAM;
|
|
921
|
+
return [
|
|
922
|
+
...fixed,
|
|
923
|
+
(ctx) => ctx.project, (ctx) => loot.pda.fees(ctx.project), (ctx) => ctx.treasury,
|
|
924
|
+
...(m.collection ? [(ctx) => ctx.collection] : []),
|
|
925
|
+
...[...m.tokens.values()].flatMap((t) => [(ctx) => ctx.mints[t.key], (ctx) => ctx.mints[t.key] && loot.ata(ctx.project, ctx.mints[t.key], programOf(ctx, t.key)), (ctx) => ctx.mints[t.key] && loot.ata(ctx.treasury, ctx.mints[t.key], programOf(ctx, t.key))]),
|
|
926
|
+
...m.pools.map((p) => (ctx) => loot.pda.pool(ctx.project, p.index)),
|
|
927
|
+
...m.items.map((it) => (ctx) => loot.pda.item(ctx.project, it.id)),
|
|
928
|
+
...m.allBoxes.map((b) => (ctx) => loot.pda.box(ctx.project, b.id)),
|
|
929
|
+
...m.stashes.map((s) => (ctx) => loot.pda.stash(ctx.project, s.id)),
|
|
930
|
+
].slice(0, 256);
|
|
682
931
|
}
|
|
683
932
|
|
|
684
|
-
/**
|
|
685
|
-
|
|
933
|
+
/** How many Core NFTs one `stash` call puts in (`Piece::another`: three accounts each after the first). */
|
|
934
|
+
const STASH_GROUP = 6;
|
|
935
|
+
/** `list` in groups of `n`. */
|
|
936
|
+
const groupsOf = (list, n) => Array.from({ length: Math.ceil(list.length / n) }, (_, i) => list.slice(i * n, i * n + n));
|
|
937
|
+
|
|
938
|
+
/** A stash's bytes with `n` NFTs: a table of NFTs (`table.rs`): its 51-byte header, a total for each group of 4,096
|
|
939
|
+
* rows and each block of 64, and 36 bytes an NFT (its weight and its address). */
|
|
940
|
+
export const stashBytes = (n) => (n ? loot.tableRowAt(36, n - 1) + 36 : 51);
|
|
686
941
|
|
|
687
942
|
/** 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
943
|
export const auctionSeller = (a, ctx) => (a.lot?.asset ? ctx.owner : ctx.authority);
|
|
@@ -690,17 +945,24 @@ export const auctionSeller = (a, ctx) => (a.lot?.asset ? ctx.owner : ctx.authori
|
|
|
690
945
|
export const recordBytesOf = (kind, body) => {
|
|
691
946
|
const args = body.args;
|
|
692
947
|
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 }),
|
|
948
|
+
Item: () => ({ project: NOBODY, item_id: 0, ...placeholders(args), issued: 0, voided: 0, compressed: false }),
|
|
949
|
+
LootBox: () => ({ project: NOBODY, box_id: 0, ...placeholders(args), in_flight: 0n, version: 0 }),
|
|
695
950
|
Recipe: () => ({ project: NOBODY, id: 0, version: 0, ...placeholders(args) }),
|
|
696
951
|
Schedule: () => ({ project: NOBODY, id: 0, ...placeholders(args), runs: 0 }),
|
|
697
|
-
|
|
698
|
-
|
|
952
|
+
// Its terms and its round number: each round keeps the rest in a record of its own.
|
|
953
|
+
Lottery: () => ({ project: NOBODY, id: 0, incarnation: 0n, revision: 0, ...placeholders(loot.withDefaults({ Lottery: { args } }).Lottery.args), round: 0 }),
|
|
954
|
+
Grant: () => ({ project: NOBODY, id: 0, incarnation: 0n, ...placeholders(args), counted: { count: 0, window: 0, in_window: 0 } }),
|
|
699
955
|
}[kind];
|
|
700
956
|
return encodedSize(kind, base());
|
|
701
957
|
};
|
|
958
|
+
/** A farm's bytes as `open_farm` writes it: its args and its numbers. */
|
|
959
|
+
export const farmBytes = (f, m) => encodedSize('Farm', {
|
|
960
|
+
project: NOBODY, id: 0, farmed: f.stakes === 'collectibles' ? 'Collectibles' : { Tokens: { mint: NOBODY } }, ...placeholders(farmArgs(f, m)), rewards_mint: NOBODY,
|
|
961
|
+
rate_carry: 0n, opened_at: 0n, total_weight: 0n, reward_per_weight: 0n, reward_remainder: 0n, owed: 0n, clock: 0n, unreleased: 0n, release_ends: 0n, release_from: 0n, release_base: 0n,
|
|
962
|
+
keeps_ends: farmEnds(f),
|
|
963
|
+
});
|
|
702
964
|
/** 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,
|
|
965
|
+
export const contestBytes = (contest, m) => encodedSize('Contest', { project: NOBODY, id: 0, incarnation: 0n, opener: NOBODY, mint: NOBODY, decided_at: 0n, ...placeholders(contestArgs(contest, m)), verdict: 'Open', winner: 0, owed: 0n, unclaimed: 0n, sides: Array(contest.sides).fill(0n), share: 0, extras: 0, picks: 0 });
|
|
704
966
|
/** Args with "@mint:" names standing in as addresses, for sizing. */
|
|
705
967
|
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
968
|
/** The token program of each Token-2022 mint a context names (the program's account lists name it). */
|
|
@@ -725,6 +987,7 @@ export function placeholderContext(m, { rent = (bytes) => (BigInt(bytes) + ACCOU
|
|
|
725
987
|
mints,
|
|
726
988
|
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
989
|
rent,
|
|
990
|
+
slot: 0n,
|
|
728
991
|
};
|
|
729
992
|
}
|
|
730
993
|
|
|
@@ -791,8 +1054,11 @@ export function pack(steps, ctx) {
|
|
|
791
1054
|
const sizeOf = (instructions) => stepBytes(instructions, ctx);
|
|
792
1055
|
for (const step of steps) {
|
|
793
1056
|
const instructions = step.build(ctx);
|
|
1057
|
+
// A step with nothing to send (the lookup table of a plan built with no recent slot) is left out.
|
|
1058
|
+
if (!instructions.length) continue;
|
|
794
1059
|
const signerKind = step.signers.includes('authority') ? 'authority' : 'owner';
|
|
795
|
-
|
|
1060
|
+
// A write that names boxes to roll takes all the compute a transaction has: alone.
|
|
1061
|
+
if (current && current.signer === signerKind && !step.alone && !current.alone) {
|
|
796
1062
|
const next = [...current.instructions, ...instructions];
|
|
797
1063
|
if (sizeOf(next) <= BUDGET) {
|
|
798
1064
|
current.instructions = next;
|
|
@@ -805,7 +1071,7 @@ export function pack(steps, ctx) {
|
|
|
805
1071
|
const size = sizeOf(instructions);
|
|
806
1072
|
if (size > BUDGET) throw new Error(`The step “${step.says}” is ${size} bytes: too big for one transaction.`);
|
|
807
1073
|
// `built`: each step's own instructions, in the order of `steps`.
|
|
808
|
-
current = { signer: signerKind, steps: [step], built: [instructions], instructions, signers: [...step.signers] };
|
|
1074
|
+
current = { signer: signerKind, steps: [step], built: [instructions], instructions, signers: [...step.signers], alone: Boolean(step.alone) };
|
|
809
1075
|
transactions.push(current);
|
|
810
1076
|
}
|
|
811
1077
|
for (const t of transactions) t.bytes = sizeOf(t.instructions);
|
|
@@ -828,7 +1094,8 @@ export function plan(economy, { now = Math.floor(Date.now() / 1000), ctx = null,
|
|
|
828
1094
|
warnings: checked.warnings,
|
|
829
1095
|
steps,
|
|
830
1096
|
transactions,
|
|
831
|
-
|
|
1097
|
+
// A wallet approves up to PER_APPROVAL transactions at once: the owner's, then the authority's (its steps go last).
|
|
1098
|
+
approvals: Math.max(1, Math.ceil(transactions.filter((t) => t.signer === 'owner').length / PER_APPROVAL)) + Math.ceil(transactions.filter((t) => t.signer === 'authority').length / PER_APPROVAL),
|
|
832
1099
|
freshKeys: [...new Set(steps.flatMap((s) => s.signers.filter((x) => x.startsWith('new:'))))],
|
|
833
1100
|
};
|
|
834
1101
|
}
|
|
@@ -844,35 +1111,36 @@ export function playSteps(economy) {
|
|
|
844
1111
|
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
1112
|
}
|
|
846
1113
|
for (const b of m.boxes) {
|
|
847
|
-
if (boxBuys(b)) {
|
|
1114
|
+
if (boxBuys(b, m)) {
|
|
848
1115
|
add(`box:${b.key}`, 'buy', 'player', `buy “${b.name}”: what it gives, at once`);
|
|
849
1116
|
continue;
|
|
850
1117
|
}
|
|
851
1118
|
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', '
|
|
1119
|
+
if (boxRolls(b, m)) add(`box:${b.key}`, 'callback_roll', 'the oracle', 'roll it');
|
|
1120
|
+
add(`box:${b.key}`, 'settle', 'the player, a key they named, or the project’s keys', 'carry out what it rolled');
|
|
854
1121
|
}
|
|
855
1122
|
for (const p of m.payments) add(`payment:${p.key}`, 'pay', 'player', `pay “${p.name}”`);
|
|
856
1123
|
add('payments', 'pay_nft', 'player', 'pay with an NFT (burned, or given to a wallet): the receipt is the record');
|
|
857
1124
|
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
1125
|
for (const r of prizeRolls(m).filter((r) => !['mission', 'recipe'].includes(r.route))) {
|
|
859
|
-
add(r.by, 'ask_roll', '
|
|
860
|
-
add(r.by, 'settle', '
|
|
1126
|
+
add(r.by, 'ask_roll', 'the winner or the project’s keys, who pay the oracle', `ask for the roll of “${m.box.get(r.box)?.name ?? r.box}” a winner got (${r.route})`);
|
|
1127
|
+
add(r.by, 'settle', 'the winner, a key they named, or the project’s keys', 'carry out what it rolled');
|
|
861
1128
|
}
|
|
862
1129
|
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', '
|
|
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}`, '
|
|
1130
|
+
for (const s of m.schedules) add(`schedule:${s.key}`, 'run_schedule', 'the project’s keys, or who it pays', `run “${s.name ?? s.key}” when it is due`);
|
|
1131
|
+
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}`, 'pull', 'anyone', 'pay one part: a refund, a place’s part of the pot or its prize, a share owed in common');
|
|
865
1132
|
for (const f of m.farms) {
|
|
866
1133
|
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
1134
|
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
1135
|
add(`farm:${f.key}`, 'harvest', 'player', 'collect rewards');
|
|
869
1136
|
add(`farm:${f.key}`, 'refresh_seat', 'anyone', 'read a seat’s boosts again');
|
|
1137
|
+
if (farmEnds(f)) add(`farm:${f.key}`, 'walk_farm', 'anyone', 'catch the farm up, in parts, when it has passed more end times than a move can walk');
|
|
870
1138
|
}
|
|
871
1139
|
for (const c of m.contests) {
|
|
872
1140
|
add(`contest:${c.key}`, 'open_contest', 'authority', `open “${c.name}”`);
|
|
873
1141
|
add(`contest:${c.key}`, 'enter', 'player', 'back a side');
|
|
874
|
-
add(`contest:${c.key}`, 'decide', 'authority (or anyone
|
|
875
|
-
add(`contest:${c.key}`, 'claim', 'anyone', 'pay an entry');
|
|
1142
|
+
add(`contest:${c.key}`, 'decide', 'authority (or anyone sending a decider’s verdict)', 'name the side that won, or call it off before its result is due');
|
|
1143
|
+
add(`contest:${c.key}`, 'claim', c.stakeNft || c.prize ? 'the winner or the project’s keys' : 'anyone', 'pay an entry');
|
|
876
1144
|
}
|
|
877
1145
|
for (const a of m.auctions) {
|
|
878
1146
|
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)`);
|