@xgamesfun/solana 0.24.0 → 0.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DISCLAIMER.md +1 -1
- package/NOTICE +4 -4
- package/README.md +100 -52
- package/bin/xsolana.mjs +32 -11
- package/examples/blind-forge.json +2 -4
- package/examples/coin-flip-arena.json +2 -5
- package/examples/duel-arena.json +1 -4
- package/examples/fair-presale.json +1 -3
- package/examples/hero-trials.json +14 -2
- package/examples/ideas.json +788 -788
- package/examples/nft-raffle.json +1 -0
- package/examples/partner-vault.json +19 -3
- package/examples/sacrifice-raffle.json +2 -10
- package/examples/shelter-fair.json +1 -0
- package/examples/stakers-draw.json +4 -7
- package/examples/trade-in-shop.json +2 -3
- package/js/approve/details.mjs +117 -48
- package/js/approve/ed25519.mjs +5 -1
- package/js/approve/live-check.mjs +83 -26
- package/js/approve/page.html +217 -78
- package/js/approve/page.js +43 -15
- package/js/approve/server.mjs +276 -74
- package/js/chain/base58.js +7 -1
- package/js/chain/borsh.js +15 -11
- package/js/chain/bytes.js +11 -8
- package/js/chain/core.js +21 -5
- package/js/chain/curve.js +1 -1
- package/js/chain/lookup.js +118 -36
- package/js/chain/mint.js +6 -2
- package/js/chain/pda.js +2 -2
- package/js/chain/programs.js +6 -6
- package/js/chain/rpc.js +40 -14
- package/js/chain/token.js +13 -2
- package/js/chain/token2022.js +55 -16
- package/js/chain/tx.js +32 -22
- package/js/client/accounts.js +270 -80
- package/js/client/compressed.js +4 -3
- package/js/client/grant.js +165 -40
- package/js/client/idl/loot.js +1 -1
- package/js/client/idl/loot.json +104 -16
- package/js/client/ix.js +2121 -457
- package/js/client/pda.js +20 -18
- package/js/client/policy.js +12 -5
- package/js/client/random.js +2 -2
- package/js/client/sizes.js +220 -65
- package/js/client/traits.js +14 -5
- package/js/connect/connect.mjs +59 -17
- package/js/economy/check.mjs +3884 -690
- package/js/economy/cost.mjs +381 -57
- package/js/economy/examples.mjs +176 -35
- package/js/economy/explain.mjs +468 -110
- package/js/economy/guide.mjs +282 -59
- package/js/economy/legal.mjs +12 -6
- package/js/economy/live.mjs +62 -20
- package/js/economy/model.mjs +320 -97
- package/js/economy/plan.mjs +1022 -203
- package/js/economy/review.mjs +760 -0
- package/js/economy/schema.json +134 -372
- package/js/economy/secrets.mjs +17 -7
- package/js/economy/simulate.mjs +771 -178
- package/js/economy/trust.mjs +15 -0
- package/js/economy/txsize.mjs +15 -3
- package/js/economy/validate.mjs +61 -24
- package/js/index.js +1 -0
- package/js/mcp/server.mjs +43 -15
- package/js/mcp/tools.mjs +318 -80
- package/js/play/index.js +1960 -461
- package/js/play/keys.js +35 -9
- package/js/results/index.js +225 -32
- package/js/results/server.js +230 -0
- package/js/results/wire.js +64 -16
- package/package.json +6 -4
- package/release.json +5 -5
- package/skills/xsolana-economy/SKILL.md +52 -22
- package/skills/xsolana-economy/references/after-launch.md +32 -21
- package/skills/xsolana-economy/references/apps.md +47 -36
- package/skills/xsolana-economy/references/sentence-to-spec.md +149 -129
- package/skills/xsolana-economy/references/telling-the-person.md +15 -6
- package/skills/xsolana-economy/references/trust-and-cheating.md +126 -0
- package/types/chain/core.d.ts +1 -1
- package/types/chain/lookup.d.ts +1 -1
- package/types/client/accounts.d.ts +24 -3
- package/types/client/grant.d.ts +2 -2
- package/types/client/idl/loot.d.ts +22 -0
- package/types/client/ix.d.ts +54 -45
- package/types/client/pda.d.ts +3 -1
- package/types/economy/cost.d.mts +1 -1
- package/types/economy/explain.d.mts +3 -0
- package/types/economy/guide.d.mts +5 -0
- package/types/economy/model.d.mts +48 -3
- package/types/economy/plan.d.mts +44 -2
- package/types/economy/review.d.mts +45 -0
- package/types/economy/trust.d.mts +8 -0
- package/types/index.d.ts +1 -0
- package/types/mcp/tools.d.mts +62 -0
- package/types/play/index.d.ts +14 -25
- package/types/results/index.d.ts +8 -7
- package/types/results/server.d.ts +126 -0
- package/types/results/wire.d.ts +1 -1
package/js/client/ix.js
CHANGED
|
@@ -18,16 +18,61 @@
|
|
|
18
18
|
import { addressBytes, encodeBase58, NO_ADDRESS } from '../chain/base58.js';
|
|
19
19
|
import { encodeAccount, encodeInstruction } from '../chain/borsh.js';
|
|
20
20
|
import { compareBytes, concat, intLE, readUintLE, toBigInt, uintLE, utf8 } from '../chain/bytes.js';
|
|
21
|
-
import {
|
|
22
|
-
|
|
21
|
+
import {
|
|
22
|
+
addressWithSeed,
|
|
23
|
+
ASSOCIATED_TOKEN_PROGRAM,
|
|
24
|
+
findProgramAddress,
|
|
25
|
+
METAPLEX_METADATA_PROGRAM,
|
|
26
|
+
SYSTEM_PROGRAM,
|
|
27
|
+
SYSVAR_INSTRUCTIONS,
|
|
28
|
+
SYSVAR_SLOT_HASHES,
|
|
29
|
+
TOKEN_2022_PROGRAM,
|
|
30
|
+
TOKEN_PROGRAM,
|
|
31
|
+
} from '../chain/pda.js';
|
|
32
|
+
import {
|
|
33
|
+
allocateWithSeedIx,
|
|
34
|
+
createAccountWithSeedIx,
|
|
35
|
+
initializeAccount3Ix,
|
|
36
|
+
MINT_BYTES,
|
|
37
|
+
TOKEN_ACCOUNT_BYTES,
|
|
38
|
+
} from '../chain/token.js';
|
|
23
39
|
import { createAssociatedTokenAccountIdempotent, meta, transferLamports } from '../chain/programs.js';
|
|
24
40
|
import { sha256 } from '../chain/sha256.js';
|
|
25
41
|
import { MEMO_PROGRAM } from '../chain/token2022.js';
|
|
26
42
|
import IDL from './idl/loot.js';
|
|
27
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
chosenAtPurchase,
|
|
45
|
+
CLAIMED,
|
|
46
|
+
decode,
|
|
47
|
+
ENDED,
|
|
48
|
+
idleRound,
|
|
49
|
+
leftoverTo,
|
|
50
|
+
lotteryPots,
|
|
51
|
+
MAX_POTS,
|
|
52
|
+
nftSlot,
|
|
53
|
+
ownStash,
|
|
54
|
+
paymentMint,
|
|
55
|
+
placesPulled,
|
|
56
|
+
potIdOf,
|
|
57
|
+
prizePlacesOwed,
|
|
58
|
+
roundFinished,
|
|
59
|
+
roundWinners,
|
|
60
|
+
shareOf,
|
|
61
|
+
tokenRun,
|
|
62
|
+
} from './accounts.js';
|
|
28
63
|
import { stream, TAG, weighted } from './random.js';
|
|
29
64
|
import { tableBudget } from './sizes.js';
|
|
30
|
-
import {
|
|
65
|
+
import {
|
|
66
|
+
ata,
|
|
67
|
+
BUBBLEGUM_PROGRAM,
|
|
68
|
+
COMPRESSION_PROGRAM,
|
|
69
|
+
LOOT_PROGRAM,
|
|
70
|
+
MPL_CORE_PROGRAM,
|
|
71
|
+
NOOP_PROGRAM,
|
|
72
|
+
pda,
|
|
73
|
+
vault,
|
|
74
|
+
VRF_QUEUE,
|
|
75
|
+
} from './pda.js';
|
|
31
76
|
|
|
32
77
|
// The options of a call made without any: read-only, and typed so the generated declarations take any option.
|
|
33
78
|
const NO_OPTIONS = Object.freeze(/** @type {Record<string, any>} */ ({}));
|
|
@@ -40,7 +85,7 @@ export const TOKEN_METADATA_PROGRAM = METAPLEX_METADATA_PROGRAM;
|
|
|
40
85
|
const ZERO_REFERENCE = new Uint8Array(32);
|
|
41
86
|
/** The default address: names nobody, and nobody signs as it. */
|
|
42
87
|
const NOBODY_KEY = '11111111111111111111111111111111';
|
|
43
|
-
const hashToAddress =
|
|
88
|
+
const hashToAddress = hash => encodeBase58(hash);
|
|
44
89
|
|
|
45
90
|
/** 32 bytes a project names a call by (a match id, a payout's reason). Zeros when not given. */
|
|
46
91
|
export function reference(value) {
|
|
@@ -53,8 +98,8 @@ export function reference(value) {
|
|
|
53
98
|
|
|
54
99
|
// ---------------------------------------------------------------- any instruction
|
|
55
100
|
|
|
56
|
-
const instructionDef =
|
|
57
|
-
const def = IDL.instructions.find(
|
|
101
|
+
const instructionDef = name => {
|
|
102
|
+
const def = IDL.instructions.find(i => i.name === name);
|
|
58
103
|
if (!def) throw new Error(`Unknown loot instruction ${name}`);
|
|
59
104
|
return def;
|
|
60
105
|
};
|
|
@@ -64,7 +109,8 @@ const instructionDef = (name) => {
|
|
|
64
109
|
const BY_TAG = [...IDL.instructions].sort((a, b) => b.discriminator.length - a.discriminator.length);
|
|
65
110
|
|
|
66
111
|
/** The IDL entry of the loot instruction whose data is `data` (its tag first), or null. */
|
|
67
|
-
export const instructionOf =
|
|
112
|
+
export const instructionOf = data =>
|
|
113
|
+
BY_TAG.find(i => i.discriminator.length <= data.length && i.discriminator.every((b, j) => data[j] === b)) ?? null;
|
|
68
114
|
|
|
69
115
|
/**
|
|
70
116
|
* An instruction's tail (`tokens::tail`): the accounts every NFT move hands Core (what a check its collection carries
|
|
@@ -74,19 +120,19 @@ export const instructionOf = (data) => BY_TAG.find((i) => i.discriminator.length
|
|
|
74
120
|
*/
|
|
75
121
|
export function tailOf(extra = []) {
|
|
76
122
|
if (!extra.length) return [];
|
|
77
|
-
return [meta(MEMO_PROGRAM, false, false), ...extra.filter(
|
|
123
|
+
return [meta(MEMO_PROGRAM, false, false), ...extra.filter(m => m.pubkey !== MEMO_PROGRAM)];
|
|
78
124
|
}
|
|
79
125
|
|
|
80
126
|
/** The accounts of an instruction in order, each with its group (`checkout`, `fee`, `side`, `piece`) or ''. */
|
|
81
127
|
function leaves(list, group = '') {
|
|
82
|
-
return list.flatMap(
|
|
128
|
+
return list.flatMap(a => (a.accounts ? leaves(a.accounts, a.name) : [{ ...a, group }]));
|
|
83
129
|
}
|
|
84
130
|
|
|
85
131
|
/** Accounts that are the same wherever an instruction names them. The program checks each by address, which the
|
|
86
132
|
* IDL doesn't carry. */
|
|
87
133
|
const DEFAULTS = {
|
|
88
134
|
platform: () => pda.platform(),
|
|
89
|
-
fees:
|
|
135
|
+
fees: known => known.project && pda.fees(known.project),
|
|
90
136
|
program_identity: () => pda.identity(),
|
|
91
137
|
vrf_program_identity: () => pda.oracleIdentity(),
|
|
92
138
|
oracle_queue: () => VRF_QUEUE,
|
|
@@ -108,7 +154,7 @@ const SEED_SIZES = { u8: 1, u16: 2, u32: 4, u64: 8 };
|
|
|
108
154
|
function seedBytes(seed, known, args, def) {
|
|
109
155
|
if (seed.kind === 'const') return Uint8Array.from(seed.value);
|
|
110
156
|
if (seed.kind === 'arg') {
|
|
111
|
-
const arg = def.args.find(
|
|
157
|
+
const arg = def.args.find(a => a.name === seed.path);
|
|
112
158
|
if (!arg || !(seed.path in args)) return null;
|
|
113
159
|
if (arg.type === 'pubkey') return addressBytes(args[seed.path]);
|
|
114
160
|
if (SEED_SIZES[arg.type]) return uintLE(args[seed.path], SEED_SIZES[arg.type]);
|
|
@@ -118,7 +164,7 @@ function seedBytes(seed, known, args, def) {
|
|
|
118
164
|
// "side.farm" names the account `farm` of the group `side`; a path into an account's data ("order.requested_slot")
|
|
119
165
|
// can't be resolved here.
|
|
120
166
|
const parts = seed.path.split('.');
|
|
121
|
-
const groups = new Set(def.accounts.filter(
|
|
167
|
+
const groups = new Set(def.accounts.filter(a => a.accounts).map(a => a.name));
|
|
122
168
|
const name = parts.length === 1 ? parts[0] : groups.has(parts[0]) && parts.length === 2 ? parts[1] : null;
|
|
123
169
|
return name && known[name] ? addressBytes(known[name]) : null;
|
|
124
170
|
}
|
|
@@ -130,7 +176,12 @@ function seedBytes(seed, known, args, def) {
|
|
|
130
176
|
* Options: `remaining` (metas after the instruction's own), `writable` and `signer` (leaf names whose flag is set
|
|
131
177
|
* on, beyond the IDL's), `readonly` (leaf names whose writable flag is set off).
|
|
132
178
|
*/
|
|
133
|
-
export function lootIx(
|
|
179
|
+
export function lootIx(
|
|
180
|
+
name,
|
|
181
|
+
args = {},
|
|
182
|
+
accounts = {},
|
|
183
|
+
{ remaining = [], writable = [], signer = [], readonly = [] } = {},
|
|
184
|
+
) {
|
|
134
185
|
const def = instructionDef(name);
|
|
135
186
|
const list = leaves(def.accounts);
|
|
136
187
|
const known = {};
|
|
@@ -150,7 +201,7 @@ export function lootIx(name, args = {}, accounts = {}, { remaining = [], writabl
|
|
|
150
201
|
continue;
|
|
151
202
|
}
|
|
152
203
|
if (a.pda && !a.pda.program) {
|
|
153
|
-
const seeds = a.pda.seeds.map(
|
|
204
|
+
const seeds = a.pda.seeds.map(s => seedBytes(s, known, args, def));
|
|
154
205
|
if (seeds.every(Boolean)) {
|
|
155
206
|
known[a.name] = findProgramAddress(seeds, LOOT_PROGRAM)[0];
|
|
156
207
|
progress = true;
|
|
@@ -159,7 +210,7 @@ export function lootIx(name, args = {}, accounts = {}, { remaining = [], writabl
|
|
|
159
210
|
}
|
|
160
211
|
if (!progress) break;
|
|
161
212
|
}
|
|
162
|
-
const keys = list.map(
|
|
213
|
+
const keys = list.map(a => {
|
|
163
214
|
const key = known[a.name];
|
|
164
215
|
if (key === null || (key === undefined && a.optional)) {
|
|
165
216
|
if (!a.optional) throw new Error(`${name}: ${a.name} is not optional`);
|
|
@@ -177,7 +228,10 @@ export function lootIx(name, args = {}, accounts = {}, { remaining = [], writabl
|
|
|
177
228
|
const tokenProgramOf = (programs, mint) => programs?.[mint] ?? TOKEN_PROGRAM;
|
|
178
229
|
|
|
179
230
|
/** A wallet, then its associated token account of `mint`: where money owed to it goes. */
|
|
180
|
-
const walletAccounts = (wallet, mint, programs) => [
|
|
231
|
+
const walletAccounts = (wallet, mint, programs) => [
|
|
232
|
+
meta(wallet, false, false),
|
|
233
|
+
meta(ata(wallet, mint, tokenProgramOf(programs, mint)), false, true),
|
|
234
|
+
];
|
|
181
235
|
|
|
182
236
|
/**
|
|
183
237
|
* The accounts one destination needs, in the order the program reads them: a pool, a named token account, a wallet
|
|
@@ -190,7 +244,7 @@ const walletAccounts = (wallet, mint, programs) => [meta(wallet, false, false),
|
|
|
190
244
|
export function destinationAccounts(project, to, mint, { holders = {}, programs, treasury } = NO_OPTIONS) {
|
|
191
245
|
if (to === 'Burn') return [];
|
|
192
246
|
if (to === 'Treasury') {
|
|
193
|
-
if (!treasury) throw new Error(
|
|
247
|
+
if (!treasury) throw new Error("A treasury share needs the project's treasury wallet (pass `treasury`).");
|
|
194
248
|
return walletAccounts(treasury, mint, programs);
|
|
195
249
|
}
|
|
196
250
|
if (to.Pool) return [meta(pda.pool(project, to.Pool[0]), false, true)];
|
|
@@ -199,7 +253,12 @@ export function destinationAccounts(project, to, mint, { holders = {}, programs,
|
|
|
199
253
|
if (to.Holder) {
|
|
200
254
|
const asset = to.Holder[0];
|
|
201
255
|
const holder = holders[asset];
|
|
202
|
-
return [
|
|
256
|
+
return [
|
|
257
|
+
meta(asset, false, false),
|
|
258
|
+
...(holder
|
|
259
|
+
? walletAccounts(holder, mint, programs)
|
|
260
|
+
: [meta(NO_ADDRESS, false, false), meta(NO_ADDRESS, false, false)]),
|
|
261
|
+
];
|
|
203
262
|
}
|
|
204
263
|
if (to.Chosen) return [];
|
|
205
264
|
throw new Error(`Unknown destination ${JSON.stringify(to)}`);
|
|
@@ -211,7 +270,10 @@ export function destinationAccounts(project, to, mint, { holders = {}, programs,
|
|
|
211
270
|
export const MAX_SHARES = 8;
|
|
212
271
|
/** `shares`, refused here when a split kept to be paid later holds more than `MAX_SHARES`. */
|
|
213
272
|
const stored = (shares = []) => {
|
|
214
|
-
if (shares.length > MAX_SHARES)
|
|
273
|
+
if (shares.length > MAX_SHARES)
|
|
274
|
+
throw new Error(
|
|
275
|
+
`A split kept to be paid later holds at most ${MAX_SHARES} shares; this one has ${shares.length} (BadShares).`,
|
|
276
|
+
);
|
|
215
277
|
return shares;
|
|
216
278
|
};
|
|
217
279
|
|
|
@@ -222,13 +284,18 @@ export const MAX_ITEM_TRAITS = 64;
|
|
|
222
284
|
export const MAX_TRAIT_KEY = 64;
|
|
223
285
|
/** An item's `traits`, refused here past the program's caps (`BadItem` when the owner signs). */
|
|
224
286
|
const traitsInCaps = (traits = []) => {
|
|
225
|
-
if (traits.length > MAX_ITEM_TRAITS)
|
|
226
|
-
|
|
227
|
-
|
|
287
|
+
if (traits.length > MAX_ITEM_TRAITS)
|
|
288
|
+
throw new Error(`An item draws at most ${MAX_ITEM_TRAITS} traits; this one draws ${traits.length} (BadItem).`);
|
|
289
|
+
const long = traits.find(t => utf8(String(t.key)).length > MAX_TRAIT_KEY);
|
|
290
|
+
if (long)
|
|
291
|
+
throw new Error(
|
|
292
|
+
`A trait's name is at most ${MAX_TRAIT_KEY} bytes; “${String(long.key).slice(0, 24)}…” is ${utf8(String(long.key)).length} (BadItem).`,
|
|
293
|
+
);
|
|
228
294
|
return traits;
|
|
229
295
|
};
|
|
230
296
|
|
|
231
|
-
export const shareAccounts = (project, shares, mint, options) =>
|
|
297
|
+
export const shareAccounts = (project, shares, mint, options) =>
|
|
298
|
+
shares.flatMap(s => destinationAccounts(project, s.to, mint, options));
|
|
232
299
|
|
|
233
300
|
/**
|
|
234
301
|
* A split with the buyer's choice written in, as the program does at `open_box` and `buy_tickets`: a wallet, the
|
|
@@ -236,15 +303,18 @@ export const shareAccounts = (project, shares, mint, options) => shares.flatMap(
|
|
|
236
303
|
* contest's decision names its wallet the same way (`decideIx`).
|
|
237
304
|
*/
|
|
238
305
|
export const chosenShares = (shares, choice) =>
|
|
239
|
-
shares.map(
|
|
306
|
+
shares.map(s => {
|
|
240
307
|
if (!s.to.Chosen) return s;
|
|
241
|
-
if (!choice && s.to.Chosen.item != null)
|
|
308
|
+
if (!choice && s.to.Chosen.item != null)
|
|
309
|
+
throw new Error(
|
|
310
|
+
'This share pays the holder of a collectible the buyer names: name one (`chosenRecipient`, `chosen`).',
|
|
311
|
+
);
|
|
242
312
|
if (!choice) return { ...s, to: 'Treasury' };
|
|
243
313
|
return { ...s, to: s.to.Chosen.item == null ? { Wallet: [choice] } : { Holder: [choice] } };
|
|
244
314
|
});
|
|
245
315
|
|
|
246
316
|
/** Whether a split burns, so the mint is written: it has a burn share. */
|
|
247
|
-
export const sharesBurn =
|
|
317
|
+
export const sharesBurn = shares => shares.some(s => s.to === 'Burn');
|
|
248
318
|
|
|
249
319
|
/**
|
|
250
320
|
* The account each prize of a box's entries comes with at the roll, in entry order and each entry's prizes in order:
|
|
@@ -255,20 +325,25 @@ export const sharesBurn = (shares) => shares.some((s) => s.to === 'Burn');
|
|
|
255
325
|
*/
|
|
256
326
|
export function entryAccounts(project, entries, items = {}) {
|
|
257
327
|
let budget = Math.max(0, tableBudget(entries));
|
|
258
|
-
return entries.flatMap(({ prizes }) =>
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
328
|
+
return entries.flatMap(({ prizes }) =>
|
|
329
|
+
prizes.flatMap(prize => {
|
|
330
|
+
// An item, compressed or not; a fresh one's tables after it, in the order of its traits (`items` maps an item's id
|
|
331
|
+
// to its decoded Item: one left out is taken as not fresh).
|
|
332
|
+
const id = prize.Item?.[0] ?? prize.Compressed?.item;
|
|
333
|
+
if (id !== undefined) {
|
|
334
|
+
const tables = traitTables(items[id]);
|
|
335
|
+
const fits = tables.length <= budget;
|
|
336
|
+
if (fits) budget -= tables.length;
|
|
337
|
+
return [
|
|
338
|
+
meta(pda.item(project, id), false, true),
|
|
339
|
+
...(fits ? tables : []).map(t => meta(pda.table(project, t), false, true)),
|
|
340
|
+
];
|
|
341
|
+
}
|
|
342
|
+
if (prize.Tokens?.from.Pool) return [meta(pda.pool(project, prize.Tokens.from.Pool[0]), false, true)];
|
|
343
|
+
const stash = prize.Nft?.stash ?? prize.Stash?.[0];
|
|
344
|
+
return stash === undefined ? [] : [meta(pda.stash(project, stash), false, true)];
|
|
345
|
+
}),
|
|
346
|
+
);
|
|
272
347
|
}
|
|
273
348
|
|
|
274
349
|
/**
|
|
@@ -277,18 +352,23 @@ export function entryAccounts(project, entries, items = {}) {
|
|
|
277
352
|
* pool not opened yet has none and is left out).
|
|
278
353
|
*/
|
|
279
354
|
export function prizeAccounts(project, entries, poolMints = {}, programs) {
|
|
280
|
-
return prizeMints(entries, poolMints)
|
|
355
|
+
return prizeMints(entries, poolMints)
|
|
356
|
+
.map(({ mint, minted }) => [
|
|
357
|
+
meta(mint, false, minted),
|
|
358
|
+
meta(vault(project, mint, tokenProgramOf(programs, mint)), false, true),
|
|
359
|
+
])
|
|
360
|
+
.flat();
|
|
281
361
|
}
|
|
282
362
|
|
|
283
363
|
/** The mints a box's tokens are in, in the order their prizes first name them: { mint, minted }. */
|
|
284
364
|
function prizeMints(entries, poolMints = {}) {
|
|
285
365
|
const mints = [];
|
|
286
|
-
for (const prize of entries.flatMap(
|
|
366
|
+
for (const prize of entries.flatMap(e => e.prizes)) {
|
|
287
367
|
const from = prize.Tokens?.from;
|
|
288
368
|
if (!from) continue;
|
|
289
369
|
const mint = from.Mint ? from.Mint[0] : poolMints[from.Pool[0]];
|
|
290
370
|
if (!mint) continue;
|
|
291
|
-
const seen = mints.find(
|
|
371
|
+
const seen = mints.find(m => m.mint === mint);
|
|
292
372
|
if (seen) seen.minted ||= Boolean(from.Mint);
|
|
293
373
|
else mints.push({ mint, minted: Boolean(from.Mint) });
|
|
294
374
|
}
|
|
@@ -297,19 +377,20 @@ function prizeMints(entries, poolMints = {}) {
|
|
|
297
377
|
|
|
298
378
|
/** Each pool a box's prizes name, mapped to its mint: `poolMints`'s, or the box's own currency when it isn't given. */
|
|
299
379
|
function boxPoolMints(box, poolMints) {
|
|
300
|
-
const pools = box.entries.flatMap(
|
|
301
|
-
return Object.fromEntries(pools.map(
|
|
380
|
+
const pools = box.entries.flatMap(e => e.prizes.flatMap(p => (p.Tokens?.from.Pool ? [p.Tokens.from.Pool[0]] : [])));
|
|
381
|
+
return Object.fromEntries(pools.map(pool => [pool, poolMints && pool in poolMints ? poolMints[pool] : box.currency]));
|
|
302
382
|
}
|
|
303
383
|
|
|
304
384
|
/** Whether a box leaves its outcome to the oracle: more than one entry, or an entry gives a stash's NFT at random. */
|
|
305
|
-
export const boxRolls =
|
|
385
|
+
export const boxRolls = entries => entries.length > 1 || entries.some(e => e.prizes.some(p => p.Stash));
|
|
306
386
|
|
|
307
387
|
/** The tables a fresh item's traits draw from, in the order of its traits (none for an item whose units are alike). */
|
|
308
|
-
export const traitTables =
|
|
388
|
+
export const traitTables = item => (item?.traits ?? []).flatMap(t => (t.from?.Table ? [t.from.Table[0]] : []));
|
|
309
389
|
|
|
310
390
|
/** Whether a box gives a fresh item (one with traits): its units' traits are drawn by the oracle (`items` maps an
|
|
311
391
|
* item's id to its decoded Item, as `entryAccounts`). */
|
|
312
|
-
export const boxFresh = (entries, items = {}) =>
|
|
392
|
+
export const boxFresh = (entries, items = {}) =>
|
|
393
|
+
entries.some(e => e.prizes.some(p => (items[p.Item?.[0] ?? p.Compressed?.item]?.traits ?? []).length > 0));
|
|
313
394
|
|
|
314
395
|
/**
|
|
315
396
|
* What one prize a settle hands over may open or mint, at most: a collectible's Core asset with the program's plugins
|
|
@@ -327,10 +408,16 @@ export const SETTLE_RENT_UNIT = 6_000_000n;
|
|
|
327
408
|
* each. A throwaway key that settles is funded with it, and gives back what it doesn't spend.
|
|
328
409
|
*/
|
|
329
410
|
export function settleRent(box, opens = 1, items = {}) {
|
|
330
|
-
const rents =
|
|
331
|
-
|
|
332
|
-
const
|
|
333
|
-
|
|
411
|
+
const rents = p =>
|
|
412
|
+
Boolean(p.Item || p.Nft || p.Stash || (p.Compressed && (items[p.Compressed.item]?.traits ?? []).length));
|
|
413
|
+
const most = Math.max(0, ...box.entries.map(e => e.prizes.filter(rents).length));
|
|
414
|
+
const sources = new Set(
|
|
415
|
+
box.entries.flatMap(e => e.prizes.flatMap(p => (p.Tokens ? [JSON.stringify(p.Tokens.from)] : []))),
|
|
416
|
+
);
|
|
417
|
+
return (
|
|
418
|
+
SETTLE_RENT_UNIT *
|
|
419
|
+
(BigInt(most * boxDraws(box) * Math.max(1, opens)) + BigInt(sources.size) + (box.subject ? 1n : 0n))
|
|
420
|
+
);
|
|
334
421
|
}
|
|
335
422
|
|
|
336
423
|
/**
|
|
@@ -349,11 +436,11 @@ export function rollNonce(order, requestedSlot, k) {
|
|
|
349
436
|
* taken until it settles or is refunded. `openBoxIx` makes a box's roll on one there; a mission's roll (`unstakeNftIx`)
|
|
350
437
|
* and a recipe's (`craftIx`) take it as their `nonce`.
|
|
351
438
|
*/
|
|
352
|
-
export const nonceOn =
|
|
439
|
+
export const nonceOn = asset => readUintLE(addressBytes(asset), 0, 8);
|
|
353
440
|
|
|
354
441
|
/** Whether a box's roll holds the NFT it rolls on, or moves its numbers: an entry burns it, gives it
|
|
355
442
|
* away or moves its numbers. Its order is at the NFT's nonce (`nonceOn`). */
|
|
356
|
-
export const boxHolds =
|
|
443
|
+
export const boxHolds = box => (box.fates ?? []).some(f => f.then != null || (f.shifts ?? []).length > 0);
|
|
357
444
|
|
|
358
445
|
/**
|
|
359
446
|
* The nonce of the order a roll a sender names is made in: a craft's roll on a collectible kept (`RollOn`) and a
|
|
@@ -366,7 +453,7 @@ export const namedRollNonce = (box, asset, named) => {
|
|
|
366
453
|
};
|
|
367
454
|
|
|
368
455
|
/** Whether a limit for each wallet ({ total, per_window, window }) limits anything. */
|
|
369
|
-
const limits =
|
|
456
|
+
const limits = perWallet => Boolean(perWallet && (Number(perWallet.total) || Number(perWallet.per_window)));
|
|
370
457
|
|
|
371
458
|
/**
|
|
372
459
|
* The accounts of an NFT the player shows, for a gate or for odds: right after the instruction's other accounts.
|
|
@@ -379,8 +466,16 @@ export function shownAccounts(project, shown) {
|
|
|
379
466
|
if (!shown) return [];
|
|
380
467
|
if (typeof shown === 'string') return [meta(shown, false, false)];
|
|
381
468
|
if (shown.asset) return [meta(shown.asset, false, false), meta(pda.sheet(project, shown.asset), false, false)];
|
|
382
|
-
if (shown.compressed)
|
|
383
|
-
|
|
469
|
+
if (shown.compressed)
|
|
470
|
+
return [shown.compressed, pda.leaf(shown.compressed), shown.tree, pda.sheet(project, shown.compressed)].map(key =>
|
|
471
|
+
meta(key, false, false),
|
|
472
|
+
);
|
|
473
|
+
return [
|
|
474
|
+
shown.mint,
|
|
475
|
+
tokenMetadata.metadata(shown.mint),
|
|
476
|
+
ata(shown.owner, shown.mint, TOKEN_PROGRAM),
|
|
477
|
+
pda.sheet(project, shown.mint),
|
|
478
|
+
].map(key => meta(key, false, false));
|
|
384
479
|
}
|
|
385
480
|
|
|
386
481
|
/**
|
|
@@ -389,9 +484,11 @@ export function shownAccounts(project, shown) {
|
|
|
389
484
|
* one, or for a box's odds.
|
|
390
485
|
*/
|
|
391
486
|
export const gateAccounts = (project, { gate, wallet, shown, listProof = [] } = NO_OPTIONS) => [
|
|
392
|
-
...listProof.map(
|
|
487
|
+
...listProof.map(node => meta(typeof node === 'string' ? node : encodeBase58(node), false, false)),
|
|
393
488
|
// A stake gate reads the wallet's token seat in its farm, then the farm (its tiers' locks), where an NFT shown would be.
|
|
394
|
-
...(gate?.Staked
|
|
489
|
+
...(gate?.Staked
|
|
490
|
+
? [meta(pda.seat(gate.Staked.farm, wallet), false, false), meta(gate.Staked.farm, false, false)]
|
|
491
|
+
: []),
|
|
395
492
|
...shownAccounts(project, shown),
|
|
396
493
|
];
|
|
397
494
|
|
|
@@ -401,7 +498,12 @@ export const gateAccounts = (project, { gate, wallet, shown, listProof = [] } =
|
|
|
401
498
|
* is then at `prizeRollNonce` of it (plus the place where the route says so), which no roll on an NFT is at.
|
|
402
499
|
*/
|
|
403
500
|
export const prizeNonce = (...parts) => {
|
|
404
|
-
const hash = sha256(
|
|
501
|
+
const hash = sha256(
|
|
502
|
+
concat(
|
|
503
|
+
utf8('loot:prize'),
|
|
504
|
+
...parts.map(p => (typeof p === 'string' ? addressBytes(p) : p instanceof Uint8Array ? p : uintLE(p, 8))),
|
|
505
|
+
),
|
|
506
|
+
);
|
|
405
507
|
return readUintLE(hash, 0, 8);
|
|
406
508
|
};
|
|
407
509
|
|
|
@@ -413,18 +515,18 @@ export const prizeNonce = (...parts) => {
|
|
|
413
515
|
* won is made at settle at `rollNonce(order, slot, k)` as it is (hashed under "roll", so no named nonce reaches it). A roll on an NFT (`nonceOn`, a recipe's `RollOn`, a mission's
|
|
414
516
|
* `roll`) and a box's own order are not. Not `prizeNonce`, which makes the `n` a route names from its parts.
|
|
415
517
|
*/
|
|
416
|
-
export const prizeRollNonce =
|
|
518
|
+
export const prizeRollNonce = n => {
|
|
417
519
|
// The program adds the place or `k` with `wrapping_add`: the same here.
|
|
418
520
|
const hash = sha256(concat(utf8('prize'), uintLE(BigInt.asUintN(64, toBigInt(n)), 8)));
|
|
419
521
|
return readUintLE(hash, 0, 8);
|
|
420
522
|
};
|
|
421
523
|
|
|
422
524
|
/** The NFT accounts every route takes as a `Piece`, in the program's order, with the writable flags the IDL has. */
|
|
423
|
-
const PIECE = leaves(instructionDef('hold').accounts).filter(
|
|
525
|
+
const PIECE = leaves(instructionDef('hold').accounts).filter(a => a.group === 'piece');
|
|
424
526
|
|
|
425
527
|
/** A `piece`'s accounts as metas, in the program's order: what a prize of an NFT names after its stash. */
|
|
426
528
|
function pieceMetas(p) {
|
|
427
|
-
return PIECE.map(
|
|
529
|
+
return PIECE.map(a => {
|
|
428
530
|
const key = p.accounts[a.name];
|
|
429
531
|
if (key == null) return meta(LOOT_PROGRAM, false, false);
|
|
430
532
|
return meta(key, false, a.writable === true || p.writable.includes(a.name));
|
|
@@ -444,25 +546,53 @@ function pieceMetas(p) {
|
|
|
444
546
|
* collectibles of the same item already made earlier in the instruction); `nft` the NFT's kind, as `piece` takes it
|
|
445
547
|
* ({ collection, tokenMetadata, compressed }).
|
|
446
548
|
*/
|
|
447
|
-
export function prizeMetas({
|
|
549
|
+
export function prizeMetas({
|
|
550
|
+
project,
|
|
551
|
+
collection,
|
|
552
|
+
prize,
|
|
553
|
+
winner,
|
|
554
|
+
nonce = 0n,
|
|
555
|
+
poolMints = {},
|
|
556
|
+
items = {},
|
|
557
|
+
nft = {},
|
|
558
|
+
made = 0,
|
|
559
|
+
projectMint,
|
|
560
|
+
programs,
|
|
561
|
+
lottery,
|
|
562
|
+
}) {
|
|
448
563
|
if (prize.Tokens) {
|
|
449
564
|
const { from } = prize.Tokens;
|
|
450
565
|
// A pool never opened (its mint given as null) gives nothing: any mint stands in for its place, the project's.
|
|
451
566
|
const pool = from.Pool?.[0];
|
|
452
567
|
const mint = from.Mint ? from.Mint[0] : (poolMints[pool] ?? (pool in poolMints ? projectMint : undefined));
|
|
453
|
-
if (!mint)
|
|
568
|
+
if (!mint)
|
|
569
|
+
throw new Error(
|
|
570
|
+
`a prize from pool ${pool} needs that pool's mint (poolMints; null for a pool never opened, with projectMint)`,
|
|
571
|
+
);
|
|
454
572
|
const pooled = from.Pool ? [meta(pda.pool(project, pool), false, true)] : [];
|
|
455
|
-
return [
|
|
573
|
+
return [
|
|
574
|
+
meta(mint, false, Boolean(from.Mint)),
|
|
575
|
+
...pooled,
|
|
576
|
+
meta(ata(winner, mint, tokenProgramOf(programs, mint)), false, true),
|
|
577
|
+
];
|
|
456
578
|
}
|
|
457
579
|
if (prize.Item) {
|
|
458
580
|
const id = prize.Item[0];
|
|
459
581
|
const item = items[id];
|
|
460
|
-
if (item === undefined)
|
|
582
|
+
if (item === undefined)
|
|
583
|
+
throw new Error(
|
|
584
|
+
`a prize of item ${id} needs the item (items: its decoded Item, or null for none written), for its kind and next serial`,
|
|
585
|
+
);
|
|
461
586
|
const address = pda.item(project, id);
|
|
462
587
|
// A normal item, or one never written, is its address alone; a collectible is minted into its own collection.
|
|
463
588
|
if (!item || item.kind !== 'Collectible') return [meta(address, false, true)];
|
|
464
589
|
const serial = Number(item.issued) + 1 + made;
|
|
465
|
-
return [
|
|
590
|
+
return [
|
|
591
|
+
meta(address, false, true),
|
|
592
|
+
meta(item.collection ?? collection, false, true),
|
|
593
|
+
meta(pda.itemAsset(address, serial), false, true),
|
|
594
|
+
meta(MPL_CORE_PROGRAM, false, false),
|
|
595
|
+
];
|
|
466
596
|
}
|
|
467
597
|
if (prize.Nft) {
|
|
468
598
|
const { stash, nft: asset } = prize.Nft;
|
|
@@ -471,23 +601,34 @@ export function prizeMetas({ project, collection, prize, winner, nonce = 0n, poo
|
|
|
471
601
|
}
|
|
472
602
|
if (prize.Held) {
|
|
473
603
|
// An NFT a lottery's place promises, from that lottery's own stash.
|
|
474
|
-
if (!lottery) throw new Error(
|
|
604
|
+
if (!lottery) throw new Error("A held NFT is a lottery's prize: name the lottery (`lottery`, its address).");
|
|
475
605
|
const p = piece({ project, asset: prize.Held[0], wallet: winner, ...nft, ...ONLY_MOVES });
|
|
476
606
|
return [meta(pda.lotteryStash(lottery), false, true), ...pieceMetas(p), ...p.proof];
|
|
477
607
|
}
|
|
478
|
-
if (prize.Stash)
|
|
608
|
+
if (prize.Stash)
|
|
609
|
+
throw new Error("A stash's NFT at random is a box entry's prize only: elsewhere, a roll of a box that gives it.");
|
|
479
610
|
if (prize.Compressed) {
|
|
480
611
|
// A compressed NFT of the item, minted into the project's tree, in the item's collection.
|
|
481
612
|
const { item: id, tree } = prize.Compressed;
|
|
482
613
|
const item = items[id];
|
|
483
|
-
if (!item)
|
|
614
|
+
if (!item)
|
|
615
|
+
throw new Error(`a compressed prize of item ${id} needs the item (items: its decoded Item), for its collection`);
|
|
484
616
|
return [
|
|
485
|
-
meta(pda.item(project, id), false, true),
|
|
486
|
-
meta(
|
|
617
|
+
meta(pda.item(project, id), false, true),
|
|
618
|
+
meta(pda.treeConfig(tree), false, true),
|
|
619
|
+
meta(tree, false, true),
|
|
620
|
+
meta(item.collection, false, true),
|
|
621
|
+
meta(BUBBLEGUM_PROGRAM, false, false),
|
|
622
|
+
meta(COMPRESSION_PROGRAM, false, false),
|
|
623
|
+
meta(NOOP_PROGRAM, false, false),
|
|
624
|
+
meta(MPL_CORE_PROGRAM, false, false),
|
|
487
625
|
meta(BUBBLEGUM_CORE_SIGNER, false, false),
|
|
488
626
|
];
|
|
489
627
|
}
|
|
490
|
-
return [
|
|
628
|
+
return [
|
|
629
|
+
meta(pda.box(project, prize.Roll[0]), false, false),
|
|
630
|
+
meta(pda.order(project, winner, prizeRollNonce(nonce)), false, true),
|
|
631
|
+
];
|
|
491
632
|
}
|
|
492
633
|
|
|
493
634
|
/**
|
|
@@ -499,12 +640,22 @@ export function prizeMetas({ project, collection, prize, winner, nonce = 0n, poo
|
|
|
499
640
|
*/
|
|
500
641
|
export function rollAccounts(project, box, rolls = {}) {
|
|
501
642
|
if (!box) return [];
|
|
502
|
-
const {
|
|
503
|
-
|
|
643
|
+
const {
|
|
644
|
+
items = {},
|
|
645
|
+
poolMints = {},
|
|
646
|
+
programs,
|
|
647
|
+
} = /** @type {{ items?: any, poolMints?: any, programs?: any }} */ (rolls);
|
|
648
|
+
for (const p of box.entries.flatMap(e => e.prizes)) {
|
|
504
649
|
const id = p.Item?.[0] ?? p.Compressed?.item;
|
|
505
|
-
if (id !== undefined && items[id] === undefined)
|
|
650
|
+
if (id !== undefined && items[id] === undefined)
|
|
651
|
+
throw new Error(
|
|
652
|
+
`a roll of box ${box.box_id ?? ''} needs item ${id} (rolls.items: its decoded Item, or null for none written), for its tables`,
|
|
653
|
+
);
|
|
506
654
|
const pool = p.Tokens?.from.Pool?.[0];
|
|
507
|
-
if (pool !== undefined && !(pool in poolMints))
|
|
655
|
+
if (pool !== undefined && !(pool in poolMints))
|
|
656
|
+
throw new Error(
|
|
657
|
+
`a roll of box ${box.box_id ?? ''} needs the mint of pool ${pool} (rolls.poolMints; null for a pool never opened)`,
|
|
658
|
+
);
|
|
508
659
|
}
|
|
509
660
|
return [...entryAccounts(project, box.entries, items), ...prizeAccounts(project, box.entries, poolMints, programs)];
|
|
510
661
|
}
|
|
@@ -514,7 +665,7 @@ export function rollAccounts(project, box, rolls = {}) {
|
|
|
514
665
|
* order they first come, and nothing after it. What a roll can give is read when it is asked.
|
|
515
666
|
*/
|
|
516
667
|
export function boxesAccounts(project, ids) {
|
|
517
|
-
return [...new Set(ids)].map(
|
|
668
|
+
return [...new Set(ids)].map(id => meta(pda.box(project, id), false, false));
|
|
518
669
|
}
|
|
519
670
|
|
|
520
671
|
/** The boxes a record names to roll later (`Def::rolls`), in order: a box's entries' rolls, a recipe's rolls and
|
|
@@ -522,12 +673,17 @@ export function boxesAccounts(project, ids) {
|
|
|
522
673
|
export function defRolls(def) {
|
|
523
674
|
const [kind] = Object.keys(def ?? {});
|
|
524
675
|
const args = def?.[kind]?.args ?? {};
|
|
525
|
-
const prizes =
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
676
|
+
const prizes =
|
|
677
|
+
kind === 'Box'
|
|
678
|
+
? (args.entries ?? []).flatMap(e => e.prizes ?? [])
|
|
679
|
+
: kind === 'Recipe'
|
|
680
|
+
? (args.outputs ?? []).map(o => o.Prize?.[0] ?? (o.RollOn ? { Roll: [o.RollOn.box_id] } : null))
|
|
681
|
+
: kind === 'Grant'
|
|
682
|
+
? [args.gives]
|
|
683
|
+
: kind === 'Lottery'
|
|
684
|
+
? (args.terms?.places ?? args.places ?? []).map(p => (typeof p === 'object' ? p.prize : null))
|
|
685
|
+
: [];
|
|
686
|
+
return prizes.flatMap(p => (p?.Roll ? [p.Roll[0]] : []));
|
|
531
687
|
}
|
|
532
688
|
|
|
533
689
|
/** Bubblegum's signer for its calls to Core, `["mpl_core_cpi_signer"]`. */
|
|
@@ -539,9 +695,19 @@ export const BUBBLEGUM_CORE_SIGNER = 'CbNY3JiXdXNE9tPNEk1aRZVEkWdj2v7kfJLNQwZZgp
|
|
|
539
695
|
* canopy)` bytes, made empty first in the same transaction (a System `createAccount` owned by mpl-account-compression, the tree's keypair signing).
|
|
540
696
|
*/
|
|
541
697
|
export const openTreeIx = ({ signer, project, tree, maxDepth, maxBufferSize }) =>
|
|
542
|
-
lootIx(
|
|
543
|
-
|
|
544
|
-
|
|
698
|
+
lootIx(
|
|
699
|
+
'open_tree',
|
|
700
|
+
{ max_depth: maxDepth, max_buffer_size: maxBufferSize },
|
|
701
|
+
{
|
|
702
|
+
signer,
|
|
703
|
+
project,
|
|
704
|
+
tree,
|
|
705
|
+
tree_config: pda.treeConfig(tree),
|
|
706
|
+
bubblegum_program: BUBBLEGUM_PROGRAM,
|
|
707
|
+
compression_program: COMPRESSION_PROGRAM,
|
|
708
|
+
log_wrapper: NOOP_PROGRAM,
|
|
709
|
+
},
|
|
710
|
+
);
|
|
545
711
|
|
|
546
712
|
/** The bytes an mpl-account-compression tree of this shape takes (its header, changelog, rightmost path and canopy). */
|
|
547
713
|
export function treeSize(maxDepth, maxBufferSize, canopy) {
|
|
@@ -556,20 +722,26 @@ export const ticketOf = (requester, slot) => {
|
|
|
556
722
|
return hashToAddress(hash);
|
|
557
723
|
};
|
|
558
724
|
|
|
559
|
-
|
|
560
725
|
// ---------------------------------------------------------------- the platform
|
|
561
726
|
|
|
562
727
|
/** Signed by the program's upgrade authority, once. `fees`: { player, project } lamports. */
|
|
563
728
|
export const initPlatformIx = ({ admin, fees }) => lootIx('init_platform', { fees }, { admin });
|
|
564
729
|
|
|
565
730
|
/** `nextAdmin` signs too when it is another key. */
|
|
566
|
-
export const setPlatformIx = ({ admin, fees, nextAdmin = admin }) =>
|
|
731
|
+
export const setPlatformIx = ({ admin, fees, nextAdmin = admin }) =>
|
|
732
|
+
lootIx('set_platform', { fees }, { admin, next_admin: nextAdmin });
|
|
567
733
|
|
|
568
734
|
/** The platform collects the fees waiting in each project's fee box, into `to`. */
|
|
569
735
|
export const collectFeesIx = ({ admin, to, projects }) =>
|
|
570
|
-
lootIx(
|
|
736
|
+
lootIx(
|
|
737
|
+
'collect_fees',
|
|
738
|
+
{},
|
|
739
|
+
{ admin, to },
|
|
740
|
+
{ remaining: projects.map(project => meta(pda.fees(project), false, true)) },
|
|
741
|
+
);
|
|
571
742
|
|
|
572
|
-
export const setSuspendedIx = ({ admin, project, suspended }) =>
|
|
743
|
+
export const setSuspendedIx = ({ admin, project, suspended }) =>
|
|
744
|
+
lootIx('set_suspended', { suspended }, { admin, project });
|
|
573
745
|
|
|
574
746
|
// ---------------------------------------------------------------- a project's settings
|
|
575
747
|
|
|
@@ -583,7 +755,12 @@ export const setSuspendedIx = ({ admin, project, suspended }) => lootIx('set_sus
|
|
|
583
755
|
* for that it can tell (an account of loot or of a token program, a program: `BadKey`).
|
|
584
756
|
*/
|
|
585
757
|
export const createProjectIx = ({ owner, id = 0, authority, treasury, collections = [] }) =>
|
|
586
|
-
lootIx(
|
|
758
|
+
lootIx(
|
|
759
|
+
'create_project',
|
|
760
|
+
{ id: toBigInt(id), authority, treasury, collections },
|
|
761
|
+
{ owner, authority, treasury, project: pda.project(owner, id) },
|
|
762
|
+
{ remaining: collections.map(c => meta(c, false, false)) },
|
|
763
|
+
);
|
|
587
764
|
|
|
588
765
|
/**
|
|
589
766
|
* `update`: { authority?, treasury?, paused?, collections? }. `collections` replaces the project's list; every one in
|
|
@@ -592,10 +769,24 @@ export const createProjectIx = ({ owner, id = 0, authority, treasury, collection
|
|
|
592
769
|
* authority or treasury comes with the account at its key (`new_authority`, `new_treasury`), which the program checks
|
|
593
770
|
* as `createProjectIx`'s.
|
|
594
771
|
*/
|
|
595
|
-
export const updateProjectIx = ({
|
|
596
|
-
|
|
772
|
+
export const updateProjectIx = ({
|
|
773
|
+
owner,
|
|
774
|
+
project,
|
|
775
|
+
authority = null,
|
|
776
|
+
paused = null,
|
|
777
|
+
treasury = null,
|
|
778
|
+
collections = null,
|
|
779
|
+
newOwner = null,
|
|
780
|
+
}) =>
|
|
781
|
+
lootIx(
|
|
782
|
+
'update_project',
|
|
783
|
+
{ update: { authority, treasury, paused, collections } },
|
|
784
|
+
{ owner, project, new_owner: newOwner, new_authority: authority, new_treasury: treasury },
|
|
785
|
+
{ remaining: (collections ?? []).map(c => meta(c, false, false)) },
|
|
786
|
+
);
|
|
597
787
|
|
|
598
|
-
export const openPoolIx = ({ owner, project, index, mint }) =>
|
|
788
|
+
export const openPoolIx = ({ owner, project, index, mint }) =>
|
|
789
|
+
lootIx('open_pool', { index }, { owner, project, mint, pool: pda.pool(project, index) });
|
|
599
790
|
|
|
600
791
|
/** Where a record of the owner's lives: its kind's seed, the project and its id. */
|
|
601
792
|
export function recordAddress(project, def) {
|
|
@@ -640,16 +831,21 @@ export function withDefaults(def) {
|
|
|
640
831
|
stored(args.shares);
|
|
641
832
|
if (!args.currency) throw new Error('A box names the mint its price is in (`currency`).');
|
|
642
833
|
// What one open draws (`LootBox::slots`): none is one draw among every entry.
|
|
643
|
-
args.slots = (args.slots ?? []).map(
|
|
834
|
+
args.slots = (args.slots ?? []).map(s => ({ only: [], distinct: false, share: 0, ...s }));
|
|
644
835
|
// An entry: its prizes (`prizeMetas`) and its weight. A box burns its price with a share to `Burn`.
|
|
645
|
-
if (!Array.isArray(args.entries))
|
|
646
|
-
|
|
836
|
+
if (!Array.isArray(args.entries))
|
|
837
|
+
throw new Error('A box needs its entries (`entries`): what its roll can give, each with its weight.');
|
|
838
|
+
args.entries = args.entries.map(e => ({ prizes: [], ...e }));
|
|
647
839
|
args.subject ??= null;
|
|
648
840
|
// What it does once an entry can't be given (`WhenOut`): stop selling while any entry couldn't cover the draws of
|
|
649
841
|
// a purchase and those waiting for their answer (the default), or draw among what is left ('DrawLeft').
|
|
650
842
|
args.when_out ??= 'StopSales';
|
|
651
843
|
// A fate's `then`: null sends the NFT back to its player; 'Burn', { Give: [wallet] } or { Stash: [id] }.
|
|
652
|
-
args.fates = (args.fates ?? []).map(
|
|
844
|
+
args.fates = (args.fates ?? []).map(f => ({
|
|
845
|
+
...f,
|
|
846
|
+
then: f.then ?? null,
|
|
847
|
+
shifts: (f.shifts ?? []).map(s => ({ from_now: false, ...s })),
|
|
848
|
+
}));
|
|
653
849
|
}
|
|
654
850
|
if (kind === 'Box' || kind === 'Recipe') {
|
|
655
851
|
args.window ??= NO_WINDOW;
|
|
@@ -657,16 +853,34 @@ export function withDefaults(def) {
|
|
|
657
853
|
}
|
|
658
854
|
if (kind === 'Recipe') {
|
|
659
855
|
// A raise moves a number on a kept collectible (`place`) as a box's fate does (`Shift`); a need tests one (`Test`).
|
|
660
|
-
args.raises = (args.raises ?? []).map(({ place, shift, ...flat }) => ({
|
|
661
|
-
|
|
856
|
+
args.raises = (args.raises ?? []).map(({ place, shift, ...flat }) => ({
|
|
857
|
+
place,
|
|
858
|
+
shift: { from_now: false, most: 0, ...(shift ?? flat) },
|
|
859
|
+
}));
|
|
860
|
+
args.needs = (args.needs ?? []).map(({ place, key, test, at_least: least }) => ({
|
|
861
|
+
place,
|
|
862
|
+
key,
|
|
863
|
+
test: test ?? { AtLeast: [least ?? 0] },
|
|
864
|
+
}));
|
|
662
865
|
}
|
|
663
866
|
if (kind === 'Box' && args.fall) args.fall = { shared: false, ...args.fall };
|
|
664
867
|
if (kind === 'Lottery') {
|
|
665
868
|
// Its terms (`LotteryTerms`, what a round copies at its first purchase), given in `terms` or beside `every`.
|
|
666
869
|
const { every, gate, per_wallet: perWallet, terms: given, ...flat } = args;
|
|
667
870
|
const terms = { nft_ticket: null, returns: false, min_sold: 0, ticket_mint: NO_ADDRESS, ...flat, ...given };
|
|
871
|
+
// Where its places are paid from is the project's to name, never a default: `{ Pot: [id] }`, a pot kept from round
|
|
872
|
+
// to round, or `{ Round: { rest } }`, each round's own ticket money with what its places leave going to `rest`.
|
|
873
|
+
const pays = terms.pays_from;
|
|
874
|
+
if (!pays || !(pays.Pot || pays.Round))
|
|
875
|
+
throw new Error(
|
|
876
|
+
'A lottery names where its places are paid from (`pays_from`): { Pot: [id] } or { Round: { rest } }.',
|
|
877
|
+
);
|
|
878
|
+
if (pays.Pot && !(Number(pays.Pot[0]) >= 0 && Number(pays.Pot[0]) < MAX_POTS))
|
|
879
|
+
throw new Error(`A lottery's pot id is below ${MAX_POTS}.`);
|
|
668
880
|
// A run of places alike is { bps, prize, times }; a number is one place's share of the pot that gives nothing else.
|
|
669
|
-
terms.places = (terms.places ?? []).map(
|
|
881
|
+
terms.places = (terms.places ?? []).map(p =>
|
|
882
|
+
typeof p === 'object' ? { prize: null, times: 1, ...p } : { bps: p, prize: null, times: 1 },
|
|
883
|
+
);
|
|
670
884
|
terms.shares = stored(terms.shares ?? []);
|
|
671
885
|
return { [kind]: { ...body, args: { terms, every: every ?? 0n, gate: gate ?? null, per_wallet: perWallet ?? 0 } } };
|
|
672
886
|
}
|
|
@@ -680,7 +894,8 @@ export function withDefaults(def) {
|
|
|
680
894
|
|
|
681
895
|
/**
|
|
682
896
|
* The owner writes one record (`def`: { Item: { id, args } }, { Box: … }, …), at its own address. A lottery names
|
|
683
|
-
* its
|
|
897
|
+
* its mint, the pot its terms pay from (which a write opens when it isn't open; pot 0's address for a lottery whose
|
|
898
|
+
* rounds pay from their own money, where any account will do), its ticket mint, both token programs and the round it waits on
|
|
684
899
|
* (`lotteryState`: the lottery as it is, null for a new one; `idleRound`: its current round's record, or round 0's once
|
|
685
900
|
* it drew once and ended: a lottery made again, edited once ended or made to repeat waits while that round runs or
|
|
686
901
|
* still owes NFTs of its own stash), then its own stash when a place holds or draws an NFT of it. A box, a recipe, a grant or a lottery that
|
|
@@ -699,19 +914,25 @@ export function upsertIx({ owner, project, def: given, lotteryState = null }) {
|
|
|
699
914
|
const vest = kind === 'Schedule' ? args.action?.from?.Vest?.[0] : null;
|
|
700
915
|
if (vest) {
|
|
701
916
|
// It pays a wallet, the treasury or a burn: no account it pays is named.
|
|
702
|
-
remaining = [
|
|
917
|
+
remaining = [
|
|
918
|
+
meta(vest, false, false),
|
|
919
|
+
meta(pda.vest(recordAddress(project, def)), false, true),
|
|
920
|
+
meta(TOKEN_PROGRAM, false, false),
|
|
921
|
+
meta(TOKEN_2022_PROGRAM, false, false),
|
|
922
|
+
];
|
|
703
923
|
}
|
|
704
924
|
if (kind === 'Lottery') {
|
|
705
925
|
remaining = [
|
|
706
926
|
meta(args.terms.mint, false, false),
|
|
707
|
-
meta(pda.
|
|
927
|
+
meta(pda.lotteryPot(recordAddress(project, def), potIdOf(args.terms) ?? 0), false, true),
|
|
708
928
|
meta(paymentMint(args.terms), false, false),
|
|
709
929
|
meta(TOKEN_PROGRAM, false, false),
|
|
710
930
|
meta(TOKEN_2022_PROGRAM, false, false),
|
|
711
931
|
meta(lotteryState ? idleRound(recordAddress(project, def), lotteryState) : SYSTEM_PROGRAM, false, false),
|
|
712
932
|
];
|
|
713
933
|
// A place that holds or draws an NFT of its own stash (`Prize::Held`, `Prize::Raffled`): that stash, after them.
|
|
714
|
-
if (args.terms.places.some(
|
|
934
|
+
if (args.terms.places.some(p => p.prize?.Held || p.prize === 'Raffled'))
|
|
935
|
+
remaining.push(meta(pda.lotteryStash(recordAddress(project, def)), false, false));
|
|
715
936
|
}
|
|
716
937
|
// Each box it names to roll later (a box's, a recipe's, a grant's, a lottery's places'), once.
|
|
717
938
|
remaining.push(...boxesAccounts(project, defRolls(def)));
|
|
@@ -720,23 +941,33 @@ export function upsertIx({ owner, project, def: given, lotteryState = null }) {
|
|
|
720
941
|
|
|
721
942
|
/**
|
|
722
943
|
* The owner closes the record at `target`, and its rent comes back: a grant's with the funds it held. A lottery
|
|
723
|
-
* (`lottery`: its decoded record) sends what
|
|
724
|
-
* by default), and closes
|
|
725
|
-
* gave out every NFT of its own stash, `idleRound`)
|
|
944
|
+
* (`lottery`: its decoded record) sends what every pot it opened holds (`Lottery::pots`) to `to`, a token account of its
|
|
945
|
+
* mint (the owner's own by default), and closes each, once its current round has no record (one that drew once and
|
|
946
|
+
* ended: once its round 0 gave out every NFT of its own stash, `idleRound`), and once the round its `owing` names has
|
|
947
|
+
* moved in what it owes its pot (pull it first, `Part::Common`). One that never opened a pot names only those rounds.
|
|
726
948
|
*/
|
|
727
949
|
export function closeRecordIx({ owner, project, target, lottery, schedule, programs, to, extra = [] }) {
|
|
728
950
|
// A sealed schedule never closes (CannotClose): its address, and its vault's, stay bound to its terms.
|
|
729
|
-
if (schedule?.action?.from?.Vest)
|
|
951
|
+
if (schedule?.action?.from?.Vest)
|
|
952
|
+
throw new Error('A sealed schedule never closes: its vault stays bound to its terms.');
|
|
730
953
|
if (!lottery) return lootIx('upsert', { def: null }, { owner, project, target });
|
|
731
954
|
const { mint } = lottery.terms;
|
|
732
955
|
const tokenProgram = tokenProgramOf(programs, mint);
|
|
956
|
+
const pots = lotteryPots(lottery);
|
|
957
|
+
const owing = lottery.owing && lottery.owing !== NO_ADDRESS ? [meta(lottery.owing, false, false)] : [];
|
|
733
958
|
const remaining = [
|
|
734
959
|
meta(idleRound(target, lottery), false, false),
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
960
|
+
// The round that may still owe a pot: the close waits until it has moved its leftover in (RoundRunning).
|
|
961
|
+
...owing,
|
|
962
|
+
...(pots.length
|
|
963
|
+
? [
|
|
964
|
+
meta(mint, false, true),
|
|
965
|
+
meta(to ?? ata(owner, mint, tokenProgram), false, true),
|
|
966
|
+
meta(TOKEN_PROGRAM, false, false),
|
|
967
|
+
meta(TOKEN_2022_PROGRAM, false, false),
|
|
968
|
+
...pots.map(id => meta(pda.lotteryPot(target, id), false, true)),
|
|
969
|
+
]
|
|
970
|
+
: []),
|
|
740
971
|
...tailOf(extra),
|
|
741
972
|
];
|
|
742
973
|
return lootIx('upsert', { def: null }, { owner, project, target }, { remaining });
|
|
@@ -748,13 +979,35 @@ export function closeRecordIx({ owner, project, target, lottery, schedule, progr
|
|
|
748
979
|
* `player` (or a sponsor, or the project's own keys funding a pool) pays `amount` of `mint`, split at once by `shares`.
|
|
749
980
|
* The rest goes to the project's `treasury` wallet. The player opens any account this pays into that is not there.
|
|
750
981
|
*/
|
|
751
|
-
export function payIx({
|
|
982
|
+
export function payIx({
|
|
983
|
+
player,
|
|
984
|
+
project,
|
|
985
|
+
mint,
|
|
986
|
+
treasury,
|
|
987
|
+
amount,
|
|
988
|
+
shares = [],
|
|
989
|
+
reference: ref,
|
|
990
|
+
playerToken,
|
|
991
|
+
holders,
|
|
992
|
+
programs,
|
|
993
|
+
extra = [],
|
|
994
|
+
}) {
|
|
752
995
|
const tokenProgram = tokenProgramOf(programs, mint);
|
|
753
996
|
return lootIx(
|
|
754
997
|
'pay',
|
|
755
998
|
{ amount, shares, reference: reference(ref) },
|
|
756
|
-
{
|
|
757
|
-
|
|
999
|
+
{
|
|
1000
|
+
player,
|
|
1001
|
+
project,
|
|
1002
|
+
mint,
|
|
1003
|
+
treasury,
|
|
1004
|
+
treasury_token: ata(treasury, mint, tokenProgram),
|
|
1005
|
+
player_token: playerToken ?? ata(player, mint, tokenProgram),
|
|
1006
|
+
},
|
|
1007
|
+
{
|
|
1008
|
+
remaining: [...shareAccounts(project, shares, mint, { holders, programs, treasury }), ...tailOf(extra)],
|
|
1009
|
+
writable: sharesBurn(shares) ? ['mint'] : [],
|
|
1010
|
+
},
|
|
758
1011
|
);
|
|
759
1012
|
}
|
|
760
1013
|
|
|
@@ -769,7 +1022,26 @@ export function payIx({ player, project, mint, treasury, amount, shares = [], re
|
|
|
769
1022
|
* The player opens the project's vault for the price's mint, and for each prize's, when it is not there. A box that
|
|
770
1023
|
* limits each wallet counts the purchase in the player's counts (`pda.counts`, its rent theirs).
|
|
771
1024
|
*/
|
|
772
|
-
export function openBoxIx({
|
|
1025
|
+
export function openBoxIx({
|
|
1026
|
+
player,
|
|
1027
|
+
project,
|
|
1028
|
+
box,
|
|
1029
|
+
boxAddress,
|
|
1030
|
+
nonce,
|
|
1031
|
+
maxPrice,
|
|
1032
|
+
count = 1,
|
|
1033
|
+
items = {},
|
|
1034
|
+
shown,
|
|
1035
|
+
listProof,
|
|
1036
|
+
subject,
|
|
1037
|
+
chosenRecipient = null,
|
|
1038
|
+
chosen = null,
|
|
1039
|
+
settler = NOBODY_KEY,
|
|
1040
|
+
poolMints,
|
|
1041
|
+
playerToken,
|
|
1042
|
+
programs,
|
|
1043
|
+
extra = [],
|
|
1044
|
+
}) {
|
|
773
1045
|
const mint = box.currency;
|
|
774
1046
|
poolMints = boxPoolMints(box, poolMints);
|
|
775
1047
|
const tokenProgram = tokenProgramOf(programs, mint);
|
|
@@ -782,14 +1054,26 @@ export function openBoxIx({ player, project, box, boxAddress, nonce, maxPrice, c
|
|
|
782
1054
|
// It is held until the roll settles when an entry burns it, gives it away or moves its numbers, and the roll is at its
|
|
783
1055
|
// nonce.
|
|
784
1056
|
const holds = boxHolds(box);
|
|
785
|
-
const on = box.subject
|
|
1057
|
+
const on = box.subject
|
|
1058
|
+
? piece({ project, wallet: player, ...subject, moves: holds, locks: false, writes: false })
|
|
1059
|
+
: standIn(address);
|
|
786
1060
|
if (box.subject && holds) nonce = nonceOn(on.accounts.asset);
|
|
787
1061
|
// A list's proof, then the NFT shown for the gate and the odds (for a box that rolls on an NFT, a compressed one's
|
|
788
1062
|
// proof instead: that NFT is the piece).
|
|
789
|
-
const gate = [
|
|
1063
|
+
const gate = [
|
|
1064
|
+
...gateAccounts(project, { gate: box.gate, wallet: player, listProof }),
|
|
1065
|
+
...(box.subject ? on.proof : shownAccounts(project, shown)),
|
|
1066
|
+
];
|
|
790
1067
|
return lootIx(
|
|
791
1068
|
'open_box',
|
|
792
|
-
{
|
|
1069
|
+
{
|
|
1070
|
+
nonce,
|
|
1071
|
+
max_price: maxPrice ?? box.price * BigInt(count),
|
|
1072
|
+
chosen_recipient: chosenRecipient,
|
|
1073
|
+
version: box.version,
|
|
1074
|
+
count,
|
|
1075
|
+
settler,
|
|
1076
|
+
},
|
|
793
1077
|
{
|
|
794
1078
|
loot_box: address,
|
|
795
1079
|
player,
|
|
@@ -801,7 +1085,15 @@ export function openBoxIx({ player, project, box, boxAddress, nonce, maxPrice, c
|
|
|
801
1085
|
counts: limits(box.per_wallet) ? pda.counts(project, player) : null,
|
|
802
1086
|
...on.accounts,
|
|
803
1087
|
},
|
|
804
|
-
{
|
|
1088
|
+
{
|
|
1089
|
+
remaining: [
|
|
1090
|
+
...entryAccounts(project, box.entries, items),
|
|
1091
|
+
...prizeAccounts(project, box.entries, poolMints, programs),
|
|
1092
|
+
...gate,
|
|
1093
|
+
...tailOf(extra),
|
|
1094
|
+
],
|
|
1095
|
+
writable: on.writable,
|
|
1096
|
+
},
|
|
805
1097
|
);
|
|
806
1098
|
}
|
|
807
1099
|
|
|
@@ -816,7 +1108,27 @@ export function openBoxIx({ player, project, box, boxAddress, nonce, maxPrice, c
|
|
|
816
1108
|
* treasury wallet; `holders` maps a collectible a share pays to its holder; each roll the entry gives is made at the
|
|
817
1109
|
* next nonce from `nonce` (`buyRolls`). `shown`/`listProof` pass the box's gate.
|
|
818
1110
|
*/
|
|
819
|
-
export function buyIx({
|
|
1111
|
+
export function buyIx({
|
|
1112
|
+
player,
|
|
1113
|
+
project,
|
|
1114
|
+
box,
|
|
1115
|
+
boxAddress,
|
|
1116
|
+
maxPrice,
|
|
1117
|
+
count = 1,
|
|
1118
|
+
treasury,
|
|
1119
|
+
chosenRecipient = null,
|
|
1120
|
+
chosen = null,
|
|
1121
|
+
nonce = 0n,
|
|
1122
|
+
shown,
|
|
1123
|
+
listProof,
|
|
1124
|
+
items = {},
|
|
1125
|
+
poolMints = {},
|
|
1126
|
+
nfts = {},
|
|
1127
|
+
holders,
|
|
1128
|
+
playerToken,
|
|
1129
|
+
programs,
|
|
1130
|
+
extra = [],
|
|
1131
|
+
}) {
|
|
820
1132
|
const mint = box.currency;
|
|
821
1133
|
const tokenProgram = tokenProgramOf(programs, mint);
|
|
822
1134
|
const address = boxAddress ?? pda.box(project, box.box_id);
|
|
@@ -824,21 +1136,48 @@ export function buyIx({ player, project, box, boxAddress, maxPrice, count = 1, t
|
|
|
824
1136
|
const made = {};
|
|
825
1137
|
const prizes = buyGives(box, count).flatMap(({ prize, k }) => {
|
|
826
1138
|
const item = prize.Item?.[0];
|
|
827
|
-
const lines = prizeMetas({
|
|
1139
|
+
const lines = prizeMetas({
|
|
1140
|
+
project,
|
|
1141
|
+
prize,
|
|
1142
|
+
winner: player,
|
|
1143
|
+
nonce: toBigInt(nonce) + BigInt(k),
|
|
1144
|
+
poolMints,
|
|
1145
|
+
items,
|
|
1146
|
+
nft: nfts[prize.Nft?.nft],
|
|
1147
|
+
made: made[item] ?? 0,
|
|
1148
|
+
projectMint: mint,
|
|
1149
|
+
programs,
|
|
1150
|
+
});
|
|
828
1151
|
if (item !== undefined) made[item] = (made[item] ?? 0) + 1;
|
|
829
1152
|
return lines;
|
|
830
1153
|
});
|
|
831
1154
|
return lootIx(
|
|
832
1155
|
'buy',
|
|
833
|
-
{ max_price: maxPrice ?? box.price * BigInt(count), chosen_recipient: chosenRecipient, version: box.version, nonce: toBigInt(nonce), count },
|
|
834
1156
|
{
|
|
835
|
-
|
|
1157
|
+
max_price: maxPrice ?? box.price * BigInt(count),
|
|
1158
|
+
chosen_recipient: chosenRecipient,
|
|
1159
|
+
version: box.version,
|
|
1160
|
+
nonce: toBigInt(nonce),
|
|
1161
|
+
count,
|
|
1162
|
+
},
|
|
1163
|
+
{
|
|
1164
|
+
player,
|
|
1165
|
+
project,
|
|
1166
|
+
loot_box: address,
|
|
1167
|
+
mint,
|
|
836
1168
|
player_token: playerToken ?? ata(player, mint, tokenProgram),
|
|
837
|
-
treasury,
|
|
838
|
-
|
|
1169
|
+
treasury,
|
|
1170
|
+
treasury_token: ata(treasury, mint, tokenProgram),
|
|
1171
|
+
chosen,
|
|
1172
|
+
counts: limits(box.per_wallet) ? pda.counts(project, player) : null,
|
|
839
1173
|
},
|
|
840
1174
|
{
|
|
841
|
-
remaining: [
|
|
1175
|
+
remaining: [
|
|
1176
|
+
...gateAccounts(project, { gate: box.gate, wallet: player, shown, listProof }),
|
|
1177
|
+
...shareAccounts(project, shares, mint, { holders, programs, treasury }),
|
|
1178
|
+
...prizes,
|
|
1179
|
+
...tailOf(extra),
|
|
1180
|
+
],
|
|
842
1181
|
writable: sharesBurn(box.shares) ? ['mint'] : [],
|
|
843
1182
|
},
|
|
844
1183
|
);
|
|
@@ -867,7 +1206,18 @@ export function buyGives(box, count = 1) {
|
|
|
867
1206
|
* for a prize in a mint with a transfer hook, the hook's accounts (`extra`). The buyer's odds boosts were fixed at
|
|
868
1207
|
* purchase.
|
|
869
1208
|
*/
|
|
870
|
-
export function callbackRollIx({
|
|
1209
|
+
export function callbackRollIx({
|
|
1210
|
+
order,
|
|
1211
|
+
orderState,
|
|
1212
|
+
box,
|
|
1213
|
+
boxAddress,
|
|
1214
|
+
randomness,
|
|
1215
|
+
project,
|
|
1216
|
+
poolMints,
|
|
1217
|
+
items = {},
|
|
1218
|
+
programs,
|
|
1219
|
+
extra = [],
|
|
1220
|
+
}) {
|
|
871
1221
|
const projectAddress = project ?? orderState.project;
|
|
872
1222
|
const keys = [
|
|
873
1223
|
meta(pda.oracleIdentity(), true, false),
|
|
@@ -878,9 +1228,19 @@ export function callbackRollIx({ order, orderState, box, boxAddress, randomness,
|
|
|
878
1228
|
...entryAccounts(orderState.project, box.entries, items),
|
|
879
1229
|
];
|
|
880
1230
|
const prizes = prizeAccounts(orderState.project, box.entries, boxPoolMints(box, poolMints), programs);
|
|
881
|
-
if (prizes.length)
|
|
1231
|
+
if (prizes.length)
|
|
1232
|
+
keys.push(
|
|
1233
|
+
meta(TOKEN_PROGRAM, false, false),
|
|
1234
|
+
meta(TOKEN_2022_PROGRAM, false, false),
|
|
1235
|
+
meta(projectAddress, false, false),
|
|
1236
|
+
...prizes,
|
|
1237
|
+
);
|
|
882
1238
|
keys.push(...tailOf(extra));
|
|
883
|
-
return {
|
|
1239
|
+
return {
|
|
1240
|
+
programId: LOOT_PROGRAM,
|
|
1241
|
+
keys,
|
|
1242
|
+
data: encodeInstruction(IDL, 'callback_roll', { randomness: Uint8Array.from(randomness) }),
|
|
1243
|
+
};
|
|
884
1244
|
}
|
|
885
1245
|
|
|
886
1246
|
/**
|
|
@@ -892,26 +1252,46 @@ export function callbackRollIx({ order, orderState, box, boxAddress, randomness,
|
|
|
892
1252
|
* Compressed the item, the tree's config, the tree, the collection, Bubblegum's programs; for a fresh one, then its
|
|
893
1253
|
* tables and the Sheet of the NFT it mints
|
|
894
1254
|
*/
|
|
895
|
-
export function wonAccounts({
|
|
1255
|
+
export function wonAccounts({
|
|
1256
|
+
order,
|
|
1257
|
+
items = {},
|
|
1258
|
+
nfts = {},
|
|
1259
|
+
prizeTo,
|
|
1260
|
+
putBack = [],
|
|
1261
|
+
programs,
|
|
1262
|
+
from = 0,
|
|
1263
|
+
upto = order.won.length,
|
|
1264
|
+
tree,
|
|
1265
|
+
treeMinted = 0n,
|
|
1266
|
+
unminted = [],
|
|
1267
|
+
}) {
|
|
896
1268
|
const project = order.project;
|
|
897
1269
|
// A compressed prize is minted into `tree` (any of the project's with room), the next NFT it mints: after
|
|
898
1270
|
// `treeMinted`, and the compressed prizes before it in this part that were minted. One of an item whose collection
|
|
899
1271
|
// lacks Bubblegum's plugin (`unminted`: those collections) gives nothing and mints nothing: the next takes its place.
|
|
900
1272
|
let minted = BigInt(treeMinted);
|
|
901
1273
|
const skips = new Set(unminted);
|
|
902
|
-
return order.won.slice(from, upto).flatMap(
|
|
1274
|
+
return order.won.slice(from, upto).flatMap(won => {
|
|
903
1275
|
if (won.Compressed) {
|
|
904
1276
|
const { item: id, drawn } = won.Compressed;
|
|
905
1277
|
const address = pda.item(project, id);
|
|
906
1278
|
const item = items[id];
|
|
907
1279
|
if (!item) throw new Error(`settling item ${id} needs the item (items), for its collection and its traits`);
|
|
908
|
-
const tables = drawn != null ? traitTables(item).map(
|
|
909
|
-
if (!tree)
|
|
1280
|
+
const tables = drawn != null ? traitTables(item).map(t => meta(pda.table(project, t), false, false)) : [];
|
|
1281
|
+
if (!tree)
|
|
1282
|
+
throw new Error('settling a compressed prize needs the project tree it is minted into (tree, treeMinted)');
|
|
910
1283
|
const asset = pda.compressedAsset(tree, minted);
|
|
911
1284
|
if (!skips.has(item.collection)) minted += 1n;
|
|
912
1285
|
return [
|
|
913
|
-
meta(address, false, true),
|
|
914
|
-
meta(
|
|
1286
|
+
meta(address, false, true),
|
|
1287
|
+
meta(pda.treeConfig(tree), false, true),
|
|
1288
|
+
meta(tree, false, true),
|
|
1289
|
+
meta(item.collection, false, true),
|
|
1290
|
+
meta(BUBBLEGUM_PROGRAM, false, false),
|
|
1291
|
+
meta(COMPRESSION_PROGRAM, false, false),
|
|
1292
|
+
meta(NOOP_PROGRAM, false, false),
|
|
1293
|
+
meta(MPL_CORE_PROGRAM, false, false),
|
|
1294
|
+
meta(BUBBLEGUM_CORE_SIGNER, false, false),
|
|
915
1295
|
// A fresh one's traits are written in the project's Sheet for the NFT it mints.
|
|
916
1296
|
...(drawn != null ? [...tables, meta(pda.sheet(project, asset), false, true)] : []),
|
|
917
1297
|
];
|
|
@@ -923,13 +1303,23 @@ export function wonAccounts({ order, items = {}, nfts = {}, prizeTo, putBack = [
|
|
|
923
1303
|
if (!item) throw new Error(`settling item ${id} needs the item (items), for its kind and collection`);
|
|
924
1304
|
if (item.kind !== 'Collectible') return [meta(address, false, true)];
|
|
925
1305
|
// A fresh unit's traits come from the rows its tables drew at the roll.
|
|
926
|
-
const tables = drawn != null ? traitTables(item).map(
|
|
927
|
-
return [
|
|
1306
|
+
const tables = drawn != null ? traitTables(item).map(t => meta(pda.table(project, t), false, false)) : [];
|
|
1307
|
+
return [
|
|
1308
|
+
meta(address, false, true),
|
|
1309
|
+
meta(item.collection, false, true),
|
|
1310
|
+
meta(pda.itemAsset(address, serial), false, true),
|
|
1311
|
+
meta(MPL_CORE_PROGRAM, false, false),
|
|
1312
|
+
...tables,
|
|
1313
|
+
];
|
|
928
1314
|
}
|
|
929
1315
|
if (won.Tokens) {
|
|
930
1316
|
const { mint } = won.Tokens;
|
|
931
1317
|
const program = tokenProgramOf(programs, mint);
|
|
932
|
-
return [
|
|
1318
|
+
return [
|
|
1319
|
+
meta(mint, false, false),
|
|
1320
|
+
meta(vault(project, mint, program), false, true),
|
|
1321
|
+
meta(prizeTo ?? ata(order.player, mint, program), false, true),
|
|
1322
|
+
];
|
|
933
1323
|
}
|
|
934
1324
|
if (won.Nft) {
|
|
935
1325
|
const { stash, nft: asset } = won.Nft;
|
|
@@ -975,21 +1365,24 @@ export function settlePartEnd(order, from = Number(order.settled ?? 0), items =
|
|
|
975
1365
|
}
|
|
976
1366
|
|
|
977
1367
|
/** How many draws one open of `box` makes: its slots' counts, or one. */
|
|
978
|
-
export const boxDraws =
|
|
1368
|
+
export const boxDraws = box => (box.slots?.length ? box.slots.reduce((n, s) => n + Number(s.count), 0) : 1);
|
|
979
1369
|
|
|
980
1370
|
/** How many settle parts an order of `opens` opens of `box` takes at most: each card, at its most calls, and with
|
|
981
1371
|
* shares one more (the first call splits the payment, alone when no prize fits beside its shares: `upto` 0). */
|
|
982
1372
|
export function settleParts(box, opens = 1) {
|
|
983
1373
|
const draws = box.slots?.length ? box.slots.reduce((n, s) => n + Number(s.count), 0) : 1;
|
|
984
|
-
const cards = Math.max(1, ...box.entries.map(
|
|
985
|
-
return
|
|
1374
|
+
const cards = Math.max(1, ...box.entries.map(e => e.prizes.length)) * draws * Math.max(1, opens);
|
|
1375
|
+
return (
|
|
1376
|
+
Math.max(1, Math.ceil(cards / Math.floor((TRACE_LIMIT - SETTLE_BASE_CALLS) / 7))) +
|
|
1377
|
+
((box.shares ?? []).length ? 1 : 0)
|
|
1378
|
+
);
|
|
986
1379
|
}
|
|
987
1380
|
|
|
988
1381
|
/** The heap a settle that moves three or more NFTs asks for (`settleHeapFrame`): all a transaction may have, 256 KiB. */
|
|
989
1382
|
export const LARGE_SETTLE_HEAP = 256 * 1024;
|
|
990
1383
|
|
|
991
1384
|
/** Whether an order rolled on an NFT: its `subject` names one (null for none). */
|
|
992
|
-
const rolledOn =
|
|
1385
|
+
const rolledOn = order => Boolean(order.subject) && order.subject !== NO_ADDRESS;
|
|
993
1386
|
|
|
994
1387
|
/**
|
|
995
1388
|
* The heap a settle of `order` asks for with Compute Budget's `RequestHeapFrame` (`requestHeapFrame`), in bytes; 0 for
|
|
@@ -1003,7 +1396,7 @@ const rolledOn = (order) => Boolean(order.subject) && order.subject !== NO_ADDRE
|
|
|
1003
1396
|
* the settle when `settleHeapFrame(order)` is not 0. A prize step of a lottery gives one place a call and needs none.
|
|
1004
1397
|
*/
|
|
1005
1398
|
export function settleHeapFrame(order) {
|
|
1006
|
-
const moves = (order.won ?? []).filter(
|
|
1399
|
+
const moves = (order.won ?? []).filter(w => w.Nft).length + (rolledOn(order) ? 1 : 0);
|
|
1007
1400
|
return moves >= 3 ? LARGE_SETTLE_HEAP : 0;
|
|
1008
1401
|
}
|
|
1009
1402
|
|
|
@@ -1020,7 +1413,7 @@ export function settleComputeUnits(order) {
|
|
|
1020
1413
|
// An NFT moved, an NFT rolled on, a compressed mint (Bubblegum's 60,000 to 90,000 units each) or a pack's part of
|
|
1021
1414
|
// many cards takes all a transaction may have.
|
|
1022
1415
|
const won = order.won ?? [];
|
|
1023
|
-
const moves = won.some(
|
|
1416
|
+
const moves = won.some(w => w.Nft || w.Compressed) || won.length > 8 || rolledOn(order);
|
|
1024
1417
|
return moves ? MAX_COMPUTE_UNITS : 800_000;
|
|
1025
1418
|
}
|
|
1026
1419
|
|
|
@@ -1044,20 +1437,52 @@ export const SETTLE_LAST = 4_294_967_295;
|
|
|
1044
1437
|
* (`settled`) up to it, and nothing more. The first call, whichever it is, splits the payment (its shares' accounts
|
|
1045
1438
|
* ride on it).
|
|
1046
1439
|
*/
|
|
1047
|
-
export function settleIx({
|
|
1440
|
+
export function settleIx({
|
|
1441
|
+
signer,
|
|
1442
|
+
orderAddress,
|
|
1443
|
+
order,
|
|
1444
|
+
projectState,
|
|
1445
|
+
items,
|
|
1446
|
+
nfts,
|
|
1447
|
+
prizeTo,
|
|
1448
|
+
putBack,
|
|
1449
|
+
subject,
|
|
1450
|
+
handBack = false,
|
|
1451
|
+
holders,
|
|
1452
|
+
programs,
|
|
1453
|
+
upto = SETTLE_LAST,
|
|
1454
|
+
tree,
|
|
1455
|
+
treeMinted,
|
|
1456
|
+
unminted,
|
|
1457
|
+
extra = [],
|
|
1458
|
+
}) {
|
|
1048
1459
|
const project = order.project;
|
|
1049
1460
|
const from = Number(order.settled ?? 0);
|
|
1050
1461
|
const last = Number(upto) > order.won.length;
|
|
1051
1462
|
const end = last ? order.won.length : Math.max(from, Number(upto));
|
|
1052
1463
|
// The payment is split once, by the first call (`drew` is 0 once it is made).
|
|
1053
1464
|
const splits = Number(order.drew ?? 0) > 0;
|
|
1054
|
-
const prizes = wonAccounts({
|
|
1465
|
+
const prizes = wonAccounts({
|
|
1466
|
+
order,
|
|
1467
|
+
items,
|
|
1468
|
+
nfts,
|
|
1469
|
+
prizeTo,
|
|
1470
|
+
putBack,
|
|
1471
|
+
programs,
|
|
1472
|
+
from,
|
|
1473
|
+
upto: end,
|
|
1474
|
+
tree,
|
|
1475
|
+
treeMinted,
|
|
1476
|
+
unminted,
|
|
1477
|
+
});
|
|
1055
1478
|
const tokenProgram = tokenProgramOf(programs, order.mint);
|
|
1056
1479
|
// The NFT it rolled on, as the settle moves it: given to a wallet that is an account of the loot program, it goes
|
|
1057
1480
|
// back to its player (`handsBack`), into the player's token account for a Token Metadata NFT.
|
|
1058
1481
|
// A part before the last gives only its prizes (and the first, the split): no NFT rolled on (its piece a stand-in).
|
|
1059
1482
|
const receiver = last ? receiverOf(order) : null;
|
|
1060
|
-
const on = last
|
|
1483
|
+
const on = last
|
|
1484
|
+
? orderPiece(project, orderAddress, order, subject, handBack && order.fate?.then?.Give ? order.player : receiver)
|
|
1485
|
+
: standIn(orderAddress);
|
|
1061
1486
|
return lootIx(
|
|
1062
1487
|
'settle',
|
|
1063
1488
|
{ upto: Number(upto), prize_accounts: prizes.length },
|
|
@@ -1076,7 +1501,14 @@ export function settleIx({ signer, orderAddress, order, projectState, items, nft
|
|
|
1076
1501
|
player_token: BigInt(order.back ?? 0) > 0n && last ? ata(order.player, order.mint, tokenProgram) : null,
|
|
1077
1502
|
},
|
|
1078
1503
|
{
|
|
1079
|
-
remaining: [
|
|
1504
|
+
remaining: [
|
|
1505
|
+
...on.proof,
|
|
1506
|
+
...prizes,
|
|
1507
|
+
...(splits
|
|
1508
|
+
? shareAccounts(project, order.shares, order.mint, { holders, programs, treasury: projectState.treasury })
|
|
1509
|
+
: []),
|
|
1510
|
+
...tailOf(extra),
|
|
1511
|
+
],
|
|
1080
1512
|
writable: [...(sharesBurn(order.shares) && splits ? ['mint'] : []), ...on.writable],
|
|
1081
1513
|
},
|
|
1082
1514
|
);
|
|
@@ -1107,7 +1539,17 @@ function orderPiece(project, orderAddress, order, subject = {}, receiver) {
|
|
|
1107
1539
|
const then = order.fate?.then;
|
|
1108
1540
|
const burned = order.held && then === 'Burn';
|
|
1109
1541
|
const stays = Boolean(then?.Stash);
|
|
1110
|
-
return piece({
|
|
1542
|
+
return piece({
|
|
1543
|
+
project,
|
|
1544
|
+
asset: order.subject,
|
|
1545
|
+
wallet: stays ? order.player : (receiver ?? order.player),
|
|
1546
|
+
...subject,
|
|
1547
|
+
moves: order.held && !stays,
|
|
1548
|
+
locks: false,
|
|
1549
|
+
writes: shifts,
|
|
1550
|
+
burns: burned,
|
|
1551
|
+
reads: shifts,
|
|
1552
|
+
});
|
|
1111
1553
|
}
|
|
1112
1554
|
|
|
1113
1555
|
/**
|
|
@@ -1116,7 +1558,7 @@ function orderPiece(project, orderAddress, order, subject = {}, receiver) {
|
|
|
1116
1558
|
* own address, a record, a stash, a fee box), where nothing could move it again. A settle's fate and an NFT lottery
|
|
1117
1559
|
* ticket's price take it as `handBack`.
|
|
1118
1560
|
*/
|
|
1119
|
-
export const handsBack =
|
|
1561
|
+
export const handsBack = account => account?.owner === LOOT_PROGRAM;
|
|
1120
1562
|
|
|
1121
1563
|
/** Where an NFT that is burned but can't be (any kind but Core) is sent: an address nobody holds the key to. */
|
|
1122
1564
|
export const INCINERATOR = '1nc1nerator11111111111111111111111111111111';
|
|
@@ -1132,9 +1574,10 @@ export function refundIx({ signer, orderAddress, order, subject, programs, extra
|
|
|
1132
1574
|
const project = order.project;
|
|
1133
1575
|
const tokenProgram = tokenProgramOf(programs, order.mint);
|
|
1134
1576
|
// A held NFT goes back to its player.
|
|
1135
|
-
const on =
|
|
1136
|
-
|
|
1137
|
-
|
|
1577
|
+
const on =
|
|
1578
|
+
order.held && rolledOn(order)
|
|
1579
|
+
? piece({ project, asset: order.subject, wallet: order.player, ...subject, ...ONLY_MOVES })
|
|
1580
|
+
: standIn(orderAddress);
|
|
1138
1581
|
return lootIx(
|
|
1139
1582
|
'refund',
|
|
1140
1583
|
{},
|
|
@@ -1170,10 +1613,20 @@ export function refundIx({ signer, orderAddress, order, subject, programs, extra
|
|
|
1170
1613
|
*/
|
|
1171
1614
|
export function askRollIx({ payer, orderAddress, order, box, settler = NOBODY_KEY, poolMints, items = {}, programs }) {
|
|
1172
1615
|
poolMints = boxPoolMints(box, poolMints);
|
|
1173
|
-
const opens = prizeMints(box.entries, poolMints).map(({ mint }) =>
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1616
|
+
const opens = prizeMints(box.entries, poolMints).map(({ mint }) =>
|
|
1617
|
+
createAssociatedTokenAccountIdempotent(payer, order.project, mint, tokenProgramOf(programs, mint)),
|
|
1618
|
+
);
|
|
1619
|
+
const ask = lootIx(
|
|
1620
|
+
'ask_roll',
|
|
1621
|
+
{ settler },
|
|
1622
|
+
{ payer, order: orderAddress, loot_box: order.loot_box, project: order.project },
|
|
1623
|
+
{
|
|
1624
|
+
remaining: [
|
|
1625
|
+
...entryAccounts(order.project, box.entries, items),
|
|
1626
|
+
...prizeAccounts(order.project, box.entries, poolMints, programs),
|
|
1627
|
+
],
|
|
1628
|
+
},
|
|
1629
|
+
);
|
|
1177
1630
|
return [...opens, ask];
|
|
1178
1631
|
}
|
|
1179
1632
|
|
|
@@ -1186,14 +1639,41 @@ export function askRollIx({ payer, orderAddress, order, box, settler = NOBODY_KE
|
|
|
1186
1639
|
* wallet), a collectible's holder or 'Burn', `amount` { Fixed: [n] } or { Bps: [bps] } of a pool. The authority opens the
|
|
1187
1640
|
* account it pays into when it is not there; a collectible nobody holds gets nothing.
|
|
1188
1641
|
*/
|
|
1189
|
-
export function moveTokensIx({
|
|
1642
|
+
export function moveTokensIx({
|
|
1643
|
+
authority,
|
|
1644
|
+
project,
|
|
1645
|
+
mint,
|
|
1646
|
+
from,
|
|
1647
|
+
to,
|
|
1648
|
+
amount,
|
|
1649
|
+
reference: ref,
|
|
1650
|
+
holders,
|
|
1651
|
+
programs,
|
|
1652
|
+
treasury,
|
|
1653
|
+
farm = null,
|
|
1654
|
+
extra = [],
|
|
1655
|
+
}) {
|
|
1190
1656
|
const writes = Boolean(from.Mint) || to === 'Burn';
|
|
1191
1657
|
return lootIx(
|
|
1192
1658
|
'move_tokens',
|
|
1193
1659
|
{ from, to, amount, reference: reference(ref) },
|
|
1194
1660
|
// What a farm holds free comes out of its rewards vault, passed as the pool, with the farm.
|
|
1195
|
-
{
|
|
1196
|
-
|
|
1661
|
+
{
|
|
1662
|
+
authority,
|
|
1663
|
+
project,
|
|
1664
|
+
mint,
|
|
1665
|
+
pool: from.Pool
|
|
1666
|
+
? pda.pool(project, from.Pool[0])
|
|
1667
|
+
: from.Farm
|
|
1668
|
+
? pda.rewards(pda.farm(project, from.Farm[0]))
|
|
1669
|
+
: null,
|
|
1670
|
+
farm: from.Farm ? pda.farm(project, from.Farm[0]) : null,
|
|
1671
|
+
farm_ends: from.Farm && farm?.keeps_ends ? pda.ends(pda.farm(project, from.Farm[0])) : null,
|
|
1672
|
+
},
|
|
1673
|
+
{
|
|
1674
|
+
remaining: [...destinationAccounts(project, to, mint, { holders, programs, treasury }), ...tailOf(extra)],
|
|
1675
|
+
writable: writes ? ['mint'] : [],
|
|
1676
|
+
},
|
|
1197
1677
|
);
|
|
1198
1678
|
}
|
|
1199
1679
|
|
|
@@ -1207,9 +1687,21 @@ export function moveTokensIx({ authority, project, mint, from, to, amount, refer
|
|
|
1207
1687
|
* what it writes goes in the NFT's Sheet, or on the NFT itself when its Attributes plugin names the project. A `Make`
|
|
1208
1688
|
* goes to `makeIx`.
|
|
1209
1689
|
*/
|
|
1210
|
-
export function collectibleIx({
|
|
1211
|
-
|
|
1212
|
-
|
|
1690
|
+
export function collectibleIx({
|
|
1691
|
+
authority,
|
|
1692
|
+
project,
|
|
1693
|
+
collection,
|
|
1694
|
+
asset,
|
|
1695
|
+
change,
|
|
1696
|
+
owner = null,
|
|
1697
|
+
hasAuthority = true,
|
|
1698
|
+
reference: ref,
|
|
1699
|
+
...make
|
|
1700
|
+
}) {
|
|
1701
|
+
if (change.Make)
|
|
1702
|
+
return makeIx({ authority, project, ...change.Make, ...make, owners: make.owners ?? [owner], reference: ref });
|
|
1703
|
+
const sheet =
|
|
1704
|
+
change.Set && !hasAuthority ? pda.sheet(project, asset) : change.Profile ? pda.profile(project, asset) : null;
|
|
1213
1705
|
const burn = change === 'Burn' || Boolean(change.Burn);
|
|
1214
1706
|
return lootIx(
|
|
1215
1707
|
'collectible',
|
|
@@ -1227,10 +1719,16 @@ export function collectibleIx({ authority, project, collection, asset, change, o
|
|
|
1227
1719
|
* refuses the whole call (`PrizeNotCovered`).
|
|
1228
1720
|
*/
|
|
1229
1721
|
export function makeIx({ authority, project, item, itemState, owners, attributes = [], tree = null, reference: ref }) {
|
|
1230
|
-
if (!itemState)
|
|
1231
|
-
|
|
1722
|
+
if (!itemState)
|
|
1723
|
+
throw new Error(
|
|
1724
|
+
`a make of item ${item} needs the item (itemState: its decoded Item), for its collection and next serial`,
|
|
1725
|
+
);
|
|
1726
|
+
if (!owners?.length || owners.some(o => !o)) throw new Error('A make names who gets each one (`owners`).');
|
|
1232
1727
|
const prize = tree ? { Compressed: { item, tree } } : { Item: [item] };
|
|
1233
|
-
const remaining = owners.flatMap((owner, made) => [
|
|
1728
|
+
const remaining = owners.flatMap((owner, made) => [
|
|
1729
|
+
meta(owner, false, false),
|
|
1730
|
+
...prizeMetas({ project, prize, winner: owner, items: { [item]: itemState }, made }),
|
|
1731
|
+
]);
|
|
1234
1732
|
return lootIx(
|
|
1235
1733
|
'collectible',
|
|
1236
1734
|
{ change: { Make: { item, tree, attributes } }, reference: reference(ref) },
|
|
@@ -1240,7 +1738,8 @@ export function makeIx({ authority, project, item, itemState, owners, attributes
|
|
|
1240
1738
|
}
|
|
1241
1739
|
|
|
1242
1740
|
/** The collectibles a `makeIx` of `count` mints (not compressed): at the item's next serials. */
|
|
1243
|
-
export const madeAssets = (project, item, itemState, count = 1) =>
|
|
1741
|
+
export const madeAssets = (project, item, itemState, count = 1) =>
|
|
1742
|
+
Array.from({ length: count }, (_, k) => pda.itemAsset(pda.item(project, item), Number(itemState.issued) + 1 + k));
|
|
1244
1743
|
|
|
1245
1744
|
// ---------------------------------------------------------------- decide
|
|
1246
1745
|
|
|
@@ -1253,23 +1752,56 @@ export const madeAssets = (project, item, itemState, count = 1) => Array.from({
|
|
|
1253
1752
|
* (`piece`): `asset` and `collection` for a Core asset, with `tokenMetadata` for a Token Metadata NFT, or `compressed`
|
|
1254
1753
|
* (after a `proveLeafIx`).
|
|
1255
1754
|
*/
|
|
1256
|
-
export function releaseIx({
|
|
1755
|
+
export function releaseIx({
|
|
1756
|
+
signer,
|
|
1757
|
+
project,
|
|
1758
|
+
collection,
|
|
1759
|
+
asset,
|
|
1760
|
+
tokenMetadata: tm,
|
|
1761
|
+
compressed,
|
|
1762
|
+
hold,
|
|
1763
|
+
address = null,
|
|
1764
|
+
to,
|
|
1765
|
+
treasury,
|
|
1766
|
+
reference: ref,
|
|
1767
|
+
more = [],
|
|
1768
|
+
}) {
|
|
1257
1769
|
// A hold whose player is the project's own address is owed to the treasury, as the project names it now (`treasury`,
|
|
1258
1770
|
// the decoded Project's).
|
|
1259
|
-
if (!to && hold.player === project && !treasury)
|
|
1771
|
+
if (!to && hold.player === project && !treasury)
|
|
1772
|
+
throw new Error('This hold is owed to the treasury: pass treasury (the project’s, as it is now).');
|
|
1260
1773
|
const receiver = to ?? (hold.player === project ? treasury : hold.player);
|
|
1261
1774
|
// Never to the project's own address, nor any account of the program, where nothing could move it again (ToTheProject).
|
|
1262
1775
|
if (receiver === project) throw new Error('A held NFT can’t go to the project’s own address: name a wallet.');
|
|
1263
1776
|
// Several holds of one player's with one end, payer and rent wallet (`more`: each { asset, collection, tokenMetadata,
|
|
1264
1777
|
// compressed }) go to one `to` together, in ascending order of their holds' addresses.
|
|
1265
|
-
const [first, ...rest] = ascending(
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1778
|
+
const [first, ...rest] = ascending(
|
|
1779
|
+
[{ asset, collection, tokenMetadata: tm, compressed, address }, ...more].map(n => {
|
|
1780
|
+
const p = piece({
|
|
1781
|
+
project,
|
|
1782
|
+
asset: n.asset,
|
|
1783
|
+
wallet: receiver,
|
|
1784
|
+
collection: n.collection,
|
|
1785
|
+
tokenMetadata: n.tokenMetadata,
|
|
1786
|
+
compressed: n.compressed,
|
|
1787
|
+
...ONLY_MOVES,
|
|
1788
|
+
});
|
|
1789
|
+
return { p, hold: n.address ?? pda.hold(p.accounts.asset, hold.free_at) };
|
|
1790
|
+
}),
|
|
1791
|
+
x => x.hold,
|
|
1792
|
+
);
|
|
1793
|
+
const p = withPrograms(
|
|
1794
|
+
first.p,
|
|
1795
|
+
rest.map(x => x.p),
|
|
1796
|
+
);
|
|
1797
|
+
const groups = rest.flatMap(x => [meta(x.hold, false, true), ...anotherMetas(x.p)]);
|
|
1271
1798
|
// The hold's rent goes back to whoever made it (`payer`), a Token Metadata NFT's emptied account's to whoever paid it.
|
|
1272
|
-
return lootIx(
|
|
1799
|
+
return lootIx(
|
|
1800
|
+
'release',
|
|
1801
|
+
{ reference: reference(ref) },
|
|
1802
|
+
{ signer, project, ...p.accounts, hold: first.hold, payer: hold.payer, rent_to: hold.rent_to, to: receiver },
|
|
1803
|
+
{ remaining: [...p.proof, ...groups], writable: p.writable },
|
|
1804
|
+
);
|
|
1273
1805
|
}
|
|
1274
1806
|
|
|
1275
1807
|
/** The mint of a contest's split prize (its `prize`, tokens): minted, or a pool's (`poolMint`). */
|
|
@@ -1288,17 +1820,39 @@ export const openContestIx = ({ authority, project, projectState, id, mint, args
|
|
|
1288
1820
|
stored(args.shares);
|
|
1289
1821
|
const contest = pda.contest(project, id);
|
|
1290
1822
|
const split = Boolean(args.prize_split);
|
|
1291
|
-
return lootIx(
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1823
|
+
return lootIx(
|
|
1824
|
+
'open_contest',
|
|
1825
|
+
{
|
|
1826
|
+
id,
|
|
1827
|
+
args: {
|
|
1828
|
+
deciders: [],
|
|
1829
|
+
min_pot: 0,
|
|
1830
|
+
stake: 0,
|
|
1831
|
+
prize: null,
|
|
1832
|
+
nft: null,
|
|
1833
|
+
prize_split: false,
|
|
1834
|
+
cap: null,
|
|
1835
|
+
...args,
|
|
1836
|
+
gate: args.gate ?? null,
|
|
1837
|
+
},
|
|
1838
|
+
},
|
|
1839
|
+
{
|
|
1840
|
+
authority,
|
|
1841
|
+
project,
|
|
1842
|
+
mint,
|
|
1843
|
+
contest,
|
|
1844
|
+
prize_mint: split ? splitMint(args.prize, prizeMint) : null,
|
|
1845
|
+
prize_vault: split ? pda.prizeVault(contest) : null,
|
|
1846
|
+
entries: pda.entries(contest, BigInt(projectState.incarnations) + 1n),
|
|
1847
|
+
// A split prize from a pool names the pool at its index (`WrongPool` without it; a prize mint not the pool's is
|
|
1848
|
+
// `WrongMint`).
|
|
1849
|
+
prize_pool: split && args.prize?.Tokens?.from?.Pool ? pda.pool(project, args.prize.Tokens.from.Pool[0]) : null,
|
|
1850
|
+
},
|
|
1851
|
+
{
|
|
1852
|
+
// A prize that is a roll: its box.
|
|
1853
|
+
remaining: args.prize?.Roll ? boxesAccounts(project, [args.prize.Roll[0]]) : [],
|
|
1854
|
+
},
|
|
1855
|
+
);
|
|
1302
1856
|
};
|
|
1303
1857
|
|
|
1304
1858
|
/** A contest's list of entries, from its decoded Contest. */
|
|
@@ -1317,7 +1871,19 @@ export const VERDICT_DOMAIN = 'loot-verdict-v4';
|
|
|
1317
1871
|
export function verdictBytes({ program = LOOT_PROGRAM, contest, incarnation, side, expires, recipient = null }) {
|
|
1318
1872
|
if (incarnation == null) throw new Error('A verdict signs the contest’s incarnation (the decoded Contest’s).');
|
|
1319
1873
|
const named = recipient == null ? new Uint8Array(0) : addressBytes(recipient);
|
|
1320
|
-
return concat(
|
|
1874
|
+
return concat(
|
|
1875
|
+
utf8(VERDICT_DOMAIN),
|
|
1876
|
+
sha256(
|
|
1877
|
+
concat(
|
|
1878
|
+
addressBytes(program),
|
|
1879
|
+
addressBytes(contest),
|
|
1880
|
+
uintLE(incarnation, 8),
|
|
1881
|
+
uintLE(side ?? 0xffff, 2),
|
|
1882
|
+
intLE(expires, 8),
|
|
1883
|
+
named,
|
|
1884
|
+
),
|
|
1885
|
+
),
|
|
1886
|
+
);
|
|
1321
1887
|
}
|
|
1322
1888
|
|
|
1323
1889
|
/**
|
|
@@ -1330,36 +1896,74 @@ export function verdictBytes({ program = LOOT_PROGRAM, contest, incarnation, sid
|
|
|
1330
1896
|
* contest with a chosen share is decided naming its `recipient`, a wallet (a bounty's worker); a decider's verdict
|
|
1331
1897
|
* signs it too (`verdictBytes`).
|
|
1332
1898
|
*/
|
|
1333
|
-
export function decideIx({
|
|
1899
|
+
export function decideIx({
|
|
1900
|
+
signer,
|
|
1901
|
+
project,
|
|
1902
|
+
projectState,
|
|
1903
|
+
contestAddress,
|
|
1904
|
+
contest,
|
|
1905
|
+
side,
|
|
1906
|
+
recipient = null,
|
|
1907
|
+
verdict,
|
|
1908
|
+
reference: ref,
|
|
1909
|
+
holders,
|
|
1910
|
+
prizeMint,
|
|
1911
|
+
programs,
|
|
1912
|
+
extra = [],
|
|
1913
|
+
}) {
|
|
1334
1914
|
const address = contestAddress ?? pda.contest(project, contest.id);
|
|
1335
1915
|
const named = recipient == null ? contest.shares : chosenShares(contest.shares, recipient);
|
|
1336
|
-
const shares =
|
|
1916
|
+
const shares =
|
|
1917
|
+
side == null
|
|
1918
|
+
? []
|
|
1919
|
+
: shareAccounts(project, named, contest.mint, { holders, programs, treasury: projectState?.treasury });
|
|
1337
1920
|
// A split prize is set aside when a side is named (minted, or from its pool). A decision never closes the contest:
|
|
1338
1921
|
// `claim` does, entered or not.
|
|
1339
1922
|
const split = Boolean(contest.prize_split);
|
|
1340
1923
|
const from = contest.prize?.Tokens?.from;
|
|
1341
1924
|
return lootIx(
|
|
1342
1925
|
'decide',
|
|
1343
|
-
{ side: side ?? null, expires: toBigInt(verdict?.expires ?? 0), reference: reference(ref), incarnation: contest.incarnation, recipient },
|
|
1344
1926
|
{
|
|
1345
|
-
|
|
1927
|
+
side: side ?? null,
|
|
1928
|
+
expires: toBigInt(verdict?.expires ?? 0),
|
|
1929
|
+
reference: reference(ref),
|
|
1930
|
+
incarnation: contest.incarnation,
|
|
1931
|
+
recipient,
|
|
1932
|
+
},
|
|
1933
|
+
{
|
|
1934
|
+
signer,
|
|
1935
|
+
project,
|
|
1936
|
+
contest: address,
|
|
1937
|
+
mint: contest.mint,
|
|
1938
|
+
instructions: verdict ? SYSVAR_INSTRUCTIONS : null,
|
|
1346
1939
|
prize_mint: split ? splitMint(contest.prize, prizeMint) : null,
|
|
1347
1940
|
// Its pool: the prize set aside from it when a side is named.
|
|
1348
1941
|
prize_pool: split && side != null && from?.Pool ? pda.pool(project, from.Pool[0]) : null,
|
|
1349
1942
|
prize_vault: split ? pda.prizeVault(address) : null,
|
|
1350
1943
|
},
|
|
1351
1944
|
// The mint is written by a burn share; the prize's mint when it is minted.
|
|
1352
|
-
{
|
|
1945
|
+
{
|
|
1946
|
+
remaining: [...shares, ...tailOf(extra)],
|
|
1947
|
+
writable: [
|
|
1948
|
+
...(side != null && sharesBurn(contest.shares) ? ['mint'] : []),
|
|
1949
|
+
...(split && side != null && from?.Mint ? ['prize_mint'] : []),
|
|
1950
|
+
],
|
|
1951
|
+
},
|
|
1353
1952
|
);
|
|
1354
1953
|
}
|
|
1355
1954
|
|
|
1356
1955
|
/** The authority asks the oracle for a random number, for a rule of the project's own. */
|
|
1357
|
-
export const rollIx = ({ authority, project, reference: ref }) =>
|
|
1956
|
+
export const rollIx = ({ authority, project, reference: ref }) =>
|
|
1957
|
+
lootIx('roll', { reference: reference(ref) }, { authority, project });
|
|
1358
1958
|
|
|
1359
1959
|
/** The oracle's answer to a `roll` (for a local simulation). */
|
|
1360
1960
|
export const callbackRandomIx = ({ project, reference: ref, randomness }) => ({
|
|
1361
1961
|
programId: LOOT_PROGRAM,
|
|
1362
|
-
keys: [
|
|
1962
|
+
keys: [
|
|
1963
|
+
meta(pda.oracleIdentity(), true, false),
|
|
1964
|
+
meta(project, false, false),
|
|
1965
|
+
meta(hashToAddress(reference(ref)), false, false),
|
|
1966
|
+
],
|
|
1363
1967
|
data: encodeInstruction(IDL, 'callback_random', { randomness: Uint8Array.from(randomness) }),
|
|
1364
1968
|
});
|
|
1365
1969
|
|
|
@@ -1371,7 +1975,18 @@ export const callbackRandomIx = ({ project, reference: ref, randomness }) => ({
|
|
|
1371
1975
|
* lapse; only a sealed schedule's fixed amount pays every run due, up to `upto` runs made (`scheduleCatchesUp`).
|
|
1372
1976
|
* `poolMint`: the mint of the pool it moves from (when it moves from one). `extra`: a hook's or a memo's accounts.
|
|
1373
1977
|
*/
|
|
1374
|
-
export function runScheduleIx({
|
|
1978
|
+
export function runScheduleIx({
|
|
1979
|
+
payer,
|
|
1980
|
+
project,
|
|
1981
|
+
id,
|
|
1982
|
+
schedule,
|
|
1983
|
+
poolMint,
|
|
1984
|
+
holders,
|
|
1985
|
+
programs,
|
|
1986
|
+
treasury,
|
|
1987
|
+
upto = 0,
|
|
1988
|
+
extra = [],
|
|
1989
|
+
}) {
|
|
1375
1990
|
const { from, to } = schedule.action;
|
|
1376
1991
|
const address = pda.schedule(project, id);
|
|
1377
1992
|
// A sealed schedule pays from its own vault (`pda.vest`), passed where a pool would be.
|
|
@@ -1383,11 +1998,13 @@ export function runScheduleIx({ payer, project, id, schedule, poolMint, holders,
|
|
|
1383
1998
|
'run_schedule',
|
|
1384
1999
|
{ upto },
|
|
1385
2000
|
{ payer, project, schedule: address, pool, mint },
|
|
1386
|
-
{
|
|
2001
|
+
{
|
|
2002
|
+
remaining: [...destinationAccounts(project, to, mint, { holders, programs, treasury }), ...tailOf(extra)],
|
|
2003
|
+
writable: writes ? ['mint'] : [],
|
|
2004
|
+
},
|
|
1387
2005
|
);
|
|
1388
2006
|
}
|
|
1389
2007
|
|
|
1390
|
-
|
|
1391
2008
|
// A lottery's rounds (`accounts.js`: `decodeRound`, `partsOwed`, `commonOwed`): each round is a record of its own, opened
|
|
1392
2009
|
// by its first purchase. Its ask (`draw_lottery`) fixes its money and moves the lottery on; after the answer, `pull`
|
|
1393
2010
|
// pays one part at a time, in any order, and the pull that leaves nothing owed closes it. Every builder takes the
|
|
@@ -1406,7 +2023,25 @@ export const currentRound = (lotteryAddress, lottery) => pda.round(lotteryAddres
|
|
|
1406
2023
|
* `treasury` wallet. A lottery whose ticket costs an NFT sells one at a time, for `nft` ({ asset, collection,
|
|
1407
2024
|
* tokenMetadata, compressed }, as `piece` takes it).
|
|
1408
2025
|
*/
|
|
1409
|
-
export function buyTicketsIx({
|
|
2026
|
+
export function buyTicketsIx({
|
|
2027
|
+
player,
|
|
2028
|
+
project,
|
|
2029
|
+
id,
|
|
2030
|
+
lottery,
|
|
2031
|
+
round = null,
|
|
2032
|
+
mint,
|
|
2033
|
+
count,
|
|
2034
|
+
shown,
|
|
2035
|
+
listProof,
|
|
2036
|
+
chosenRecipient = null,
|
|
2037
|
+
chosen = null,
|
|
2038
|
+
nft,
|
|
2039
|
+
holders = {},
|
|
2040
|
+
treasury,
|
|
2041
|
+
playerToken,
|
|
2042
|
+
programs,
|
|
2043
|
+
extra = [],
|
|
2044
|
+
}) {
|
|
1410
2045
|
const address = pda.lottery(project, id);
|
|
1411
2046
|
const key = currentRound(address, lottery);
|
|
1412
2047
|
// The terms the round copied, or for its first purchase the lottery's.
|
|
@@ -1420,13 +2055,25 @@ export function buyTicketsIx({ player, project, id, lottery, round = null, mint,
|
|
|
1420
2055
|
'buy_tickets',
|
|
1421
2056
|
{ count, chosen_recipient: chosenRecipient, revision: round ? round.revision : lottery.revision },
|
|
1422
2057
|
{
|
|
1423
|
-
player,
|
|
2058
|
+
player,
|
|
2059
|
+
project,
|
|
2060
|
+
lottery: address,
|
|
2061
|
+
round: key,
|
|
1424
2062
|
player_token: playerToken ?? ata(player, mint, tokenProgramOf(programs, mint)),
|
|
1425
|
-
payment_vault: pda.roundTokens(key, mint),
|
|
2063
|
+
payment_vault: pda.roundTokens(key, mint),
|
|
2064
|
+
mint,
|
|
2065
|
+
chosen,
|
|
1426
2066
|
counts: Number(lottery.per_wallet) ? pda.counts(project, player) : null,
|
|
1427
2067
|
associated_token_program: choices.length ? ASSOCIATED_TOKEN_PROGRAM : null,
|
|
1428
2068
|
},
|
|
1429
|
-
{
|
|
2069
|
+
{
|
|
2070
|
+
remaining: [
|
|
2071
|
+
...gateAccounts(project, { gate: lottery.gate, wallet: player, shown, listProof }),
|
|
2072
|
+
...recipients,
|
|
2073
|
+
...(held ? [...pieceMetas(held), ...held.proof] : []),
|
|
2074
|
+
...tailOf(extra),
|
|
2075
|
+
],
|
|
2076
|
+
},
|
|
1430
2077
|
);
|
|
1431
2078
|
}
|
|
1432
2079
|
|
|
@@ -1435,12 +2082,26 @@ export function buyTicketsIx({ player, project, id, lottery, round = null, mint,
|
|
|
1435
2082
|
* round asked for (null when nobody bought into the current one: it moves on, or a lottery that draws once ends, with
|
|
1436
2083
|
* no ask), and `roundAddress` its address when it is not the current round's (an older round still waiting for its
|
|
1437
2084
|
* answer: asked again after `STALE_SECONDS`, or after the timeout cancelled, with no lottery account, so `lottery` may be
|
|
1438
|
-
* null then; without `round`, the current round's, with the lottery's terms). A round's first ask fixes its money
|
|
1439
|
-
*
|
|
1440
|
-
*
|
|
1441
|
-
*
|
|
2085
|
+
* null then; without `round`, the current round's, with the lottery's terms). A round's first ask fixes its money. Paid
|
|
2086
|
+
* from a pot, it takes the pot, its mint and the round's account of it, then, when the lottery names a round that may
|
|
2087
|
+
* still owe a pot (`lottery.owing`), that round, its account of the mint and its pot (`owing`: that round's decoded
|
|
2088
|
+
* record, whose terms name its pot; without it, or when the record is gone, the pot this round pays from). A round paid
|
|
2089
|
+
* from its own money takes none of these. Then each run of token prizes in place order, its mint and for a pool, the
|
|
2090
|
+
* pool and the round's account of the mint (`poolMints`: a pool's index to its mint). With `skipHooked` (the project's
|
|
2091
|
+
* authority only) a hooked pool or pot gives the round nothing. `extra`: hook accounts.
|
|
1442
2092
|
*/
|
|
1443
|
-
export function drawLotteryIx({
|
|
2093
|
+
export function drawLotteryIx({
|
|
2094
|
+
payer,
|
|
2095
|
+
project,
|
|
2096
|
+
id,
|
|
2097
|
+
lottery,
|
|
2098
|
+
round = null,
|
|
2099
|
+
roundAddress = null,
|
|
2100
|
+
owing = null,
|
|
2101
|
+
skipHooked = false,
|
|
2102
|
+
poolMints = {},
|
|
2103
|
+
extra = [],
|
|
2104
|
+
}) {
|
|
1444
2105
|
const address = id === undefined || id === null ? null : pda.lottery(project, id);
|
|
1445
2106
|
const key = roundAddress ?? currentRound(address, lottery);
|
|
1446
2107
|
const rest = [];
|
|
@@ -1449,20 +2110,49 @@ export function drawLotteryIx({ payer, project, id, lottery, round = null, round
|
|
|
1449
2110
|
const current = !round || round.status === 'Open';
|
|
1450
2111
|
if (current) {
|
|
1451
2112
|
const { terms } = round ?? lottery;
|
|
1452
|
-
|
|
2113
|
+
const potId = potIdOf(terms);
|
|
2114
|
+
if (potId != null) {
|
|
2115
|
+
rest.push(
|
|
2116
|
+
meta(pda.lotteryPot(address, potId), false, true),
|
|
2117
|
+
meta(terms.mint, false, false),
|
|
2118
|
+
meta(pda.roundTokens(key, terms.mint), false, true),
|
|
2119
|
+
);
|
|
2120
|
+
// What the round before left its pot moves in first: that round, its account of the mint, and its pot.
|
|
2121
|
+
const earlier = lottery?.owing;
|
|
2122
|
+
if (earlier && earlier !== NO_ADDRESS) {
|
|
2123
|
+
const earlierPot = potIdOf(owing?.terms) ?? potId;
|
|
2124
|
+
rest.push(
|
|
2125
|
+
meta(earlier, false, true),
|
|
2126
|
+
meta(pda.roundTokens(earlier, terms.mint), false, true),
|
|
2127
|
+
meta(pda.lotteryPot(address, earlierPot), false, true),
|
|
2128
|
+
);
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
1453
2131
|
for (const place of terms.places) {
|
|
1454
2132
|
const from = place.prize?.Tokens?.from;
|
|
1455
2133
|
if (from?.Mint) rest.push(meta(from.Mint[0], false, false));
|
|
1456
2134
|
else if (from?.Pool) {
|
|
1457
2135
|
const mint = poolMints[from.Pool[0]];
|
|
1458
|
-
if (!mint)
|
|
1459
|
-
|
|
2136
|
+
if (!mint)
|
|
2137
|
+
throw new Error(
|
|
2138
|
+
`The ask of a round whose place gives tokens from pool ${from.Pool[0]} needs that pool's mint (poolMints).`,
|
|
2139
|
+
);
|
|
2140
|
+
rest.push(
|
|
2141
|
+
meta(mint, false, false),
|
|
2142
|
+
meta(pda.pool(project, from.Pool[0]), false, true),
|
|
2143
|
+
meta(pda.roundTokens(key, mint), false, true),
|
|
2144
|
+
);
|
|
1460
2145
|
}
|
|
1461
2146
|
}
|
|
1462
2147
|
}
|
|
1463
2148
|
// The lottery goes with its current round only: a round whose draw waits is asked again or cancelled without it, so a
|
|
1464
2149
|
// lottery closed since never strands it.
|
|
1465
|
-
return lootIx(
|
|
2150
|
+
return lootIx(
|
|
2151
|
+
'draw_lottery',
|
|
2152
|
+
{ skip_hooked: skipHooked },
|
|
2153
|
+
{ payer, project, lottery: current ? address : null, round: key },
|
|
2154
|
+
{ remaining: [...rest, ...tailOf(extra)] },
|
|
2155
|
+
);
|
|
1466
2156
|
}
|
|
1467
2157
|
|
|
1468
2158
|
/** The oracle's answer to a round's draw (for a local simulation): the round's record is its only account. */
|
|
@@ -1475,10 +2165,17 @@ export const callbackLotteryIx = ({ round, randomness }) => ({
|
|
|
1475
2165
|
/** The accounts a round's close takes after a pull's own: each of its token accounts, its mint and who opened it (the
|
|
1476
2166
|
* lottery, which gets its spare back, or a buyer who fronted it; `owner` for one the lottery opened, once it is closed). */
|
|
1477
2167
|
export const roundCloseMetas = (key, r, owner = null) =>
|
|
1478
|
-
r.accounts
|
|
2168
|
+
r.accounts
|
|
2169
|
+
.filter(o => o.mint !== NO_ADDRESS)
|
|
2170
|
+
.flatMap(o => [
|
|
2171
|
+
meta(pda.roundTokens(key, o.mint), false, true),
|
|
2172
|
+
meta(o.mint, false, true),
|
|
2173
|
+
meta(owner && o.by === r.lottery ? owner : o.by, false, true),
|
|
2174
|
+
]);
|
|
1479
2175
|
|
|
1480
2176
|
/** The NFT a raffle's place draws from the lottery's own stash (`stash` decoded, as it is now) with the round's answer. */
|
|
1481
|
-
export const raffledNft = (stash, answer, place) =>
|
|
2177
|
+
export const raffledNft = (stash, answer, place) =>
|
|
2178
|
+
stash?.nfts[weighted(stream(answer, [TAG.LOTTERY, place]), stash.weights)] ?? null;
|
|
1482
2179
|
|
|
1483
2180
|
/**
|
|
1484
2181
|
* The NFT of the lottery's own stash each place of a drawn round that holds or draws one gets, in place order, as the
|
|
@@ -1489,7 +2186,7 @@ export const raffledNft = (stash, answer, place) => stash?.nfts[weighted(stream(
|
|
|
1489
2186
|
export function stashNfts(r, stash) {
|
|
1490
2187
|
const nfts = r.nfts.map((n, k) => (k < Number(r.assigned) ? (n === NO_ADDRESS ? null : n) : undefined));
|
|
1491
2188
|
const rows = { nfts: [...(stash?.nfts ?? [])], weights: [...(stash?.weights ?? [])] };
|
|
1492
|
-
const take =
|
|
2189
|
+
const take = row => {
|
|
1493
2190
|
if (row == null || row < 0) return null;
|
|
1494
2191
|
const nft = rows.nfts[row];
|
|
1495
2192
|
const last = rows.nfts.length - 1;
|
|
@@ -1505,13 +2202,15 @@ export function stashNfts(r, stash) {
|
|
|
1505
2202
|
if (ownStash(p)) {
|
|
1506
2203
|
for (let place = first; place < first + times && place < w; place++, k++) {
|
|
1507
2204
|
if (nfts[k] !== undefined) continue;
|
|
1508
|
-
nfts[k] = p.prize?.Held
|
|
2205
|
+
nfts[k] = p.prize?.Held
|
|
2206
|
+
? take(rows.nfts.indexOf(p.prize.Held[0]))
|
|
2207
|
+
: take(weighted(stream(r.answer, [TAG.LOTTERY, place]), rows.weights));
|
|
1509
2208
|
}
|
|
1510
2209
|
k += Math.max(0, first + times - Math.max(first, w));
|
|
1511
2210
|
}
|
|
1512
2211
|
first += times;
|
|
1513
2212
|
}
|
|
1514
|
-
return nfts.map(
|
|
2213
|
+
return nfts.map(n => n ?? null);
|
|
1515
2214
|
}
|
|
1516
2215
|
|
|
1517
2216
|
/** The program's rule for where a part nobody can take goes: the treasury's wallet, then its account of the mint. */
|
|
@@ -1523,13 +2222,14 @@ const treasuryAccounts = (treasury, mint, programs) => walletAccounts(treasury,
|
|
|
1523
2222
|
* 'Own' entry `entry`'s own part: a refund, a no-loss ticket's price, or its NFT ticket back or where its
|
|
1524
2223
|
* price sends it (`ticket` its kind as `piece` takes it; `handBack` when the wallet its price gives
|
|
1525
2224
|
* it to is an account of the loot program; `hold`: made a hold for whoever it is owed to instead);
|
|
1526
|
-
* { Pot: [run] } the entry's places in the run, their part of the
|
|
2225
|
+
* { Pot: [run] } the entry's places in the run, their part of what the round pays from, paid into the buyer's account;
|
|
1527
2226
|
* { Prize: [run] } their prize: tokens into the buyer's account, or each place's prize (`prize`: { stash, the lottery's
|
|
1528
2227
|
* own stash decoded, for NFTs not taken out yet (`stashNfts`); stashes, a project stash's id to its
|
|
1529
2228
|
* decoded stash; nfts, an NFT's kind by its asset; hold, NFTs made holds; most, the most places a
|
|
1530
2229
|
* pull carries; and what `prizeMetas` takes: collection, poolMints, items, projectMint }), a roll's
|
|
1531
2230
|
* order at `prizeRollNonce(nonce + place)` (ask it in the same transaction: `pullRolls`);
|
|
1532
|
-
* { Common: [i] } a part owed in common: share `i`, the
|
|
2231
|
+
* { Common: [i] } a part owed in common: share `i`, what the places leave (`i` = the shares' count: to the pot, or
|
|
2232
|
+
* to `rest` for a round paid from its own money; never owed when that burns), a pool's prize back after a
|
|
1533
2233
|
* cancel (past it), paid where it goes (`holders`, `programs`); `toTreasury` when that can't take it
|
|
1534
2234
|
* (a pool never opened, the pot closed since or made again in another mint, an account gone: a
|
|
1535
2235
|
* holder nobody is is found here).
|
|
@@ -1537,7 +2237,25 @@ const treasuryAccounts = (treasury, mint, programs) => walletAccounts(treasury,
|
|
|
1537
2237
|
* lottery is gone, so rent it lent goes to the owner). A round that owes nothing and waits only for its close is closed
|
|
1538
2238
|
* by any pull.
|
|
1539
2239
|
*/
|
|
1540
|
-
export function pullIx({
|
|
2240
|
+
export function pullIx({
|
|
2241
|
+
payer,
|
|
2242
|
+
project,
|
|
2243
|
+
roundAddress: key,
|
|
2244
|
+
round: r,
|
|
2245
|
+
entry = 0,
|
|
2246
|
+
part = 'Own',
|
|
2247
|
+
projectState,
|
|
2248
|
+
ticket = {},
|
|
2249
|
+
handBack = false,
|
|
2250
|
+
hold = false,
|
|
2251
|
+
nonce = 0n,
|
|
2252
|
+
prize = {},
|
|
2253
|
+
holders = {},
|
|
2254
|
+
toTreasury = false,
|
|
2255
|
+
lotteryClosed = false,
|
|
2256
|
+
programs,
|
|
2257
|
+
extra = [],
|
|
2258
|
+
}) {
|
|
1541
2259
|
const rest = [];
|
|
1542
2260
|
let [mint, to, settled, buyer, partial] = [null, null, 1, payer, false];
|
|
1543
2261
|
const account = (m, wallet) => ata(wallet, m, tokenProgramOf(programs, m));
|
|
@@ -1554,8 +2272,17 @@ export function pullIx({ payer, project, roundAddress: key, round: r, entry = 0,
|
|
|
1554
2272
|
} else {
|
|
1555
2273
|
// Where its price sends it (a stash keeps it where it is), or back to its buyer.
|
|
1556
2274
|
const receiver = then ? spentTo(project, then) : null;
|
|
1557
|
-
const holder = then?.Stash || (handBack && then?.Give) ? e.buyer : receiver ?? e.buyer;
|
|
1558
|
-
const p = piece({
|
|
2275
|
+
const holder = then?.Stash || (handBack && then?.Give) ? e.buyer : (receiver ?? e.buyer);
|
|
2276
|
+
const p = piece({
|
|
2277
|
+
project,
|
|
2278
|
+
asset: e.nft,
|
|
2279
|
+
wallet: holder,
|
|
2280
|
+
...ticket,
|
|
2281
|
+
moves: !then?.Stash,
|
|
2282
|
+
locks: false,
|
|
2283
|
+
burns: then === 'Burn' || Boolean(then?.Burn),
|
|
2284
|
+
reads: false,
|
|
2285
|
+
});
|
|
1559
2286
|
rest.push(...pieceMetas(p), ...p.proof, ...(receiver ? [meta(receiver, false, true)] : []));
|
|
1560
2287
|
}
|
|
1561
2288
|
} else if (part === 'Own') [mint, to] = [paymentMint(r.terms), account(paymentMint(r.terms), e.buyer)];
|
|
@@ -1564,7 +2291,7 @@ export function pullIx({ payer, project, roundAddress: key, round: r, entry = 0,
|
|
|
1564
2291
|
const n = r.terms.shares.length;
|
|
1565
2292
|
let dest;
|
|
1566
2293
|
if (i < n) [mint, dest] = [paymentMint(r.terms), r.terms.shares[i].to];
|
|
1567
|
-
else if (i === n) [mint, dest] = [r.terms.mint,
|
|
2294
|
+
else if (i === n) [mint, dest] = [r.terms.mint, leftoverTo(r.lottery, r.terms)];
|
|
1568
2295
|
else {
|
|
1569
2296
|
const p = r.prizes[i - n - 1];
|
|
1570
2297
|
[mint, dest] = [p.mint, r.terms.places[Number(p.run)].prize.Tokens.from];
|
|
@@ -1574,7 +2301,7 @@ export function pullIx({ payer, project, roundAddress: key, round: r, entry = 0,
|
|
|
1574
2301
|
if (toTreasury || nobody) rest.push(...treasuryAccounts(projectState?.treasury, mint, programs));
|
|
1575
2302
|
} else if (part.Pot || tokenRun(r, Number(part.Prize[0]))) {
|
|
1576
2303
|
const run = Number((part.Pot ?? part.Prize)[0]);
|
|
1577
|
-
const p = part.Prize ? r.prizes.find(
|
|
2304
|
+
const p = part.Prize ? r.prizes.find(x => Number(x.run) === run) : null;
|
|
1578
2305
|
mint = p ? p.mint : r.terms.mint;
|
|
1579
2306
|
to = account(mint, e.buyer);
|
|
1580
2307
|
settled = placesPulled(r, entry, run, Boolean(part.Prize));
|
|
@@ -1586,10 +2313,13 @@ export function pullIx({ payer, project, roundAddress: key, round: r, entry = 0,
|
|
|
1586
2313
|
const own = ownStash(r.terms.places[run]);
|
|
1587
2314
|
const nfts = own ? stashNfts(r, prize.stash) : [];
|
|
1588
2315
|
if (own) rest.push(meta(pda.lotteryStash(r.lottery), false, true));
|
|
1589
|
-
const handed =
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
2316
|
+
const handed = asset =>
|
|
2317
|
+
prize.hold
|
|
2318
|
+
? [meta(pda.hold(asset, 0), false, true)]
|
|
2319
|
+
: (() => {
|
|
2320
|
+
const p = piece({ project, asset, wallet: e.buyer, ...(prize.nfts?.[asset] ?? {}), ...ONLY_MOVES });
|
|
2321
|
+
return [...pieceMetas(p), ...p.proof];
|
|
2322
|
+
})();
|
|
1593
2323
|
let [carried, made, stashed] = [0, 0, false];
|
|
1594
2324
|
for (const place of places) {
|
|
1595
2325
|
if (own && nfts[nftSlot(r, place)] == null) continue;
|
|
@@ -1607,20 +2337,45 @@ export function pullIx({ payer, project, roundAddress: key, round: r, entry = 0,
|
|
|
1607
2337
|
if (holds && !stashed) rest.push(...handed(asset));
|
|
1608
2338
|
stashed ||= holds;
|
|
1609
2339
|
} else {
|
|
1610
|
-
rest.push(
|
|
2340
|
+
rest.push(
|
|
2341
|
+
...prizeMetas({
|
|
2342
|
+
project,
|
|
2343
|
+
collection: prize.collection,
|
|
2344
|
+
prize: given,
|
|
2345
|
+
winner: e.buyer,
|
|
2346
|
+
nonce: BigInt.asUintN(64, toBigInt(nonce) + BigInt(place)),
|
|
2347
|
+
poolMints: prize.poolMints,
|
|
2348
|
+
items: prize.items,
|
|
2349
|
+
nft: prize.nfts?.[given.Item?.[0]],
|
|
2350
|
+
made,
|
|
2351
|
+
projectMint: prize.projectMint,
|
|
2352
|
+
programs,
|
|
2353
|
+
}),
|
|
2354
|
+
);
|
|
1611
2355
|
if (given.Item) made += 1;
|
|
1612
2356
|
}
|
|
1613
2357
|
}
|
|
1614
2358
|
settled = places.length;
|
|
1615
2359
|
}
|
|
1616
|
-
const closes =
|
|
2360
|
+
const closes =
|
|
2361
|
+
finished || (!partial && (r.status === 'Drawn' || r.status === 'Cancelled') && Number(r.owed) <= settled);
|
|
1617
2362
|
const owner = lotteryClosed ? projectState?.owner : null;
|
|
1618
2363
|
if (closes) rest.push(...roundCloseMetas(key, r, owner));
|
|
1619
2364
|
const opener = owner && r.opener === r.lottery ? owner : r.opener;
|
|
1620
2365
|
return lootIx(
|
|
1621
2366
|
'pull',
|
|
1622
2367
|
{ entry: part.Common || finished ? 0 : entry, part, nonce: toBigInt(nonce) },
|
|
1623
|
-
{
|
|
2368
|
+
{
|
|
2369
|
+
payer,
|
|
2370
|
+
project,
|
|
2371
|
+
round: key,
|
|
2372
|
+
buyer,
|
|
2373
|
+
mint,
|
|
2374
|
+
vault: mint ? pda.roundTokens(key, mint) : null,
|
|
2375
|
+
to,
|
|
2376
|
+
opener: closes ? opener : null,
|
|
2377
|
+
treasury: closes ? projectState?.treasury : null,
|
|
2378
|
+
},
|
|
1624
2379
|
{ remaining: [...rest, ...tailOf(extra)] },
|
|
1625
2380
|
);
|
|
1626
2381
|
}
|
|
@@ -1632,7 +2387,10 @@ export function pullRolls({ project, round: r, entry, part, nonce = 0n }) {
|
|
|
1632
2387
|
const given = r.terms.places[run]?.prize;
|
|
1633
2388
|
if (!given?.Roll) return [];
|
|
1634
2389
|
const buyer = r.entries[entry].buyer;
|
|
1635
|
-
return prizePlacesOwed(r, entry, run).map(
|
|
2390
|
+
return prizePlacesOwed(r, entry, run).map(place => ({
|
|
2391
|
+
order: pda.order(project, buyer, prizeRollNonce(BigInt.asUintN(64, toBigInt(nonce) + BigInt(place)))),
|
|
2392
|
+
box: given.Roll[0],
|
|
2393
|
+
}));
|
|
1636
2394
|
}
|
|
1637
2395
|
|
|
1638
2396
|
// ---------------------------------------------------------------- farms
|
|
@@ -1656,20 +2414,36 @@ export function pullRolls({ project, round: r, entry, part, nonce = 0n }) {
|
|
|
1656
2414
|
export const openFarmIx = ({ owner, project, id, args, rewardsMint, stakedMint = null }) => {
|
|
1657
2415
|
if (!Array.isArray(args?.tiers)) throw new Error('A farm needs its lock tiers (`args.tiers`: [{ lock, bps }, …]).');
|
|
1658
2416
|
const full = {
|
|
1659
|
-
gates: [],
|
|
2417
|
+
gates: [],
|
|
2418
|
+
ends_at: 0,
|
|
2419
|
+
lock_key: '',
|
|
2420
|
+
upkeep_key: '',
|
|
2421
|
+
exit_to: null,
|
|
2422
|
+
...args,
|
|
1660
2423
|
rate: args.rate ? { mints: false, shared: false, halve_every: 0, ...args.rate } : null,
|
|
1661
|
-
tiers: args.tiers.map(
|
|
1662
|
-
growth: (args.growth ?? []).map(
|
|
2424
|
+
tiers: args.tiers.map(t => ({ prize: null, roll: null, exit_bps: 0, earns_for: 0, ...t })),
|
|
2425
|
+
growth: (args.growth ?? []).map(g => ({ profile: false, at_least: 0n, ...g })),
|
|
1663
2426
|
};
|
|
1664
|
-
return lootIx(
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
2427
|
+
return lootIx(
|
|
2428
|
+
'open_farm',
|
|
2429
|
+
{ id, args: full },
|
|
2430
|
+
{
|
|
2431
|
+
owner,
|
|
2432
|
+
project,
|
|
2433
|
+
rewards_mint: rewardsMint,
|
|
2434
|
+
staked_mint: stakedMint,
|
|
2435
|
+
staked_vault: stakedMint ? pda.staked(pda.farm(project, id)) : null,
|
|
2436
|
+
exit_pool: sinkPool(project, full.exit_to),
|
|
2437
|
+
ends: farmKeepsEnds(full) ? pda.ends(pda.farm(project, id)) : null,
|
|
2438
|
+
},
|
|
2439
|
+
{
|
|
2440
|
+
// Each box its tiers name to roll, tier by tier (its roll, then a prize that is a roll), once.
|
|
2441
|
+
remaining: boxesAccounts(
|
|
2442
|
+
project,
|
|
2443
|
+
full.tiers.flatMap(t => [t.roll, t.prize?.Roll?.[0]].filter(x => x != null)),
|
|
2444
|
+
),
|
|
2445
|
+
},
|
|
2446
|
+
);
|
|
1673
2447
|
};
|
|
1674
2448
|
|
|
1675
2449
|
/**
|
|
@@ -1677,8 +2451,9 @@ export const openFarmIx = ({ owner, project, id, args, rewardsMint, stakedMint =
|
|
|
1677
2451
|
* (`earns_for`) or the farm reads an upkeep (`upkeep_key`), and it pays something (no rate, or a rate above 0). The farm
|
|
1678
2452
|
* and every move of it then pass the record (`WrongEnds` otherwise).
|
|
1679
2453
|
*/
|
|
1680
|
-
export const farmKeepsEnds =
|
|
1681
|
-
((args.tiers ?? []).some(
|
|
2454
|
+
export const farmKeepsEnds = args =>
|
|
2455
|
+
((args.tiers ?? []).some(t => BigInt(t.earns_for ?? 0) > 0n) || Boolean(args.upkeep_key)) &&
|
|
2456
|
+
(!args.rate || BigInt(args.rate.per_day ?? 0) > 0n);
|
|
1682
2457
|
|
|
1683
2458
|
/** Where a farm's fee goes (`Sink`): the pool's address for `{ Pool: [index] }`, null for a burn, its own vault or none. */
|
|
1684
2459
|
const sinkPool = (project, sink) => (sink?.Pool ? pda.pool(project, sink.Pool[0]) : null);
|
|
@@ -1699,7 +2474,7 @@ export function endsToWalk(farm, ends, now) {
|
|
|
1699
2474
|
const clock = BigInt(farm.clock ?? 0);
|
|
1700
2475
|
const over = BigInt(farm.ends_at ?? 0);
|
|
1701
2476
|
const stop = over > 0n && over < BigInt(now) ? over : BigInt(now);
|
|
1702
|
-
return ends.filter(
|
|
2477
|
+
return ends.filter(e => e.at > clock && e.at <= stop).length;
|
|
1703
2478
|
}
|
|
1704
2479
|
|
|
1705
2480
|
/** How many ends `walk_farm` walks in one transaction at `WALK_COMPUTE_UNITS`: an end takes at most some 2,300 units, with a
|
|
@@ -1725,7 +2500,8 @@ export const farmNeedsWalk = (farm, ends, now) => endsToWalk(farm, ends, now) >
|
|
|
1725
2500
|
export function walkFarmParts({ project, id, farm, ends, now, perTx = WALK_PER_TX }) {
|
|
1726
2501
|
const left = endsToWalk(farm, ends, now);
|
|
1727
2502
|
const parts = [];
|
|
1728
|
-
for (let done = 0; done < left; done += perTx)
|
|
2503
|
+
for (let done = 0; done < left; done += perTx)
|
|
2504
|
+
parts.push(walkFarmIx({ project, id, farm, most: Math.min(perTx, left - done) }));
|
|
1729
2505
|
return parts;
|
|
1730
2506
|
}
|
|
1731
2507
|
|
|
@@ -1738,11 +2514,13 @@ export const walkFarmIx = ({ project, id, farm, most = WALK_PER_TX }) =>
|
|
|
1738
2514
|
lootIx('walk_farm', { most }, farmSide(project, id, farm));
|
|
1739
2515
|
/** Whether a farm reads or writes its seats' players' profiles (`Farm::uses_profile`): a gate or a boost on a profile,
|
|
1740
2516
|
* or a growth written on one. Its moves then pass the player's profile. */
|
|
1741
|
-
const usesProfile =
|
|
2517
|
+
const usesProfile = farm =>
|
|
2518
|
+
[...(farm.gates ?? []), ...(farm.boosts ?? []).map(b => b.rule)].some(r => r?.of === 'Profile') ||
|
|
2519
|
+
(farm.growth ?? []).some(g => g.profile);
|
|
1742
2520
|
const profileOf = (project, farm, player) => (usesProfile(farm) ? pda.profile(project, player) : null);
|
|
1743
2521
|
|
|
1744
2522
|
/** What a move that pays a seat writes: the rewards' mint, when the farm mints at a fixed rate (it mints at payout). */
|
|
1745
|
-
const payWrites =
|
|
2523
|
+
const payWrites = farm => (farm.rate?.mints ? ['rewards_mint'] : []);
|
|
1746
2524
|
|
|
1747
2525
|
/** A token seat's accounts; `paid`: rewards may be paid (an unstake), so the player's account of them comes. */
|
|
1748
2526
|
function tokenSeat({ player, project, id, farm, programs, paid = false }) {
|
|
@@ -1767,7 +2545,9 @@ function tokenSeat({ player, project, id, farm, programs, paid = false }) {
|
|
|
1767
2545
|
* (`stakeRefreshFirst`).
|
|
1768
2546
|
*/
|
|
1769
2547
|
export const stakeTokensIx = ({ player, project, id, farm, amount, tier = 0, programs, shown, extra = [] }) =>
|
|
1770
|
-
lootIx('stake_tokens', { amount, tier }, tokenSeat({ player, project, id, farm, programs }), {
|
|
2548
|
+
lootIx('stake_tokens', { amount, tier }, tokenSeat({ player, project, id, farm, programs }), {
|
|
2549
|
+
remaining: [...shownAccounts(project, shown), ...tailOf(extra)],
|
|
2550
|
+
});
|
|
1771
2551
|
|
|
1772
2552
|
/**
|
|
1773
2553
|
* `player` takes `amount` out (0: only collects what is owed); `forfeit` gives up rewards that can't move. Before the
|
|
@@ -1787,7 +2567,9 @@ export const unstakeTokensIx = ({ player, project, id, farm, amount, forfeit = f
|
|
|
1787
2567
|
* what it grew since its last refresh is written before it goes. Never under `forfeit`.
|
|
1788
2568
|
*/
|
|
1789
2569
|
export const refreshFirst = (farm, seat, amount, forfeit = false) =>
|
|
1790
|
-
!forfeit &&
|
|
2570
|
+
!forfeit &&
|
|
2571
|
+
BigInt(amount) > 0n &&
|
|
2572
|
+
(farm.growth ?? []).some(g => {
|
|
1791
2573
|
const least = BigInt(g.at_least ?? 0) > 0n ? BigInt(g.at_least) : 1n;
|
|
1792
2574
|
return BigInt(seat.amount) >= least && BigInt(seat.amount) - BigInt(amount) < least;
|
|
1793
2575
|
});
|
|
@@ -1807,10 +2589,13 @@ export function stakeRefreshFirst(farm, seat, { tier = seat?.tier, now, compound
|
|
|
1807
2589
|
if (!seat) return false;
|
|
1808
2590
|
const growth = farm.growth ?? [];
|
|
1809
2591
|
const booster = seat.asset !== undefined && seat.asset !== NO_ADDRESS;
|
|
1810
|
-
const grows = BigInt(seat.amount) > 0n && growth.some(
|
|
1811
|
-
const amountGrows = growth.some(
|
|
2592
|
+
const grows = BigInt(seat.amount) > 0n && growth.some(g => g.profile || booster);
|
|
2593
|
+
const amountGrows = growth.some(g => g.profile || BigInt(g.at_least ?? 0) > 0n);
|
|
1812
2594
|
if (compound) return BigInt(seat.amount) > 0n && (booster || (grows && amountGrows));
|
|
1813
|
-
const changes =
|
|
2595
|
+
const changes =
|
|
2596
|
+
Number(tier) !== Number(seat.tier) ||
|
|
2597
|
+
amountGrows ||
|
|
2598
|
+
(now !== undefined && BigInt(seat.earns_until ?? 0) <= BigInt(now));
|
|
1814
2599
|
return booster || (grows && changes);
|
|
1815
2600
|
}
|
|
1816
2601
|
|
|
@@ -1819,7 +2604,7 @@ export function stakeRefreshFirst(farm, seat, { tier = seat?.tier, now, compound
|
|
|
1819
2604
|
* (`stake_nft`'s `RefreshFirst`): in a farm of tokens whose growth writes its players' profiles, the booster's growth
|
|
1820
2605
|
* starts from now, so what the seat grew before is written first.
|
|
1821
2606
|
*/
|
|
1822
|
-
export const boosterRefreshFirst =
|
|
2607
|
+
export const boosterRefreshFirst = farm => Boolean(farm.farmed?.Tokens) && (farm.growth ?? []).some(g => g.profile);
|
|
1823
2608
|
|
|
1824
2609
|
/**
|
|
1825
2610
|
* Whether a farm (decoded) takes a stake at lock tier `tier` at `now` (Unix seconds, the chain's clock), as the program
|
|
@@ -1838,10 +2623,28 @@ export const AUTH_RULES_PROGRAM = 'auth9SigNpDKz4sJJ1DfCTuZrZNSAgh9sFD3rboVmgg';
|
|
|
1838
2623
|
// ---------------------------------------------------------------- an NFT on a route (a `Piece`)
|
|
1839
2624
|
|
|
1840
2625
|
const NO_PIECE = {
|
|
1841
|
-
sheet: null,
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
2626
|
+
sheet: null,
|
|
2627
|
+
collection: null,
|
|
2628
|
+
mpl_core_program: null,
|
|
2629
|
+
metadata: null,
|
|
2630
|
+
edition: null,
|
|
2631
|
+
token: null,
|
|
2632
|
+
token_record: null,
|
|
2633
|
+
project_token: null,
|
|
2634
|
+
project_record: null,
|
|
2635
|
+
rule_set: null,
|
|
2636
|
+
rules_program: null,
|
|
2637
|
+
token_metadata_program: null,
|
|
2638
|
+
instructions: null,
|
|
2639
|
+
spl_token_program: null,
|
|
2640
|
+
spl_ata_program: null,
|
|
2641
|
+
leaf: null,
|
|
2642
|
+
tree: null,
|
|
2643
|
+
tree_config: null,
|
|
2644
|
+
leaf_delegate: null,
|
|
2645
|
+
bubblegum_program: null,
|
|
2646
|
+
compression_program: null,
|
|
2647
|
+
log_wrapper: null,
|
|
1845
2648
|
};
|
|
1846
2649
|
|
|
1847
2650
|
/**
|
|
@@ -1859,19 +2662,40 @@ const NO_PIECE = {
|
|
|
1859
2662
|
* `burns`: burns a Core asset (false when left out); `reads`: reads its Sheet; `held`: reads it in the project's token
|
|
1860
2663
|
* account (an escrow farm).
|
|
1861
2664
|
*/
|
|
1862
|
-
export function piece({
|
|
1863
|
-
|
|
2665
|
+
export function piece({
|
|
2666
|
+
project,
|
|
2667
|
+
asset,
|
|
2668
|
+
wallet,
|
|
2669
|
+
collection,
|
|
2670
|
+
hasAuthority = true,
|
|
2671
|
+
tokenMetadata: tm,
|
|
2672
|
+
compressed: c,
|
|
2673
|
+
moves = true,
|
|
2674
|
+
locks = true,
|
|
2675
|
+
writes = true,
|
|
2676
|
+
burns = false,
|
|
2677
|
+
reads = true,
|
|
2678
|
+
held = moves,
|
|
2679
|
+
}) {
|
|
2680
|
+
const sheetOf = nft => (reads || writes || burns ? pda.sheet(project, nft) : null);
|
|
1864
2681
|
if (c) {
|
|
1865
2682
|
// Bubblegum is called (and writes the leaf, its tree and its config) only to lock or move it.
|
|
1866
2683
|
const calls = moves || locks;
|
|
1867
2684
|
const delegate = calls && c.delegate !== c.owner && c.delegate !== project ? c.delegate : null;
|
|
1868
2685
|
const bubblegum = calls
|
|
1869
|
-
? {
|
|
2686
|
+
? {
|
|
2687
|
+
collection: c.collection ?? null,
|
|
2688
|
+
tree_config: pda.treeConfig(c.tree),
|
|
2689
|
+
leaf_delegate: delegate,
|
|
2690
|
+
bubblegum_program: BUBBLEGUM_PROGRAM,
|
|
2691
|
+
compression_program: COMPRESSION_PROGRAM,
|
|
2692
|
+
log_wrapper: NOOP_PROGRAM,
|
|
2693
|
+
}
|
|
1870
2694
|
: {};
|
|
1871
2695
|
return {
|
|
1872
2696
|
kind: 'compressed',
|
|
1873
2697
|
accounts: { ...NO_PIECE, asset: c.id, sheet: sheetOf(c.id), leaf: pda.leaf(c.id), tree: c.tree, ...bubblegum },
|
|
1874
|
-
proof: c.proof.map(
|
|
2698
|
+
proof: c.proof.map(node => meta(typeof node === 'string' ? node : encodeBase58(node), false, false)),
|
|
1875
2699
|
writable: calls ? ['leaf', 'tree', 'tree_config'] : [],
|
|
1876
2700
|
};
|
|
1877
2701
|
}
|
|
@@ -1887,12 +2711,16 @@ export function piece({ project, asset, wallet, collection, hasAuthority = true,
|
|
|
1887
2711
|
return {
|
|
1888
2712
|
kind: 'tokenMetadata',
|
|
1889
2713
|
accounts: {
|
|
1890
|
-
...NO_PIECE,
|
|
2714
|
+
...NO_PIECE,
|
|
2715
|
+
asset,
|
|
2716
|
+
sheet: sheetOf(asset),
|
|
2717
|
+
metadata: tokenMetadata.metadata(asset),
|
|
2718
|
+
token,
|
|
1891
2719
|
edition: calls ? tokenMetadata.edition(asset) : null,
|
|
1892
2720
|
token_record: records ? tokenMetadata.record(asset, token) : null,
|
|
1893
2721
|
project_token: held || moves ? projectToken : null,
|
|
1894
2722
|
project_record: records && moves ? tokenMetadata.record(asset, projectToken) : null,
|
|
1895
|
-
rule_set: records ? tm.ruleSet ?? TOKEN_METADATA_PROGRAM : null,
|
|
2723
|
+
rule_set: records ? (tm.ruleSet ?? TOKEN_METADATA_PROGRAM) : null,
|
|
1896
2724
|
rules_program: records ? (tm.ruleSet ? AUTH_RULES_PROGRAM : TOKEN_METADATA_PROGRAM) : null,
|
|
1897
2725
|
token_metadata_program: calls ? TOKEN_METADATA_PROGRAM : null,
|
|
1898
2726
|
instructions: moves || records ? SYSVAR_INSTRUCTIONS : null,
|
|
@@ -1910,8 +2738,11 @@ export function piece({ project, asset, wallet, collection, hasAuthority = true,
|
|
|
1910
2738
|
return {
|
|
1911
2739
|
kind: 'core',
|
|
1912
2740
|
accounts: {
|
|
1913
|
-
...NO_PIECE,
|
|
1914
|
-
|
|
2741
|
+
...NO_PIECE,
|
|
2742
|
+
asset,
|
|
2743
|
+
sheet: hasAuthority ? null : sheetOf(asset),
|
|
2744
|
+
collection: calls && !inNone ? collection : null,
|
|
2745
|
+
mpl_core_program: calls ? MPL_CORE_PROGRAM : null,
|
|
1915
2746
|
},
|
|
1916
2747
|
proof: [],
|
|
1917
2748
|
writable: burns && !inNone ? ['collection'] : [],
|
|
@@ -1922,18 +2753,33 @@ export function piece({ project, asset, wallet, collection, hasAuthority = true,
|
|
|
1922
2753
|
* needs, the program's own address for one left out, then its proof. The first NFT passes every program any needs. */
|
|
1923
2754
|
const ANOTHER = {
|
|
1924
2755
|
core: ['asset', 'sheet', 'collection'],
|
|
1925
|
-
tokenMetadata: [
|
|
2756
|
+
tokenMetadata: [
|
|
2757
|
+
'asset',
|
|
2758
|
+
'sheet',
|
|
2759
|
+
'metadata',
|
|
2760
|
+
'edition',
|
|
2761
|
+
'token',
|
|
2762
|
+
'token_record',
|
|
2763
|
+
'project_token',
|
|
2764
|
+
'project_record',
|
|
2765
|
+
'rule_set',
|
|
2766
|
+
],
|
|
1926
2767
|
compressed: ['asset', 'sheet', 'collection', 'leaf', 'tree', 'tree_config', 'leaf_delegate'],
|
|
1927
2768
|
};
|
|
1928
2769
|
export function anotherMetas(p) {
|
|
1929
|
-
const writable = new Set([...p.writable, ...PIECE.filter(
|
|
1930
|
-
return [
|
|
2770
|
+
const writable = new Set([...p.writable, ...PIECE.filter(a => a.writable).map(a => a.name)]);
|
|
2771
|
+
return [
|
|
2772
|
+
...ANOTHER[p.kind].map(name =>
|
|
2773
|
+
p.accounts[name] == null ? meta(LOOT_PROGRAM, false, false) : meta(p.accounts[name], false, writable.has(name)),
|
|
2774
|
+
),
|
|
2775
|
+
...p.proof,
|
|
2776
|
+
];
|
|
1931
2777
|
}
|
|
1932
2778
|
|
|
1933
2779
|
/** The programs every NFT of a call of several needs, on the first NFT's piece (the call's own): Core, Token Metadata's
|
|
1934
2780
|
* and Bubblegum's, by what the others are. */
|
|
1935
2781
|
function withPrograms(first, others) {
|
|
1936
|
-
const kinds = new Set(others.map(
|
|
2782
|
+
const kinds = new Set(others.map(p => p.kind));
|
|
1937
2783
|
const a = { ...first.accounts };
|
|
1938
2784
|
if (kinds.has('core')) a.mpl_core_program ??= MPL_CORE_PROGRAM;
|
|
1939
2785
|
if (kinds.has('tokenMetadata')) {
|
|
@@ -1941,9 +2787,18 @@ function withPrograms(first, others) {
|
|
|
1941
2787
|
a.spl_token_program ??= TOKEN_PROGRAM;
|
|
1942
2788
|
a.spl_ata_program ??= ASSOCIATED_TOKEN_PROGRAM;
|
|
1943
2789
|
a.instructions ??= SYSVAR_INSTRUCTIONS;
|
|
1944
|
-
a.rules_program ??= others.some(
|
|
2790
|
+
a.rules_program ??= others.some(p => p.accounts.rule_set && p.accounts.rule_set !== TOKEN_METADATA_PROGRAM)
|
|
2791
|
+
? AUTH_RULES_PROGRAM
|
|
2792
|
+
: others.some(p => p.accounts.token_record)
|
|
2793
|
+
? TOKEN_METADATA_PROGRAM
|
|
2794
|
+
: null;
|
|
1945
2795
|
}
|
|
1946
|
-
if (kinds.has('compressed'))
|
|
2796
|
+
if (kinds.has('compressed'))
|
|
2797
|
+
Object.assign(a, {
|
|
2798
|
+
bubblegum_program: a.bubblegum_program ?? BUBBLEGUM_PROGRAM,
|
|
2799
|
+
compression_program: a.compression_program ?? COMPRESSION_PROGRAM,
|
|
2800
|
+
log_wrapper: a.log_wrapper ?? NOOP_PROGRAM,
|
|
2801
|
+
});
|
|
1947
2802
|
return { ...first, accounts: a };
|
|
1948
2803
|
}
|
|
1949
2804
|
|
|
@@ -1951,7 +2806,8 @@ function withPrograms(first, others) {
|
|
|
1951
2806
|
* Several NFTs on one call (a hold, a stash, a farm's seats): their groups sorted by key (`keyOf`), the first the
|
|
1952
2807
|
* call's own and each further one after it, in ascending order (`OutOfOrder` otherwise). Returns [first, ...rest].
|
|
1953
2808
|
*/
|
|
1954
|
-
const ascending = (items, keyOf) =>
|
|
2809
|
+
const ascending = (items, keyOf) =>
|
|
2810
|
+
[...items].sort((a, b) => compareBytes(addressBytes(keyOf(a)), addressBytes(keyOf(b))));
|
|
1955
2811
|
|
|
1956
2812
|
/** The account an NFT paid goes to: the wallet it is given to, the stash it goes into, or the
|
|
1957
2813
|
* incinerator for a burn (any account stands in for a Core asset, which Core burns). `then`: 'Burn', { Give: [wallet] }
|
|
@@ -1972,15 +2828,58 @@ export function nftPaid({ project, owner, then, asset, collection, hasAuthority,
|
|
|
1972
2828
|
const to = spentTo(project, then);
|
|
1973
2829
|
// The route reads it first (its Sheet, for one the project has no authority over: `hasAuthority` false).
|
|
1974
2830
|
const p = then.Stash
|
|
1975
|
-
? piece({
|
|
1976
|
-
|
|
2831
|
+
? piece({
|
|
2832
|
+
project,
|
|
2833
|
+
asset,
|
|
2834
|
+
wallet: owner,
|
|
2835
|
+
collection,
|
|
2836
|
+
hasAuthority,
|
|
2837
|
+
tokenMetadata: tm,
|
|
2838
|
+
compressed,
|
|
2839
|
+
...ONLY_MOVES,
|
|
2840
|
+
reads: true,
|
|
2841
|
+
})
|
|
2842
|
+
: passedOn({
|
|
2843
|
+
project,
|
|
2844
|
+
owner,
|
|
2845
|
+
receiver: to,
|
|
2846
|
+
burns: then === 'Burn' || Boolean(then.Burn),
|
|
2847
|
+
asset,
|
|
2848
|
+
collection,
|
|
2849
|
+
hasAuthority,
|
|
2850
|
+
tokenMetadata: tm,
|
|
2851
|
+
compressed,
|
|
2852
|
+
reads: true,
|
|
2853
|
+
});
|
|
1977
2854
|
return [...pieceMetas(p), ...p.proof, meta(to, false, Boolean(then.Stash))];
|
|
1978
2855
|
}
|
|
1979
2856
|
|
|
1980
2857
|
/** An NFT moving straight from `owner` to `receiver`, or burned by its owner (`burns`). */
|
|
1981
|
-
function passedOn({
|
|
2858
|
+
function passedOn({
|
|
2859
|
+
project,
|
|
2860
|
+
owner,
|
|
2861
|
+
receiver,
|
|
2862
|
+
burns,
|
|
2863
|
+
asset,
|
|
2864
|
+
collection,
|
|
2865
|
+
hasAuthority,
|
|
2866
|
+
tokenMetadata: tm,
|
|
2867
|
+
compressed,
|
|
2868
|
+
reads = false,
|
|
2869
|
+
}) {
|
|
1982
2870
|
// `tm.into` is the receiver's account here: the NFT leaves the owner's associated one.
|
|
1983
|
-
const p = piece({
|
|
2871
|
+
const p = piece({
|
|
2872
|
+
project,
|
|
2873
|
+
asset,
|
|
2874
|
+
wallet: owner,
|
|
2875
|
+
collection,
|
|
2876
|
+
hasAuthority,
|
|
2877
|
+
tokenMetadata: tm && { ...tm, into: undefined },
|
|
2878
|
+
compressed,
|
|
2879
|
+
...ONLY_MOVES,
|
|
2880
|
+
burns: burns && !tm && !compressed,
|
|
2881
|
+
reads,
|
|
2882
|
+
});
|
|
1984
2883
|
if (tm) {
|
|
1985
2884
|
const token = tm.into ?? ata(receiver, asset, TOKEN_PROGRAM);
|
|
1986
2885
|
p.accounts.project_token = token;
|
|
@@ -1994,12 +2893,38 @@ function passedOn({ project, owner, receiver, burns, asset, collection, hasAutho
|
|
|
1994
2893
|
* place), or given to the wallet `to` (the treasury, a partner). The receipt (`PaidNft`) is the record: an app admits the player when the NFT, where it went (`to`: a payment to the player's own wallet pays nothing) and the reference
|
|
1995
2894
|
* are the ones it asked for. `extra`: Core's transfer adapters.
|
|
1996
2895
|
*/
|
|
1997
|
-
export function payNftIx({
|
|
2896
|
+
export function payNftIx({
|
|
2897
|
+
player,
|
|
2898
|
+
project,
|
|
2899
|
+
asset,
|
|
2900
|
+
to = null,
|
|
2901
|
+
collection,
|
|
2902
|
+
hasAuthority,
|
|
2903
|
+
tokenMetadata: tm,
|
|
2904
|
+
compressed,
|
|
2905
|
+
reference: ref,
|
|
2906
|
+
extra = [],
|
|
2907
|
+
}) {
|
|
1998
2908
|
// An NFT paid goes to a wallet, never to the project's own address, where nothing could ever move it (ToTheProject).
|
|
1999
2909
|
if (to === project) throw new Error('An NFT paid can’t go to the project’s own address: name a wallet, or burn it.');
|
|
2000
2910
|
const receiver = to ?? INCINERATOR;
|
|
2001
|
-
const p = passedOn({
|
|
2002
|
-
|
|
2911
|
+
const p = passedOn({
|
|
2912
|
+
project,
|
|
2913
|
+
owner: player,
|
|
2914
|
+
receiver,
|
|
2915
|
+
burns: receiver === INCINERATOR,
|
|
2916
|
+
asset,
|
|
2917
|
+
collection,
|
|
2918
|
+
hasAuthority,
|
|
2919
|
+
tokenMetadata: tm,
|
|
2920
|
+
compressed,
|
|
2921
|
+
});
|
|
2922
|
+
return lootIx(
|
|
2923
|
+
'pay_nft',
|
|
2924
|
+
{ reference: reference(ref) },
|
|
2925
|
+
{ player, project, ...p.accounts, to: receiver },
|
|
2926
|
+
{ remaining: [...p.proof, ...tailOf(extra)], writable: p.writable },
|
|
2927
|
+
);
|
|
2003
2928
|
}
|
|
2004
2929
|
|
|
2005
2930
|
/**
|
|
@@ -2011,7 +2936,9 @@ export function payNftIx({ player, project, asset, to = null, collection, hasAut
|
|
|
2011
2936
|
export function takesNft(account, holder, mint) {
|
|
2012
2937
|
if (!account || account.owner !== TOKEN_PROGRAM || account.data.length < TOKEN_ACCOUNT_BYTES) return false;
|
|
2013
2938
|
const data = account.data;
|
|
2014
|
-
return
|
|
2939
|
+
return (
|
|
2940
|
+
encodeBase58(data.subarray(0, 32)) === mint && encodeBase58(data.subarray(32, 64)) === holder && data[108] === 1
|
|
2941
|
+
);
|
|
2015
2942
|
}
|
|
2016
2943
|
|
|
2017
2944
|
/**
|
|
@@ -2031,7 +2958,21 @@ export function freshNftAccount({ payer, holder, mint, index = 0, there = null }
|
|
|
2031
2958
|
const seed = freshNftSeed(holder, mint, index);
|
|
2032
2959
|
const address = addressWithSeed(payer, seed, TOKEN_PROGRAM);
|
|
2033
2960
|
const init = initializeAccount3Ix({ account: address, mint, owner: holder });
|
|
2034
|
-
if (!there)
|
|
2961
|
+
if (!there)
|
|
2962
|
+
return {
|
|
2963
|
+
address,
|
|
2964
|
+
instructions: [
|
|
2965
|
+
createAccountWithSeedIx({
|
|
2966
|
+
from: payer,
|
|
2967
|
+
address,
|
|
2968
|
+
seed,
|
|
2969
|
+
lamports: TOKEN_ACCOUNT_RENT,
|
|
2970
|
+
space: TOKEN_ACCOUNT_BYTES,
|
|
2971
|
+
owner: TOKEN_PROGRAM,
|
|
2972
|
+
}),
|
|
2973
|
+
init,
|
|
2974
|
+
],
|
|
2975
|
+
};
|
|
2035
2976
|
if (takesNft(there, holder, mint)) return { address, instructions: [] };
|
|
2036
2977
|
if (there.owner !== SYSTEM_PROGRAM || there.data.length) return null;
|
|
2037
2978
|
// The RPC's figure only lowers the top-up, never raises it past the rent: lamports that are negative or not a whole
|
|
@@ -2052,7 +2993,8 @@ export function freshNftAccount({ payer, holder, mint, index = 0, there = null }
|
|
|
2052
2993
|
}
|
|
2053
2994
|
|
|
2054
2995
|
/** The seed of a holder's fresh account of a mint: `nft<index>:` and 26 base58 characters of sha256(holder | mint). */
|
|
2055
|
-
const freshNftSeed = (holder, mint, index) =>
|
|
2996
|
+
const freshNftSeed = (holder, mint, index) =>
|
|
2997
|
+
`nft${index}:${encodeBase58(sha256(concat(addressBytes(holder), addressBytes(mint)))).slice(0, 26)}`;
|
|
2056
2998
|
|
|
2057
2999
|
/** How many fresh accounts a payer may open for one holder and mint (`freshNftAccount`'s `index`). */
|
|
2058
3000
|
export const FRESH_NFT_ACCOUNTS = 8;
|
|
@@ -2068,17 +3010,23 @@ const ONLY_MOVES = { moves: true, locks: false, writes: false, reads: false };
|
|
|
2068
3010
|
const holds = (hold, asset) => (Array.isArray(hold) ? hold.includes(asset) : Boolean(hold));
|
|
2069
3011
|
|
|
2070
3012
|
/** A piece that reads no NFT: `at` (an auction's or a seat's own address) stands in for one. */
|
|
2071
|
-
const standIn =
|
|
3013
|
+
const standIn = at => ({ accounts: { ...NO_PIECE, asset: at }, proof: [], writable: [] });
|
|
2072
3014
|
|
|
2073
3015
|
/**
|
|
2074
3016
|
* A compressed NFT's record (`pda.leaf`) closes, its rent back to `payer`, who paid it: at any time with `payer` signing
|
|
2075
3017
|
* (`signs`, the default), or by anyone once it no longer matches its tree. A route that finds no record asks for a proof
|
|
2076
3018
|
* again (`NotProven`), so a client closes it in the transaction that used it (`withLeafCloses`).
|
|
2077
3019
|
*/
|
|
2078
|
-
export const closeLeafIx = ({ asset, tree, payer, signs = true }) =>
|
|
3020
|
+
export const closeLeafIx = ({ asset, tree, payer, signs = true }) =>
|
|
3021
|
+
lootIx(
|
|
3022
|
+
'close_leaf',
|
|
3023
|
+
{},
|
|
3024
|
+
{ record: pda.leaf(asset), tree, payer, project: null, edition: null },
|
|
3025
|
+
{ signer: signs ? ['payer'] : [] },
|
|
3026
|
+
);
|
|
2079
3027
|
|
|
2080
|
-
const PROVE_LEAF = leaves(instructionDef('prove_leaf').accounts).map(
|
|
2081
|
-
const CLOSE_LEAF = leaves(instructionDef('close_leaf').accounts).map(
|
|
3028
|
+
const PROVE_LEAF = leaves(instructionDef('prove_leaf').accounts).map(a => a.name);
|
|
3029
|
+
const CLOSE_LEAF = leaves(instructionDef('close_leaf').accounts).map(a => a.name);
|
|
2082
3030
|
const isLoot = (ix, name) => ix.programId === LOOT_PROGRAM && instructionOf(ix.data)?.name === name;
|
|
2083
3031
|
|
|
2084
3032
|
/**
|
|
@@ -2088,13 +3036,19 @@ const isLoot = (ix, name) => ix.programId === LOOT_PROGRAM && instructionOf(ix.d
|
|
|
2088
3036
|
*/
|
|
2089
3037
|
export function withLeafCloses(instructions) {
|
|
2090
3038
|
const at = (ix, names, name) => ix.keys[names.indexOf(name)]?.pubkey;
|
|
2091
|
-
const closed = new Set(instructions.filter(
|
|
3039
|
+
const closed = new Set(instructions.filter(ix => isLoot(ix, 'close_leaf')).map(ix => at(ix, CLOSE_LEAF, 'record')));
|
|
2092
3040
|
const closes = [];
|
|
2093
|
-
for (const ix of instructions.filter(
|
|
3041
|
+
for (const ix of instructions.filter(x => isLoot(x, 'prove_leaf'))) {
|
|
2094
3042
|
const record = at(ix, PROVE_LEAF, 'record');
|
|
2095
3043
|
if (closed.has(record)) continue;
|
|
2096
3044
|
closed.add(record);
|
|
2097
|
-
closes.push(
|
|
3045
|
+
closes.push(
|
|
3046
|
+
closeLeafIx({
|
|
3047
|
+
asset: at(ix, PROVE_LEAF, 'asset'),
|
|
3048
|
+
tree: at(ix, PROVE_LEAF, 'tree'),
|
|
3049
|
+
payer: at(ix, PROVE_LEAF, 'signer'),
|
|
3050
|
+
}),
|
|
3051
|
+
);
|
|
2098
3052
|
}
|
|
2099
3053
|
return closes.length ? [...instructions, ...closes] : instructions;
|
|
2100
3054
|
}
|
|
@@ -2108,8 +3062,25 @@ export function withLeafCloses(instructions) {
|
|
|
2108
3062
|
* (`pda.leaf`). A Token Metadata NFT's (`tokenMetadata`: true, or its kind) names its edition, which must be closed
|
|
2109
3063
|
* too: its mint holds none, with no mint authority but its edition (`NotBurned` otherwise).
|
|
2110
3064
|
*/
|
|
2111
|
-
export const closeSheetIx = ({
|
|
2112
|
-
|
|
3065
|
+
export const closeSheetIx = ({
|
|
3066
|
+
project,
|
|
3067
|
+
nft,
|
|
3068
|
+
tree = null,
|
|
3069
|
+
burned = tree ? pda.leaf(nft) : nft,
|
|
3070
|
+
treasury,
|
|
3071
|
+
tokenMetadata: tm = null,
|
|
3072
|
+
}) =>
|
|
3073
|
+
lootIx(
|
|
3074
|
+
'close_leaf',
|
|
3075
|
+
{},
|
|
3076
|
+
{
|
|
3077
|
+
record: pda.sheet(project, nft),
|
|
3078
|
+
tree: burned,
|
|
3079
|
+
payer: treasury,
|
|
3080
|
+
project,
|
|
3081
|
+
edition: tm ? tokenMetadata.edition(nft) : tree,
|
|
3082
|
+
},
|
|
3083
|
+
);
|
|
2113
3084
|
|
|
2114
3085
|
/**
|
|
2115
3086
|
* Anyone proves a compressed NFT's leaf (`compressed` from `compressedFromDas`): put it before the instruction that
|
|
@@ -2118,19 +3089,42 @@ export const closeSheetIx = ({ project, nft, tree = null, burned = tree ? pda.le
|
|
|
2118
3089
|
* with it: only a tree Bubblegum made has one.
|
|
2119
3090
|
*/
|
|
2120
3091
|
export const proveLeafIx = ({ signer, compressed: c }) =>
|
|
2121
|
-
lootIx(
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
3092
|
+
lootIx(
|
|
3093
|
+
'prove_leaf',
|
|
3094
|
+
{
|
|
3095
|
+
leaf: {
|
|
3096
|
+
root: c.root,
|
|
3097
|
+
data_hash: c.dataHash,
|
|
3098
|
+
creator_hash: c.creatorHash,
|
|
3099
|
+
asset_data_hash: c.assetDataHash ?? null,
|
|
3100
|
+
flags: c.flags,
|
|
3101
|
+
nonce: c.nonce,
|
|
3102
|
+
},
|
|
3103
|
+
},
|
|
3104
|
+
{
|
|
3105
|
+
signer,
|
|
3106
|
+
asset: c.id,
|
|
3107
|
+
record: pda.leaf(c.id),
|
|
3108
|
+
owner: c.owner,
|
|
3109
|
+
delegate: c.delegate,
|
|
3110
|
+
collection: c.collection ?? BUBBLEGUM_PROGRAM,
|
|
3111
|
+
tree: c.tree,
|
|
3112
|
+
tree_config: pda.treeConfig(c.tree),
|
|
3113
|
+
},
|
|
3114
|
+
{ remaining: c.proof.map(node => meta(typeof node === 'string' ? node : encodeBase58(node), false, false)) },
|
|
3115
|
+
);
|
|
2127
3116
|
|
|
2128
3117
|
/** The project's owner or authority vouches for NFTs' off-chain traits: `root` of a `traitsTree`, under `key`. */
|
|
2129
|
-
export const setTraitsIx = ({ signer, project, key, root }) =>
|
|
3118
|
+
export const setTraitsIx = ({ signer, project, key, root }) =>
|
|
3119
|
+
lootIx('set_traits', { key, root }, { signer, project, traits: pda.traits(project, key) });
|
|
2130
3120
|
|
|
2131
3121
|
/** Anyone writes `nft`'s traits ([{ key, value }]) into its Sheet in `project`, by its `proof` in the tree under `key`. */
|
|
2132
3122
|
export const proveTraitsIx = ({ signer, project, key, nft, attributes, proof }) =>
|
|
2133
|
-
lootIx(
|
|
3123
|
+
lootIx(
|
|
3124
|
+
'prove_traits',
|
|
3125
|
+
{ attributes, proof },
|
|
3126
|
+
{ signer, traits: pda.traits(project, key), nft, sheet: pda.sheet(project, nft) },
|
|
3127
|
+
);
|
|
2134
3128
|
|
|
2135
3129
|
/** What a farm does with a seat's NFT (`piece`'s flags): kept as its custody says, written on as it grows. */
|
|
2136
3130
|
const farmKeeps = (farm, { writes = false, rolls = false } = {}) => ({
|
|
@@ -2140,9 +3134,32 @@ const farmKeeps = (farm, { writes = false, rolls = false } = {}) => ({
|
|
|
2140
3134
|
held: farm.custody === 'Escrow',
|
|
2141
3135
|
});
|
|
2142
3136
|
|
|
2143
|
-
function nftSeat({
|
|
3137
|
+
function nftSeat({
|
|
3138
|
+
player,
|
|
3139
|
+
project,
|
|
3140
|
+
id,
|
|
3141
|
+
farm,
|
|
3142
|
+
asset,
|
|
3143
|
+
collection,
|
|
3144
|
+
hasAuthority = true,
|
|
3145
|
+
tokenMetadata: tm,
|
|
3146
|
+
compressed,
|
|
3147
|
+
previous = null,
|
|
3148
|
+
programs,
|
|
3149
|
+
leaving = false,
|
|
3150
|
+
keeps,
|
|
3151
|
+
}) {
|
|
2144
3152
|
const farmAddress = pda.farm(project, id);
|
|
2145
|
-
const p = piece({
|
|
3153
|
+
const p = piece({
|
|
3154
|
+
project,
|
|
3155
|
+
asset,
|
|
3156
|
+
wallet: player,
|
|
3157
|
+
collection,
|
|
3158
|
+
hasAuthority,
|
|
3159
|
+
tokenMetadata: tm,
|
|
3160
|
+
compressed,
|
|
3161
|
+
...keeps,
|
|
3162
|
+
});
|
|
2146
3163
|
const key = compressed ? compressed.id : asset;
|
|
2147
3164
|
return {
|
|
2148
3165
|
kind: p.kind,
|
|
@@ -2153,7 +3170,10 @@ function nftSeat({ player, project, id, farm, asset, collection, hasAuthority =
|
|
|
2153
3170
|
previous,
|
|
2154
3171
|
seat: pda.seat(farmAddress, farm.farmed.Tokens ? player : key),
|
|
2155
3172
|
// Rewards are paid only as an NFT's own seat leaves.
|
|
2156
|
-
player_rewards:
|
|
3173
|
+
player_rewards:
|
|
3174
|
+
leaving && !farm.farmed.Tokens
|
|
3175
|
+
? ata(player, farm.rewards_mint, tokenProgramOf(programs, farm.rewards_mint))
|
|
3176
|
+
: null,
|
|
2157
3177
|
profile: profileOf(project, farm, player),
|
|
2158
3178
|
},
|
|
2159
3179
|
proof: p.proof,
|
|
@@ -2163,7 +3183,13 @@ function nftSeat({ player, project, id, farm, asset, collection, hasAuthority =
|
|
|
2163
3183
|
|
|
2164
3184
|
/** A further seat's group of a call of several (`NftSeat`): the seat, its NFT's accounts (`anotherMetas`), its box and
|
|
2165
3185
|
* order leaving a mission that rolls (the program's address for none), then its proof (and an unstake's prize's). */
|
|
2166
|
-
const seatGroup = (seat, p, box = null, order = null) => [
|
|
3186
|
+
const seatGroup = (seat, p, box = null, order = null) => [
|
|
3187
|
+
meta(seat, false, true),
|
|
3188
|
+
...anotherMetas({ ...p, proof: [] }),
|
|
3189
|
+
meta(box ?? LOOT_PROGRAM, false, false),
|
|
3190
|
+
meta(order ?? LOOT_PROGRAM, false, Boolean(order)),
|
|
3191
|
+
...p.proof,
|
|
3192
|
+
];
|
|
2167
3193
|
|
|
2168
3194
|
/**
|
|
2169
3195
|
* `player` stakes an NFT: on a seat of its own in a farm of collectibles, or as the booster of the player's token seat
|
|
@@ -2173,13 +3199,36 @@ const seatGroup = (seat, p, box = null, order = null) => [meta(seat, false, true
|
|
|
2173
3199
|
* collection, hasAuthority, tokenMetadata, compressed }) go in the same call at the same tier, one fee, by their seats'
|
|
2174
3200
|
* addresses. `extra` carries reward token hooks/memos and Core transfer adapter accounts.
|
|
2175
3201
|
*/
|
|
2176
|
-
export const stakeNftIx = ({
|
|
3202
|
+
export const stakeNftIx = ({
|
|
3203
|
+
player,
|
|
3204
|
+
project,
|
|
3205
|
+
id,
|
|
3206
|
+
farm,
|
|
3207
|
+
asset,
|
|
3208
|
+
collection,
|
|
3209
|
+
hasAuthority = true,
|
|
3210
|
+
tokenMetadata: tm,
|
|
3211
|
+
compressed,
|
|
3212
|
+
previous = null,
|
|
3213
|
+
tier = 0,
|
|
3214
|
+
programs,
|
|
3215
|
+
more = [],
|
|
3216
|
+
extra = [],
|
|
3217
|
+
}) => {
|
|
2177
3218
|
// Its lock's end, when the farm keeps one, is written on it.
|
|
2178
3219
|
const keeps = farmKeeps(farm, { writes: Boolean(farm.lock_key) });
|
|
2179
|
-
const [first, ...rest] = ascending(
|
|
3220
|
+
const [first, ...rest] = ascending(
|
|
3221
|
+
[{ asset, collection, hasAuthority, tokenMetadata: tm, compressed }, ...more].map(n =>
|
|
3222
|
+
nftSeat({ player, project, id, farm, ...n, previous, programs, keeps }),
|
|
3223
|
+
),
|
|
3224
|
+
x => x.accounts.seat,
|
|
3225
|
+
);
|
|
2180
3226
|
const seat = withPrograms({ ...first, kind: first.kind }, rest);
|
|
2181
|
-
const groups = rest.flatMap(
|
|
2182
|
-
return lootIx('stake_nft', { tier }, seat.accounts, {
|
|
3227
|
+
const groups = rest.flatMap(x => seatGroup(x.accounts.seat, x));
|
|
3228
|
+
return lootIx('stake_nft', { tier }, seat.accounts, {
|
|
3229
|
+
remaining: [...seat.proof, ...groups, ...tailOf(extra)],
|
|
3230
|
+
writable: seat.writable,
|
|
3231
|
+
});
|
|
2183
3232
|
};
|
|
2184
3233
|
|
|
2185
3234
|
/**
|
|
@@ -2192,12 +3241,37 @@ export const stakeNftIx = ({ player, project, id, farm, asset, collection, hasAu
|
|
|
2192
3241
|
* in the same call, one fee, one transfer of rewards: by their seats' addresses, the `i`th's roll at `nonce + 2i` and its
|
|
2193
3242
|
* prize's at `nonce + 2i + 1` (`seatNonces` says which NFT has which). One still locked refuses the whole call.
|
|
2194
3243
|
*/
|
|
2195
|
-
export const unstakeNftIx = ({
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
3244
|
+
export const unstakeNftIx = ({
|
|
3245
|
+
player,
|
|
3246
|
+
project,
|
|
3247
|
+
id,
|
|
3248
|
+
farm,
|
|
3249
|
+
asset,
|
|
3250
|
+
collection,
|
|
3251
|
+
hasAuthority = true,
|
|
3252
|
+
tokenMetadata: tm,
|
|
3253
|
+
compressed,
|
|
3254
|
+
forfeit = false,
|
|
3255
|
+
roll = null,
|
|
3256
|
+
nonce = 0n,
|
|
3257
|
+
tier,
|
|
3258
|
+
projectCollection,
|
|
3259
|
+
poolMints,
|
|
3260
|
+
items,
|
|
3261
|
+
nft,
|
|
3262
|
+
boxes = {},
|
|
3263
|
+
programs,
|
|
3264
|
+
more = [],
|
|
3265
|
+
extra = [],
|
|
3266
|
+
}) => {
|
|
3267
|
+
const seats = ascending(
|
|
3268
|
+
[{ asset, collection, hasAuthority, tokenMetadata: tm, compressed, tier, roll }, ...more].map(n => {
|
|
3269
|
+
// It grows on its way out; a mission that rolls may hand it to the project.
|
|
3270
|
+
const keeps = farmKeeps(farm, { writes: Boolean(farm.growth?.length), rolls: n.roll != null });
|
|
3271
|
+
return { n, seat: nftSeat({ player, project, id, farm, ...n, programs, leaving: true, keeps }) };
|
|
3272
|
+
}),
|
|
3273
|
+
x => x.seat.accounts.seat,
|
|
3274
|
+
);
|
|
2201
3275
|
const several = seats.length > 1;
|
|
2202
3276
|
// Each seat's mission: the box and the order its roll is made in, at `nonce + 2i`; its prize (its tier's) after its
|
|
2203
3277
|
// proof, a roll's order at `nonce + 2i + 1`. A forfeit takes neither the prize nor the rewards (the program reads
|
|
@@ -2206,25 +3280,64 @@ export const unstakeNftIx = ({ player, project, id, farm, asset, collection, has
|
|
|
2206
3280
|
const parts = seats.map(({ n, seat }, i) => {
|
|
2207
3281
|
const at = BigInt.asUintN(64, toBigInt(nonce) + 2n * BigInt(i));
|
|
2208
3282
|
const prize = n.tier != null && (!forfeit || several) ? farm.tiers[n.tier]?.prize : null;
|
|
2209
|
-
const prized = prize
|
|
2210
|
-
|
|
3283
|
+
const prized = prize
|
|
3284
|
+
? prizeMetas({
|
|
3285
|
+
project,
|
|
3286
|
+
collection: projectCollection ?? n.collection,
|
|
3287
|
+
prize,
|
|
3288
|
+
winner: player,
|
|
3289
|
+
nonce: BigInt.asUintN(64, at + 1n),
|
|
3290
|
+
poolMints,
|
|
3291
|
+
items,
|
|
3292
|
+
nft,
|
|
3293
|
+
programs,
|
|
3294
|
+
})
|
|
3295
|
+
: [];
|
|
3296
|
+
const rolls =
|
|
3297
|
+
n.roll != null
|
|
3298
|
+
? {
|
|
3299
|
+
box: pda.box(project, n.roll),
|
|
3300
|
+
order: pda.order(project, player, namedRollNonce(boxes[n.roll], seat.accounts.asset, at)),
|
|
3301
|
+
}
|
|
3302
|
+
: { box: null, order: null };
|
|
2211
3303
|
return { seat, prize, prized, rolls };
|
|
2212
3304
|
});
|
|
2213
3305
|
const [first, ...rest] = parts;
|
|
2214
|
-
const seat = withPrograms(
|
|
3306
|
+
const seat = withPrograms(
|
|
3307
|
+
first.seat,
|
|
3308
|
+
rest.map(x => x.seat),
|
|
3309
|
+
);
|
|
2215
3310
|
if (forfeit) seat.accounts.player_rewards = null;
|
|
2216
|
-
const groups = rest.flatMap(
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
3311
|
+
const groups = rest.flatMap(x => [
|
|
3312
|
+
...seatGroup(x.seat.accounts.seat, x.seat, x.rolls.box, x.rolls.order),
|
|
3313
|
+
...x.prized,
|
|
3314
|
+
]);
|
|
3315
|
+
return lootIx(
|
|
3316
|
+
'unstake_nft',
|
|
3317
|
+
{ forfeit, nonce },
|
|
3318
|
+
{
|
|
3319
|
+
...seat.accounts,
|
|
3320
|
+
loot_box: first.rolls.box,
|
|
3321
|
+
order: first.rolls.order,
|
|
3322
|
+
associated_token_program: parts.some(x => x.prize?.Tokens) ? ASSOCIATED_TOKEN_PROGRAM : null,
|
|
3323
|
+
},
|
|
3324
|
+
{
|
|
3325
|
+
remaining: [...seat.proof, ...first.prized, ...groups, ...tailOf(extra)],
|
|
3326
|
+
writable: [...(forfeit ? [] : payWrites(farm)), ...seat.writable],
|
|
3327
|
+
},
|
|
3328
|
+
);
|
|
2221
3329
|
};
|
|
2222
3330
|
|
|
2223
3331
|
/** The nonce each NFT's mission roll is named by in an `unstakeNftIx` of several (`assets`), in the order the call
|
|
2224
3332
|
* takes them: the `i`th by its seat's address `nonce + 2i` (its order at `namedRollNonce` of it), its prize's roll
|
|
2225
3333
|
* `nonce + 2i + 1` (at `prizeRollNonce`). */
|
|
2226
3334
|
export const seatNonces = (project, id, farm, player, assets, nonce = 0n) =>
|
|
2227
|
-
Object.fromEntries(
|
|
3335
|
+
Object.fromEntries(
|
|
3336
|
+
ascending(assets, a => pda.seat(pda.farm(project, id), farm.farmed.Tokens ? player : a)).map((a, i) => [
|
|
3337
|
+
a,
|
|
3338
|
+
toBigInt(nonce) + 2n * BigInt(i),
|
|
3339
|
+
]),
|
|
3340
|
+
);
|
|
2228
3341
|
|
|
2229
3342
|
/**
|
|
2230
3343
|
* `player` collects what the seat of `by` owes (their own address for tokens, a collectible's asset); with `compound`, a
|
|
@@ -2234,20 +3347,44 @@ export const seatNonces = (project, id, farm, player, assets, nonce = 0n) =>
|
|
|
2234
3347
|
* (`NotProven`). `more` seats of the player's in the farm (each { by, held, tree }) collect in the same call, one fee,
|
|
2235
3348
|
* one transfer.
|
|
2236
3349
|
*/
|
|
2237
|
-
export const harvestIx = ({
|
|
3350
|
+
export const harvestIx = ({
|
|
3351
|
+
player,
|
|
3352
|
+
project,
|
|
3353
|
+
id,
|
|
3354
|
+
farm,
|
|
3355
|
+
by = player,
|
|
3356
|
+
held = null,
|
|
3357
|
+
tree = null,
|
|
3358
|
+
compound = false,
|
|
3359
|
+
programs,
|
|
3360
|
+
more = [],
|
|
3361
|
+
extra = [],
|
|
3362
|
+
}) => {
|
|
2238
3363
|
const farmAddress = pda.farm(project, id);
|
|
2239
|
-
const [first, ...rest] = ascending(
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
3364
|
+
const [first, ...rest] = ascending(
|
|
3365
|
+
[{ by, held, tree }, ...more].map(x => ({ ...x, seat: pda.seat(farmAddress, x.by) })),
|
|
3366
|
+
x => x.seat,
|
|
3367
|
+
);
|
|
3368
|
+
const groups = rest.flatMap(x => [
|
|
3369
|
+
meta(x.seat, false, true),
|
|
3370
|
+
meta(x.held ?? LOOT_PROGRAM, false, false),
|
|
3371
|
+
meta(x.tree ?? LOOT_PROGRAM, false, false),
|
|
3372
|
+
]);
|
|
3373
|
+
return lootIx(
|
|
3374
|
+
'harvest',
|
|
3375
|
+
{ compound },
|
|
3376
|
+
{
|
|
3377
|
+
...farmSide(project, id, farm),
|
|
3378
|
+
player,
|
|
3379
|
+
seat: first.seat,
|
|
3380
|
+
player_rewards: ata(player, farm.rewards_mint, tokenProgramOf(programs, farm.rewards_mint)),
|
|
3381
|
+
held: first.held,
|
|
3382
|
+
tree: first.tree,
|
|
3383
|
+
// Compounding moves what is owed into the staked vault (a farm of tokens whose rewards are the staked token).
|
|
3384
|
+
staked_vault: compound ? pda.staked(farmAddress) : null,
|
|
3385
|
+
},
|
|
3386
|
+
{ remaining: [...groups, ...tailOf(extra)], writable: payWrites(farm) },
|
|
3387
|
+
);
|
|
2251
3388
|
};
|
|
2252
3389
|
|
|
2253
3390
|
/**
|
|
@@ -2258,13 +3395,49 @@ export const harvestIx = ({ player, project, id, farm, by = player, held = null,
|
|
|
2258
3395
|
* NFT's token account that holds it, `holder`'s (its staker's, or a watched NFT's buyer's). `signer` pays for what the
|
|
2259
3396
|
* write adds. `extra`: the reward mint's hook accounts.
|
|
2260
3397
|
*/
|
|
2261
|
-
export const refreshSeatIx = ({
|
|
3398
|
+
export const refreshSeatIx = ({
|
|
3399
|
+
signer,
|
|
3400
|
+
project,
|
|
3401
|
+
id,
|
|
3402
|
+
farm,
|
|
3403
|
+
by,
|
|
3404
|
+
seat,
|
|
3405
|
+
collection,
|
|
3406
|
+
hasAuthority = true,
|
|
3407
|
+
tokenMetadata: tm,
|
|
3408
|
+
compressed,
|
|
3409
|
+
holder = seat.player,
|
|
3410
|
+
extra = [],
|
|
3411
|
+
}) => {
|
|
2262
3412
|
const address = pda.seat(pda.farm(project, id), by);
|
|
2263
3413
|
// A token seat with no booster: its own address stands in for the NFT.
|
|
2264
|
-
const p =
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
3414
|
+
const p =
|
|
3415
|
+
seat.asset === NO_ADDRESS
|
|
3416
|
+
? standIn(address)
|
|
3417
|
+
: piece({
|
|
3418
|
+
project,
|
|
3419
|
+
asset: seat.asset,
|
|
3420
|
+
wallet: holder,
|
|
3421
|
+
collection,
|
|
3422
|
+
hasAuthority,
|
|
3423
|
+
tokenMetadata: tm,
|
|
3424
|
+
compressed,
|
|
3425
|
+
...farmKeeps(farm, { writes: Boolean(farm.growth?.length) }),
|
|
3426
|
+
moves: false,
|
|
3427
|
+
locks: false,
|
|
3428
|
+
});
|
|
3429
|
+
return lootIx(
|
|
3430
|
+
'refresh_seat',
|
|
3431
|
+
{},
|
|
3432
|
+
{
|
|
3433
|
+
signer,
|
|
3434
|
+
...farmSide(project, id, farm),
|
|
3435
|
+
seat: address,
|
|
3436
|
+
...p.accounts,
|
|
3437
|
+
profile: profileOf(project, farm, seat.player),
|
|
3438
|
+
},
|
|
3439
|
+
{ remaining: tailOf(extra), writable: p.writable },
|
|
3440
|
+
);
|
|
2268
3441
|
};
|
|
2269
3442
|
|
|
2270
3443
|
// ---------------------------------------------------------------- recipes and holds
|
|
@@ -2278,7 +3451,9 @@ export const refreshSeatIx = ({ signer, project, id, farm, by, seat, collection,
|
|
|
2278
3451
|
* it lowers a number: its seat and farm follow it, and the raise may not lower a number that farm reads; for a `Trade` line the NFT traded in, as `piece` takes it: `{ asset, collection, tokenMetadata,
|
|
2279
3452
|
* compressed }`). A collectible used that is staked in a farm holding it for the player (escrow) or locking it in their
|
|
2280
3453
|
* wallet (freeze) is `{ asset, farm }`, `farm` that farm's id: its seat and farm follow it (a raise on it may not lower a
|
|
2281
|
-
* number its farm reads: `BadRecipe`).
|
|
3454
|
+
* number its farm reads: `BadRecipe`). One that boosts the player's token seat in a farm of tokens is
|
|
3455
|
+
* `{ asset, farm, booster: true }`: the player's token seat (`["seat", farm, player]`) and that farm follow it, on a kept
|
|
3456
|
+
* `Use` line or a `Show` line alike. A `Staked` line names the farm its collectible
|
|
2282
3457
|
* is staked in (`Input::Staked { item, farm }`, escrowed or frozen there): its address, or `{ asset, farm }` with that
|
|
2283
3458
|
* farm, and its seat and that farm follow it. Each item line passes its item's collection
|
|
2284
3459
|
* (`items[id].collection`, the decoded Items; `collection` when not given), written for a burn: Core writes it as it
|
|
@@ -2289,35 +3464,85 @@ export const refreshSeatIx = ({ signer, project, id, farm, by, seat, collection,
|
|
|
2289
3464
|
* (`askRollIx`, in the same transaction when it fits).
|
|
2290
3465
|
* A craft makes several Core calls: raise the compute limit. `extra`: a token's hook accounts.
|
|
2291
3466
|
*/
|
|
2292
|
-
export function craftIx({
|
|
3467
|
+
export function craftIx({
|
|
3468
|
+
player,
|
|
3469
|
+
project,
|
|
3470
|
+
collection,
|
|
3471
|
+
id,
|
|
3472
|
+
recipe,
|
|
3473
|
+
assets = [],
|
|
3474
|
+
times = 1,
|
|
3475
|
+
nonce = 0n,
|
|
3476
|
+
holders,
|
|
3477
|
+
programs,
|
|
3478
|
+
treasury,
|
|
3479
|
+
poolMints,
|
|
3480
|
+
items = {},
|
|
3481
|
+
nfts = {},
|
|
3482
|
+
boxes = {},
|
|
3483
|
+
extra = [],
|
|
3484
|
+
}) {
|
|
2293
3485
|
const remaining = [];
|
|
2294
3486
|
const given = [...assets];
|
|
2295
|
-
const tokens =
|
|
2296
|
-
const itemLine = (itemId, burns = false) => [
|
|
3487
|
+
const tokens = mint => ata(player, mint, tokenProgramOf(programs, mint));
|
|
3488
|
+
const itemLine = (itemId, burns = false) => [
|
|
3489
|
+
meta(pda.item(project, itemId), false, false),
|
|
3490
|
+
meta(items[itemId]?.collection ?? collection, false, burns),
|
|
3491
|
+
];
|
|
2297
3492
|
for (const input of recipe.inputs) {
|
|
2298
3493
|
if (input.Burn) {
|
|
2299
3494
|
remaining.push(...itemLine(input.Burn.item, true));
|
|
2300
|
-
for (let i = 0; i < input.Burn.count; i++)
|
|
3495
|
+
for (let i = 0; i < input.Burn.count; i++)
|
|
3496
|
+
remaining.push(meta(take(given, 'a collectible to burn'), false, true));
|
|
2301
3497
|
} else if (input.Use || input.Staked) {
|
|
2302
3498
|
remaining.push(...itemLine((input.Use ?? input.Staked).item));
|
|
2303
3499
|
const used = take(given, 'a collectible to use');
|
|
2304
3500
|
// A staked one is on its seat in the farm its line names.
|
|
2305
|
-
if (
|
|
2306
|
-
|
|
2307
|
-
|
|
3501
|
+
if (
|
|
3502
|
+
input.Staked &&
|
|
3503
|
+
typeof used !== 'string' &&
|
|
3504
|
+
used.farm !== undefined &&
|
|
3505
|
+
Number(used.farm) !== input.Staked.farm
|
|
3506
|
+
)
|
|
3507
|
+
throw new Error(
|
|
3508
|
+
`craft: the recipe takes this collectible staked in farm ${input.Staked.farm}, not farm ${used.farm}`,
|
|
3509
|
+
);
|
|
3510
|
+
// A booster of a token seat is on the player's seat, not one of its own: a `Staked` line refuses it (NotFarmed).
|
|
3511
|
+
if (input.Staked && used?.booster)
|
|
3512
|
+
throw new Error(
|
|
3513
|
+
`craft: the recipe takes a collectible staked on a seat of its own in farm ${input.Staked.farm}; one boosting a token seat is taken by a kept line ("use")`,
|
|
3514
|
+
);
|
|
3515
|
+
const seated = input.Staked
|
|
3516
|
+
? { asset: typeof used === 'string' ? used : used.asset, farm: input.Staked.farm }
|
|
3517
|
+
: used;
|
|
2308
3518
|
if (typeof seated === 'string') remaining.push(meta(seated, false, true));
|
|
2309
3519
|
else {
|
|
2310
|
-
// On a seat: its seat and its farm follow it.
|
|
3520
|
+
// On a seat: its seat (its own, or the player's token seat it boosts) and its farm follow it.
|
|
2311
3521
|
const farm = pda.farm(project, seated.farm);
|
|
2312
|
-
remaining.push(
|
|
3522
|
+
remaining.push(
|
|
3523
|
+
meta(seated.asset, false, true),
|
|
3524
|
+
meta(pda.seat(farm, seated.booster ? player : seated.asset), false, false),
|
|
3525
|
+
meta(farm, false, false),
|
|
3526
|
+
);
|
|
2313
3527
|
}
|
|
2314
3528
|
} else if (input.Pay) {
|
|
2315
3529
|
const { mint, to } = input.Pay;
|
|
2316
|
-
remaining.push(
|
|
3530
|
+
remaining.push(
|
|
3531
|
+
meta(mint, false, true),
|
|
3532
|
+
meta(tokens(mint), false, true),
|
|
3533
|
+
...destinationAccounts(project, to, mint, { holders, programs, treasury }),
|
|
3534
|
+
);
|
|
2317
3535
|
} else if (input.Trade) {
|
|
2318
3536
|
// Any NFT of the line's collection, traded in: burned, given on or stashed (`nftPaid`).
|
|
2319
3537
|
const nft = take(given, 'an NFT to trade in');
|
|
2320
|
-
remaining.push(
|
|
3538
|
+
remaining.push(
|
|
3539
|
+
...nftPaid({
|
|
3540
|
+
project,
|
|
3541
|
+
owner: player,
|
|
3542
|
+
then: input.Trade.then,
|
|
3543
|
+
...(typeof nft === 'string' ? { asset: nft, collection: input.Trade.collection } : nft),
|
|
3544
|
+
}),
|
|
3545
|
+
);
|
|
2321
3546
|
} else if (input.Show) {
|
|
2322
3547
|
// Any NFT, kept: one of the project's (its address), a Core asset of another collection ({ asset, collection }:
|
|
2323
3548
|
// with its Sheet, then its collection), a Token Metadata NFT ({ mint, owner }: its metadata, the holder's
|
|
@@ -2327,14 +3552,33 @@ export function craftIx({ player, project, collection, id, recipe, assets = [],
|
|
|
2327
3552
|
// seat and farm after its accounts, when a raise on it lowers a number.
|
|
2328
3553
|
const place = assets.length - given.length;
|
|
2329
3554
|
const nft = take(given, 'an NFT to show');
|
|
2330
|
-
if (typeof nft === 'string' || (nft.asset && nft.farm !== undefined && !('collection' in nft)))
|
|
2331
|
-
|
|
2332
|
-
else if (nft.
|
|
2333
|
-
|
|
2334
|
-
|
|
3555
|
+
if (typeof nft === 'string' || (nft.asset && nft.farm !== undefined && !('collection' in nft)))
|
|
3556
|
+
remaining.push(meta(nft.asset ?? nft, false, true), meta(input.Show.collection, false, false));
|
|
3557
|
+
else if (nft.asset)
|
|
3558
|
+
remaining.push(
|
|
3559
|
+
meta(nft.asset, false, true),
|
|
3560
|
+
meta(pda.sheet(project, nft.asset), false, true),
|
|
3561
|
+
meta(nft.collection ?? MPL_CORE_PROGRAM, false, false),
|
|
3562
|
+
);
|
|
3563
|
+
else if (nft.compressed)
|
|
3564
|
+
remaining.push(
|
|
3565
|
+
...[nft.compressed, pda.leaf(nft.compressed), nft.tree].map(key => meta(key, false, false)),
|
|
3566
|
+
meta(pda.sheet(project, nft.compressed), false, true),
|
|
3567
|
+
);
|
|
3568
|
+
else
|
|
3569
|
+
remaining.push(
|
|
3570
|
+
...[nft.mint, tokenMetadata.metadata(nft.mint), ata(nft.owner ?? player, nft.mint, TOKEN_PROGRAM)].map(key =>
|
|
3571
|
+
meta(key, false, false),
|
|
3572
|
+
),
|
|
3573
|
+
meta(pda.sheet(project, nft.mint), false, true),
|
|
3574
|
+
);
|
|
3575
|
+
const lowers = (recipe.raises ?? []).some(raise => Number(raise.place) === place && BigInt(raise.shift.by) < 0n);
|
|
2335
3576
|
if (lowers && typeof nft !== 'string' && nft.farm !== undefined && nft.farm !== null) {
|
|
2336
3577
|
const farm = pda.farm(project, nft.farm);
|
|
2337
|
-
remaining.push(
|
|
3578
|
+
remaining.push(
|
|
3579
|
+
meta(pda.seat(farm, nft.booster ? player : (nft.asset ?? nft.compressed ?? nft.mint)), false, false),
|
|
3580
|
+
meta(farm, false, false),
|
|
3581
|
+
);
|
|
2338
3582
|
}
|
|
2339
3583
|
}
|
|
2340
3584
|
}
|
|
@@ -2344,18 +3588,44 @@ export function craftIx({ player, project, collection, id, recipe, assets = [],
|
|
|
2344
3588
|
// A prize (`prizeMetas`): collectibles of one item made in order, each at the next serial.
|
|
2345
3589
|
const prize = output.Prize[0];
|
|
2346
3590
|
const item = prize.Item?.[0];
|
|
2347
|
-
remaining.push(
|
|
3591
|
+
remaining.push(
|
|
3592
|
+
...prizeMetas({
|
|
3593
|
+
project,
|
|
3594
|
+
collection,
|
|
3595
|
+
prize,
|
|
3596
|
+
winner: player,
|
|
3597
|
+
nonce,
|
|
3598
|
+
poolMints,
|
|
3599
|
+
items,
|
|
3600
|
+
nft: nfts[prize.Nft?.nft],
|
|
3601
|
+
made: made[item] ?? 0,
|
|
3602
|
+
programs,
|
|
3603
|
+
}),
|
|
3604
|
+
);
|
|
2348
3605
|
if (item !== undefined) made[item] = (made[item] ?? 0) + 1;
|
|
2349
3606
|
} else if (output.RollOn) {
|
|
2350
3607
|
// The box, then the order its roll is made in (`namedRollNonce` of the craft's `nonce`).
|
|
2351
3608
|
const on = assets[output.RollOn.place];
|
|
2352
|
-
const order = pda.order(
|
|
3609
|
+
const order = pda.order(
|
|
3610
|
+
project,
|
|
3611
|
+
player,
|
|
3612
|
+
namedRollNonce(
|
|
3613
|
+
boxes[output.RollOn.box_id],
|
|
3614
|
+
typeof on === 'string' ? on : (on?.asset ?? on?.mint ?? on?.compressed),
|
|
3615
|
+
nonce,
|
|
3616
|
+
),
|
|
3617
|
+
);
|
|
2353
3618
|
remaining.push(meta(pda.box(project, output.RollOn.box_id), false, false), meta(order, false, true));
|
|
2354
3619
|
}
|
|
2355
3620
|
}
|
|
2356
3621
|
const address = pda.recipe(project, id);
|
|
2357
3622
|
const counts = limits(recipe.per_wallet) ? pda.counts(project, player) : null;
|
|
2358
|
-
return lootIx(
|
|
3623
|
+
return lootIx(
|
|
3624
|
+
'craft',
|
|
3625
|
+
{ version: recipe.version, times, nonce },
|
|
3626
|
+
{ player, project, recipe: address, counts },
|
|
3627
|
+
{ remaining: [...remaining, ...tailOf(extra)] },
|
|
3628
|
+
);
|
|
2359
3629
|
}
|
|
2360
3630
|
|
|
2361
3631
|
function take(list, what) {
|
|
@@ -2364,23 +3634,52 @@ function take(list, what) {
|
|
|
2364
3634
|
}
|
|
2365
3635
|
|
|
2366
3636
|
/** The collectibles a craft of `recipe` makes: one per `Item` prize among its outputs. */
|
|
2367
|
-
export const craftNewAssets =
|
|
3637
|
+
export const craftNewAssets = recipe => recipe.outputs.filter(o => o.Prize?.[0]?.Item).length;
|
|
2368
3638
|
|
|
2369
3639
|
/**
|
|
2370
3640
|
* `player` hands an NFT to the project until `freeAt` (Unix seconds; the hold's address names it, `pda.hold`): a Core
|
|
2371
3641
|
* asset (`collection` its own, Core's address for none), a Token Metadata NFT (`tokenMetadata`), or `compressed` (after
|
|
2372
3642
|
* a `proveLeafIx`).
|
|
2373
3643
|
*/
|
|
2374
|
-
export function holdIx({
|
|
3644
|
+
export function holdIx({
|
|
3645
|
+
player,
|
|
3646
|
+
project,
|
|
3647
|
+
collection,
|
|
3648
|
+
asset,
|
|
3649
|
+
tokenMetadata: tm,
|
|
3650
|
+
compressed,
|
|
3651
|
+
freeAt,
|
|
3652
|
+
reference: ref,
|
|
3653
|
+
more = [],
|
|
3654
|
+
}) {
|
|
2375
3655
|
// Several NFTs (`more`: each { asset, collection, tokenMetadata, compressed }) go on holds of their own with one end, one
|
|
2376
3656
|
// fee: by their holds' addresses, the first the call's own.
|
|
2377
|
-
const [first, ...rest] = ascending(
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
3657
|
+
const [first, ...rest] = ascending(
|
|
3658
|
+
[{ asset, collection, tokenMetadata: tm, compressed }, ...more].map(n => {
|
|
3659
|
+
const p = piece({
|
|
3660
|
+
project,
|
|
3661
|
+
asset: n.asset,
|
|
3662
|
+
wallet: player,
|
|
3663
|
+
collection: n.collection,
|
|
3664
|
+
tokenMetadata: n.tokenMetadata,
|
|
3665
|
+
compressed: n.compressed,
|
|
3666
|
+
...ONLY_MOVES,
|
|
3667
|
+
});
|
|
3668
|
+
return { p, hold: pda.hold(p.accounts.asset, freeAt) };
|
|
3669
|
+
}),
|
|
3670
|
+
x => x.hold,
|
|
3671
|
+
);
|
|
3672
|
+
const p = withPrograms(
|
|
3673
|
+
first.p,
|
|
3674
|
+
rest.map(x => x.p),
|
|
3675
|
+
);
|
|
3676
|
+
const groups = rest.flatMap(x => [meta(x.hold, false, true), ...anotherMetas(x.p)]);
|
|
3677
|
+
return lootIx(
|
|
3678
|
+
'hold',
|
|
3679
|
+
{ free_at: toBigInt(freeAt), reference: reference(ref) },
|
|
3680
|
+
{ player, project, ...p.accounts, hold: first.hold },
|
|
3681
|
+
{ remaining: [...p.proof, ...groups], writable: p.writable },
|
|
3682
|
+
);
|
|
2384
3683
|
}
|
|
2385
3684
|
|
|
2386
3685
|
// ---------------------------------------------------------------- stashes
|
|
@@ -2393,7 +3692,21 @@ export function holdIx({ player, project, collection, asset, tokenMetadata: tm,
|
|
|
2393
3692
|
* treasury in its hold (`["hold", nft, 0]`, the signer paying its rent until anyone `release`s it to the treasury, one
|
|
2394
3693
|
* NFT a call). `out` is never the project's own address nor another account of the program (`ToTheProject`).
|
|
2395
3694
|
*/
|
|
2396
|
-
export function stashIx({
|
|
3695
|
+
export function stashIx({
|
|
3696
|
+
signer,
|
|
3697
|
+
project,
|
|
3698
|
+
id,
|
|
3699
|
+
lottery = null,
|
|
3700
|
+
lotteryState = null,
|
|
3701
|
+
asset,
|
|
3702
|
+
out = null,
|
|
3703
|
+
drop = false,
|
|
3704
|
+
weight = 1,
|
|
3705
|
+
collection,
|
|
3706
|
+
tokenMetadata: tm,
|
|
3707
|
+
compressed,
|
|
3708
|
+
more = [],
|
|
3709
|
+
}) {
|
|
2397
3710
|
if (drop && more.length) throw new Error('A stash drops one NFT a call.');
|
|
2398
3711
|
if (drop) out = pda.hold(compressed?.id ?? asset, 0);
|
|
2399
3712
|
const to = out ?? signer;
|
|
@@ -2405,18 +3718,40 @@ export function stashIx({ signer, project, id, lottery = null, lotteryState = nu
|
|
|
2405
3718
|
// box), where no record names the NFT and nothing could move it again (ToTheProject): the ones this call names are
|
|
2406
3719
|
// refused before sending, and the program refuses any other (js/play only takes one out to the signer's wallet).
|
|
2407
3720
|
if (out && !drop && [project, stash, own, pda.fees(project), pda.platform()].includes(out))
|
|
2408
|
-
throw new Error(
|
|
3721
|
+
throw new Error(
|
|
3722
|
+
'An NFT taken out of a stash can’t go to the project or an account of the program: name a wallet, or drop it (`drop`: it stays, owed to the treasury).',
|
|
3723
|
+
);
|
|
2409
3724
|
// Several NFTs (`more`: each { asset, weight, collection, tokenMetadata, compressed }) go in or out in one call, one
|
|
2410
3725
|
// fee, in ascending order of their addresses; put in, each weighs its `weight` in the stash's draws.
|
|
2411
|
-
const [first, ...rest] = ascending(
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
3726
|
+
const [first, ...rest] = ascending(
|
|
3727
|
+
[{ asset, weight, collection, tokenMetadata: tm, compressed }, ...more].map(n => ({
|
|
3728
|
+
n,
|
|
3729
|
+
p: piece({
|
|
3730
|
+
project,
|
|
3731
|
+
asset: n.asset,
|
|
3732
|
+
wallet: drop ? project : to,
|
|
3733
|
+
collection: n.collection,
|
|
3734
|
+
tokenMetadata: n.tokenMetadata,
|
|
3735
|
+
compressed: n.compressed,
|
|
3736
|
+
...ONLY_MOVES,
|
|
3737
|
+
}),
|
|
3738
|
+
})),
|
|
3739
|
+
x => x.p.accounts.asset,
|
|
3740
|
+
);
|
|
3741
|
+
const p = withPrograms(
|
|
3742
|
+
first.p,
|
|
3743
|
+
rest.map(x => x.p),
|
|
3744
|
+
);
|
|
3745
|
+
const weights = out ? [] : [first, ...rest].map(x => x.n.weight ?? 1);
|
|
2416
3746
|
// A lottery's own stash names the lottery, and the round it waits on (`idleRound`; `lotteryState`: the decoded
|
|
2417
3747
|
// Lottery; none for one never written), whose places say what it locks.
|
|
2418
3748
|
const round = own && lotteryState ? idleRound(own, lotteryState) : null;
|
|
2419
|
-
return lootIx(
|
|
3749
|
+
return lootIx(
|
|
3750
|
+
'stash',
|
|
3751
|
+
{ of: own ? { Lottery: [lottery] } : { Project: [id] }, out: Boolean(out), weights },
|
|
3752
|
+
{ signer, project, stash, to, ...p.accounts, lottery: own, round },
|
|
3753
|
+
{ remaining: [...p.proof, ...rest.flatMap(x => anotherMetas(x.p))], writable: p.writable },
|
|
3754
|
+
);
|
|
2420
3755
|
}
|
|
2421
3756
|
|
|
2422
3757
|
// ---------------------------------------------------------------- contests
|
|
@@ -2427,7 +3762,20 @@ export function stashIx({ signer, project, id, lottery = null, lotteryState = nu
|
|
|
2427
3762
|
* compressed }, as `piece` takes it), which the contest holds until it is claimed. `shown`/`listProof` pass its gate.
|
|
2428
3763
|
* A new entry's record is the player's to pay (not refunded). `extra`: a hook's accounts.
|
|
2429
3764
|
*/
|
|
2430
|
-
export function enterIx({
|
|
3765
|
+
export function enterIx({
|
|
3766
|
+
player,
|
|
3767
|
+
project,
|
|
3768
|
+
id,
|
|
3769
|
+
contest,
|
|
3770
|
+
side,
|
|
3771
|
+
amount,
|
|
3772
|
+
shown,
|
|
3773
|
+
listProof,
|
|
3774
|
+
nft,
|
|
3775
|
+
playerToken,
|
|
3776
|
+
programs,
|
|
3777
|
+
extra = [],
|
|
3778
|
+
}) {
|
|
2431
3779
|
const address = pda.contest(project, id);
|
|
2432
3780
|
// Read for its rule (with its Sheet, for an NFT the project has no authority over), then handed to the project.
|
|
2433
3781
|
const staked = contest.nft ? piece({ project, wallet: player, ...nft, ...ONLY_MOVES, reads: true }) : null;
|
|
@@ -2442,7 +3790,13 @@ export function enterIx({ player, project, id, contest, side, amount, shown, lis
|
|
|
2442
3790
|
player_token: playerToken ?? ata(player, contest.mint, tokenProgramOf(programs, contest.mint)),
|
|
2443
3791
|
mint: contest.mint,
|
|
2444
3792
|
},
|
|
2445
|
-
{
|
|
3793
|
+
{
|
|
3794
|
+
remaining: [
|
|
3795
|
+
...gateAccounts(project, { gate: contest.gate, wallet: player, shown, listProof }),
|
|
3796
|
+
...(staked ? [...pieceMetas(staked), ...staked.proof] : []),
|
|
3797
|
+
...tailOf(extra),
|
|
3798
|
+
],
|
|
3799
|
+
},
|
|
2446
3800
|
);
|
|
2447
3801
|
}
|
|
2448
3802
|
|
|
@@ -2459,12 +3813,33 @@ export function enterIx({ player, project, id, contest, side, amount, shown, lis
|
|
|
2459
3813
|
* closes the contest (`projectState`: its treasury); a contest that owes nothing (nobody entered, every part claimed,
|
|
2460
3814
|
* off by itself with none in) is closed by any claim, the `payer`'s. `extra`: a hook's accounts.
|
|
2461
3815
|
*/
|
|
2462
|
-
export function claimIx({
|
|
3816
|
+
export function claimIx({
|
|
3817
|
+
payer,
|
|
3818
|
+
project,
|
|
3819
|
+
projectState,
|
|
3820
|
+
id,
|
|
3821
|
+
contest,
|
|
3822
|
+
entries,
|
|
3823
|
+
player,
|
|
3824
|
+
nfts,
|
|
3825
|
+
kinds = {},
|
|
3826
|
+
nonce,
|
|
3827
|
+
collection,
|
|
3828
|
+
poolMints,
|
|
3829
|
+
items,
|
|
3830
|
+
nft,
|
|
3831
|
+
prizeMint,
|
|
3832
|
+
forgo = false,
|
|
3833
|
+
end = false,
|
|
3834
|
+
hold = false,
|
|
3835
|
+
programs,
|
|
3836
|
+
extra = [],
|
|
3837
|
+
}) {
|
|
2463
3838
|
const address = pda.contest(project, id);
|
|
2464
3839
|
// A contest that owes nothing (nobody entered, or every part claimed) is only closed: by any claim, entry 0, any player.
|
|
2465
3840
|
const closing = BigInt(contest.owed) === 0n;
|
|
2466
3841
|
player ??= payer;
|
|
2467
|
-
const i = closing ? 0 : entries.findIndex(
|
|
3842
|
+
const i = closing ? 0 : entries.findIndex(e => e.player === player);
|
|
2468
3843
|
if (i < 0) throw new Error(`${player} has no entry in this contest.`);
|
|
2469
3844
|
const me = closing ? { side: 0, bits: CLAIMED | ENDED, amount: 0n, taken: 0 } : entries[i];
|
|
2470
3845
|
nonce = toBigInt(nonce ?? prizeNonce(address, player));
|
|
@@ -2473,8 +3848,23 @@ export function claimIx({ payer, project, projectState, id, contest, entries, pl
|
|
|
2473
3848
|
// A winner's first claim of a contest with a prize: the prize's accounts, after the NFTs'. A split prize is paid from
|
|
2474
3849
|
// the contest's vault (named accounts below), not given as a prize line.
|
|
2475
3850
|
const split = Boolean(contest.prize_split);
|
|
2476
|
-
const prize =
|
|
2477
|
-
|
|
3851
|
+
const prize =
|
|
3852
|
+
contest.prize != null && first && !split && !forgo
|
|
3853
|
+
? prizeMetas({
|
|
3854
|
+
project,
|
|
3855
|
+
collection,
|
|
3856
|
+
prize: contest.prize,
|
|
3857
|
+
winner: player,
|
|
3858
|
+
nonce,
|
|
3859
|
+
poolMints,
|
|
3860
|
+
items,
|
|
3861
|
+
nft,
|
|
3862
|
+
programs,
|
|
3863
|
+
})
|
|
3864
|
+
: [];
|
|
3865
|
+
const pMint = split
|
|
3866
|
+
? splitMint(contest.prize, prizeMint ?? poolMints?.[contest.prize?.Tokens?.from.Pool?.[0]])
|
|
3867
|
+
: null;
|
|
2478
3868
|
nfts ??= !closing && contest.nft && Number(me.amount) === 1 ? [i] : [];
|
|
2479
3869
|
const moved = [];
|
|
2480
3870
|
let taken = Number(me.taken);
|
|
@@ -2482,7 +3872,8 @@ export function claimIx({ payer, project, projectState, id, contest, entries, pl
|
|
|
2482
3872
|
const theirs = entries[j];
|
|
2483
3873
|
let to = contest.opener;
|
|
2484
3874
|
if (contest.verdict !== 'Won' || Number(theirs.side) === Number(contest.winner)) to = theirs.player;
|
|
2485
|
-
else if (won && payer === player && !(me.bits & ENDED) && taken < shareOf(contest, me))
|
|
3875
|
+
else if (won && payer === player && !(me.bits & ENDED) && taken < shareOf(contest, me))
|
|
3876
|
+
[to, taken] = [player, taken + 1];
|
|
2486
3877
|
moved.push(meta(theirs.player, false, true));
|
|
2487
3878
|
if (holds(hold, theirs.nft)) moved.push(meta(pda.hold(theirs.nft, 0), false, true));
|
|
2488
3879
|
else {
|
|
@@ -2492,9 +3883,14 @@ export function claimIx({ payer, project, projectState, id, contest, entries, pl
|
|
|
2492
3883
|
}
|
|
2493
3884
|
// What it leaves owed: at 0 it closes the contest, burning what its pot (and a split prize's vault) still hold.
|
|
2494
3885
|
const owed = BigInt(contest.owed);
|
|
2495
|
-
const left = closing
|
|
3886
|
+
const left = closing
|
|
3887
|
+
? 0n
|
|
3888
|
+
: !contest.nft
|
|
3889
|
+
? owed - (contest.verdict === 'Won' ? (first ? BigInt(me.amount) : 0n) : 1n)
|
|
3890
|
+
: owed - BigInt(nfts.length) - (first && contest.prize ? 1n : 0n);
|
|
2496
3891
|
// The claim that leaves nothing owed closes the contest, its rent and what is left to the treasury: it names it.
|
|
2497
|
-
if (left <= 0n && !projectState?.treasury)
|
|
3892
|
+
if (left <= 0n && !projectState?.treasury)
|
|
3893
|
+
throw new Error('This claim closes the contest: pass projectState, for its treasury.');
|
|
2498
3894
|
return lootIx(
|
|
2499
3895
|
'claim',
|
|
2500
3896
|
{ entry: i, nonce, forgo, end, nfts },
|
|
@@ -2509,12 +3905,16 @@ export function claimIx({ payer, project, projectState, id, contest, entries, pl
|
|
|
2509
3905
|
mint: contest.mint,
|
|
2510
3906
|
prize_mint: pMint,
|
|
2511
3907
|
// A split prize from a pool: what its vault holds at the close goes back to that pool (`WrongPool` without it).
|
|
2512
|
-
prize_pool:
|
|
3908
|
+
prize_pool:
|
|
3909
|
+
split && contest.prize?.Tokens?.from?.Pool ? pda.pool(project, contest.prize.Tokens.from.Pool[0]) : null,
|
|
2513
3910
|
prize_vault: split ? pda.prizeVault(address) : null,
|
|
2514
3911
|
prize_to: split ? ata(player, pMint, tokenProgramOf(programs, pMint)) : null,
|
|
2515
3912
|
treasury: projectState?.treasury ?? null,
|
|
2516
3913
|
},
|
|
2517
|
-
{
|
|
3914
|
+
{
|
|
3915
|
+
writable: left <= 0n ? ['mint', ...(split ? ['prize_mint'] : [])] : [],
|
|
3916
|
+
remaining: [...moved, ...prize, ...tailOf(extra)],
|
|
3917
|
+
},
|
|
2518
3918
|
);
|
|
2519
3919
|
}
|
|
2520
3920
|
|
|
@@ -2539,14 +3939,18 @@ function recordTerms(name, record, span) {
|
|
|
2539
3939
|
export function auctionTerms(auction) {
|
|
2540
3940
|
if (auction instanceof Uint8Array) {
|
|
2541
3941
|
const room = decode('Auction', auction).nft_bids ? 32 * MAX_BID_NFTS : 0;
|
|
2542
|
-
return recordTerms('Auction', auction,
|
|
3942
|
+
return recordTerms('Auction', auction, data => [data.length - 60 - room, data.length]);
|
|
2543
3943
|
}
|
|
2544
3944
|
// Written back with no bid's NFTs, its bytes end at the bid's 60.
|
|
2545
|
-
return recordTerms(
|
|
3945
|
+
return recordTerms(
|
|
3946
|
+
'Auction',
|
|
3947
|
+
{ ...auction, gate: auction.gate ?? null, nft_bids: auction.nft_bids ?? null, held: [] },
|
|
3948
|
+
data => [data.length - 60, data.length],
|
|
3949
|
+
);
|
|
2546
3950
|
}
|
|
2547
3951
|
|
|
2548
3952
|
/** When bids close, as the program reckons it: `ends_at`, or `extend` seconds after the last bid when that is later. */
|
|
2549
|
-
export const auctionEnds =
|
|
3953
|
+
export const auctionEnds = auction => {
|
|
2550
3954
|
if (BigInt(auction.last_bid_at) === 0n) return BigInt(auction.ends_at);
|
|
2551
3955
|
const late = BigInt(auction.last_bid_at) + BigInt(auction.extend);
|
|
2552
3956
|
return late > BigInt(auction.ends_at) ? late : BigInt(auction.ends_at);
|
|
@@ -2575,11 +3979,21 @@ export function openAuctionIx({ seller, project, collection, tokenMetadata: tm,
|
|
|
2575
3979
|
const auction = pda.auction(project, seller, id);
|
|
2576
3980
|
const asset = args.lot.Asset?.[0] ?? null;
|
|
2577
3981
|
// An item lot reads no NFT: the auction's own address stands in.
|
|
2578
|
-
const p = asset
|
|
3982
|
+
const p = asset
|
|
3983
|
+
? piece({ project, asset, wallet: seller, collection, tokenMetadata: tm, compressed, ...ONLY_MOVES })
|
|
3984
|
+
: standIn(auction);
|
|
2579
3985
|
return lootIx(
|
|
2580
3986
|
'open_auction',
|
|
2581
3987
|
{ id, args: { ...args, gate: args.gate ?? null, nft_bids: args.nft_bids ?? null } },
|
|
2582
|
-
{
|
|
3988
|
+
{
|
|
3989
|
+
seller,
|
|
3990
|
+
project,
|
|
3991
|
+
auction,
|
|
3992
|
+
mint,
|
|
3993
|
+
item: args.lot.Item ? pda.item(project, args.lot.Item[0]) : null,
|
|
3994
|
+
loot_box: args.lot.Roll ? pda.box(project, args.lot.Roll[0]) : null,
|
|
3995
|
+
...p.accounts,
|
|
3996
|
+
},
|
|
2583
3997
|
{ remaining: p.proof, writable: p.writable },
|
|
2584
3998
|
);
|
|
2585
3999
|
}
|
|
@@ -2596,7 +4010,21 @@ export const MAX_BID_NFTS = 3;
|
|
|
2596
4010
|
* (`held`, the kinds of `auction.held`, in order) back to its bidder; one that can't move now (`hold`: true for all, or a
|
|
2597
4011
|
* list of assets) becomes a hold for them instead, `bidder` paying its rent until its release. `extra`: a hook's accounts.
|
|
2598
4012
|
*/
|
|
2599
|
-
export function bidIx({
|
|
4013
|
+
export function bidIx({
|
|
4014
|
+
bidder,
|
|
4015
|
+
project,
|
|
4016
|
+
id,
|
|
4017
|
+
auction,
|
|
4018
|
+
amount,
|
|
4019
|
+
shown,
|
|
4020
|
+
listProof,
|
|
4021
|
+
nfts = [],
|
|
4022
|
+
held = [],
|
|
4023
|
+
hold = false,
|
|
4024
|
+
bidderToken,
|
|
4025
|
+
programs,
|
|
4026
|
+
extra = [],
|
|
4027
|
+
}) {
|
|
2600
4028
|
const tokenProgram = tokenProgramOf(programs, auction.mint);
|
|
2601
4029
|
const outbid = BigInt(auction.high) > 0n;
|
|
2602
4030
|
const bundle = [];
|
|
@@ -2626,7 +4054,14 @@ export function bidIx({ bidder, project, id, auction, amount, shown, listProof,
|
|
|
2626
4054
|
mint: auction.mint,
|
|
2627
4055
|
},
|
|
2628
4056
|
// The vault takes a bid in tokens; NFT bids leave it alone, unlocked.
|
|
2629
|
-
{
|
|
4057
|
+
{
|
|
4058
|
+
remaining: [
|
|
4059
|
+
...gateAccounts(project, { gate: auction.gate, wallet: bidder, shown, listProof }),
|
|
4060
|
+
...bundle,
|
|
4061
|
+
...tailOf(extra),
|
|
4062
|
+
],
|
|
4063
|
+
writable: auction.nft_bids ? [] : ['vault'],
|
|
4064
|
+
},
|
|
2630
4065
|
);
|
|
2631
4066
|
}
|
|
2632
4067
|
|
|
@@ -2643,7 +4078,24 @@ export function bidIx({ bidder, project, id, auction, amount, shown, listProof,
|
|
|
2643
4078
|
* piece nor proof), and a winning NFT of a bid a hold for `proceeds` (its hold's address in its piece's place); anyone
|
|
2644
4079
|
* then delivers each with `releaseIx`, and the end's sender gets the rent back then.
|
|
2645
4080
|
*/
|
|
2646
|
-
export function endAuctionIx({
|
|
4081
|
+
export function endAuctionIx({
|
|
4082
|
+
signer,
|
|
4083
|
+
project,
|
|
4084
|
+
projectState,
|
|
4085
|
+
id,
|
|
4086
|
+
auction,
|
|
4087
|
+
item = null,
|
|
4088
|
+
collection,
|
|
4089
|
+
tokenMetadata: tm,
|
|
4090
|
+
compressed,
|
|
4091
|
+
held = [],
|
|
4092
|
+
hold = false,
|
|
4093
|
+
reference: ref,
|
|
4094
|
+
nonce,
|
|
4095
|
+
holders,
|
|
4096
|
+
programs,
|
|
4097
|
+
extra = [],
|
|
4098
|
+
}) {
|
|
2647
4099
|
// The lot went at an earlier end, and its seller's account couldn't take the money then (`proceeds_owed`): this end pays
|
|
2648
4100
|
// it and closes the auction, naming no lot, share or winning NFT.
|
|
2649
4101
|
const owed = BigInt(auction.proceeds_owed ?? 0) > 0n;
|
|
@@ -2651,8 +4103,9 @@ export function endAuctionIx({ signer, project, projectState, id, auction, item
|
|
|
2651
4103
|
const tokenProgram = tokenProgramOf(programs, auction.mint);
|
|
2652
4104
|
const sold = BigInt(auction.high) > 0n;
|
|
2653
4105
|
const itemId = owed ? undefined : auction.lot.Item?.[0];
|
|
2654
|
-
const made =
|
|
2655
|
-
|
|
4106
|
+
const made =
|
|
4107
|
+
!owed && sold && item?.kind === 'Collectible' ? pda.itemAsset(pda.item(project, itemId), auction.pin) : null;
|
|
4108
|
+
const asset = owed ? null : (auction.lot.Asset?.[0] ?? made);
|
|
2656
4109
|
const proceeds = auction.lot.Asset ? auction.seller : projectState.treasury;
|
|
2657
4110
|
const address = pda.auction(project, auction.seller, id);
|
|
2658
4111
|
const receiver = sold ? auction.high_bidder : auction.seller;
|
|
@@ -2665,20 +4118,24 @@ export function endAuctionIx({ signer, project, projectState, id, auction, item
|
|
|
2665
4118
|
const p = owed
|
|
2666
4119
|
? standIn(address)
|
|
2667
4120
|
: lotHeld
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
4121
|
+
? standIn(asset)
|
|
4122
|
+
: auction.lot.Asset
|
|
4123
|
+
? piece({ project, asset, wallet: receiver, collection, tokenMetadata: tm, compressed, ...ONLY_MOVES })
|
|
4124
|
+
: asset
|
|
4125
|
+
? {
|
|
4126
|
+
...piece({ project, asset, collection: item.collection, moves: false, locks: false, reads: false }),
|
|
4127
|
+
writable: ['collection'],
|
|
4128
|
+
}
|
|
4129
|
+
: standIn(address);
|
|
2674
4130
|
// A winning bid of NFTs: each to where the rest of a bid in tokens would go (`held`, their kinds in order).
|
|
2675
|
-
const winning =
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
4131
|
+
const winning =
|
|
4132
|
+
!owed && sold && auction.nft_bids
|
|
4133
|
+
? (auction.held ?? []).flatMap((nft, i) => {
|
|
4134
|
+
if (holds(hold, nft)) return [meta(pda.hold(nft, 0), false, true)];
|
|
4135
|
+
const q = piece({ project, asset: nft, wallet: proceeds, ...held[i], ...ONLY_MOVES });
|
|
4136
|
+
return [...pieceMetas(q), ...q.proof];
|
|
4137
|
+
})
|
|
4138
|
+
: [];
|
|
2682
4139
|
return lootIx(
|
|
2683
4140
|
'end_auction',
|
|
2684
4141
|
{ reference: reference(ref), nonce },
|
|
@@ -2695,11 +4152,27 @@ export function endAuctionIx({ signer, project, projectState, id, auction, item
|
|
|
2695
4152
|
item: itemId !== undefined ? pda.item(project, itemId) : null,
|
|
2696
4153
|
...p.accounts,
|
|
2697
4154
|
loot_box: roll !== undefined ? pda.box(project, roll) : null,
|
|
2698
|
-
order:
|
|
4155
|
+
order:
|
|
4156
|
+
roll !== undefined
|
|
4157
|
+
? pda.order(project, auction.high_bidder, prizeRollNonce(nonce))
|
|
4158
|
+
: lotHeld
|
|
4159
|
+
? pda.hold(asset, 0)
|
|
4160
|
+
: null,
|
|
2699
4161
|
},
|
|
2700
4162
|
{
|
|
2701
|
-
remaining: [
|
|
2702
|
-
|
|
4163
|
+
remaining: [
|
|
4164
|
+
...p.proof,
|
|
4165
|
+
...(!owed && sold && roll === undefined
|
|
4166
|
+
? shareAccounts(project, auction.shares, auction.mint, { holders, programs, treasury: projectState.treasury })
|
|
4167
|
+
: []),
|
|
4168
|
+
...winning,
|
|
4169
|
+
...tailOf(extra),
|
|
4170
|
+
],
|
|
4171
|
+
writable: [
|
|
4172
|
+
...(!owed && sold && sharesBurn(auction.shares) ? ['mint'] : []),
|
|
4173
|
+
...(sold && !auction.nft_bids ? ['vault'] : []),
|
|
4174
|
+
...p.writable,
|
|
4175
|
+
],
|
|
2703
4176
|
},
|
|
2704
4177
|
);
|
|
2705
4178
|
}
|
|
@@ -2712,10 +4185,10 @@ export function endAuctionIx({ signer, project, projectState, id, auction, item
|
|
|
2712
4185
|
|
|
2713
4186
|
/** What a taker signs: the offer's bytes but `left`, its last 2 (what a take changes). `offer`: the decoded Offer, or its
|
|
2714
4187
|
* account's bytes. */
|
|
2715
|
-
export const offerTerms =
|
|
4188
|
+
export const offerTerms = offer => recordTerms('Offer', offer, data => [data.length - 2, data.length]);
|
|
2716
4189
|
|
|
2717
4190
|
/** What one take of a deal pays out of its offer's money: a bid's price, a loan's amount. */
|
|
2718
|
-
export const dealUnit =
|
|
4191
|
+
export const dealUnit = deal => BigInt(deal.Buy?.price ?? deal.Lend.amount);
|
|
2719
4192
|
|
|
2720
4193
|
/**
|
|
2721
4194
|
* `maker` (any wallet, signing alone) opens offer `id` in `mint`, and puts all of its money (`left` times the deal's
|
|
@@ -2728,7 +4201,14 @@ export function openOfferIx({ maker, project, id, mint, args, makerToken, progra
|
|
|
2728
4201
|
return lootIx(
|
|
2729
4202
|
'open_offer',
|
|
2730
4203
|
{ id, args: { shares: [], ...args } },
|
|
2731
|
-
{
|
|
4204
|
+
{
|
|
4205
|
+
maker,
|
|
4206
|
+
project,
|
|
4207
|
+
offer: pda.offer(project, maker, id),
|
|
4208
|
+
mint,
|
|
4209
|
+
maker_token: makerToken ?? ata(maker, mint, tokenProgram),
|
|
4210
|
+
vault: vault(project, mint, tokenProgram),
|
|
4211
|
+
},
|
|
2732
4212
|
{ remaining: tailOf(extra) },
|
|
2733
4213
|
);
|
|
2734
4214
|
}
|
|
@@ -2739,7 +4219,14 @@ export function closeOfferIx({ maker, project, id, offer, programs, extra = [] }
|
|
|
2739
4219
|
return lootIx(
|
|
2740
4220
|
'close_offer',
|
|
2741
4221
|
{},
|
|
2742
|
-
{
|
|
4222
|
+
{
|
|
4223
|
+
maker,
|
|
4224
|
+
project,
|
|
4225
|
+
offer: pda.offer(project, maker, id),
|
|
4226
|
+
mint: offer.mint,
|
|
4227
|
+
vault: vault(project, offer.mint, tokenProgram),
|
|
4228
|
+
maker_token: ata(maker, offer.mint, tokenProgram),
|
|
4229
|
+
},
|
|
2743
4230
|
{ remaining: tailOf(extra) },
|
|
2744
4231
|
);
|
|
2745
4232
|
}
|
|
@@ -2750,15 +4237,53 @@ export function closeOfferIx({ maker, project, id, offer, programs, extra = [] }
|
|
|
2750
4237
|
* any kind, as `piece` takes it (`collection`, `hasAuthority` false for a Core asset of another collection,
|
|
2751
4238
|
* `tokenMetadata`, or `compressed` after a `proveLeafIx`).
|
|
2752
4239
|
*/
|
|
2753
|
-
export function sellToOfferIx({
|
|
4240
|
+
export function sellToOfferIx({
|
|
4241
|
+
seller,
|
|
4242
|
+
project,
|
|
4243
|
+
projectState,
|
|
4244
|
+
id,
|
|
4245
|
+
offer,
|
|
4246
|
+
asset,
|
|
4247
|
+
collection,
|
|
4248
|
+
hasAuthority,
|
|
4249
|
+
tokenMetadata: tm,
|
|
4250
|
+
compressed,
|
|
4251
|
+
holders,
|
|
4252
|
+
programs,
|
|
4253
|
+
extra = [],
|
|
4254
|
+
}) {
|
|
2754
4255
|
const tokenProgram = tokenProgramOf(programs, offer.mint);
|
|
2755
|
-
const p = passedOn({
|
|
4256
|
+
const p = passedOn({
|
|
4257
|
+
project,
|
|
4258
|
+
owner: seller,
|
|
4259
|
+
receiver: offer.maker,
|
|
4260
|
+
burns: false,
|
|
4261
|
+
asset,
|
|
4262
|
+
collection,
|
|
4263
|
+
hasAuthority,
|
|
4264
|
+
tokenMetadata: tm,
|
|
4265
|
+
compressed,
|
|
4266
|
+
reads: true,
|
|
4267
|
+
});
|
|
2756
4268
|
return lootIx(
|
|
2757
4269
|
'sell_to_offer',
|
|
2758
4270
|
{ terms: offerTerms(offer) },
|
|
2759
|
-
{ seller, project, offer: pda.offer(project, offer.maker, id), maker: offer.maker, ...p.accounts, mint: offer.mint, vault: vault(project, offer.mint, tokenProgram), seller_token: ata(seller, offer.mint, tokenProgram) },
|
|
2760
4271
|
{
|
|
2761
|
-
|
|
4272
|
+
seller,
|
|
4273
|
+
project,
|
|
4274
|
+
offer: pda.offer(project, offer.maker, id),
|
|
4275
|
+
maker: offer.maker,
|
|
4276
|
+
...p.accounts,
|
|
4277
|
+
mint: offer.mint,
|
|
4278
|
+
vault: vault(project, offer.mint, tokenProgram),
|
|
4279
|
+
seller_token: ata(seller, offer.mint, tokenProgram),
|
|
4280
|
+
},
|
|
4281
|
+
{
|
|
4282
|
+
remaining: [
|
|
4283
|
+
...p.proof,
|
|
4284
|
+
...shareAccounts(project, offer.shares, offer.mint, { holders, programs, treasury: projectState?.treasury }),
|
|
4285
|
+
...tailOf(extra),
|
|
4286
|
+
],
|
|
2762
4287
|
writable: [...(sharesBurn(offer.shares) ? ['mint'] : []), ...p.writable],
|
|
2763
4288
|
},
|
|
2764
4289
|
);
|
|
@@ -2770,15 +4295,45 @@ export function sellToOfferIx({ seller, project, projectState, id, offer, asset,
|
|
|
2770
4295
|
* `pda.hold(asset, due)` until it is repaid (`repayIx`, then `releaseIx` sends it home) or, once due, sent to the
|
|
2771
4296
|
* lender (`releaseIx` with `to` the lender); the loan goes to the borrower. The NFT is any kind, as `sellToOfferIx` takes it.
|
|
2772
4297
|
*/
|
|
2773
|
-
export function borrowIx({
|
|
4298
|
+
export function borrowIx({
|
|
4299
|
+
borrower,
|
|
4300
|
+
project,
|
|
4301
|
+
id,
|
|
4302
|
+
offer,
|
|
4303
|
+
due,
|
|
4304
|
+
asset,
|
|
4305
|
+
collection,
|
|
4306
|
+
hasAuthority,
|
|
4307
|
+
tokenMetadata: tm,
|
|
4308
|
+
compressed,
|
|
4309
|
+
programs,
|
|
4310
|
+
extra = [],
|
|
4311
|
+
}) {
|
|
2774
4312
|
const tokenProgram = tokenProgramOf(programs, offer.mint);
|
|
2775
|
-
const p = piece({
|
|
4313
|
+
const p = piece({
|
|
4314
|
+
project,
|
|
4315
|
+
asset,
|
|
4316
|
+
wallet: borrower,
|
|
4317
|
+
collection,
|
|
4318
|
+
hasAuthority,
|
|
4319
|
+
tokenMetadata: tm,
|
|
4320
|
+
compressed,
|
|
4321
|
+
...ONLY_MOVES,
|
|
4322
|
+
reads: true,
|
|
4323
|
+
});
|
|
2776
4324
|
return lootIx(
|
|
2777
4325
|
'borrow',
|
|
2778
4326
|
{ due: toBigInt(due), terms: offerTerms(offer) },
|
|
2779
4327
|
{
|
|
2780
|
-
borrower,
|
|
2781
|
-
|
|
4328
|
+
borrower,
|
|
4329
|
+
project,
|
|
4330
|
+
offer: pda.offer(project, offer.maker, id),
|
|
4331
|
+
maker: offer.maker,
|
|
4332
|
+
hold: pda.hold(p.accounts.asset, due),
|
|
4333
|
+
...p.accounts,
|
|
4334
|
+
mint: offer.mint,
|
|
4335
|
+
vault: vault(project, offer.mint, tokenProgram),
|
|
4336
|
+
borrower_token: ata(borrower, offer.mint, tokenProgram),
|
|
2782
4337
|
},
|
|
2783
4338
|
{ remaining: [...p.proof, ...tailOf(extra)], writable: p.writable },
|
|
2784
4339
|
);
|
|
@@ -2792,15 +4347,21 @@ export function borrowIx({ borrower, project, id, offer, due, asset, collection,
|
|
|
2792
4347
|
* transfer hook's accounts.
|
|
2793
4348
|
*/
|
|
2794
4349
|
export function repayIx({ payer, project, hold, repay, payerToken, programs, extra = [] }) {
|
|
2795
|
-
if (repay === undefined || repay === null)
|
|
4350
|
+
if (repay === undefined || repay === null)
|
|
4351
|
+
throw new Error('A repay names the debt the payer was shown (repay: hold.debt.repay as read).');
|
|
2796
4352
|
const { lender, mint } = hold.debt;
|
|
2797
4353
|
const tokenProgram = tokenProgramOf(programs, mint);
|
|
2798
4354
|
return lootIx(
|
|
2799
4355
|
'repay',
|
|
2800
4356
|
{ repay: toBigInt(repay) },
|
|
2801
4357
|
{
|
|
2802
|
-
payer,
|
|
2803
|
-
|
|
4358
|
+
payer,
|
|
4359
|
+
project,
|
|
4360
|
+
hold: pda.hold(hold.asset, hold.free_at),
|
|
4361
|
+
lender,
|
|
4362
|
+
lender_token: ata(lender, mint, tokenProgram),
|
|
4363
|
+
payer_token: payerToken ?? ata(payer, mint, tokenProgram),
|
|
4364
|
+
mint,
|
|
2804
4365
|
},
|
|
2805
4366
|
{ remaining: tailOf(extra) },
|
|
2806
4367
|
);
|
|
@@ -2810,7 +4371,7 @@ export function repayIx({ payer, project, hold, repay, payerToken, programs, ext
|
|
|
2810
4371
|
|
|
2811
4372
|
/** What a taker signs: the listing's bytes but `user`, `until` and `stopped` (bytes 129 to 170, what a rental changes).
|
|
2812
4373
|
* `listing`: the decoded Listing, or its account's bytes. */
|
|
2813
|
-
export const listingTerms =
|
|
4374
|
+
export const listingTerms = listing => recordTerms('Listing', listing, () => [129, 170]);
|
|
2814
4375
|
|
|
2815
4376
|
/**
|
|
2816
4377
|
* `maker` (any wallet, signing alone) lists an NFT they hold, of any kind (as `piece` takes it), for sale
|
|
@@ -2819,9 +4380,35 @@ export const listingTerms = (listing) => recordTerms('Listing', listing, () => [
|
|
|
2819
4380
|
* goes to the project until it is sold or the listing is closed. The listing's address names the NFT
|
|
2820
4381
|
* (`pda.listing(project, asset)`), so a game finds it, and its renter, with one account read.
|
|
2821
4382
|
*/
|
|
2822
|
-
export function openListingIx({
|
|
2823
|
-
|
|
2824
|
-
|
|
4383
|
+
export function openListingIx({
|
|
4384
|
+
maker,
|
|
4385
|
+
project,
|
|
4386
|
+
asset,
|
|
4387
|
+
mint,
|
|
4388
|
+
deal,
|
|
4389
|
+
shares = [],
|
|
4390
|
+
collection,
|
|
4391
|
+
hasAuthority,
|
|
4392
|
+
tokenMetadata: tm,
|
|
4393
|
+
compressed,
|
|
4394
|
+
extra = [],
|
|
4395
|
+
}) {
|
|
4396
|
+
const p = piece({
|
|
4397
|
+
project,
|
|
4398
|
+
asset,
|
|
4399
|
+
wallet: maker,
|
|
4400
|
+
collection,
|
|
4401
|
+
hasAuthority,
|
|
4402
|
+
tokenMetadata: tm,
|
|
4403
|
+
compressed,
|
|
4404
|
+
...ONLY_MOVES,
|
|
4405
|
+
});
|
|
4406
|
+
return lootIx(
|
|
4407
|
+
'open_listing',
|
|
4408
|
+
{ deal, shares: stored(shares) },
|
|
4409
|
+
{ maker, project, listing: pda.listing(project, p.accounts.asset), ...p.accounts, mint },
|
|
4410
|
+
{ remaining: [...p.proof, ...tailOf(extra)], writable: p.writable },
|
|
4411
|
+
);
|
|
2825
4412
|
}
|
|
2826
4413
|
|
|
2827
4414
|
/**
|
|
@@ -2832,22 +4419,65 @@ export function openListingIx({ maker, project, asset, mint, deal, shares = [],
|
|
|
2832
4419
|
* take's own accounts come a sale's compressed NFT's proof, then the shares' accounts, then (in `extra`) Core's
|
|
2833
4420
|
* transfer adapters and any transfer hook's accounts, which the program finds by address after the shares'.
|
|
2834
4421
|
*/
|
|
2835
|
-
export function takeListingIx({
|
|
4422
|
+
export function takeListingIx({
|
|
4423
|
+
taker,
|
|
4424
|
+
project,
|
|
4425
|
+
projectState,
|
|
4426
|
+
listing,
|
|
4427
|
+
periods = 1,
|
|
4428
|
+
collection,
|
|
4429
|
+
hasAuthority,
|
|
4430
|
+
tokenMetadata: tm,
|
|
4431
|
+
compressed,
|
|
4432
|
+
holders,
|
|
4433
|
+
programs,
|
|
4434
|
+
takerToken,
|
|
4435
|
+
extra = [],
|
|
4436
|
+
}) {
|
|
2836
4437
|
const tokenProgram = tokenProgramOf(programs, listing.mint);
|
|
2837
4438
|
const sale = Boolean(listing.deal.Sell);
|
|
2838
4439
|
// A rental moves nothing: a Core or Token Metadata NFT by its address; a compressed one by its record and tree
|
|
2839
4440
|
// (`proveLeafIx` first, in the same transaction), with no proof.
|
|
2840
|
-
const p = sale
|
|
2841
|
-
|
|
4441
|
+
const p = sale
|
|
4442
|
+
? piece({
|
|
4443
|
+
project,
|
|
4444
|
+
asset: listing.asset,
|
|
4445
|
+
wallet: taker,
|
|
4446
|
+
collection,
|
|
4447
|
+
hasAuthority,
|
|
4448
|
+
tokenMetadata: tm,
|
|
4449
|
+
compressed,
|
|
4450
|
+
...ONLY_MOVES,
|
|
4451
|
+
})
|
|
4452
|
+
: compressed
|
|
4453
|
+
? {
|
|
4454
|
+
...piece({ project, asset: listing.asset, wallet: taker, compressed, ...ONLY_MOVES, moves: false }),
|
|
4455
|
+
proof: [],
|
|
4456
|
+
}
|
|
4457
|
+
: standIn(listing.asset);
|
|
2842
4458
|
return lootIx(
|
|
2843
4459
|
'take_listing',
|
|
2844
4460
|
{ terms: listingTerms(listing), periods },
|
|
2845
4461
|
{
|
|
2846
|
-
taker,
|
|
2847
|
-
|
|
4462
|
+
taker,
|
|
4463
|
+
project,
|
|
4464
|
+
listing: pda.listing(project, listing.asset),
|
|
4465
|
+
maker: listing.maker,
|
|
4466
|
+
maker_token: ata(listing.maker, listing.mint, tokenProgram),
|
|
4467
|
+
...p.accounts,
|
|
4468
|
+
mint: listing.mint,
|
|
4469
|
+
taker_token: takerToken ?? ata(taker, listing.mint, tokenProgram),
|
|
2848
4470
|
},
|
|
2849
4471
|
{
|
|
2850
|
-
remaining: [
|
|
4472
|
+
remaining: [
|
|
4473
|
+
...p.proof,
|
|
4474
|
+
...shareAccounts(project, listing.shares ?? [], listing.mint, {
|
|
4475
|
+
holders,
|
|
4476
|
+
programs,
|
|
4477
|
+
treasury: projectState?.treasury,
|
|
4478
|
+
}),
|
|
4479
|
+
...tailOf(extra),
|
|
4480
|
+
],
|
|
2851
4481
|
writable: [...(sharesBurn(listing.shares ?? []) ? ['mint'] : []), ...p.writable],
|
|
2852
4482
|
},
|
|
2853
4483
|
);
|
|
@@ -2857,14 +4487,41 @@ export function takeListingIx({ taker, project, projectState, listing, periods =
|
|
|
2857
4487
|
* Its maker closes a listing (`listing` the decoded Listing): the NFT goes home. While a rental runs it only stops (no
|
|
2858
4488
|
* renter takes it again), and the same call once the rental is over takes it home. Free.
|
|
2859
4489
|
*/
|
|
2860
|
-
export function closeListingIx({
|
|
2861
|
-
|
|
2862
|
-
|
|
4490
|
+
export function closeListingIx({
|
|
4491
|
+
maker,
|
|
4492
|
+
project,
|
|
4493
|
+
listing,
|
|
4494
|
+
collection,
|
|
4495
|
+
hasAuthority,
|
|
4496
|
+
tokenMetadata: tm,
|
|
4497
|
+
compressed,
|
|
4498
|
+
extra = [],
|
|
4499
|
+
}) {
|
|
4500
|
+
const p = piece({
|
|
4501
|
+
project,
|
|
4502
|
+
asset: listing.asset,
|
|
4503
|
+
wallet: maker,
|
|
4504
|
+
collection,
|
|
4505
|
+
hasAuthority,
|
|
4506
|
+
tokenMetadata: tm,
|
|
4507
|
+
compressed,
|
|
4508
|
+
...ONLY_MOVES,
|
|
4509
|
+
});
|
|
4510
|
+
return lootIx(
|
|
4511
|
+
'close_listing',
|
|
4512
|
+
{},
|
|
4513
|
+
{ maker, project, listing: pda.listing(project, listing.asset), ...p.accounts },
|
|
4514
|
+
{ remaining: [...p.proof, ...tailOf(extra)], writable: p.writable },
|
|
4515
|
+
);
|
|
2863
4516
|
}
|
|
2864
4517
|
|
|
2865
4518
|
// ---------------------------------------------------------------- Token Metadata NFTs
|
|
2866
4519
|
|
|
2867
|
-
const tmPda =
|
|
4520
|
+
const tmPda = seeds =>
|
|
4521
|
+
findProgramAddress(
|
|
4522
|
+
seeds.map(s => (typeof s === 'string' ? utf8(s) : s)),
|
|
4523
|
+
TOKEN_METADATA_PROGRAM,
|
|
4524
|
+
)[0];
|
|
2868
4525
|
|
|
2869
4526
|
/**
|
|
2870
4527
|
* Whether a classic mint's account data (`data`, 82 bytes) is an NFT's, as the program takes one:
|
|
@@ -2875,13 +4532,20 @@ export function isNftMint(mint, data) {
|
|
|
2875
4532
|
if (!data || data.length !== MINT_BYTES || data[44] !== 0 || data[45] !== 1) return false;
|
|
2876
4533
|
if (data.subarray(36, 44).some((b, i) => b !== (i === 0 ? 1 : 0))) return false;
|
|
2877
4534
|
const tag = data.subarray(0, 4);
|
|
2878
|
-
if (tag.every(
|
|
2879
|
-
return
|
|
4535
|
+
if (tag.every(b => b === 0)) return true;
|
|
4536
|
+
return (
|
|
4537
|
+
tag[0] === 1 &&
|
|
4538
|
+
tag[1] === 0 &&
|
|
4539
|
+
tag[2] === 0 &&
|
|
4540
|
+
tag[3] === 0 &&
|
|
4541
|
+
encodeBase58(data.subarray(4, 36)) === tokenMetadata.edition(mint)
|
|
4542
|
+
);
|
|
2880
4543
|
}
|
|
2881
4544
|
|
|
2882
4545
|
/** A Token Metadata NFT's own addresses: its metadata, its edition, and a pNFT's record of a token account. */
|
|
2883
4546
|
export const tokenMetadata = {
|
|
2884
|
-
metadata:
|
|
2885
|
-
edition:
|
|
2886
|
-
record: (mint, token) =>
|
|
4547
|
+
metadata: mint => tmPda(['metadata', addressBytes(TOKEN_METADATA_PROGRAM), addressBytes(mint)]),
|
|
4548
|
+
edition: mint => tmPda(['metadata', addressBytes(TOKEN_METADATA_PROGRAM), addressBytes(mint), 'edition']),
|
|
4549
|
+
record: (mint, token) =>
|
|
4550
|
+
tmPda(['metadata', addressBytes(TOKEN_METADATA_PROGRAM), addressBytes(mint), 'token_record', addressBytes(token)]),
|
|
2887
4551
|
};
|