@xgamesfun/solana 0.20.1 → 0.25.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 +95 -52
- package/bin/xsolana.mjs +23 -8
- package/examples/blind-forge.json +2 -4
- package/examples/coin-flip-arena.json +4 -12
- package/examples/duel-arena.json +1 -4
- package/examples/elements-set.json +2 -9
- package/examples/expedition-guild.json +1 -8
- package/examples/fair-presale.json +1 -3
- package/examples/guild-missions.json +3 -10
- package/examples/halving-seasons.json +4 -9
- package/examples/hero-trials.json +15 -6
- package/examples/holder-den.json +38 -0
- package/examples/holders-drop.json +105 -0
- package/examples/ideas.json +788 -788
- package/examples/match-predictions.json +24 -0
- package/examples/merge-2048.json +5 -5
- package/examples/nft-raffle.json +1 -0
- package/examples/pack-league.json +108 -0
- package/examples/paper-hands-farm.json +4 -16
- package/examples/partner-vault.json +20 -7
- package/examples/sacrifice-raffle.json +2 -10
- package/examples/shelter-fair.json +1 -0
- package/examples/stakers-draw.json +4 -15
- package/examples/star-fleet.json +1 -8
- package/examples/starforce.json +6 -6
- package/examples/stride-sneakers.json +164 -0
- package/examples/trade-in-shop.json +2 -3
- package/js/approve/details.mjs +122 -52
- package/js/approve/ed25519.mjs +5 -1
- package/js/approve/live-check.mjs +83 -26
- package/js/approve/page.html +217 -78
- package/js/approve/page.js +43 -15
- package/js/approve/server.mjs +290 -73
- package/js/chain/base58.js +7 -1
- package/js/chain/borsh.js +24 -11
- package/js/chain/bytes.js +11 -8
- package/js/chain/core.js +35 -7
- package/js/chain/curve.js +1 -1
- package/js/chain/lookup.js +124 -36
- package/js/chain/mint.js +6 -2
- package/js/chain/pda.js +2 -2
- package/js/chain/programs.js +6 -6
- package/js/chain/rpc.js +44 -16
- package/js/chain/token.js +13 -2
- package/js/chain/token2022.js +55 -16
- package/js/chain/tx.js +32 -22
- package/js/client/accounts.js +653 -131
- package/js/client/compressed.js +4 -3
- package/js/client/grant.js +267 -83
- package/js/client/idl/loot.js +1 -1
- package/js/client/idl/loot.json +2406 -2176
- package/js/client/index.js +1 -0
- package/js/client/ix.js +3059 -664
- package/js/client/pda.js +39 -33
- package/js/client/policy.js +52 -24
- package/js/client/random.js +70 -0
- package/js/client/sizes.js +414 -16
- package/js/client/traits.js +14 -5
- package/js/connect/connect.mjs +59 -17
- package/js/economy/check.mjs +4153 -725
- package/js/economy/cost.mjs +403 -64
- package/js/economy/examples.mjs +176 -35
- package/js/economy/explain.mjs +466 -111
- package/js/economy/guide.mjs +296 -72
- package/js/economy/legal.mjs +12 -6
- package/js/economy/live.mjs +83 -19
- package/js/economy/model.mjs +462 -124
- package/js/economy/plan.mjs +1306 -219
- package/js/economy/schema.json +390 -484
- package/js/economy/secrets.mjs +17 -7
- package/js/economy/simulate.mjs +883 -223
- package/js/economy/txsize.mjs +15 -3
- package/js/economy/validate.mjs +61 -24
- package/js/mcp/server.mjs +42 -14
- package/js/mcp/tools.mjs +307 -88
- package/js/play/index.js +2864 -619
- package/js/play/keys.js +193 -0
- package/js/results/index.js +223 -30
- package/js/results/server.js +230 -0
- package/js/results/wire.js +64 -16
- package/package.json +6 -4
- package/release.json +5 -5
- package/skills/xsolana-economy/SKILL.md +39 -21
- package/skills/xsolana-economy/references/after-launch.md +58 -38
- package/skills/xsolana-economy/references/apps.md +97 -50
- package/skills/xsolana-economy/references/sentence-to-spec.md +148 -121
- package/skills/xsolana-economy/references/telling-the-person.md +61 -17
- package/types/chain/core.d.ts +4 -2
- package/types/chain/lookup.d.ts +5 -2
- package/types/chain/rpc.d.ts +3 -1
- package/types/client/accounts.d.ts +217 -56
- package/types/client/grant.d.ts +72 -27
- package/types/client/idl/loot.d.ts +602 -549
- package/types/client/index.d.ts +1 -0
- package/types/client/ix.d.ts +3884 -247
- package/types/client/pda.d.ts +22 -16
- 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/guide.d.mts +5 -0
- package/types/economy/live.d.mts +16 -1
- package/types/economy/model.d.mts +151 -38
- package/types/economy/plan.d.mts +237 -194
- package/types/economy/simulate.d.mts +21 -13
- package/types/mcp/tools.d.mts +21 -13
- package/types/play/index.d.ts +453 -53
- package/types/play/keys.d.ts +51 -0
- package/types/results/index.d.ts +7 -6
- package/types/results/server.d.ts +126 -0
- package/types/results/wire.d.ts +1 -1
package/js/economy/plan.mjs
CHANGED
|
@@ -7,16 +7,71 @@
|
|
|
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 {
|
|
13
|
+
import {
|
|
14
|
+
ASSOCIATED_TOKEN_PROGRAM,
|
|
15
|
+
NATIVE_MINT,
|
|
16
|
+
SYSTEM_PROGRAM,
|
|
17
|
+
SYSVAR_INSTRUCTIONS,
|
|
18
|
+
SYSVAR_SLOT_HASHES,
|
|
19
|
+
TOKEN_2022_PROGRAM,
|
|
20
|
+
TOKEN_PROGRAM,
|
|
21
|
+
} from '../chain/pda.js';
|
|
22
|
+
import { createTableIx, extendTableIx } from '../chain/lookup.js';
|
|
23
|
+
import { MEMO_PROGRAM } from '../chain/token2022.js';
|
|
12
24
|
import { createAssociatedTokenAccountIdempotent, syncNative, transferLamports } from '../chain/programs.js';
|
|
13
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
createAccountIx,
|
|
27
|
+
initializeMint2Ix,
|
|
28
|
+
MINT_BYTES,
|
|
29
|
+
mintToIx,
|
|
30
|
+
setAuthorityIx,
|
|
31
|
+
transferCheckedIx,
|
|
32
|
+
} from '../chain/token.js';
|
|
14
33
|
import { compileMessage, transactionSize } from '../chain/tx.js';
|
|
15
34
|
import { sha256 } from '../chain/sha256.js';
|
|
16
35
|
import { utf8 } from '../chain/bytes.js';
|
|
17
36
|
import * as loot from '../client/index.js';
|
|
18
37
|
import { check } from './check.mjs';
|
|
19
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
ACCOUNT_OVERHEAD,
|
|
40
|
+
boxBuys,
|
|
41
|
+
rent,
|
|
42
|
+
RENT_PER_BYTE,
|
|
43
|
+
boxRolls,
|
|
44
|
+
compressedTree,
|
|
45
|
+
destination,
|
|
46
|
+
effectOf,
|
|
47
|
+
gateOf,
|
|
48
|
+
listOf,
|
|
49
|
+
lotterySplit,
|
|
50
|
+
model,
|
|
51
|
+
MOST_RENT_PER_BYTE,
|
|
52
|
+
paysFromOf,
|
|
53
|
+
outcomeParts,
|
|
54
|
+
perWalletOf,
|
|
55
|
+
placeOf,
|
|
56
|
+
lifeLines,
|
|
57
|
+
prizeLine,
|
|
58
|
+
prizePool,
|
|
59
|
+
prizeRolls,
|
|
60
|
+
prizeToken,
|
|
61
|
+
rarityNumber,
|
|
62
|
+
ruleOf,
|
|
63
|
+
sharesOf,
|
|
64
|
+
spendOf,
|
|
65
|
+
stashNfts,
|
|
66
|
+
toBy,
|
|
67
|
+
toEnum,
|
|
68
|
+
toSeconds,
|
|
69
|
+
toTime,
|
|
70
|
+
toUnits,
|
|
71
|
+
weights,
|
|
72
|
+
wholeCount,
|
|
73
|
+
windowOf,
|
|
74
|
+
} from './model.mjs';
|
|
20
75
|
import { BUDGET } from './txsize.mjs';
|
|
21
76
|
|
|
22
77
|
const owner = 'owner';
|
|
@@ -24,17 +79,27 @@ const owner = 'owner';
|
|
|
24
79
|
export const PER_APPROVAL = 12;
|
|
25
80
|
|
|
26
81
|
const amount = (value, t) => toUnits(value ?? 0, t?.decimals ?? 0);
|
|
27
|
-
const shares = (split, m) => sharesOf(split).map(
|
|
28
|
-
const rise = (r, t) =>
|
|
29
|
-
|
|
30
|
-
const
|
|
82
|
+
const shares = (split, m) => sharesOf(split).map(s => ({ to: toEnum(s.to, m), bps: s.bps ?? 0 }));
|
|
83
|
+
const rise = (r, t) =>
|
|
84
|
+
r ? { step: amount(r.step ?? 0, t), growth_bps: Math.round((r.growthPercent ?? 0) * 100) } : null;
|
|
85
|
+
const fall = (f, t, now) =>
|
|
86
|
+
f
|
|
87
|
+
? {
|
|
88
|
+
step: amount(f.step, t),
|
|
89
|
+
every: toSeconds(f.every),
|
|
90
|
+
floor: amount(f.floor ?? 0, t),
|
|
91
|
+
since: BigInt(toTime(f.start, now)),
|
|
92
|
+
shared: f.clock === 'shared',
|
|
93
|
+
}
|
|
94
|
+
: null;
|
|
95
|
+
const tokenProgramOf = t => (t?.program === 'token-2022' ? TOKEN_2022_PROGRAM : TOKEN_PROGRAM);
|
|
31
96
|
|
|
32
97
|
// ---------------------------------------------------------------- spec parts → the program's args
|
|
33
98
|
|
|
34
|
-
export function itemArgs(it) {
|
|
99
|
+
export function itemArgs(it, m) {
|
|
35
100
|
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
|
-
|
|
101
|
+
// A collectible is minted into its own `collection`, or the one the setup makes ("@collection" until it exists). Its
|
|
102
|
+
// life is written into each recipe that takes it (`lifeLines`).
|
|
38
103
|
return {
|
|
39
104
|
kind,
|
|
40
105
|
rarity: rarityNumber(it.rarity),
|
|
@@ -43,19 +108,73 @@ export function itemArgs(it) {
|
|
|
43
108
|
name: it.name,
|
|
44
109
|
uri: it.uri ?? '',
|
|
45
110
|
max_supply: it.supply ?? 0,
|
|
46
|
-
life,
|
|
47
111
|
active: it.active !== false,
|
|
48
|
-
collection: kind === 'Normal' ? NOBODY : it.collection ?? '@collection',
|
|
112
|
+
collection: kind === 'Normal' ? NOBODY : (it.collection ?? '@collection'),
|
|
113
|
+
// A fresh collectible's traits, each drawn per unit (a table's row, or a number in a range).
|
|
114
|
+
traits:
|
|
115
|
+
kind === 'Normal'
|
|
116
|
+
? []
|
|
117
|
+
: (it.drawn ?? []).map(t => ({
|
|
118
|
+
key: t.attribute,
|
|
119
|
+
from:
|
|
120
|
+
t.table !== undefined
|
|
121
|
+
? { Table: [m?.table.get(t.table)?.id ?? 0] }
|
|
122
|
+
: { Range: { min: BigInt(t.min ?? 0), max: BigInt(t.max ?? 0) } },
|
|
123
|
+
})),
|
|
49
124
|
};
|
|
50
125
|
}
|
|
51
126
|
|
|
127
|
+
/**
|
|
128
|
+
* The bytes of an order of box `b` (`orderBytes`), its spec's names standing in as addresses, with the shares its split
|
|
129
|
+
* stores. `opened`: an open's (`open_box`, `Order::sized`: each draw at its dearest outcome's prizes as they are kept,
|
|
130
|
+
* rows only for the tables its fresh items draw); otherwise a roll made inside another call (a craft, a grant's claim,
|
|
131
|
+
* a mission, a lottery's place, a roll lot: `Order::space_for`, the box's whole table budget a draw).
|
|
132
|
+
*/
|
|
133
|
+
export const boxOrderBytes = (b, m, opens = 1, { opened = false } = {}) => {
|
|
134
|
+
const args = boxArgs(b, m);
|
|
135
|
+
const items = opened ? Object.fromEntries(m.items.map(it => [it.id, itemArgs(it, m)])) : undefined;
|
|
136
|
+
return loot.orderBytes(placeholders(args), (args.shares ?? []).length, opens, items);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/** The most bytes of rows one table write carries: what a 1,232-byte transaction leaves after the upsert's accounts,
|
|
140
|
+
* its signature and its own bytes. */
|
|
141
|
+
export const TABLE_BATCH_BYTES = 800;
|
|
142
|
+
|
|
143
|
+
/** A spec table's rows as the program takes them ({ weight, left, label }), in batches that fit a transaction each. */
|
|
144
|
+
export function tableBatches(t) {
|
|
145
|
+
const rows = t.rows.map(r => ({
|
|
146
|
+
weight: r.weight,
|
|
147
|
+
left: r.stock ?? 4294967295,
|
|
148
|
+
label: t.kind === 'numbers' ? '' : String(r.label ?? ''),
|
|
149
|
+
}));
|
|
150
|
+
const batches = [[]];
|
|
151
|
+
let bytes = 0;
|
|
152
|
+
for (const r of rows) {
|
|
153
|
+
const size = 12 + new TextEncoder().encode(r.label).length;
|
|
154
|
+
if (bytes + size > TABLE_BATCH_BYTES && batches.at(-1).length) (batches.push([]), (bytes = 0));
|
|
155
|
+
batches.at(-1).push(r);
|
|
156
|
+
bytes += size;
|
|
157
|
+
}
|
|
158
|
+
return batches;
|
|
159
|
+
}
|
|
160
|
+
|
|
52
161
|
/** The program's `Prize` for one part of a box outcome (`model.outcomeParts`). */
|
|
53
162
|
export function partPrize(o, m) {
|
|
163
|
+
// An item, or a compressed NFT of it, minted at the settle into the project's tree.
|
|
164
|
+
if (o.gives === 'item' && o.compressed) return { Compressed: { item: m.item.get(o.item)?.id ?? 0, tree: '@tree' } };
|
|
54
165
|
if (o.gives === 'item') return { Item: [m.item.get(o.item)?.id ?? 0] };
|
|
55
166
|
if (o.gives === 'stash') return { Stash: [m.stash.get(o.stash)?.id ?? 0] };
|
|
56
|
-
if (o.gives === 'pay')
|
|
57
|
-
|
|
58
|
-
|
|
167
|
+
if (o.gives === 'pay')
|
|
168
|
+
return {
|
|
169
|
+
Tokens: { from: { Pool: [m.pool.get(o.pool)?.index ?? 0] }, amount: amount(o.amount, m.poolToken(o.pool)) },
|
|
170
|
+
};
|
|
171
|
+
if (o.gives === 'mint')
|
|
172
|
+
return {
|
|
173
|
+
Tokens: {
|
|
174
|
+
from: { Mint: [`@mint:${o.currency ?? m.currencyKey}`] },
|
|
175
|
+
amount: amount(o.amount, m.tokens.get(o.currency ?? m.currencyKey)),
|
|
176
|
+
},
|
|
177
|
+
};
|
|
59
178
|
return prizeArgs(o, m);
|
|
60
179
|
}
|
|
61
180
|
|
|
@@ -68,60 +187,95 @@ export function boxArgs(b, m) {
|
|
|
68
187
|
rise: rise(b.rise, t),
|
|
69
188
|
fall: fall(b.fall, t, m.now),
|
|
70
189
|
stock_odds: !!b.stockOdds,
|
|
190
|
+
when_out: b.whenOut === 'drawLeft' ? 'DrawLeft' : 'StopSales',
|
|
71
191
|
active: b.active !== false,
|
|
72
192
|
shares: shares(b.split, m),
|
|
73
|
-
//
|
|
74
|
-
|
|
193
|
+
// What one open draws: each slot so many draws among the outcomes it names (all, when none), no outcome twice
|
|
194
|
+
// when distinct. None is one draw among every outcome.
|
|
195
|
+
slots: (b.slots ?? []).map(s => ({
|
|
196
|
+
count: s.count,
|
|
197
|
+
only: s.from ?? [],
|
|
198
|
+
distinct: Boolean(s.distinct),
|
|
199
|
+
share: Math.round((s.share ?? 0) * 100),
|
|
200
|
+
})),
|
|
201
|
+
// An entry gives its prizes (a bundle of any number), and may burn the payment in place of the split.
|
|
202
|
+
entries: (b.outcomes ?? []).map((o, i) => ({
|
|
203
|
+
prizes: outcomeParts(o).map(part => partPrize(part, m)),
|
|
204
|
+
weight: w[i],
|
|
205
|
+
})),
|
|
75
206
|
gate: gateOf(b.gate, m),
|
|
76
|
-
odds: (b.odds ?? []).map(
|
|
207
|
+
odds: (b.odds ?? []).map(o => ({ entry: o.outcome, boost: { rule: ruleOf(o.when, m), effect: effectOf(o.add) } })),
|
|
77
208
|
window: windowOf(b, m.now),
|
|
78
209
|
per_wallet: perWalletOf(b),
|
|
79
210
|
// A box that rolls on an NFT: the rule it must pass, and what each outcome does to it.
|
|
80
211
|
subject: b.rollsOn ? ruleOf(b.rollsOn, m) : null,
|
|
81
212
|
fates: (b.outcomes ?? []).flatMap((o, i) => {
|
|
82
|
-
const shifts = (o.shifts ?? []).map(
|
|
213
|
+
const shifts = (o.shifts ?? []).map(x => ({
|
|
214
|
+
key: x.attribute,
|
|
215
|
+
by: toBy(x.by),
|
|
216
|
+
most: toSeconds(x.upTo ?? 0),
|
|
217
|
+
from_now: Boolean(x.fromNow),
|
|
218
|
+
}));
|
|
83
219
|
// An outcome's own `then`, or the box's: an NFT taken as its price goes there whatever the roll gives.
|
|
84
220
|
const then = thenOf(o.then ?? b.then, m);
|
|
85
|
-
return shifts.length || then !==
|
|
221
|
+
return shifts.length || then !== null ? [{ entry: i, shifts, then }] : [];
|
|
86
222
|
}),
|
|
87
223
|
};
|
|
88
224
|
}
|
|
89
225
|
|
|
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. */
|
|
226
|
+
/** 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
227
|
export const thenOf = (then, m) => {
|
|
92
|
-
if (!then || then === 'back') return
|
|
228
|
+
if (!then || then === 'back') return null;
|
|
93
229
|
if (then === 'burn') return 'Burn';
|
|
94
230
|
if (then.stash) return { Stash: [m?.stash.get(then.stash)?.id ?? 0] };
|
|
95
231
|
return { Give: [then === 'treasury' ? '@treasury' : then.giveTo] };
|
|
96
232
|
};
|
|
97
233
|
|
|
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
234
|
export function recipeArgs(r, m) {
|
|
104
|
-
const tok =
|
|
105
|
-
const inputs = (r.in ?? []).map(
|
|
235
|
+
const tok = key => m.tokens.get(key ?? m.currencyKey);
|
|
236
|
+
const inputs = (r.in ?? []).map(l => {
|
|
106
237
|
if (l.takes === 'burn') return { Burn: { item: m.item.get(l.item)?.id ?? 0, count: l.count ?? 1 } };
|
|
107
238
|
if (l.takes === 'use') return { Use: { item: m.item.get(l.item)?.id ?? 0 } };
|
|
108
239
|
// 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')
|
|
110
|
-
|
|
240
|
+
if (l.takes === 'staked')
|
|
241
|
+
return { Staked: { item: m.item.get(l.item)?.id ?? 0, farm: m.farm.get(l.farm)?.id ?? 0 } };
|
|
242
|
+
// A renew keeps the collectible as a use does; its life's raise counts its uses from 0 again (`lifeLines`).
|
|
243
|
+
if (l.takes === 'renew') return { Use: { item: m.item.get(l.item)?.id ?? 0 } };
|
|
111
244
|
if (l.takes === 'show') return { Show: { collection: l.collection ?? '@collection' } };
|
|
112
245
|
// A trade-in: one NFT of `collection`, any kind, burned, given on, or put in a stash.
|
|
113
246
|
if (l.takes === 'trade') return { Trade: { collection: l.collection ?? '@collection', then: spendOf(l.then, m) } };
|
|
114
|
-
return {
|
|
247
|
+
return {
|
|
248
|
+
Pay: {
|
|
249
|
+
mint: `@mint:${l.currency ?? m.currencyKey}`,
|
|
250
|
+
amount: amount(l.amount, tok(l.currency)),
|
|
251
|
+
to: toEnum(l.to, m),
|
|
252
|
+
},
|
|
253
|
+
};
|
|
115
254
|
});
|
|
255
|
+
const life = lifeLines(r, m);
|
|
116
256
|
// A line is a prize (prize.rs), or a roll on a collectible it keeps (`on`).
|
|
117
|
-
const outputs = (r.out ?? []).map(
|
|
257
|
+
const outputs = (r.out ?? []).map(l =>
|
|
258
|
+
l.gives === 'roll' && l.on !== undefined
|
|
259
|
+
? { RollOn: { box_id: m.box.get(l.box)?.id ?? 0, place: l.on } }
|
|
260
|
+
: { Prize: [prizeArgs(l, m)] },
|
|
261
|
+
);
|
|
118
262
|
return {
|
|
119
263
|
active: r.active !== false,
|
|
120
264
|
inputs,
|
|
121
265
|
outputs,
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
266
|
+
// The recipe's own needs and raises, then what the life of each collectible it takes adds (`lifeLines`).
|
|
267
|
+
needs: [
|
|
268
|
+
...(r.needs ?? []).map(n => ({ place: n.place, key: n.key, test: { AtLeast: [n.atLeast ?? 0] } })),
|
|
269
|
+
...life.needs,
|
|
270
|
+
],
|
|
271
|
+
keeps: (r.keeps ?? []).map(k => ({ key: k.key, from: k.from, to: k.to })),
|
|
272
|
+
raises: [
|
|
273
|
+
...(r.raises ?? []).map(x => ({
|
|
274
|
+
place: x.place,
|
|
275
|
+
shift: { key: x.key, by: toBy(x.by), most: toSeconds(x.upTo ?? 0), from_now: Boolean(x.fromNow) },
|
|
276
|
+
})),
|
|
277
|
+
...life.raises,
|
|
278
|
+
],
|
|
125
279
|
window: windowOf(r, m.now),
|
|
126
280
|
per_wallet: perWalletOf(r),
|
|
127
281
|
};
|
|
@@ -129,11 +283,18 @@ export function recipeArgs(r, m) {
|
|
|
129
283
|
|
|
130
284
|
export function scheduleArgs(s, m) {
|
|
131
285
|
const every = toSeconds(s.every);
|
|
132
|
-
const poolOf =
|
|
286
|
+
const poolOf = key => m.pool.get(key)?.index ?? 0;
|
|
133
287
|
let action;
|
|
134
288
|
if (s.does === 'issue') {
|
|
135
|
-
|
|
136
|
-
|
|
289
|
+
// Minted into a pool, or into a farm's rewards vault ("to": "farm:<key>"): an emission its stakers share.
|
|
290
|
+
const farm = destination(s.to ?? '').kind === 'farm' ? m.farm.get(destination(s.to).key) : null;
|
|
291
|
+
const t = m.tokens.get(s.currency ?? (farm ? farm.rewardsToken : m.pool.get(s.pool)?.currency));
|
|
292
|
+
action = {
|
|
293
|
+
from: { Mint: [`@mint:${t?.key}`] },
|
|
294
|
+
to: farm ? toEnum(s.to, m) : { Pool: [poolOf(s.pool)] },
|
|
295
|
+
amount: { Fixed: [amount(s.amount, t)] },
|
|
296
|
+
halve_every: s.halveEvery ?? 0,
|
|
297
|
+
};
|
|
137
298
|
} else if (s.sealed) {
|
|
138
299
|
// A sealed schedule pays from its own vault, in `currency`.
|
|
139
300
|
const t = m.tokens.get(s.currency ?? m.currencyKey);
|
|
@@ -146,7 +307,13 @@ export function scheduleArgs(s, m) {
|
|
|
146
307
|
const how = s.percent !== undefined ? { Bps: [Math.round(s.percent * 100)] } : { Fixed: [amount(s.amount, t)] };
|
|
147
308
|
action = { from: { Pool: [poolOf(from)] }, to, amount: how, halve_every: s.halveEvery ?? 0 };
|
|
148
309
|
}
|
|
149
|
-
return {
|
|
310
|
+
return {
|
|
311
|
+
active: s.active !== false,
|
|
312
|
+
action,
|
|
313
|
+
every: BigInt(every),
|
|
314
|
+
next_at: BigInt(s.start ? toTime(s.start, m.now) : m.now + every),
|
|
315
|
+
max_runs: s.maxRuns ?? 0,
|
|
316
|
+
};
|
|
150
317
|
}
|
|
151
318
|
|
|
152
319
|
/**
|
|
@@ -155,16 +322,18 @@ export function scheduleArgs(s, m) {
|
|
|
155
322
|
*/
|
|
156
323
|
export function prizeArgs(p, m) {
|
|
157
324
|
const line = prizeLine(p);
|
|
158
|
-
if (line.gives === 'item' && line.compressed)
|
|
325
|
+
if (line.gives === 'item' && line.compressed)
|
|
326
|
+
return { Compressed: { item: m.item.get(line.item)?.id ?? 0, tree: '@tree' } };
|
|
159
327
|
if (line.gives === 'item') return { Item: [m.item.get(line.item)?.id ?? 0] };
|
|
160
328
|
// An NFT a lottery's place holds in the lottery's own stash (a raffle), or one of a project stash.
|
|
161
329
|
if (line.gives === 'nft' && line.held) return { Held: [line.nft] };
|
|
330
|
+
// An NFT drawn at random from the lottery's own stash (a raffle of many NFTs).
|
|
331
|
+
if (line.gives === 'raffled') return 'Raffled';
|
|
162
332
|
if (line.gives === 'nft') return { Nft: { stash: m.stash.get(line.stash)?.id ?? 0, nft: line.nft } };
|
|
163
333
|
if (line.gives === 'roll') return { Roll: [m.box.get(line.box)?.id ?? 0] };
|
|
164
334
|
const pool = prizePool(line);
|
|
165
335
|
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))] } } };
|
|
336
|
+
return { Tokens: { from, amount: amount(line.amount, prizeToken(line, m)) } };
|
|
168
337
|
}
|
|
169
338
|
|
|
170
339
|
/**
|
|
@@ -172,14 +341,20 @@ export function prizeArgs(p, m) {
|
|
|
172
341
|
* its own amount (whole units of `t`, the grant's token) and round, so one wallet listed under several rounds claims
|
|
173
342
|
* each once (monthly vesting, a season's airdrop).
|
|
174
343
|
*/
|
|
175
|
-
export const listLeaves = (list, t) =>
|
|
344
|
+
export const listLeaves = (list, t) =>
|
|
345
|
+
list.map(e =>
|
|
346
|
+
typeof e === 'string'
|
|
347
|
+
? loot.listLeaf(e, null)
|
|
348
|
+
: loot.listLeaf(e.wallet, null, loot.listRound(e.round ?? 0), e.amount !== undefined ? amount(e.amount, t) : 0n),
|
|
349
|
+
);
|
|
176
350
|
|
|
177
351
|
/** A prize line's `Prize`, or null for none. */
|
|
178
352
|
const prizeOrNull = (p, m) => (prizeLine(p) ? prizeArgs(p, m) : null);
|
|
179
353
|
|
|
180
354
|
/**
|
|
181
|
-
* A lottery's args: its
|
|
182
|
-
* `cutPercent` to the treasury) with the rest
|
|
355
|
+
* A lottery's args: its money in `currency` (the project's by default), ticket money split by `split` (and
|
|
356
|
+
* `cutPercent` to the treasury) with the rest for the places, which are paid from its pot (`pot`) or from each round's
|
|
357
|
+
* own money, what they leave going to `rest` (`paysFromOf`). No `every` draws once, at `firstDraw`.
|
|
183
358
|
*/
|
|
184
359
|
export function lotteryArgs(l, m) {
|
|
185
360
|
const potKey = l.currency ?? m.currencyKey;
|
|
@@ -187,25 +362,103 @@ export function lotteryArgs(l, m) {
|
|
|
187
362
|
const ticket = l.ticketCurrency ? m.tokens.get(l.ticketCurrency) : potToken;
|
|
188
363
|
const every = toSeconds(l.every);
|
|
189
364
|
return {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
365
|
+
// What a round copies at its first purchase.
|
|
366
|
+
terms: {
|
|
367
|
+
price: l.ticketNft ? 0n : amount(l.price, ticket),
|
|
368
|
+
mint: `@mint:${potKey}`,
|
|
369
|
+
// Where the places are paid from: a pot kept from round to round, or each round's own money.
|
|
370
|
+
pays_from: paysFromOf(l, m),
|
|
371
|
+
shares: shares(lotterySplit(l), m),
|
|
372
|
+
// A place takes a share of the pot, a prize for its winner, or both.
|
|
373
|
+
// A run of `times` places alike: "places 2 to 201 each take 1%" is one.
|
|
374
|
+
places: (l.places ?? [])
|
|
375
|
+
.map(placeOf)
|
|
376
|
+
.map(p => ({ bps: Math.round(p.percent * 100), prize: prizeOrNull(p.prize, m), times: p.times })),
|
|
377
|
+
ticket_mint: l.ticketCurrency ? `@mint:${l.ticketCurrency}` : '11111111111111111111111111111111',
|
|
378
|
+
draws_at: BigInt(l.firstDraw ? toTime(l.firstDraw, m.now) : m.now + every),
|
|
379
|
+
// A ticket paid with one NFT that passes the rule, in place of tokens.
|
|
380
|
+
nft_ticket: l.ticketNft ? { rule: ruleOf(l.ticketNft.rule, m), then: spendOf(l.ticketNft.then, m) } : null,
|
|
381
|
+
// No loss: every ticket goes back to its buyer once the round is drawn; the winners take the pot alone.
|
|
382
|
+
returns: Boolean(l.noLoss),
|
|
383
|
+
// A least sale: a round that sold fewer tickets is cancelled at its draw and refunded.
|
|
384
|
+
min_sold: BigInt(l.minSold ?? 0),
|
|
385
|
+
},
|
|
196
386
|
every: BigInt(every),
|
|
197
|
-
draws_at: BigInt(l.firstDraw ? toTime(l.firstDraw, m.now) : m.now + every),
|
|
198
387
|
gate: gateOf(l.gate, m),
|
|
199
388
|
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
389
|
};
|
|
207
390
|
}
|
|
208
391
|
|
|
392
|
+
/**
|
|
393
|
+
* A lottery round's header as its first purchase sizes it (`LotteryRound::new`: its `head`), and the token accounts a
|
|
394
|
+
* round may open (`accounts`: the payment mint's, the pot's, one for each run whose prize comes from a pool). The
|
|
395
|
+
* 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
|
|
396
|
+
* NFT slot for each place of the lottery's own stash.
|
|
397
|
+
*/
|
|
398
|
+
export function roundSize(l, m) {
|
|
399
|
+
const { terms } = placeholders(lotteryArgs(l, m));
|
|
400
|
+
const places = terms.places.reduce((n, p) => n + Number(p.times ?? 1), 0);
|
|
401
|
+
const tokens = terms.places.filter(p => p.prize?.Tokens).length;
|
|
402
|
+
const pools = terms.places.filter(p => p.prize?.Tokens?.from.Pool).length;
|
|
403
|
+
const own = terms.places
|
|
404
|
+
.filter(p => p.prize?.Held || p.prize === 'Raffled' || p.prize?.Raffled !== undefined)
|
|
405
|
+
.reduce((n, p) => n + Number(p.times ?? 1), 0);
|
|
406
|
+
const termsBytes = encodeBorsh(IDL, { defined: { name: 'LotteryTerms' } }, terms).length;
|
|
407
|
+
// The tag; project, lottery, id, incarnation, round, bump, opener, lent, head, status, revision; the terms; fee, sold,
|
|
408
|
+
// to_pot; common; owns, owed, the draw's slots and times, answer and first ticket, the pot's four numbers; paid; prizes (59
|
|
409
|
+
// bytes each); accounts (64 each); nfts; assigned.
|
|
410
|
+
const head =
|
|
411
|
+
1 +
|
|
412
|
+
130 +
|
|
413
|
+
termsBytes +
|
|
414
|
+
17 +
|
|
415
|
+
(4 + 8 * terms.shares.length) +
|
|
416
|
+
112 +
|
|
417
|
+
(4 + Math.ceil((2 * places) / 8)) +
|
|
418
|
+
(4 + 59 * tokens) +
|
|
419
|
+
(4 + 64 * (2 + pools)) +
|
|
420
|
+
(4 + 32 * own) +
|
|
421
|
+
4;
|
|
422
|
+
const payment = l.ticketCurrency ?? l.currency ?? m.currencyKey;
|
|
423
|
+
const accounts = (l.ticketNft ? 1 : payment === (l.currency ?? m.currencyKey) ? 1 : 2) + pools;
|
|
424
|
+
return { head, accounts };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** How many rounds of a lottery are open or still paying out at once, and how many purchases a round takes, as the
|
|
428
|
+
* spec expects them (`expect`): by default two rounds of a lottery that draws again (one selling, one paying out) and
|
|
429
|
+
* one of a lottery that draws once; a round's purchases from the scenario's purchases a day over a round, else 100. */
|
|
430
|
+
export function roundsExpected(l, m) {
|
|
431
|
+
const every = toSeconds(l.every ?? 0);
|
|
432
|
+
const perDay = Number(m.scenario?.perDay?.[`lottery:${l.key}`] ?? 0);
|
|
433
|
+
const days =
|
|
434
|
+
every > 0 ? every / 86_400 : l.firstDraw ? Math.max(1, (toTime(l.firstDraw, m.now) - m.now) / 86_400) : 1;
|
|
435
|
+
const rounds = l.expect?.rounds ?? (every > 0 ? 2 : 1);
|
|
436
|
+
const entries = l.expect?.entries ?? (perDay > 0 ? Math.ceil(perDay * days) : 100);
|
|
437
|
+
return { rounds, entries };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* The SOL, in lamports, a lottery holds for its rounds' rent: the spec's `roundRent` (0 turns it off), or by default
|
|
442
|
+
* what its expected rounds take at once (`roundsExpected`), each its header, its token accounts and its expected
|
|
443
|
+
* entries' bytes (`LotteryRound.lent`: in a round the lottery opened, its spare SOL pays each entry while it covers
|
|
444
|
+
* them, so buyers pay no program rent for a ticket), at devnet's and mainnet's rent, rounded up to 0.001 SOL. Short
|
|
445
|
+
* of it (a busier round, a cluster that charges more), the first buyer fronts the header and later buyers pay their
|
|
446
|
+
* own entries. Each round gives back what it took when it closes; the lottery gives the rest back to the owner when it closes.
|
|
447
|
+
*/
|
|
448
|
+
export function roundRentOf(l, m) {
|
|
449
|
+
if (l.roundRent !== undefined) return toUnits(l.roundRent, 9);
|
|
450
|
+
const { head, accounts } = roundSize(l, m);
|
|
451
|
+
const { rounds, entries } = roundsExpected(l, m);
|
|
452
|
+
const width = loot.entryWidth(
|
|
453
|
+
lotterySplit(l).some(s => String(s.to).startsWith('chosen')),
|
|
454
|
+
Boolean(l.ticketNft),
|
|
455
|
+
);
|
|
456
|
+
// The header and its entries' list head (6 bytes), each token account, each entry's bytes.
|
|
457
|
+
const one = rent(head + 6) + BigInt(accounts) * rent(165) + BigInt(entries * width) * RENT_PER_BYTE;
|
|
458
|
+
const all = BigInt(rounds) * one;
|
|
459
|
+
return ((all + 999_999n) / 1_000_000n) * 1_000_000n;
|
|
460
|
+
}
|
|
461
|
+
|
|
209
462
|
/**
|
|
210
463
|
* The args the authority opens a contest with, `at` (Unix seconds) its opening: its sides, when entries close and the
|
|
211
464
|
* result is due, its split, its gate (a list of wallets for a duel), the keys that may decide it, the least pot (an
|
|
@@ -217,8 +470,16 @@ export function contestArgs(c, m, at = m.now) {
|
|
|
217
470
|
// program refuses them at once): with no "entriesClose", an hour before the result is due (halfway for a contest
|
|
218
471
|
// shorter than two hours); with no "resultDue", an hour after entries close.
|
|
219
472
|
const deciders = (c.deciders ?? []).length > 0;
|
|
220
|
-
const due =
|
|
221
|
-
|
|
473
|
+
const due =
|
|
474
|
+
c.resultDue === undefined && c.entriesClose !== undefined && deciders
|
|
475
|
+
? toSeconds(c.entriesClose) + 3600
|
|
476
|
+
: toSeconds(c.resultDue ?? c.entriesClose ?? '1d');
|
|
477
|
+
const closes =
|
|
478
|
+
c.entriesClose !== undefined
|
|
479
|
+
? toSeconds(c.entriesClose)
|
|
480
|
+
: deciders
|
|
481
|
+
? due - Math.min(3600, Math.floor(due / 2))
|
|
482
|
+
: due;
|
|
222
483
|
return {
|
|
223
484
|
sides: c.sides,
|
|
224
485
|
closes_at: BigInt(at + closes),
|
|
@@ -227,62 +488,97 @@ export function contestArgs(c, m, at = m.now) {
|
|
|
227
488
|
gate: gateOf(c.gate, m),
|
|
228
489
|
deciders: c.deciders ?? [],
|
|
229
490
|
// A contest of NFTs counts its pot in NFTs.
|
|
230
|
-
min_pot: c.stakeNft ? wholeCount(c.minPot) ?? 0n : amount(c.minPot ?? 0, t),
|
|
491
|
+
min_pot: c.stakeNft ? (wholeCount(c.minPot) ?? 0n) : amount(c.minPot ?? 0, t),
|
|
231
492
|
stake: amount(c.stake ?? 0, t),
|
|
232
493
|
prize: prizeOrNull(c.prize, m),
|
|
233
494
|
// A token prize set aside whole at the decision and split among the winners by what each put in.
|
|
234
495
|
prize_split: Boolean(c.prize?.split),
|
|
235
|
-
// Each entry stakes one NFT that passes this rule, and the NFTs are the pot
|
|
496
|
+
// Each entry stakes one NFT that passes this rule, and the NFTs are the pot: each winner shares the losers', at most
|
|
497
|
+
// `losersEach` of them (none: no cap).
|
|
236
498
|
nft: c.stakeNft ? ruleOf(c.stakeNft, m) : null,
|
|
499
|
+
cap: c.stakeNft && c.losersEach ? c.losersEach : null,
|
|
237
500
|
};
|
|
238
501
|
}
|
|
239
502
|
|
|
240
503
|
export function farmArgs(f, m) {
|
|
241
|
-
const tiers = (f.tiers ?? [{ lock: 0, multiplier: 1 }]).map(
|
|
504
|
+
const tiers = (f.tiers ?? [{ lock: 0, multiplier: 1 }]).map(t => ({
|
|
242
505
|
lock: toSeconds(t.lock),
|
|
243
506
|
bps: Math.round((t.multiplier ?? 1) * 10_000),
|
|
244
507
|
prize: prizeOrNull(t.prize, m),
|
|
245
|
-
roll: t.roll ? m.box.get(t.roll)?.id ?? 0 : null,
|
|
508
|
+
roll: t.roll ? (m.box.get(t.roll)?.id ?? 0) : null,
|
|
246
509
|
// A token seat may leave before this tier's lock by leaving this share of what it takes out.
|
|
247
510
|
exit_bps: Math.round((t.exitFee ?? 0) * 100),
|
|
511
|
+
// How long a seat of this tier earns and grows from when its lock started (0: no end): a term.
|
|
512
|
+
earns_for: t.earnsFor ? toSeconds(t.earnsFor) : 0,
|
|
248
513
|
}));
|
|
249
514
|
// Every rule of its gate; with none, a farm of one item's collectibles takes that item.
|
|
250
515
|
const named = listOf(f.gate);
|
|
251
|
-
const gates =
|
|
516
|
+
const gates =
|
|
517
|
+
f.stakes !== 'collectibles'
|
|
518
|
+
? []
|
|
519
|
+
: (named.length ? named : f.item ? [{ item: f.item, attribute: 'item_id' }] : []).map(r => ruleOf(r, m));
|
|
252
520
|
// A rate is per NFT_UNIT of weight: one collectible, or 1,000,000 base units of a staked token.
|
|
253
|
-
const rewards = m.
|
|
521
|
+
const rewards = m.tokens.get(f.rewardsToken);
|
|
254
522
|
const staked = f.stakes === 'collectibles' ? null : m.tokens.get(f.stakes);
|
|
255
523
|
const perDay = f.rate ? amount(f.rate.perDay, rewards) : 0n;
|
|
524
|
+
// A shared rate is the whole farm's a day, shared by weight; any other is for each unit of weight.
|
|
525
|
+
const shared = Boolean(f.rate?.shared);
|
|
256
526
|
return {
|
|
257
527
|
custody: { freeze: 'Freeze', watch: 'Watch' }[f.custody] ?? 'Escrow',
|
|
258
|
-
rewards_pool: m.pool.get(f.rewards)?.index ?? 0,
|
|
259
528
|
tiers,
|
|
260
|
-
boosts: (f.boosts ?? []).map(
|
|
529
|
+
boosts: (f.boosts ?? []).map(b => ({ rule: ruleOf(b.when, m), effect: effectOf(b.add) })),
|
|
261
530
|
max_boost_bps: Math.round((f.maxBoostPercent ?? 0) * 100),
|
|
262
531
|
gates,
|
|
263
|
-
rate: f.rate
|
|
264
|
-
|
|
532
|
+
rate: f.rate
|
|
533
|
+
? {
|
|
534
|
+
per_day: staked && !shared ? (perDay * 1_000_000n) / 10n ** BigInt(staked.decimals ?? 0) : perDay,
|
|
535
|
+
mints: Boolean(f.rate.minted),
|
|
536
|
+
shared,
|
|
537
|
+
halve_every: f.rate.halveEvery ? toSeconds(f.rate.halveEvery) : 0,
|
|
538
|
+
}
|
|
539
|
+
: null,
|
|
540
|
+
growth: listOf(f.growth).map(g => ({
|
|
541
|
+
key: g.attribute ?? 'xp',
|
|
542
|
+
per_day: g.perDay,
|
|
543
|
+
level: (g.levels ?? []).length ? (g.level ?? 'level') : '',
|
|
544
|
+
steps: g.levels ?? [],
|
|
545
|
+
streak: Boolean(g.streak),
|
|
546
|
+
profile: false,
|
|
547
|
+
at_least: 0n,
|
|
548
|
+
})),
|
|
265
549
|
// A farm that runs for a time ends that long after it goes live.
|
|
266
550
|
ends_at: f.runs ? BigInt(m.now + toSeconds(f.runs)) : 0n,
|
|
267
551
|
// The times it keeps on what is staked: its lock, and an upkeep.
|
|
268
552
|
lock_key: f.lockAttribute ?? '',
|
|
269
553
|
upkeep_key: f.upkeepAttribute ?? '',
|
|
270
|
-
// Where an early exit's fee goes
|
|
554
|
+
// Where an early exit's fee goes.
|
|
271
555
|
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
556
|
};
|
|
275
557
|
}
|
|
276
558
|
|
|
277
|
-
/** A fee's sink (`Sink`): "burn", or "pool:<key>", one of the project's pools. */
|
|
278
|
-
export const sinkOf = (to, m) =>
|
|
559
|
+
/** A fee's sink (`Sink`): "burn", "rewards" (the farm's own vault), or "pool:<key>", one of the project's pools. */
|
|
560
|
+
export const sinkOf = (to, m) =>
|
|
561
|
+
to === 'burn'
|
|
562
|
+
? 'Burn'
|
|
563
|
+
: to === 'rewards'
|
|
564
|
+
? 'Rewards'
|
|
565
|
+
: { Pool: [m.pool.get(String(to).replace(/^pool:/, ''))?.index ?? 0] };
|
|
566
|
+
|
|
567
|
+
/** Whether a spec's farm keeps a record of when its seats stop earning (`loot.farmKeepsEnds`): a tier's term or an upkeep,
|
|
568
|
+
* and it pays something. Each different time waiting costs whoever writes it `loot.END_BYTES` of rent. */
|
|
569
|
+
export const farmEnds = f =>
|
|
570
|
+
((f.tiers ?? []).some(t => t.earnsFor) || Boolean(f.upkeepAttribute)) && (!f.rate || Number(f.rate.perDay) > 0);
|
|
279
571
|
|
|
280
572
|
/** An auction's args: its lot, reserve, least raise, times and split. "start" is when it goes live unless it says. */
|
|
281
573
|
export function auctionArgs(a, m) {
|
|
282
574
|
const t = m.priceToken(a);
|
|
283
575
|
const startsAt = toTime(a.start, m.now);
|
|
284
576
|
return {
|
|
285
|
-
lot: a.lot?.asset
|
|
577
|
+
lot: a.lot?.asset
|
|
578
|
+
? { Asset: [a.lot.asset] }
|
|
579
|
+
: a.lot?.roll
|
|
580
|
+
? { Roll: [rollOf(a.lot.roll, m)] }
|
|
581
|
+
: { Item: [m.item.get(a.lot?.item)?.id ?? 0] },
|
|
286
582
|
// With NFT bids, the reserve counts NFTs.
|
|
287
583
|
reserve: a.bidNfts ? BigInt(a.reserve ?? 1) : amount(a.reserve ?? 0, t),
|
|
288
584
|
min_raise_bps: Math.round((a.minRaise ?? 5) * 100),
|
|
@@ -298,7 +594,7 @@ export function auctionArgs(a, m) {
|
|
|
298
594
|
|
|
299
595
|
const NOBODY = '11111111111111111111111111111111';
|
|
300
596
|
/** 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);
|
|
597
|
+
const rollOf = (box, m) => (box ? (m.box.get(box)?.id ?? 0) : null);
|
|
302
598
|
|
|
303
599
|
export function grantArgs(g, m) {
|
|
304
600
|
const gives = prizeArgs(g.prize ?? g.gives, m);
|
|
@@ -309,20 +605,24 @@ export function grantArgs(g, m) {
|
|
|
309
605
|
return {
|
|
310
606
|
gives,
|
|
311
607
|
vouchers,
|
|
312
|
-
list_root: list.length
|
|
313
|
-
|
|
608
|
+
list_root: list.length
|
|
609
|
+
? loot.merkleRoot(listLeaves(list, prizeToken(prizeLine(g.prize ?? g.gives), m)))
|
|
610
|
+
: new Uint8Array(32),
|
|
611
|
+
key_root: keys.length ? loot.merkleRoot(keys.map(k => loot.keyLeaf(k))) : new Uint8Array(32),
|
|
314
612
|
// A grant that names no voucher key and no list is one any wallet may claim, within its limits.
|
|
315
613
|
open: !(g.vouchers ?? []).length && !list.length && !keys.length,
|
|
316
614
|
limits: {
|
|
317
615
|
wallet: perWalletOf(l),
|
|
318
616
|
per_person: l.perPerson ?? 0,
|
|
319
617
|
per_key: l.perKey ?? 0,
|
|
320
|
-
|
|
321
|
-
total: l.total ?? 0,
|
|
618
|
+
// Every wallet's claims together: in all, and a day (a window of 86,400 seconds, from the unix epoch).
|
|
619
|
+
all: { total: l.total ?? 0, per_window: l.perDay ?? 0, window: l.perDay ? 86_400 : 0 },
|
|
322
620
|
window: windowOf({ opens: l.opens, closes: l.until }, m.now),
|
|
323
621
|
},
|
|
324
622
|
active: g.active !== false,
|
|
325
623
|
gate: gateOf(g.gate, m),
|
|
624
|
+
// Its records in 2^bits lists, the fewest that keep each under ~100,000 over its life (wallets × rounds).
|
|
625
|
+
shard_bits: loot.shardBitsFor(g.wallets ?? list.length ?? 0, g.rounds ?? 1),
|
|
326
626
|
};
|
|
327
627
|
}
|
|
328
628
|
|
|
@@ -330,7 +630,8 @@ export function grantArgs(g, m) {
|
|
|
330
630
|
|
|
331
631
|
/**
|
|
332
632
|
* 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
|
|
633
|
+
* wallet's token account for it, "@pot:<lottery id>:<pot id>" with a lottery's pot, "@farm:<id>" with a farm's address and "@rewards:<id>"
|
|
634
|
+
* with a farm's rewards vault.
|
|
334
635
|
*/
|
|
335
636
|
function resolve(value, ctx) {
|
|
336
637
|
if (value === '@treasury') {
|
|
@@ -355,9 +656,14 @@ function resolve(value, ctx) {
|
|
|
355
656
|
if (!ctx.project) throw new Error('No project yet for a stake gate’s farm.');
|
|
356
657
|
return loot.pda.farm(ctx.project, Number(value.slice(6)));
|
|
357
658
|
}
|
|
659
|
+
if (typeof value === 'string' && value.startsWith('@rewards:')) {
|
|
660
|
+
if (!ctx.project) throw new Error('No project yet for a farm’s rewards vault.');
|
|
661
|
+
return loot.pda.rewards(loot.pda.farm(ctx.project, Number(value.slice(9))));
|
|
662
|
+
}
|
|
358
663
|
if (typeof value === 'string' && value.startsWith('@pot:')) {
|
|
359
664
|
if (!ctx.project) throw new Error('No project yet for a lottery’s pot.');
|
|
360
|
-
|
|
665
|
+
const [, lottery, pot] = value.split(':');
|
|
666
|
+
return loot.pda.lotteryPot(loot.pda.lottery(ctx.project, Number(lottery)), Number(pot));
|
|
361
667
|
}
|
|
362
668
|
if (typeof value === 'string' && value.startsWith('@ata:')) {
|
|
363
669
|
const [, wallet, key, program] = value.split(':');
|
|
@@ -365,7 +671,7 @@ function resolve(value, ctx) {
|
|
|
365
671
|
if (!mint) throw new Error(`No mint yet for the token “${key}”.`);
|
|
366
672
|
return loot.ata(wallet, mint, program === 'token-2022' ? TOKEN_2022_PROGRAM : TOKEN_PROGRAM);
|
|
367
673
|
}
|
|
368
|
-
if (Array.isArray(value)) return value.map(
|
|
674
|
+
if (Array.isArray(value)) return value.map(v => resolve(v, ctx));
|
|
369
675
|
if (value instanceof Uint8Array || typeof value !== 'object' || value === null) return value;
|
|
370
676
|
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, resolve(v, ctx)]));
|
|
371
677
|
}
|
|
@@ -375,14 +681,14 @@ function resolve(value, ctx) {
|
|
|
375
681
|
const encodedSize = loot.accountBytes;
|
|
376
682
|
/** Bubblegum's config of a tree. */
|
|
377
683
|
const TREE_CONFIG_BYTES = 96;
|
|
378
|
-
const placeholderKey =
|
|
684
|
+
const placeholderKey = name => encodeBase58(sha256(utf8(`loot-plan:${name}`)));
|
|
379
685
|
|
|
380
686
|
/**
|
|
381
687
|
* 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
688
|
* farms, auctions, then the seeds and the grants' funds. `only` limits it to these part ids (a change to a live economy).
|
|
383
689
|
*
|
|
384
|
-
* A farm opens before
|
|
385
|
-
* farm credits nothing at once, and releases what lands in its
|
|
690
|
+
* A farm opens before its seed lands in its rewards vault, and anyone may stake in between. That is safe: a sharing
|
|
691
|
+
* farm credits nothing at once, and releases what lands in its vault over its longest lock (a day at least) to the
|
|
386
692
|
* seats there as it is released, its clock stopped while nobody is in. A seed that lands
|
|
387
693
|
* before the first stake or after it is released the same way from that stake on, so seeding first would change nothing.
|
|
388
694
|
*/
|
|
@@ -390,15 +696,15 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
|
|
|
390
696
|
const m = model(economy, { now });
|
|
391
697
|
const live = madeSoFar(given);
|
|
392
698
|
const steps = [];
|
|
393
|
-
const want =
|
|
699
|
+
const want = id => !only || only.has(id);
|
|
394
700
|
const landed = new Set(live?.landed ?? []);
|
|
395
|
-
const add =
|
|
396
|
-
const project =
|
|
701
|
+
const add = step => want(step.part) && !landed.has(step.id) && steps.push({ creates: [], signers: [owner], ...step });
|
|
702
|
+
const project = ctx => ctx.project;
|
|
397
703
|
const fresh = !live?.project;
|
|
398
704
|
|
|
399
705
|
// Tokens the project mints: a classic mint with the owner as its authority, handed to the project right after.
|
|
400
706
|
for (const t of m.tokens.values()) {
|
|
401
|
-
if (t.source !== 'game' || live?.mints?.[t.key] || t.mint && !t.mint.startsWith('@')) continue;
|
|
707
|
+
if (t.source !== 'game' || live?.mints?.[t.key] || (t.mint && !t.mint.startsWith('@'))) continue;
|
|
402
708
|
const key = `new:mint/${t.key}`;
|
|
403
709
|
if (t.fixedSupply) {
|
|
404
710
|
const supply = amount(t.supply, t);
|
|
@@ -407,12 +713,26 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
|
|
|
407
713
|
part: `token:${t.key}`,
|
|
408
714
|
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
715
|
signers: [owner, key],
|
|
410
|
-
creates: [
|
|
411
|
-
|
|
412
|
-
|
|
716
|
+
creates: [
|
|
717
|
+
{ what: `the mint of ${t.symbol}`, bytes: MINT_BYTES },
|
|
718
|
+
{ what: `your ${t.symbol} account`, bytes: 165 },
|
|
719
|
+
],
|
|
720
|
+
build: ctx => [
|
|
721
|
+
createAccountIx({
|
|
722
|
+
from: ctx.owner,
|
|
723
|
+
address: ctx.keys[key],
|
|
724
|
+
lamports: ctx.rent(MINT_BYTES),
|
|
725
|
+
space: MINT_BYTES,
|
|
726
|
+
owner: TOKEN_PROGRAM,
|
|
727
|
+
}),
|
|
413
728
|
initializeMint2Ix({ mint: ctx.keys[key], decimals: t.decimals, authority: ctx.owner }),
|
|
414
729
|
createAssociatedTokenAccountIdempotent(ctx.owner, ctx.owner, ctx.keys[key]),
|
|
415
|
-
mintToIx({
|
|
730
|
+
mintToIx({
|
|
731
|
+
mint: ctx.keys[key],
|
|
732
|
+
to: loot.ata(ctx.owner, ctx.keys[key]),
|
|
733
|
+
authority: ctx.owner,
|
|
734
|
+
amount: supply,
|
|
735
|
+
}),
|
|
416
736
|
setAuthorityIx({ account: ctx.keys[key], current: ctx.owner, newAuthority: null }),
|
|
417
737
|
],
|
|
418
738
|
});
|
|
@@ -424,8 +744,14 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
|
|
|
424
744
|
says: `Make the token ${t.symbol} (${t.decimals} decimals), then hand its minting to the project`,
|
|
425
745
|
signers: [owner, key],
|
|
426
746
|
creates: [{ what: `the mint of ${t.symbol}`, bytes: MINT_BYTES }],
|
|
427
|
-
build:
|
|
428
|
-
createAccountIx({
|
|
747
|
+
build: ctx => [
|
|
748
|
+
createAccountIx({
|
|
749
|
+
from: ctx.owner,
|
|
750
|
+
address: ctx.keys[key],
|
|
751
|
+
lamports: ctx.rent(MINT_BYTES),
|
|
752
|
+
space: MINT_BYTES,
|
|
753
|
+
owner: TOKEN_PROGRAM,
|
|
754
|
+
}),
|
|
429
755
|
initializeMint2Ix({ mint: ctx.keys[key], decimals: t.decimals, authority: ctx.owner }),
|
|
430
756
|
setAuthorityIx({ account: ctx.keys[key], current: ctx.owner, newAuthority: ctx.project }),
|
|
431
757
|
],
|
|
@@ -443,20 +769,47 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
|
|
|
443
769
|
part: 'project',
|
|
444
770
|
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
771
|
signers: [owner, 'new:collection'],
|
|
446
|
-
creates: [
|
|
447
|
-
|
|
772
|
+
creates: [
|
|
773
|
+
{
|
|
774
|
+
what: 'the collection',
|
|
775
|
+
bytes: 1 + 32 + 4 + c.name.length + 4 + (c.uri ?? '').length + 140 + (tree ? 3 : 0),
|
|
776
|
+
approx: true,
|
|
777
|
+
},
|
|
778
|
+
],
|
|
779
|
+
build: ctx => [
|
|
780
|
+
createCollectionInstruction({
|
|
781
|
+
collection: ctx.keys['new:collection'],
|
|
782
|
+
creator: ctx.owner,
|
|
783
|
+
project: ctx.project,
|
|
784
|
+
name: c.name,
|
|
785
|
+
uri: c.uri ?? '',
|
|
786
|
+
royaltyBps: Math.round((c.royaltyPercent ?? 5) * 100),
|
|
787
|
+
compressed: Boolean(tree),
|
|
788
|
+
}),
|
|
789
|
+
],
|
|
448
790
|
});
|
|
449
791
|
// The treasury is a wallet: the remainder of every split goes to its account of that mint, which the program
|
|
450
792
|
// opens when it is not there. The project's vaults, and every account a share pays, open the same way when first
|
|
451
793
|
// paid, so the setup opens none of them.
|
|
452
|
-
const collections =
|
|
794
|
+
const collections = ctx => [...(ctx.collection ? [ctx.collection] : []), ...m.collections];
|
|
453
795
|
const count = (c ? 1 : 0) + m.collections.length;
|
|
454
796
|
add({
|
|
455
797
|
id: 'project',
|
|
456
798
|
part: 'project',
|
|
457
799
|
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: [
|
|
459
|
-
|
|
800
|
+
creates: [
|
|
801
|
+
{ what: 'the project', bytes: loot.projectBytes(count) },
|
|
802
|
+
{ what: 'the project’s fee box', bytes: 0 },
|
|
803
|
+
],
|
|
804
|
+
build: ctx => [
|
|
805
|
+
loot.createProjectIx({
|
|
806
|
+
owner: ctx.owner,
|
|
807
|
+
id: m.projectNumber,
|
|
808
|
+
collections: collections(ctx),
|
|
809
|
+
treasury: ctx.treasury,
|
|
810
|
+
authority: ctx.authority,
|
|
811
|
+
}),
|
|
812
|
+
],
|
|
460
813
|
});
|
|
461
814
|
}
|
|
462
815
|
|
|
@@ -467,47 +820,222 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
|
|
|
467
820
|
part: 'tree',
|
|
468
821
|
says: `Open the project’s tree of compressed NFTs, for ${(2 ** tree.depth).toLocaleString('en-US')} of them in all`,
|
|
469
822
|
signers: [owner, 'new:tree'],
|
|
470
|
-
creates: [
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
823
|
+
creates: [
|
|
824
|
+
{ what: 'the tree of compressed NFTs', bytes: tree.bytes },
|
|
825
|
+
{ what: 'the tree’s config (Bubblegum’s)', bytes: TREE_CONFIG_BYTES },
|
|
826
|
+
],
|
|
827
|
+
build: ctx => [
|
|
828
|
+
createAccountIx({
|
|
829
|
+
from: ctx.owner,
|
|
830
|
+
address: ctx.keys['new:tree'],
|
|
831
|
+
lamports: ctx.rent(tree.bytes),
|
|
832
|
+
space: tree.bytes,
|
|
833
|
+
owner: loot.COMPRESSION_PROGRAM,
|
|
834
|
+
}),
|
|
835
|
+
loot.openTreeIx({
|
|
836
|
+
signer: ctx.owner,
|
|
837
|
+
project: project(ctx),
|
|
838
|
+
tree: ctx.keys['new:tree'],
|
|
839
|
+
maxDepth: tree.depth,
|
|
840
|
+
maxBufferSize: tree.buffer,
|
|
841
|
+
}),
|
|
474
842
|
],
|
|
475
843
|
});
|
|
476
844
|
|
|
845
|
+
// The token accounts players' purchases would otherwise open (the owner pays their rent now, about 0.002 SOL each,
|
|
846
|
+
// so no player's purchase pays another wallet's): the treasury's of each coin the economy takes, the project's vault
|
|
847
|
+
// of each coin a box or an auction holds, and each fixed wallet's a split pays.
|
|
848
|
+
const accounts = goLiveAccounts(m);
|
|
849
|
+
if (fresh && accounts.length)
|
|
850
|
+
add({
|
|
851
|
+
id: 'accounts',
|
|
852
|
+
part: 'project',
|
|
853
|
+
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`,
|
|
854
|
+
creates: accounts.map(x => ({
|
|
855
|
+
what: `${x.who}’s account of ${m.tokens.get(x.token)?.symbol ?? x.token}`,
|
|
856
|
+
bytes: 165,
|
|
857
|
+
})),
|
|
858
|
+
build: ctx =>
|
|
859
|
+
accounts.map(x =>
|
|
860
|
+
createAssociatedTokenAccountIdempotent(
|
|
861
|
+
ctx.owner,
|
|
862
|
+
x.who === 'the project' ? ctx.project : x.who === 'the treasury' ? ctx.treasury : x.who,
|
|
863
|
+
ctx.mints[x.token],
|
|
864
|
+
programsOf(ctx)[ctx.mints[x.token]] ?? TOKEN_PROGRAM,
|
|
865
|
+
),
|
|
866
|
+
),
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
// The project's standing lookup table (the owner its authority): the addresses its transactions name most, so a
|
|
870
|
+
// transaction too big for a legacy one goes as v0 with it in one approval, never with a table of its own. Made with
|
|
871
|
+
// the first of its addresses, then extended a group a step.
|
|
872
|
+
if (fresh) {
|
|
873
|
+
const named = standingAddresses(m);
|
|
874
|
+
groupsOf(named, TABLE_GROUP).forEach((group, i) =>
|
|
875
|
+
add({
|
|
876
|
+
id: i ? `table:${i}` : 'table',
|
|
877
|
+
part: 'project',
|
|
878
|
+
says: i
|
|
879
|
+
? `Add ${group.length} more addresses to the project’s lookup table`
|
|
880
|
+
: `Make the project’s lookup table of the ${named.length} addresses its transactions name most, so a large one takes one approval`,
|
|
881
|
+
creates: i
|
|
882
|
+
? [{ what: `room for ${group.length} addresses in the lookup table`, bytes: 32 * group.length, grows: true }]
|
|
883
|
+
: [{ what: 'the project’s lookup table', bytes: 56 + 32 * group.length }],
|
|
884
|
+
build: ctx => {
|
|
885
|
+
// With no recent slot known (a plan built offline), there is no table to make.
|
|
886
|
+
if (ctx.slot == null) return [];
|
|
887
|
+
const { address: table, instruction } = createTableIx({ authority: ctx.owner, slot: ctx.slot });
|
|
888
|
+
return [
|
|
889
|
+
...(i ? [] : [instruction]),
|
|
890
|
+
extendTableIx({ table, authority: ctx.owner, addresses: group.map(at => at(ctx)).filter(Boolean) }),
|
|
891
|
+
];
|
|
892
|
+
},
|
|
893
|
+
}),
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
|
|
477
897
|
for (const p of m.pools) {
|
|
478
898
|
const t = m.tokens.get(p.currency);
|
|
479
|
-
add({
|
|
899
|
+
add({
|
|
900
|
+
id: `pool:${p.key}`,
|
|
901
|
+
part: `pool:${p.key}`,
|
|
902
|
+
says: `Open the pool “${p.key}” for ${t?.symbol}`,
|
|
903
|
+
creates: [{ what: `the pool “${p.key}”`, bytes: 165 }],
|
|
904
|
+
build: ctx => [
|
|
905
|
+
loot.openPoolIx({ owner: ctx.owner, project: project(ctx), index: p.index, mint: ctx.mints[p.currency] }),
|
|
906
|
+
],
|
|
907
|
+
});
|
|
480
908
|
}
|
|
481
909
|
const record = (part, def, says, kind, what, options = () => ({})) => {
|
|
482
910
|
const [variant] = Object.keys(def);
|
|
483
911
|
const bytes = recordBytesOf(kind, def[variant]);
|
|
484
|
-
// A lottery's
|
|
485
|
-
|
|
486
|
-
|
|
912
|
+
// A lottery's write opens the pot it pays from: a token account as well as the record (each round holds its own
|
|
913
|
+
// money). One paid from each round's own money opens none.
|
|
914
|
+
const pot = def.Lottery?.args.terms.pays_from.Pot
|
|
915
|
+
? [{ what: `${what}’s pot`, bytes: 165 }]
|
|
916
|
+
: def.Schedule?.args.action.from.Vest
|
|
917
|
+
? [{ what: `${what}’s sealed vault`, bytes: 165 }]
|
|
918
|
+
: [];
|
|
919
|
+
add({
|
|
920
|
+
id: `${part}:${kind}`,
|
|
921
|
+
part,
|
|
922
|
+
says,
|
|
923
|
+
creates: [{ what, bytes }, ...pot],
|
|
924
|
+
build: ctx => [
|
|
925
|
+
loot.upsertIx({ owner: ctx.owner, project: project(ctx), def: resolve(def, ctx), ...options(ctx) }),
|
|
926
|
+
],
|
|
927
|
+
});
|
|
487
928
|
};
|
|
488
|
-
|
|
929
|
+
// Each table, a batch of rows a transaction (`TABLE_BATCH_BYTES` of rows at most), sealed with its last batch.
|
|
930
|
+
for (const t of m.tables) {
|
|
931
|
+
const batches = tableBatches(t);
|
|
932
|
+
const len = t.kind === 'numbers' ? 8 : 40;
|
|
933
|
+
batches.forEach((rows, i) => {
|
|
934
|
+
const seal = i === batches.length - 1;
|
|
935
|
+
add({
|
|
936
|
+
id: `table:${t.key}:${i}`,
|
|
937
|
+
part: `table:${t.key}`,
|
|
938
|
+
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' : ''}`,
|
|
939
|
+
creates:
|
|
940
|
+
i === 0
|
|
941
|
+
? [
|
|
942
|
+
{
|
|
943
|
+
what: `the table “${t.key}”, ${t.rows.length} rows`,
|
|
944
|
+
bytes: loot.tableRowAt(len, t.rows.length - 1) + len,
|
|
945
|
+
},
|
|
946
|
+
]
|
|
947
|
+
: [],
|
|
948
|
+
build: ctx => [
|
|
949
|
+
loot.upsertIx({
|
|
950
|
+
owner: ctx.owner,
|
|
951
|
+
project: project(ctx),
|
|
952
|
+
def: { Table: { id: t.id, args: { payload: t.kind === 'numbers' ? 'Number' : 'Label', rows, seal } } },
|
|
953
|
+
}),
|
|
954
|
+
],
|
|
955
|
+
});
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
for (const it of m.items)
|
|
959
|
+
record(
|
|
960
|
+
`item:${it.key}`,
|
|
961
|
+
{ Item: { id: it.id, args: itemArgs(it, m) } },
|
|
962
|
+
`${it.active === false ? 'Switch off' : 'Write'} the item “${it.name}”`,
|
|
963
|
+
'Item',
|
|
964
|
+
`the item “${it.name}”`,
|
|
965
|
+
);
|
|
489
966
|
// 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 =
|
|
967
|
+
const odds = b => {
|
|
491
968
|
const ws = weights(b);
|
|
492
969
|
const total = ws.reduce((n, w) => n + w, 0) || 1;
|
|
493
|
-
const part =
|
|
494
|
-
|
|
970
|
+
const part = o =>
|
|
971
|
+
o.gives === 'item'
|
|
972
|
+
? (m.item.get(o.item)?.name ?? o.item)
|
|
973
|
+
: o.gives === 'stash'
|
|
974
|
+
? `an NFT of “${m.stash.get(o.stash)?.name ?? o.stash}”`
|
|
975
|
+
: o.gives === 'pay'
|
|
976
|
+
? `${o.amount} ${m.poolToken(o.pool)?.symbol ?? ''}`.trim()
|
|
977
|
+
: o.gives === 'share'
|
|
978
|
+
? `${o.percent}% of “${o.pool}”`
|
|
979
|
+
: o.gives === 'mint'
|
|
980
|
+
? `${o.amount} ${m.tokens.get(o.currency ?? m.currencyKey)?.symbol ?? ''}`.trim()
|
|
981
|
+
: o.gives === 'nft'
|
|
982
|
+
? `the NFT ${String(o.nft).slice(0, 4)}…`
|
|
983
|
+
: o.gives === 'roll'
|
|
984
|
+
? `a roll of “${m.box.get(o.box)?.name ?? o.box}”`
|
|
985
|
+
: '';
|
|
986
|
+
const what = o => outcomeParts(o).map(part).join(' + ') || 'nothing';
|
|
495
987
|
return (b.outcomes ?? []).map((o, i) => `${what(o)} ${Math.round((ws[i] / total) * 10_000) / 100}%`).join(', ');
|
|
496
988
|
};
|
|
497
|
-
const wasLive = new Set((live?.published?.boxes ?? []).map(
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
989
|
+
const wasLive = new Set((live?.published?.boxes ?? []).map(b => b.key));
|
|
990
|
+
// A box is written before any box that names it to roll (one may name itself): in that order.
|
|
991
|
+
const boxOrder = [];
|
|
992
|
+
const visit = (b, path = new Set()) => {
|
|
993
|
+
if (boxOrder.includes(b) || path.has(b)) return;
|
|
994
|
+
path.add(b);
|
|
995
|
+
for (const id of loot.defRolls({ Box: { args: boxArgs(b, m) } })) {
|
|
996
|
+
const named = m.allBoxes.find(x => x.id === id);
|
|
997
|
+
if (named && named !== b) visit(named, path);
|
|
998
|
+
}
|
|
999
|
+
boxOrder.push(b);
|
|
1000
|
+
};
|
|
1001
|
+
for (const b of m.allBoxes) visit(b);
|
|
1002
|
+
for (const b of boxOrder) {
|
|
1003
|
+
const says = b.shopOf
|
|
1004
|
+
? `Put “${b.name}” in the shop at ${b.price} ${m.priceToken(b)?.symbol}`
|
|
1005
|
+
: `${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)}`}`;
|
|
1006
|
+
record(
|
|
1007
|
+
b.shopOf ? `item:${b.shopOf}` : `box:${b.key}`,
|
|
1008
|
+
{ Box: { id: b.id, args: boxArgs(b, m) } },
|
|
1009
|
+
says,
|
|
1010
|
+
'LootBox',
|
|
1011
|
+
b.shopOf ? `the shop listing of “${b.name}”` : `the box “${b.name}”`,
|
|
1012
|
+
);
|
|
501
1013
|
}
|
|
502
|
-
for (const r of m.recipes)
|
|
1014
|
+
for (const r of m.recipes)
|
|
1015
|
+
record(
|
|
1016
|
+
`recipe:${r.key}`,
|
|
1017
|
+
{ Recipe: { id: r.id, args: recipeArgs(r, m) } },
|
|
1018
|
+
`Write the recipe “${r.name}”`,
|
|
1019
|
+
'Recipe',
|
|
1020
|
+
`the recipe “${r.name}”`,
|
|
1021
|
+
);
|
|
503
1022
|
for (const s of m.schedules) {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
1023
|
+
record(
|
|
1024
|
+
`schedule:${s.key}`,
|
|
1025
|
+
{ Schedule: { id: s.id, args: scheduleArgs(s, m) } },
|
|
1026
|
+
`${s.active === false ? 'Stop' : 'Start'} the ${s.sealed ? 'sealed ' : ''}schedule “${s.name ?? s.key}”`,
|
|
1027
|
+
'Schedule',
|
|
1028
|
+
`the schedule “${s.name ?? s.key}”`,
|
|
1029
|
+
);
|
|
508
1030
|
// A sealed schedule's vault is funded once, at go-live: from the owner's wallet, minted, or from a pool.
|
|
509
1031
|
const id = `fund:${s.key}`;
|
|
510
|
-
if (
|
|
1032
|
+
if (
|
|
1033
|
+
!s.sealed ||
|
|
1034
|
+
!Number(s.fund ?? 0) ||
|
|
1035
|
+
(live?.landed ?? []).includes(id) ||
|
|
1036
|
+
live?.published?.schedules?.some(x => x.key === s.key)
|
|
1037
|
+
)
|
|
1038
|
+
continue;
|
|
511
1039
|
const t = m.tokens.get(s.currency ?? m.currencyKey);
|
|
512
1040
|
const from = s.fundFrom ?? 'wallet';
|
|
513
1041
|
add({
|
|
@@ -516,51 +1044,126 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
|
|
|
516
1044
|
signers: from === 'wallet' || m.authority === 'owner' ? [owner] : ['authority'],
|
|
517
1045
|
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
1046
|
creates: [],
|
|
519
|
-
build:
|
|
1047
|
+
build: ctx => {
|
|
520
1048
|
const mint = ctx.mints[t.key];
|
|
521
1049
|
const vest = loot.pda.vest(loot.pda.schedule(project(ctx), s.id));
|
|
522
1050
|
const value = amount(s.fund, t);
|
|
523
1051
|
if (from === 'wallet') {
|
|
524
1052
|
const tokenProgram = programsOf(ctx)[mint] ?? TOKEN_PROGRAM;
|
|
525
1053
|
// 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
|
|
527
|
-
|
|
1054
|
+
const wrap = t.wsol
|
|
1055
|
+
? [
|
|
1056
|
+
createAssociatedTokenAccountIdempotent(ctx.owner, ctx.owner, NATIVE_MINT),
|
|
1057
|
+
transferLamports(ctx.owner, loot.ata(ctx.owner, NATIVE_MINT), value),
|
|
1058
|
+
syncNative(loot.ata(ctx.owner, NATIVE_MINT)),
|
|
1059
|
+
]
|
|
1060
|
+
: [];
|
|
1061
|
+
return [
|
|
1062
|
+
...wrap,
|
|
1063
|
+
transferCheckedIx({
|
|
1064
|
+
from: loot.ata(ctx.owner, mint, tokenProgram),
|
|
1065
|
+
mint,
|
|
1066
|
+
to: vest,
|
|
1067
|
+
owner: ctx.owner,
|
|
1068
|
+
amount: value,
|
|
1069
|
+
decimals: t.decimals ?? 0,
|
|
1070
|
+
tokenProgram,
|
|
1071
|
+
}),
|
|
1072
|
+
];
|
|
528
1073
|
}
|
|
529
1074
|
const source = from === 'mint' ? { Mint: [mint] } : { Pool: [m.pool.get(from.slice(5))?.index ?? 0] };
|
|
530
|
-
return [
|
|
1075
|
+
return [
|
|
1076
|
+
loot.moveTokensIx({
|
|
1077
|
+
authority: ctx.authority,
|
|
1078
|
+
project: project(ctx),
|
|
1079
|
+
mint,
|
|
1080
|
+
from: source,
|
|
1081
|
+
to: { Account: [vest] },
|
|
1082
|
+
amount: { Fixed: [value] },
|
|
1083
|
+
programs: programsOf(ctx),
|
|
1084
|
+
}),
|
|
1085
|
+
];
|
|
531
1086
|
},
|
|
532
1087
|
});
|
|
533
1088
|
}
|
|
534
1089
|
for (const l of m.lotteries) {
|
|
535
|
-
// A raffle's
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
1090
|
+
// A raffle's NFTs go into the lottery's own stash first, several a call: the lottery is written naming NFTs already
|
|
1091
|
+
// there, or drawing from as many as its places.
|
|
1092
|
+
const raffled = [
|
|
1093
|
+
...(l.places ?? [])
|
|
1094
|
+
.map(placeOf)
|
|
1095
|
+
.filter(p => p.prize?.held)
|
|
1096
|
+
.map(p => ({ nft: p.prize.nft, collection: p.prize.collection })),
|
|
1097
|
+
...(l.nfts ?? []).map(n => (typeof n === 'string' ? { nft: n } : n)),
|
|
1098
|
+
];
|
|
1099
|
+
const opened = live?.published?.lotteries?.some(x => x.key === l.key);
|
|
1100
|
+
groupsOf(
|
|
1101
|
+
raffled.filter(n => !(live?.landed ?? []).includes(`stash:lottery-${l.key}:${n.nft}`)),
|
|
1102
|
+
STASH_GROUP,
|
|
1103
|
+
).forEach((group, i) => {
|
|
1104
|
+
if (opened) return;
|
|
539
1105
|
add({
|
|
540
|
-
id,
|
|
1106
|
+
id: `stash:lottery-${l.key}:${group.map(n => n.nft).join(',')}`,
|
|
541
1107
|
part: `lottery:${l.key}`,
|
|
542
|
-
says: `Put your NFT ${
|
|
543
|
-
creates: [{ what: `the lottery “${l.name}”’s stash`, bytes:
|
|
544
|
-
build:
|
|
1108
|
+
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`,
|
|
1109
|
+
creates: i === 0 ? [{ what: `the lottery “${l.name}”’s stash`, bytes: stashBytes(raffled.length) }] : [],
|
|
1110
|
+
build: ctx => [
|
|
1111
|
+
loot.stashIx({
|
|
1112
|
+
signer: ctx.owner,
|
|
1113
|
+
project: project(ctx),
|
|
1114
|
+
lottery: l.id,
|
|
1115
|
+
asset: group[0].nft,
|
|
1116
|
+
collection: group[0].collection ?? ctx.collection,
|
|
1117
|
+
more: group.slice(1).map(n => ({ asset: n.nft, collection: n.collection ?? ctx.collection })),
|
|
1118
|
+
}),
|
|
1119
|
+
],
|
|
545
1120
|
});
|
|
546
|
-
}
|
|
547
|
-
record(
|
|
1121
|
+
});
|
|
1122
|
+
record(
|
|
1123
|
+
`lottery:${l.key}`,
|
|
1124
|
+
{ Lottery: { id: l.id, args: lotteryArgs(l, m) } },
|
|
1125
|
+
`Start the ${l.raffle ? 'raffle' : 'lottery'} “${l.name}”`,
|
|
1126
|
+
'Lottery',
|
|
1127
|
+
`the lottery “${l.name}”`,
|
|
1128
|
+
);
|
|
548
1129
|
}
|
|
549
|
-
for (const g of m.grants)
|
|
1130
|
+
for (const g of m.grants)
|
|
1131
|
+
record(
|
|
1132
|
+
`grant:${g.key}`,
|
|
1133
|
+
{ Grant: { id: g.id, args: grantArgs(g, m) } },
|
|
1134
|
+
`Write the grant “${g.name}”`,
|
|
1135
|
+
'Grant',
|
|
1136
|
+
`the grant “${g.name}”`,
|
|
1137
|
+
);
|
|
1138
|
+
/** The boxes a farm's tiers roll, tier by tier (its roll, then a prize that is a roll). */
|
|
550
1139
|
for (const f of m.farms) {
|
|
551
1140
|
if (live?.farms?.includes(f.key)) continue;
|
|
552
|
-
const rewards = m.
|
|
1141
|
+
const rewards = m.tokens.get(f.rewardsToken);
|
|
553
1142
|
add({
|
|
554
1143
|
id: `farm:${f.key}`,
|
|
555
1144
|
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: [
|
|
558
|
-
|
|
1145
|
+
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'}`,
|
|
1146
|
+
creates: [
|
|
1147
|
+
{ what: `the farm “${f.name}”`, bytes: farmBytes(f, m) },
|
|
1148
|
+
{ what: 'its rewards vault', bytes: 165 },
|
|
1149
|
+
...(f.stakes === 'collectibles' ? [] : [{ what: 'its stake vault', bytes: 165 }]),
|
|
1150
|
+
...(farmEnds(f) ? [{ what: 'its record of when seats stop earning', bytes: loot.ENDS_HEAD_BYTES }] : []),
|
|
1151
|
+
],
|
|
1152
|
+
build: ctx => [
|
|
1153
|
+
loot.openFarmIx({
|
|
1154
|
+
owner: ctx.owner,
|
|
1155
|
+
project: project(ctx),
|
|
1156
|
+
id: f.id,
|
|
1157
|
+
args: resolve(farmArgs(f, m), ctx),
|
|
1158
|
+
rewardsMint: ctx.mints[rewards.key],
|
|
1159
|
+
stakedMint: f.stakes === 'collectibles' ? null : ctx.mints[f.stakes],
|
|
1160
|
+
}),
|
|
1161
|
+
],
|
|
559
1162
|
});
|
|
560
1163
|
}
|
|
561
1164
|
|
|
562
1165
|
// 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(
|
|
1166
|
+
const vouched = new Map((live?.published?.traits ?? []).map(t => [t.collection, t.root]));
|
|
564
1167
|
for (const t of m.traits) {
|
|
565
1168
|
if (vouched.get(t.collection) === t.root) continue;
|
|
566
1169
|
add({
|
|
@@ -568,7 +1171,14 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
|
|
|
568
1171
|
part: `traits:${t.collection}`,
|
|
569
1172
|
says: `Vouch for the traits of the NFTs of ${t.collection} (a root of their off-chain metadata)`,
|
|
570
1173
|
creates: [{ what: `the traits of ${t.collection}`, bytes: 1 + 32 * 3 }],
|
|
571
|
-
build:
|
|
1174
|
+
build: ctx => [
|
|
1175
|
+
loot.setTraitsIx({
|
|
1176
|
+
signer: ctx.owner,
|
|
1177
|
+
project: project(ctx),
|
|
1178
|
+
key: t.collection,
|
|
1179
|
+
root: Uint8Array.from(t.root.match(/../g).map(h => parseInt(h, 16))),
|
|
1180
|
+
}),
|
|
1181
|
+
],
|
|
572
1182
|
});
|
|
573
1183
|
}
|
|
574
1184
|
|
|
@@ -576,30 +1186,68 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
|
|
|
576
1186
|
// needs a proof fetched when it is signed, so the checker leaves those to `loot.stashIx` after go-live.
|
|
577
1187
|
// What is in a live stash already: what its last publish listed, and the steps of this one that landed.
|
|
578
1188
|
const stashed = new Set([
|
|
579
|
-
...(live?.published?.stashes ?? []).flatMap(
|
|
580
|
-
|
|
1189
|
+
...(live?.published?.stashes ?? []).flatMap(s => stashNfts(s).map(n => `${s.key}:${n.asset}`)),
|
|
1190
|
+
// A step puts in a group of NFTs: `stash:<key>:<asset>,<asset>…`.
|
|
1191
|
+
...(live?.landed ?? [])
|
|
1192
|
+
.filter(id => id.startsWith('stash:'))
|
|
1193
|
+
.flatMap(id => {
|
|
1194
|
+
const [key, assets = ''] = id.slice(6).split(/:(.*)/s);
|
|
1195
|
+
return assets.split(',').map(asset => `${key}:${asset}`);
|
|
1196
|
+
}),
|
|
581
1197
|
]);
|
|
582
1198
|
for (const s of m.stashes) {
|
|
583
1199
|
const nfts = stashNfts(s);
|
|
584
|
-
const putting = nfts.filter(
|
|
1200
|
+
const putting = nfts.filter(n => n.kind !== 'compressed' && !stashed.has(`${s.key}:${n.asset}`));
|
|
585
1201
|
// 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(
|
|
587
|
-
|
|
1202
|
+
const fresh = !nfts.some(n => stashed.has(`${s.key}:${n.asset}`));
|
|
1203
|
+
// Several NFTs a call, each with its weight: Core NFTs a group of `STASH_GROUP`, Token Metadata ones two (each takes
|
|
1204
|
+
// more accounts).
|
|
1205
|
+
const kindOf = n => ({
|
|
1206
|
+
asset: n.asset,
|
|
1207
|
+
weight: n.weight,
|
|
1208
|
+
collection: n.kind === 'core' ? n.collection : undefined,
|
|
1209
|
+
tokenMetadata:
|
|
1210
|
+
n.kind === 'token-metadata' ? { programmable: n.programmable, ruleSet: n.ruleSet ?? undefined } : undefined,
|
|
1211
|
+
});
|
|
1212
|
+
const groups = [
|
|
1213
|
+
...groupsOf(
|
|
1214
|
+
putting.filter(n => n.kind === 'core'),
|
|
1215
|
+
STASH_GROUP,
|
|
1216
|
+
),
|
|
1217
|
+
...groupsOf(
|
|
1218
|
+
putting.filter(n => n.kind !== 'core'),
|
|
1219
|
+
2,
|
|
1220
|
+
),
|
|
1221
|
+
];
|
|
1222
|
+
for (const [i, group] of groups.entries()) {
|
|
588
1223
|
add({
|
|
589
|
-
id: `stash:${s.key}:${n.asset}`,
|
|
1224
|
+
id: `stash:${s.key}:${group.map(n => n.asset).join(',')}`,
|
|
590
1225
|
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:
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
1226
|
+
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`,
|
|
1227
|
+
creates:
|
|
1228
|
+
i > 0
|
|
1229
|
+
? []
|
|
1230
|
+
: fresh
|
|
1231
|
+
? [
|
|
1232
|
+
{
|
|
1233
|
+
what: `the stash “${s.name}”, with ${putting.length} NFT${putting.length === 1 ? '' : 's'}`,
|
|
1234
|
+
bytes: stashBytes(putting.length),
|
|
1235
|
+
},
|
|
1236
|
+
]
|
|
1237
|
+
: [
|
|
1238
|
+
{
|
|
1239
|
+
what: `room for ${putting.length} more NFT${putting.length === 1 ? '' : 's'} in “${s.name}”`,
|
|
1240
|
+
bytes: 36 * putting.length,
|
|
1241
|
+
grows: true,
|
|
1242
|
+
},
|
|
1243
|
+
],
|
|
1244
|
+
build: ctx => {
|
|
1245
|
+
const [first, ...more] = group.map(n => {
|
|
1246
|
+
const k = kindOf(n);
|
|
1247
|
+
return { ...k, collection: n.kind === 'core' ? (k.collection ?? ctx.collection) : undefined };
|
|
1248
|
+
});
|
|
1249
|
+
return [loot.stashIx({ signer: ctx.owner, project: project(ctx), id: s.id, ...first, more })];
|
|
1250
|
+
},
|
|
603
1251
|
});
|
|
604
1252
|
}
|
|
605
1253
|
}
|
|
@@ -621,10 +1269,47 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
|
|
|
621
1269
|
: a.lot?.roll
|
|
622
1270
|
? `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
1271
|
: `Open the auction “${a.name}” of one new “${m.item.get(a.lot?.item)?.name ?? a.lot?.item}” (${terms})`,
|
|
624
|
-
|
|
625
|
-
|
|
1272
|
+
// An auction of NFT bids is made with room for the most a bid holds (`MAX_BID_NFTS`), its seller's rent.
|
|
1273
|
+
creates: [
|
|
1274
|
+
{
|
|
1275
|
+
what: `the auction “${a.name}” (back when it ends)`,
|
|
1276
|
+
bytes:
|
|
1277
|
+
encodedSize('Auction', {
|
|
1278
|
+
project: NOBODY,
|
|
1279
|
+
id: 0,
|
|
1280
|
+
seller: NOBODY,
|
|
1281
|
+
mint: NOBODY,
|
|
1282
|
+
lot: args.lot,
|
|
1283
|
+
ours: false,
|
|
1284
|
+
pin: 0,
|
|
1285
|
+
reserve: 0n,
|
|
1286
|
+
min_raise_bps: 0,
|
|
1287
|
+
starts_at: 0n,
|
|
1288
|
+
ends_at: 0n,
|
|
1289
|
+
extend: 0,
|
|
1290
|
+
shares: args.shares,
|
|
1291
|
+
gate: args.gate,
|
|
1292
|
+
nft_bids: placeholders(args).nft_bids,
|
|
1293
|
+
high_bidder: NOBODY,
|
|
1294
|
+
high: 0n,
|
|
1295
|
+
last_bid_at: 0n,
|
|
1296
|
+
proceeds_owed: 0n,
|
|
1297
|
+
held: [],
|
|
1298
|
+
}) + (args.nft_bids ? 32 * loot.MAX_BID_NFTS : 0),
|
|
1299
|
+
},
|
|
1300
|
+
],
|
|
1301
|
+
build: ctx => {
|
|
626
1302
|
const seller = auctionSeller(a, ctx);
|
|
627
|
-
return [
|
|
1303
|
+
return [
|
|
1304
|
+
loot.openAuctionIx({
|
|
1305
|
+
seller,
|
|
1306
|
+
project: project(ctx),
|
|
1307
|
+
collection: a.lot?.collection ?? ctx.collection,
|
|
1308
|
+
id: a.id,
|
|
1309
|
+
mint: ctx.mints[t.key],
|
|
1310
|
+
args: resolve(args, ctx),
|
|
1311
|
+
}),
|
|
1312
|
+
];
|
|
628
1313
|
},
|
|
629
1314
|
});
|
|
630
1315
|
}
|
|
@@ -641,48 +1326,259 @@ export function setupSteps(economy, { now = 0, only = null, live: given = null }
|
|
|
641
1326
|
part: `seed:${p.key}`,
|
|
642
1327
|
signers: [signer],
|
|
643
1328
|
says: `Mint ${p.seed} ${t.symbol} into “${p.key}”`,
|
|
644
|
-
build:
|
|
1329
|
+
build: ctx => [
|
|
1330
|
+
loot.moveTokensIx({
|
|
1331
|
+
authority: ctx.authority,
|
|
1332
|
+
project: project(ctx),
|
|
1333
|
+
mint: ctx.mints[t.key],
|
|
1334
|
+
from: { Mint: [ctx.mints[t.key]] },
|
|
1335
|
+
to: { Pool: [p.index] },
|
|
1336
|
+
amount: { Fixed: [units] },
|
|
1337
|
+
reference: `seed ${p.key}`,
|
|
1338
|
+
}),
|
|
1339
|
+
],
|
|
645
1340
|
});
|
|
646
1341
|
} else {
|
|
647
1342
|
add({
|
|
648
1343
|
id: `seed:${p.key}`,
|
|
649
1344
|
part: `seed:${p.key}`,
|
|
650
1345
|
says: `Put ${p.seed} ${t.symbol} into “${p.key}”`,
|
|
651
|
-
build:
|
|
652
|
-
...(t.wsol
|
|
653
|
-
|
|
1346
|
+
build: ctx => [
|
|
1347
|
+
...(t.wsol
|
|
1348
|
+
? [
|
|
1349
|
+
createAssociatedTokenAccountIdempotent(ctx.owner, ctx.owner, NATIVE_MINT),
|
|
1350
|
+
transferLamports(ctx.owner, loot.ata(ctx.owner, NATIVE_MINT), units),
|
|
1351
|
+
syncNative(loot.ata(ctx.owner, NATIVE_MINT)),
|
|
1352
|
+
]
|
|
1353
|
+
: []),
|
|
1354
|
+
loot.payIx({
|
|
1355
|
+
player: ctx.owner,
|
|
1356
|
+
project: project(ctx),
|
|
1357
|
+
mint: ctx.mints[t.key],
|
|
1358
|
+
treasury: ctx.treasury,
|
|
1359
|
+
amount: units,
|
|
1360
|
+
shares: [{ to: { Pool: [p.index] }, bps: 10_000 }],
|
|
1361
|
+
reference: `seed ${p.key}`,
|
|
1362
|
+
programs: { [ctx.mints[t.key]]: tokenProgramOf(t) },
|
|
1363
|
+
}),
|
|
654
1364
|
],
|
|
655
1365
|
});
|
|
656
1366
|
}
|
|
657
1367
|
}
|
|
1368
|
+
/** A seed in a token the project mints (no fixed supply): the owner's wallet holds none of it, so the authority
|
|
1369
|
+
* mints it straight into its account, as a pool's seed is minted into the pool. */
|
|
1370
|
+
const mintedSeed = (id, part, t, units, says, account) => {
|
|
1371
|
+
const signer = m.authority === 'owner' ? owner : 'authority';
|
|
1372
|
+
add({
|
|
1373
|
+
id,
|
|
1374
|
+
part,
|
|
1375
|
+
signers: [signer],
|
|
1376
|
+
says,
|
|
1377
|
+
build: ctx => [
|
|
1378
|
+
loot.moveTokensIx({
|
|
1379
|
+
authority: ctx.authority,
|
|
1380
|
+
project: project(ctx),
|
|
1381
|
+
mint: ctx.mints[t.key],
|
|
1382
|
+
from: { Mint: [ctx.mints[t.key]] },
|
|
1383
|
+
to: { Account: [account(ctx)] },
|
|
1384
|
+
amount: { Fixed: [units] },
|
|
1385
|
+
reference: id.replace(/^seed:[a-z]+:/, 'seed '),
|
|
1386
|
+
}),
|
|
1387
|
+
],
|
|
1388
|
+
});
|
|
1389
|
+
};
|
|
1390
|
+
// A farm's seed: the owner puts it straight into the farm's rewards vault, once the farm is open.
|
|
1391
|
+
for (const f of m.farms) {
|
|
1392
|
+
if (!(Number(f.seed ?? 0) > 0) || live?.seeded?.includes(`farm:${f.key}`)) continue;
|
|
1393
|
+
const t = m.tokens.get(f.rewardsToken);
|
|
1394
|
+
const units = amount(f.seed, t);
|
|
1395
|
+
if (t.gameMints) {
|
|
1396
|
+
mintedSeed(
|
|
1397
|
+
`seed:farm:${f.key}`,
|
|
1398
|
+
`farm:${f.key}`,
|
|
1399
|
+
t,
|
|
1400
|
+
units,
|
|
1401
|
+
`Mint ${f.seed} ${t.symbol} into the rewards of “${f.name}”`,
|
|
1402
|
+
ctx => loot.pda.rewards(loot.pda.farm(project(ctx), f.id)),
|
|
1403
|
+
);
|
|
1404
|
+
continue;
|
|
1405
|
+
}
|
|
1406
|
+
add({
|
|
1407
|
+
id: `seed:farm:${f.key}`,
|
|
1408
|
+
part: `farm:${f.key}`,
|
|
1409
|
+
says: `Put ${f.seed} ${t.symbol} into the rewards of “${f.name}”`,
|
|
1410
|
+
build: ctx => {
|
|
1411
|
+
const vault = loot.pda.rewards(loot.pda.farm(project(ctx), f.id));
|
|
1412
|
+
return [
|
|
1413
|
+
...(t.wsol
|
|
1414
|
+
? [
|
|
1415
|
+
createAssociatedTokenAccountIdempotent(ctx.owner, ctx.owner, NATIVE_MINT),
|
|
1416
|
+
transferLamports(ctx.owner, loot.ata(ctx.owner, NATIVE_MINT), units),
|
|
1417
|
+
syncNative(loot.ata(ctx.owner, NATIVE_MINT)),
|
|
1418
|
+
]
|
|
1419
|
+
: []),
|
|
1420
|
+
loot.payIx({
|
|
1421
|
+
player: ctx.owner,
|
|
1422
|
+
project: project(ctx),
|
|
1423
|
+
mint: ctx.mints[t.key],
|
|
1424
|
+
treasury: ctx.treasury,
|
|
1425
|
+
amount: units,
|
|
1426
|
+
shares: [{ to: { Account: [vault] }, bps: 10_000 }],
|
|
1427
|
+
reference: `seed ${f.key}`,
|
|
1428
|
+
programs: programsOf(ctx),
|
|
1429
|
+
}),
|
|
1430
|
+
];
|
|
1431
|
+
},
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
658
1434
|
// A lottery's seed: the owner puts it straight into the lottery's pot.
|
|
659
1435
|
for (const l of m.lotteries) {
|
|
660
1436
|
if (!(Number(l.seed ?? 0) > 0) || live?.seeded?.includes(`lottery:${l.key}`)) continue;
|
|
661
1437
|
const t = m.tokens.get(l.currency ?? m.currencyKey);
|
|
662
1438
|
const units = amount(l.seed, t);
|
|
1439
|
+
if (t.gameMints) {
|
|
1440
|
+
mintedSeed(
|
|
1441
|
+
`seed:lottery:${l.key}`,
|
|
1442
|
+
`lottery:${l.key}`,
|
|
1443
|
+
t,
|
|
1444
|
+
units,
|
|
1445
|
+
`Mint ${l.seed} ${t.symbol} into the pot of “${l.name}”`,
|
|
1446
|
+
ctx => loot.pda.lotteryPot(loot.pda.lottery(project(ctx), l.id), l.pot),
|
|
1447
|
+
);
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
663
1450
|
add({
|
|
664
1451
|
id: `seed:lottery:${l.key}`,
|
|
665
1452
|
part: `lottery:${l.key}`,
|
|
666
1453
|
says: `Put ${l.seed} ${t.symbol} into the pot of “${l.name}”`,
|
|
667
|
-
build:
|
|
668
|
-
const pot = loot.pda.
|
|
1454
|
+
build: ctx => {
|
|
1455
|
+
const pot = loot.pda.lotteryPot(loot.pda.lottery(project(ctx), l.id), l.pot);
|
|
669
1456
|
return [
|
|
670
|
-
...(t.wsol
|
|
671
|
-
|
|
1457
|
+
...(t.wsol
|
|
1458
|
+
? [
|
|
1459
|
+
createAssociatedTokenAccountIdempotent(ctx.owner, ctx.owner, NATIVE_MINT),
|
|
1460
|
+
transferLamports(ctx.owner, loot.ata(ctx.owner, NATIVE_MINT), units),
|
|
1461
|
+
syncNative(loot.ata(ctx.owner, NATIVE_MINT)),
|
|
1462
|
+
]
|
|
1463
|
+
: []),
|
|
1464
|
+
loot.payIx({
|
|
1465
|
+
player: ctx.owner,
|
|
1466
|
+
project: project(ctx),
|
|
1467
|
+
mint: ctx.mints[t.key],
|
|
1468
|
+
treasury: ctx.treasury,
|
|
1469
|
+
amount: units,
|
|
1470
|
+
shares: [{ to: { Account: [pot] }, bps: 10_000 }],
|
|
1471
|
+
reference: `seed ${l.key}`,
|
|
1472
|
+
programs: programsOf(ctx),
|
|
1473
|
+
}),
|
|
672
1474
|
];
|
|
673
1475
|
},
|
|
674
1476
|
});
|
|
675
1477
|
}
|
|
1478
|
+
// SOL a lottery holds for its rounds' rent (`roundRentOf`: the spec's `roundRent`, or by default what its expected
|
|
1479
|
+
// rounds and their entries take): each round borrows it at its first purchase (its header and payment account), each
|
|
1480
|
+
// purchase's entry, and its ask (the pot's and pools' accounts), back at its close. A lottery live already keeps what
|
|
1481
|
+
// it holds unless the spec names a `roundRent` to add.
|
|
1482
|
+
for (const l of m.lotteries) {
|
|
1483
|
+
const lamports = roundRentOf(l, m);
|
|
1484
|
+
if (lamports <= 0n || (l.roundRent === undefined && (live?.published?.lotteries ?? []).some(x => x.key === l.key)))
|
|
1485
|
+
continue;
|
|
1486
|
+
const sol = Number(lamports) / 1e9;
|
|
1487
|
+
add({
|
|
1488
|
+
id: `rent:lottery:${l.key}`,
|
|
1489
|
+
part: `lottery:${l.key}`,
|
|
1490
|
+
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`,
|
|
1491
|
+
build: ctx => [transferLamports(ctx.owner, loot.pda.lottery(project(ctx), l.id), lamports)],
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
676
1494
|
for (const g of m.grants) {
|
|
677
1495
|
if (!(Number(g.fund ?? 0) > 0) || live?.funded?.includes(g.key)) continue;
|
|
678
1496
|
const lamports = toUnits(g.fund, 9);
|
|
679
|
-
add({
|
|
1497
|
+
add({
|
|
1498
|
+
id: `fund:${g.key}`,
|
|
1499
|
+
part: `fund:${g.key}`,
|
|
1500
|
+
says: `Put ${g.fund} SOL in the funds of the grant “${g.name}” (they pay each claim’s rent)`,
|
|
1501
|
+
build: ctx => [transferLamports(ctx.owner, loot.pda.grant(project(ctx), g.id), lamports)],
|
|
1502
|
+
});
|
|
680
1503
|
}
|
|
681
|
-
|
|
1504
|
+
// The authority's steps go last: the owner approves theirs in one batch, the authority theirs in another.
|
|
1505
|
+
return {
|
|
1506
|
+
steps: [
|
|
1507
|
+
...steps.filter(x => !x.signers.includes('authority')),
|
|
1508
|
+
...steps.filter(x => x.signers.includes('authority')),
|
|
1509
|
+
],
|
|
1510
|
+
model: m,
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
/** The token accounts going live opens (`setupSteps`): [{ who ('the treasury', 'the project' or a wallet), token }], each
|
|
1515
|
+
* once, for the coins the economy takes in. */
|
|
1516
|
+
export function goLiveAccounts(m) {
|
|
1517
|
+
const out = new Map();
|
|
1518
|
+
const add = (who, token) => token && out.set(`${who}|${token}`, { who, token });
|
|
1519
|
+
const takes = (part, token = part.currency ?? m.currencyKey) => {
|
|
1520
|
+
add('the treasury', token);
|
|
1521
|
+
for (const s of sharesOf(part.split)) if (s.kind === 'wallet') add(s.key, token);
|
|
1522
|
+
};
|
|
1523
|
+
for (const b of m.allBoxes) (takes(b), add('the project', b.currency ?? m.currencyKey));
|
|
1524
|
+
for (const p of m.payments) takes(p);
|
|
1525
|
+
for (const l of m.lotteries) if (!l.ticketNft) takes(l, l.ticketCurrency ?? l.currency ?? m.currencyKey);
|
|
1526
|
+
for (const a of m.auctions) if (!a.bidNfts) (takes(a), add('the project', a.currency ?? m.currencyKey));
|
|
1527
|
+
return [...out.values()];
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
/** How many addresses one step adds to the project's lookup table (each extension its own transaction). */
|
|
1531
|
+
const TABLE_GROUP = 24;
|
|
1532
|
+
|
|
1533
|
+
/**
|
|
1534
|
+
* The addresses a project's transactions name most, for its standing lookup table: loot's own fixed accounts (the
|
|
1535
|
+
* platform, the oracle's), the project and its fee box, its treasury and collections, each coin's mint, its vault and
|
|
1536
|
+
* the treasury's account of it, then each pool, item, box and stash. Each a function of the go-live context.
|
|
1537
|
+
*/
|
|
1538
|
+
export function standingAddresses(m) {
|
|
1539
|
+
const fixed = [
|
|
1540
|
+
loot.LOOT_PROGRAM,
|
|
1541
|
+
loot.pda.platform(),
|
|
1542
|
+
loot.pda.identity(),
|
|
1543
|
+
loot.VRF_PROGRAM,
|
|
1544
|
+
loot.VRF_QUEUE,
|
|
1545
|
+
loot.MPL_CORE_PROGRAM,
|
|
1546
|
+
TOKEN_PROGRAM,
|
|
1547
|
+
TOKEN_2022_PROGRAM,
|
|
1548
|
+
ASSOCIATED_TOKEN_PROGRAM,
|
|
1549
|
+
SYSTEM_PROGRAM,
|
|
1550
|
+
SYSVAR_SLOT_HASHES,
|
|
1551
|
+
SYSVAR_INSTRUCTIONS,
|
|
1552
|
+
MEMO_PROGRAM,
|
|
1553
|
+
].map(a => () => a);
|
|
1554
|
+
const programOf = (ctx, key) => programsOf(ctx)[ctx.mints[key]] ?? TOKEN_PROGRAM;
|
|
1555
|
+
return [
|
|
1556
|
+
...fixed,
|
|
1557
|
+
ctx => ctx.project,
|
|
1558
|
+
ctx => loot.pda.fees(ctx.project),
|
|
1559
|
+
ctx => ctx.treasury,
|
|
1560
|
+
...(m.collection ? [ctx => ctx.collection] : []),
|
|
1561
|
+
...[...m.tokens.values()].flatMap(t => [
|
|
1562
|
+
ctx => ctx.mints[t.key],
|
|
1563
|
+
ctx => ctx.mints[t.key] && loot.ata(ctx.project, ctx.mints[t.key], programOf(ctx, t.key)),
|
|
1564
|
+
ctx => ctx.mints[t.key] && loot.ata(ctx.treasury, ctx.mints[t.key], programOf(ctx, t.key)),
|
|
1565
|
+
]),
|
|
1566
|
+
...m.pools.map(p => ctx => loot.pda.pool(ctx.project, p.index)),
|
|
1567
|
+
...m.items.map(it => ctx => loot.pda.item(ctx.project, it.id)),
|
|
1568
|
+
...m.allBoxes.map(b => ctx => loot.pda.box(ctx.project, b.id)),
|
|
1569
|
+
...m.stashes.map(s => ctx => loot.pda.stash(ctx.project, s.id)),
|
|
1570
|
+
].slice(0, 256);
|
|
682
1571
|
}
|
|
683
1572
|
|
|
684
|
-
/**
|
|
685
|
-
|
|
1573
|
+
/** How many Core NFTs one `stash` call puts in (`Piece::another`: three accounts each after the first). */
|
|
1574
|
+
const STASH_GROUP = 6;
|
|
1575
|
+
/** `list` in groups of `n`. */
|
|
1576
|
+
const groupsOf = (list, n) =>
|
|
1577
|
+
Array.from({ length: Math.ceil(list.length / n) }, (_, i) => list.slice(i * n, i * n + n));
|
|
1578
|
+
|
|
1579
|
+
/** 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
|
|
1580
|
+
* rows and each block of 64, and 36 bytes an NFT (its weight and its address). */
|
|
1581
|
+
export const stashBytes = n => (n ? loot.tableRowAt(36, n - 1) + 36 : 51);
|
|
686
1582
|
|
|
687
1583
|
/** 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
1584
|
export const auctionSeller = (a, ctx) => (a.lot?.asset ? ctx.owner : ctx.authority);
|
|
@@ -690,31 +1586,98 @@ export const auctionSeller = (a, ctx) => (a.lot?.asset ? ctx.owner : ctx.authori
|
|
|
690
1586
|
export const recordBytesOf = (kind, body) => {
|
|
691
1587
|
const args = body.args;
|
|
692
1588
|
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 }),
|
|
1589
|
+
Item: () => ({ project: NOBODY, item_id: 0, ...placeholders(args), issued: 0, voided: 0, compressed: false }),
|
|
1590
|
+
LootBox: () => ({ project: NOBODY, box_id: 0, ...placeholders(args), in_flight: 0n, version: 0 }),
|
|
695
1591
|
Recipe: () => ({ project: NOBODY, id: 0, version: 0, ...placeholders(args) }),
|
|
696
1592
|
Schedule: () => ({ project: NOBODY, id: 0, ...placeholders(args), runs: 0 }),
|
|
697
|
-
|
|
698
|
-
|
|
1593
|
+
// Its terms and its round number: each round keeps the rest in a record of its own.
|
|
1594
|
+
Lottery: () => ({
|
|
1595
|
+
project: NOBODY,
|
|
1596
|
+
id: 0,
|
|
1597
|
+
incarnation: 0n,
|
|
1598
|
+
revision: 0,
|
|
1599
|
+
...placeholders(loot.withDefaults({ Lottery: { args } }).Lottery.args),
|
|
1600
|
+
round: 0,
|
|
1601
|
+
pots: 0,
|
|
1602
|
+
owing: NOBODY,
|
|
1603
|
+
}),
|
|
1604
|
+
Grant: () => ({
|
|
1605
|
+
project: NOBODY,
|
|
1606
|
+
id: 0,
|
|
1607
|
+
incarnation: 0n,
|
|
1608
|
+
...placeholders(args),
|
|
1609
|
+
counted: { count: 0, window: 0, in_window: 0 },
|
|
1610
|
+
}),
|
|
699
1611
|
}[kind];
|
|
700
1612
|
return encodedSize(kind, base());
|
|
701
1613
|
};
|
|
1614
|
+
/** A farm's bytes as `open_farm` writes it: its args and its numbers. */
|
|
1615
|
+
export const farmBytes = (f, m) =>
|
|
1616
|
+
encodedSize('Farm', {
|
|
1617
|
+
project: NOBODY,
|
|
1618
|
+
id: 0,
|
|
1619
|
+
farmed: f.stakes === 'collectibles' ? 'Collectibles' : { Tokens: { mint: NOBODY } },
|
|
1620
|
+
...placeholders(farmArgs(f, m)),
|
|
1621
|
+
rewards_mint: NOBODY,
|
|
1622
|
+
rate_carry: 0n,
|
|
1623
|
+
opened_at: 0n,
|
|
1624
|
+
total_weight: 0n,
|
|
1625
|
+
reward_per_weight: 0n,
|
|
1626
|
+
reward_remainder: 0n,
|
|
1627
|
+
owed: 0n,
|
|
1628
|
+
clock: 0n,
|
|
1629
|
+
unreleased: 0n,
|
|
1630
|
+
release_ends: 0n,
|
|
1631
|
+
release_from: 0n,
|
|
1632
|
+
release_base: 0n,
|
|
1633
|
+
keeps_ends: farmEnds(f),
|
|
1634
|
+
});
|
|
702
1635
|
/** Exact initial contest record size for its configured shares, gate, sides and deciders. */
|
|
703
|
-
export const contestBytes = (contest, m) =>
|
|
1636
|
+
export const contestBytes = (contest, m) =>
|
|
1637
|
+
encodedSize('Contest', {
|
|
1638
|
+
project: NOBODY,
|
|
1639
|
+
id: 0,
|
|
1640
|
+
incarnation: 0n,
|
|
1641
|
+
opener: NOBODY,
|
|
1642
|
+
mint: NOBODY,
|
|
1643
|
+
decided_at: 0n,
|
|
1644
|
+
...placeholders(contestArgs(contest, m)),
|
|
1645
|
+
verdict: 'Open',
|
|
1646
|
+
winner: 0,
|
|
1647
|
+
owed: 0n,
|
|
1648
|
+
unclaimed: 0n,
|
|
1649
|
+
pot_won: 0n,
|
|
1650
|
+
prize_won: 0n,
|
|
1651
|
+
sides: Array(contest.sides).fill(0n),
|
|
1652
|
+
share: 0,
|
|
1653
|
+
extras: 0,
|
|
1654
|
+
picks: 0,
|
|
1655
|
+
});
|
|
704
1656
|
/** Args with "@mint:" names standing in as addresses, for sizing. */
|
|
705
|
-
const placeholders =
|
|
1657
|
+
const placeholders = args =>
|
|
1658
|
+
resolve(args, {
|
|
1659
|
+
project: placeholderKey('project'),
|
|
1660
|
+
collection: placeholderKey('new:collection'),
|
|
1661
|
+
tree: placeholderKey('new:tree'),
|
|
1662
|
+
treasury: placeholderKey('treasury'),
|
|
1663
|
+
mints: new Proxy({}, { get: (_, k) => placeholderKey(`mint/${String(k)}`) }),
|
|
1664
|
+
});
|
|
706
1665
|
/** The token program of each Token-2022 mint a context names (the program's account lists name it). */
|
|
707
|
-
const programsOf =
|
|
1666
|
+
const programsOf = ctx => ctx.tokenPrograms ?? {};
|
|
708
1667
|
|
|
709
1668
|
// ---------------------------------------------------------------- contexts
|
|
710
1669
|
|
|
711
1670
|
/** Stand-in addresses for every name a plan uses, so it can be built and sized without a wallet. */
|
|
712
|
-
export function placeholderContext(
|
|
1671
|
+
export function placeholderContext(
|
|
1672
|
+
m,
|
|
1673
|
+
{ rent = bytes => (BigInt(bytes) + ACCOUNT_OVERHEAD) * MOST_RENT_PER_BYTE } = {},
|
|
1674
|
+
) {
|
|
713
1675
|
const ownerKey = placeholderKey('owner');
|
|
714
1676
|
const collection = m.collection ? placeholderKey('new:collection') : null;
|
|
715
1677
|
const tree = compressedTree(m) ? placeholderKey('new:tree') : null;
|
|
716
1678
|
const mints = {};
|
|
717
|
-
for (const t of m.tokens.values())
|
|
1679
|
+
for (const t of m.tokens.values())
|
|
1680
|
+
mints[t.key] = t.mint?.startsWith('@') ? placeholderKey(`new:mint/${t.key}`) : t.mint;
|
|
718
1681
|
return {
|
|
719
1682
|
owner: ownerKey,
|
|
720
1683
|
authority: m.authority === 'owner' ? ownerKey : m.authority,
|
|
@@ -723,8 +1686,15 @@ export function placeholderContext(m, { rent = (bytes) => (BigInt(bytes) + ACCOU
|
|
|
723
1686
|
project: loot.pda.project(ownerKey, m.projectNumber),
|
|
724
1687
|
treasury: m.treasuryWallet ?? ownerKey,
|
|
725
1688
|
mints,
|
|
726
|
-
keys: {
|
|
1689
|
+
keys: {
|
|
1690
|
+
...(collection ? { 'new:collection': collection } : {}),
|
|
1691
|
+
...(tree ? { 'new:tree': tree } : {}),
|
|
1692
|
+
...Object.fromEntries(
|
|
1693
|
+
[...m.tokens.values()].map(t => [`new:mint/${t.key}`, placeholderKey(`new:mint/${t.key}`)]),
|
|
1694
|
+
),
|
|
1695
|
+
},
|
|
727
1696
|
rent,
|
|
1697
|
+
slot: 0n,
|
|
728
1698
|
};
|
|
729
1699
|
}
|
|
730
1700
|
|
|
@@ -750,7 +1720,8 @@ export function madeSoFar(live) {
|
|
|
750
1720
|
}
|
|
751
1721
|
if (!made('project', 'collection')) delete out.collection;
|
|
752
1722
|
if (!made('tree', 'tree')) delete out.tree;
|
|
753
|
-
if (live.mints)
|
|
1723
|
+
if (live.mints)
|
|
1724
|
+
out.mints = Object.fromEntries(Object.entries(live.mints).filter(([key]) => made(`token:${key}`, `token:${key}`)));
|
|
754
1725
|
return out;
|
|
755
1726
|
}
|
|
756
1727
|
|
|
@@ -759,13 +1730,17 @@ export function setupContext(m, { owner: ownerKey, keys, live: given = {}, rent
|
|
|
759
1730
|
const live = madeSoFar(given) ?? {};
|
|
760
1731
|
// A setup goes on only in the owner's own project: never in one a file names for them.
|
|
761
1732
|
const own = loot.pda.project(ownerKey, m.projectNumber);
|
|
762
|
-
if (live.project && live.project !== own)
|
|
1733
|
+
if (live.project && live.project !== own)
|
|
1734
|
+
throw new Error(
|
|
1735
|
+
`The live section names the project ${live.project}, which is not the project ${own} of the wallet ${ownerKey}.`,
|
|
1736
|
+
);
|
|
763
1737
|
const collection = live.collection ?? keys['new:collection'] ?? null;
|
|
764
1738
|
const tree = live.tree ?? keys['new:tree'] ?? null;
|
|
765
1739
|
const mints = {};
|
|
766
1740
|
// A mint the spec names by its address is that one, whatever the live section says; one the setup makes is the
|
|
767
1741
|
// 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())
|
|
1742
|
+
for (const t of m.tokens.values())
|
|
1743
|
+
mints[t.key] = t.mint && !t.mint.startsWith('@') ? t.mint : (live.mints?.[t.key] ?? keys[`new:mint/${t.key}`]);
|
|
769
1744
|
return {
|
|
770
1745
|
owner: ownerKey,
|
|
771
1746
|
authority: m.authority === 'owner' ? ownerKey : m.authority,
|
|
@@ -782,17 +1757,21 @@ export function setupContext(m, { owner: ownerKey, keys, live: given = {}, rent
|
|
|
782
1757
|
// ---------------------------------------------------------------- packing
|
|
783
1758
|
|
|
784
1759
|
/** The bytes of a transaction of `instructions` the owner pays for: what `pack` holds against the budget. */
|
|
785
|
-
export const stepBytes = (instructions, ctx) =>
|
|
1760
|
+
export const stepBytes = (instructions, ctx) =>
|
|
1761
|
+
transactionSize(compileMessage({ payer: ctx.owner, blockhash: placeholderKey('blockhash'), instructions }));
|
|
786
1762
|
|
|
787
1763
|
/** Packs steps into transactions under the size budget, one signer set at a time; each step whole. */
|
|
788
1764
|
export function pack(steps, ctx) {
|
|
789
1765
|
const transactions = [];
|
|
790
1766
|
let current = null;
|
|
791
|
-
const sizeOf =
|
|
1767
|
+
const sizeOf = instructions => stepBytes(instructions, ctx);
|
|
792
1768
|
for (const step of steps) {
|
|
793
1769
|
const instructions = step.build(ctx);
|
|
1770
|
+
// A step with nothing to send (the lookup table of a plan built with no recent slot) is left out.
|
|
1771
|
+
if (!instructions.length) continue;
|
|
794
1772
|
const signerKind = step.signers.includes('authority') ? 'authority' : 'owner';
|
|
795
|
-
|
|
1773
|
+
// A write that names boxes to roll takes all the compute a transaction has: alone.
|
|
1774
|
+
if (current && current.signer === signerKind && !step.alone && !current.alone) {
|
|
796
1775
|
const next = [...current.instructions, ...instructions];
|
|
797
1776
|
if (sizeOf(next) <= BUDGET) {
|
|
798
1777
|
current.instructions = next;
|
|
@@ -805,7 +1784,14 @@ export function pack(steps, ctx) {
|
|
|
805
1784
|
const size = sizeOf(instructions);
|
|
806
1785
|
if (size > BUDGET) throw new Error(`The step “${step.says}” is ${size} bytes: too big for one transaction.`);
|
|
807
1786
|
// `built`: each step's own instructions, in the order of `steps`.
|
|
808
|
-
current = {
|
|
1787
|
+
current = {
|
|
1788
|
+
signer: signerKind,
|
|
1789
|
+
steps: [step],
|
|
1790
|
+
built: [instructions],
|
|
1791
|
+
instructions,
|
|
1792
|
+
signers: [...step.signers],
|
|
1793
|
+
alone: Boolean(step.alone),
|
|
1794
|
+
};
|
|
809
1795
|
transactions.push(current);
|
|
810
1796
|
}
|
|
811
1797
|
for (const t of transactions) t.bytes = sizeOf(t.instructions);
|
|
@@ -820,7 +1806,12 @@ export function plan(economy, { now = Math.floor(Date.now() / 1000), ctx = null,
|
|
|
820
1806
|
const checked = check(economy, { now });
|
|
821
1807
|
if (!checked.ok) return { ok: false, refusals: checked.refusals, warnings: checked.warnings };
|
|
822
1808
|
const { steps, model: m } = setupSteps(economy, { now, only, live });
|
|
823
|
-
if (ctx)
|
|
1809
|
+
if (ctx)
|
|
1810
|
+
ctx.tokenPrograms ??= Object.fromEntries(
|
|
1811
|
+
[...m.tokens.values()]
|
|
1812
|
+
.filter(t => t.program === 'token-2022' && ctx.mints[t.key])
|
|
1813
|
+
.map(t => [ctx.mints[t.key], TOKEN_2022_PROGRAM]),
|
|
1814
|
+
);
|
|
824
1815
|
const context = ctx ?? placeholderContext(m);
|
|
825
1816
|
const transactions = pack(steps, context);
|
|
826
1817
|
return {
|
|
@@ -828,8 +1819,11 @@ export function plan(economy, { now = Math.floor(Date.now() / 1000), ctx = null,
|
|
|
828
1819
|
warnings: checked.warnings,
|
|
829
1820
|
steps,
|
|
830
1821
|
transactions,
|
|
831
|
-
|
|
832
|
-
|
|
1822
|
+
// A wallet approves up to PER_APPROVAL transactions at once: the owner's, then the authority's (its steps go last).
|
|
1823
|
+
approvals:
|
|
1824
|
+
Math.max(1, Math.ceil(transactions.filter(t => t.signer === 'owner').length / PER_APPROVAL)) +
|
|
1825
|
+
Math.ceil(transactions.filter(t => t.signer === 'authority').length / PER_APPROVAL),
|
|
1826
|
+
freshKeys: [...new Set(steps.flatMap(s => s.signers.filter(x => x.startsWith('new:'))))],
|
|
833
1827
|
};
|
|
834
1828
|
}
|
|
835
1829
|
|
|
@@ -841,47 +1835,140 @@ export function playSteps(economy) {
|
|
|
841
1835
|
// A shop item, and any box with one outcome and nothing random, sells in one instruction: price split, prize given.
|
|
842
1836
|
for (const b of m.shop) {
|
|
843
1837
|
const it = m.item.get(b.shopOf);
|
|
844
|
-
add(
|
|
1838
|
+
add(
|
|
1839
|
+
`item:${it.key}`,
|
|
1840
|
+
'buy',
|
|
1841
|
+
'player',
|
|
1842
|
+
it.kind === 'normal'
|
|
1843
|
+
? `buy “${it.name}”: the project’s backend delivers it from the receipt`
|
|
1844
|
+
: `buy “${it.name}”: minted to them at once`,
|
|
1845
|
+
);
|
|
845
1846
|
}
|
|
846
1847
|
for (const b of m.boxes) {
|
|
847
|
-
if (boxBuys(b)) {
|
|
1848
|
+
if (boxBuys(b, m)) {
|
|
848
1849
|
add(`box:${b.key}`, 'buy', 'player', `buy “${b.name}”: what it gives, at once`);
|
|
849
1850
|
continue;
|
|
850
1851
|
}
|
|
851
1852
|
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', '
|
|
1853
|
+
if (boxRolls(b, m)) add(`box:${b.key}`, 'callback_roll', 'the oracle', 'roll it');
|
|
1854
|
+
add(`box:${b.key}`, 'settle', 'the player, a key they named, or the project’s keys', 'carry out what it rolled');
|
|
854
1855
|
}
|
|
855
1856
|
for (const p of m.payments) add(`payment:${p.key}`, 'pay', 'player', `pay “${p.name}”`);
|
|
856
1857
|
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)
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
1858
|
+
for (const s of m.stashes)
|
|
1859
|
+
add(
|
|
1860
|
+
`stash:${s.key}`,
|
|
1861
|
+
'stash',
|
|
1862
|
+
'the owner or the authority',
|
|
1863
|
+
`put an NFT it holds in “${s.name}”, or take one out to any wallet`,
|
|
1864
|
+
);
|
|
1865
|
+
for (const r of prizeRolls(m).filter(r => !['mission', 'recipe'].includes(r.route))) {
|
|
1866
|
+
add(
|
|
1867
|
+
r.by,
|
|
1868
|
+
'ask_roll',
|
|
1869
|
+
'the winner or the project’s keys, who pay the oracle',
|
|
1870
|
+
`ask for the roll of “${m.box.get(r.box)?.name ?? r.box}” a winner got (${r.route})`,
|
|
1871
|
+
);
|
|
1872
|
+
add(r.by, 'settle', 'the winner, a key they named, or the project’s keys', 'carry out what it rolled');
|
|
861
1873
|
}
|
|
862
1874
|
for (const r of m.recipes) add(`recipe:${r.key}`, 'craft', 'player', `craft “${r.name}”`);
|
|
863
|
-
for (const s of m.schedules)
|
|
864
|
-
|
|
1875
|
+
for (const s of m.schedules)
|
|
1876
|
+
add(
|
|
1877
|
+
`schedule:${s.key}`,
|
|
1878
|
+
'run_schedule',
|
|
1879
|
+
'the project’s keys, or who it pays',
|
|
1880
|
+
`run “${s.name ?? s.key}” when it is due`,
|
|
1881
|
+
);
|
|
1882
|
+
for (const l of m.lotteries)
|
|
1883
|
+
(add(`lottery:${l.key}`, 'buy_tickets', 'player', 'buy tickets'),
|
|
1884
|
+
add(`lottery:${l.key}`, 'draw_lottery', 'anyone', 'ask for the draw when it is due'),
|
|
1885
|
+
add(
|
|
1886
|
+
`lottery:${l.key}`,
|
|
1887
|
+
'pull',
|
|
1888
|
+
'anyone',
|
|
1889
|
+
'pay one part: a refund, a place’s part of the pot or its prize, a share owed in common',
|
|
1890
|
+
));
|
|
865
1891
|
for (const f of m.farms) {
|
|
866
|
-
if (f.stakes === 'collectibles')
|
|
867
|
-
|
|
1892
|
+
if (f.stakes === 'collectibles')
|
|
1893
|
+
(add(`farm:${f.key}`, 'stake_nft', 'player', 'stake a collectible'),
|
|
1894
|
+
add(`farm:${f.key}`, 'unstake_nft', 'player', 'take it out once its lock has run'));
|
|
1895
|
+
else
|
|
1896
|
+
(add(`farm:${f.key}`, 'stake_tokens', 'player', 'stake'),
|
|
1897
|
+
add(`farm:${f.key}`, 'unstake_tokens', 'player', 'unstake or collect'),
|
|
1898
|
+
add(`farm:${f.key}`, 'stake_nft', 'player', 'put a collectible on as a booster'));
|
|
868
1899
|
add(`farm:${f.key}`, 'harvest', 'player', 'collect rewards');
|
|
869
1900
|
add(`farm:${f.key}`, 'refresh_seat', 'anyone', 'read a seat’s boosts again');
|
|
1901
|
+
if (farmEnds(f))
|
|
1902
|
+
add(
|
|
1903
|
+
`farm:${f.key}`,
|
|
1904
|
+
'walk_farm',
|
|
1905
|
+
'anyone',
|
|
1906
|
+
'catch the farm up, in parts, when it has passed more end times than a move can walk',
|
|
1907
|
+
);
|
|
870
1908
|
}
|
|
871
1909
|
for (const c of m.contests) {
|
|
872
1910
|
add(`contest:${c.key}`, 'open_contest', 'authority', `open “${c.name}”`);
|
|
873
1911
|
add(`contest:${c.key}`, 'enter', 'player', 'back a side');
|
|
874
|
-
add(
|
|
875
|
-
|
|
1912
|
+
add(
|
|
1913
|
+
`contest:${c.key}`,
|
|
1914
|
+
'decide',
|
|
1915
|
+
'authority (or anyone sending a decider’s verdict)',
|
|
1916
|
+
'name the side that won, or call it off before its result is due',
|
|
1917
|
+
);
|
|
1918
|
+
add(
|
|
1919
|
+
`contest:${c.key}`,
|
|
1920
|
+
'claim',
|
|
1921
|
+
c.stakeNft || c.prize ? 'the winner or the project’s keys' : 'anyone',
|
|
1922
|
+
'pay an entry',
|
|
1923
|
+
);
|
|
876
1924
|
}
|
|
877
1925
|
for (const a of m.auctions) {
|
|
878
|
-
add(
|
|
1926
|
+
add(
|
|
1927
|
+
`auction:${a.key}`,
|
|
1928
|
+
'open_auction',
|
|
1929
|
+
a.lot?.asset ? 'the owner, alone (a collectible of theirs)' : 'the authority, alone (a new unit of an item)',
|
|
1930
|
+
`put the lot up (at go-live)`,
|
|
1931
|
+
);
|
|
879
1932
|
add(`auction:${a.key}`, 'bid', 'player', 'bid: the bid it tops goes back at once');
|
|
880
|
-
add(
|
|
1933
|
+
add(
|
|
1934
|
+
`auction:${a.key}`,
|
|
1935
|
+
'end_auction',
|
|
1936
|
+
'anyone once it is over; its seller, to call off one with no bid',
|
|
1937
|
+
'sell to the high bidder, or send the lot home',
|
|
1938
|
+
);
|
|
881
1939
|
}
|
|
882
|
-
for (const h of m.holds)
|
|
883
|
-
|
|
884
|
-
|
|
1940
|
+
for (const h of m.holds)
|
|
1941
|
+
(add(
|
|
1942
|
+
`hold:${h.key}`,
|
|
1943
|
+
'hold',
|
|
1944
|
+
'player',
|
|
1945
|
+
`hand in a “${h.item}” until ${h.until ?? `${h.for ?? 'the time they choose'} from then`}`,
|
|
1946
|
+
),
|
|
1947
|
+
add(`hold:${h.key}`, 'release', 'authority while it runs, anyone after', 'say where it goes, or send it home'));
|
|
1948
|
+
for (const g of m.grants)
|
|
1949
|
+
add(
|
|
1950
|
+
`grant:${g.key}`,
|
|
1951
|
+
'claim_grant',
|
|
1952
|
+
'the winning wallet, which signs it (with a voucher, a list proof, or an open grant); anyone may pay its fees',
|
|
1953
|
+
`claim “${g.name}”`,
|
|
1954
|
+
);
|
|
1955
|
+
for (const d of m.decisions)
|
|
1956
|
+
add(
|
|
1957
|
+
`decision:${d.key}`,
|
|
1958
|
+
{
|
|
1959
|
+
payout: 'move_tokens',
|
|
1960
|
+
burn: 'move_tokens',
|
|
1961
|
+
issue: 'move_tokens',
|
|
1962
|
+
make: 'collectible',
|
|
1963
|
+
unmake: 'collectible',
|
|
1964
|
+
set: 'collectible',
|
|
1965
|
+
award: 'release',
|
|
1966
|
+
decide: 'decide',
|
|
1967
|
+
roll: 'roll',
|
|
1968
|
+
open_contest: 'open_contest',
|
|
1969
|
+
}[d.does],
|
|
1970
|
+
'authority',
|
|
1971
|
+
d.when,
|
|
1972
|
+
);
|
|
885
1973
|
return rows;
|
|
886
1974
|
}
|
|
887
|
-
|