@xgamesfun/solana 0.20.1 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -10
- package/examples/coin-flip-arena.json +2 -7
- package/examples/elements-set.json +2 -9
- package/examples/expedition-guild.json +1 -8
- package/examples/guild-missions.json +3 -10
- package/examples/halving-seasons.json +4 -9
- package/examples/hero-trials.json +1 -4
- package/examples/merge-2048.json +5 -5
- package/examples/paper-hands-farm.json +4 -16
- package/examples/partner-vault.json +1 -4
- package/examples/stakers-draw.json +1 -8
- package/examples/star-fleet.json +1 -8
- package/examples/starforce.json +6 -6
- package/js/approve/details.mjs +8 -7
- package/js/approve/server.mjs +20 -5
- package/js/chain/borsh.js +9 -0
- package/js/chain/core.js +14 -2
- package/js/chain/lookup.js +8 -2
- package/js/chain/rpc.js +4 -2
- package/js/client/accounts.js +448 -116
- package/js/client/grant.js +125 -66
- package/js/client/idl/loot.js +1 -1
- package/js/client/idl/loot.json +2360 -2218
- package/js/client/index.js +1 -0
- package/js/client/ix.js +1204 -473
- package/js/client/pda.js +20 -16
- package/js/client/policy.js +44 -23
- package/js/client/random.js +70 -0
- package/js/client/sizes.js +255 -12
- package/js/economy/check.mjs +455 -210
- package/js/economy/cost.mjs +45 -30
- package/js/economy/examples.mjs +2 -2
- package/js/economy/explain.mjs +24 -20
- package/js/economy/guide.mjs +31 -30
- package/js/economy/live.mjs +30 -7
- package/js/economy/model.mjs +171 -56
- package/js/economy/plan.mjs +380 -112
- package/js/economy/schema.json +291 -143
- package/js/economy/simulate.mjs +154 -87
- package/js/mcp/tools.mjs +14 -8
- package/js/play/index.js +1121 -375
- package/js/play/keys.js +167 -0
- package/package.json +1 -1
- package/release.json +5 -5
- package/skills/xsolana-economy/SKILL.md +15 -11
- package/skills/xsolana-economy/references/after-launch.md +37 -27
- package/skills/xsolana-economy/references/apps.md +55 -19
- package/skills/xsolana-economy/references/sentence-to-spec.md +30 -22
- package/skills/xsolana-economy/references/telling-the-person.md +52 -11
- package/types/chain/core.d.ts +3 -1
- package/types/chain/lookup.d.ts +5 -2
- package/types/chain/rpc.d.ts +3 -1
- package/types/client/accounts.d.ts +195 -55
- package/types/client/grant.d.ts +72 -27
- package/types/client/idl/loot.d.ts +590 -559
- package/types/client/index.d.ts +1 -0
- package/types/client/ix.d.ts +3862 -234
- package/types/client/pda.d.ts +19 -15
- package/types/client/policy.d.ts +39 -14
- package/types/client/random.d.ts +26 -0
- package/types/client/sizes.d.ts +93 -5
- package/types/economy/check.d.mts +5 -10
- package/types/economy/cost.d.mts +2 -1
- package/types/economy/live.d.mts +16 -1
- package/types/economy/model.d.mts +105 -37
- package/types/economy/plan.d.mts +193 -192
- package/types/economy/simulate.d.mts +21 -13
- package/types/mcp/tools.d.mts +21 -13
- package/types/play/index.d.ts +450 -39
- package/types/play/keys.d.ts +51 -0
package/js/client/ix.js
CHANGED
|
@@ -16,14 +16,17 @@
|
|
|
16
16
|
// Values are the IDL's own shapes (`chain/borsh.js`): addresses base58 strings, u64/u128/i64 bigints (or numbers),
|
|
17
17
|
// enums 'Variant', { Variant: [values] } or { Variant: { field: value } }, structs with snake_case fields.
|
|
18
18
|
import { addressBytes, encodeBase58, NO_ADDRESS } from '../chain/base58.js';
|
|
19
|
-
import {
|
|
20
|
-
import { concat, intLE, readUintLE, toBigInt, uintLE, utf8 } from '../chain/bytes.js';
|
|
19
|
+
import { encodeAccount, encodeInstruction } from '../chain/borsh.js';
|
|
20
|
+
import { compareBytes, concat, intLE, readUintLE, toBigInt, uintLE, utf8 } from '../chain/bytes.js';
|
|
21
21
|
import { addressWithSeed, ASSOCIATED_TOKEN_PROGRAM, findProgramAddress, METAPLEX_METADATA_PROGRAM, SYSTEM_PROGRAM, SYSVAR_INSTRUCTIONS, SYSVAR_SLOT_HASHES, TOKEN_2022_PROGRAM, TOKEN_PROGRAM } from '../chain/pda.js';
|
|
22
22
|
import { allocateWithSeedIx, createAccountWithSeedIx, initializeAccount3Ix, MINT_BYTES, TOKEN_ACCOUNT_BYTES } from '../chain/token.js';
|
|
23
23
|
import { createAssociatedTokenAccountIdempotent, meta, transferLamports } from '../chain/programs.js';
|
|
24
24
|
import { sha256 } from '../chain/sha256.js';
|
|
25
|
+
import { MEMO_PROGRAM } from '../chain/token2022.js';
|
|
25
26
|
import IDL from './idl/loot.js';
|
|
26
|
-
import {
|
|
27
|
+
import { chosenAtPurchase, CLAIMED, decode, ENDED, idleRound, nftSlot, ownStash, paymentMint, placesPulled, prizePlacesOwed, roundFinished, roundWinners, shareOf, tokenRun } from './accounts.js';
|
|
28
|
+
import { stream, TAG, weighted } from './random.js';
|
|
29
|
+
import { tableBudget } from './sizes.js';
|
|
27
30
|
import { ata, BUBBLEGUM_PROGRAM, COMPRESSION_PROGRAM, LOOT_PROGRAM, MPL_CORE_PROGRAM, NOOP_PROGRAM, pda, vault, VRF_QUEUE } from './pda.js';
|
|
28
31
|
|
|
29
32
|
// The options of a call made without any: read-only, and typed so the generated declarations take any option.
|
|
@@ -35,6 +38,8 @@ export { IDL, ASSOCIATED_TOKEN_PROGRAM };
|
|
|
35
38
|
export const TOKEN_METADATA_PROGRAM = METAPLEX_METADATA_PROGRAM;
|
|
36
39
|
|
|
37
40
|
const ZERO_REFERENCE = new Uint8Array(32);
|
|
41
|
+
/** The default address: names nobody, and nobody signs as it. */
|
|
42
|
+
const NOBODY_KEY = '11111111111111111111111111111111';
|
|
38
43
|
const hashToAddress = (hash) => encodeBase58(hash);
|
|
39
44
|
|
|
40
45
|
/** 32 bytes a project names a call by (a match id, a payout's reason). Zeros when not given. */
|
|
@@ -54,6 +59,24 @@ const instructionDef = (name) => {
|
|
|
54
59
|
return def;
|
|
55
60
|
};
|
|
56
61
|
|
|
62
|
+
/** The IDL's instructions, the longest tags first: the oracle's three callbacks keep 8-byte tags, every other
|
|
63
|
+
* instruction has a 1-byte one, which the first byte of an 8-byte tag may equal. */
|
|
64
|
+
const BY_TAG = [...IDL.instructions].sort((a, b) => b.discriminator.length - a.discriminator.length);
|
|
65
|
+
|
|
66
|
+
/** The IDL entry of the loot instruction whose data is `data` (its tag first), or null. */
|
|
67
|
+
export const instructionOf = (data) => BY_TAG.find((i) => i.discriminator.length <= data.length && i.discriminator.every((b, j) => data[j] === b)) ?? null;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* An instruction's tail (`tokens::tail`): the accounts every NFT move hands Core (what a check its collection carries
|
|
71
|
+
* reads, an oracle's account) and every token transfer hands a transfer hook, after the route's own. It starts at the
|
|
72
|
+
* Memo program's account, which serves a transfer into an account that requires a memo; with none, it is empty. So
|
|
73
|
+
* `extra` (hook accounts, Core's adapters) goes after the Memo program, which this puts first.
|
|
74
|
+
*/
|
|
75
|
+
export function tailOf(extra = []) {
|
|
76
|
+
if (!extra.length) return [];
|
|
77
|
+
return [meta(MEMO_PROGRAM, false, false), ...extra.filter((m) => m.pubkey !== MEMO_PROGRAM)];
|
|
78
|
+
}
|
|
79
|
+
|
|
57
80
|
/** The accounts of an instruction in order, each with its group (`checkout`, `fee`, `side`, `piece`) or ''. */
|
|
58
81
|
function leaves(list, group = '') {
|
|
59
82
|
return list.flatMap((a) => (a.accounts ? leaves(a.accounts, a.name) : [{ ...a, group }]));
|
|
@@ -183,6 +206,28 @@ export function destinationAccounts(project, to, mint, { holders = {}, programs,
|
|
|
183
206
|
}
|
|
184
207
|
|
|
185
208
|
/** A split's destinations, in order. */
|
|
209
|
+
/** The most shares a split kept to be paid later holds (`MAX_SHARES`, not in the IDL): a box's, a lottery's, a contest's,
|
|
210
|
+
* an auction's, an offer's or a listing's (a ninth is `BadShares`); a payment's own split has no such cap. */
|
|
211
|
+
export const MAX_SHARES = 8;
|
|
212
|
+
/** `shares`, refused here when a split kept to be paid later holds more than `MAX_SHARES`. */
|
|
213
|
+
const stored = (shares = []) => {
|
|
214
|
+
if (shares.length > MAX_SHARES) throw new Error(`A split kept to be paid later holds at most ${MAX_SHARES} shares; this one has ${shares.length} (BadShares).`);
|
|
215
|
+
return shares;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
/** The most traits a fresh item draws (`MAX_ITEM_TRAITS`, not in the IDL): each is written as its unit is minted, which
|
|
219
|
+
* one transaction must hold. */
|
|
220
|
+
export const MAX_ITEM_TRAITS = 64;
|
|
221
|
+
/** The longest name a fresh item's trait has, in bytes (`MAX_TRAIT_KEY`, not in the IDL). */
|
|
222
|
+
export const MAX_TRAIT_KEY = 64;
|
|
223
|
+
/** An item's `traits`, refused here past the program's caps (`BadItem` when the owner signs). */
|
|
224
|
+
const traitsInCaps = (traits = []) => {
|
|
225
|
+
if (traits.length > MAX_ITEM_TRAITS) throw new Error(`An item draws at most ${MAX_ITEM_TRAITS} traits; this one draws ${traits.length} (BadItem).`);
|
|
226
|
+
const long = traits.find((t) => utf8(String(t.key)).length > MAX_TRAIT_KEY);
|
|
227
|
+
if (long) throw new Error(`A trait's name is at most ${MAX_TRAIT_KEY} bytes; “${String(long.key).slice(0, 24)}…” is ${utf8(String(long.key)).length} (BadItem).`);
|
|
228
|
+
return traits;
|
|
229
|
+
};
|
|
230
|
+
|
|
186
231
|
export const shareAccounts = (project, shares, mint, options) => shares.flatMap((s) => destinationAccounts(project, s.to, mint, options));
|
|
187
232
|
|
|
188
233
|
/**
|
|
@@ -204,11 +249,22 @@ export const sharesBurn = (shares) => shares.some((s) => s.to === 'Burn');
|
|
|
204
249
|
/**
|
|
205
250
|
* The account each prize of a box's entries comes with at the roll, in entry order and each entry's prizes in order:
|
|
206
251
|
* its item for an item, its pool for tokens from one, its stash for an NFT of one (a named one, or one at random). All
|
|
207
|
-
* written: the roll takes from them. Tokens minted and rolls take none here.
|
|
252
|
+
* written: the roll takes from them. Tokens minted and rolls take none here. A fresh item's tables follow it while
|
|
253
|
+
* they fit the box's table budget (`tableBudget`), walked in entry order; an item whose tables don't fit what is left
|
|
254
|
+
* takes none, and is out of stock for that roll.
|
|
208
255
|
*/
|
|
209
|
-
export function entryAccounts(project, entries) {
|
|
256
|
+
export function entryAccounts(project, entries, items = {}) {
|
|
257
|
+
let budget = Math.max(0, tableBudget(entries));
|
|
210
258
|
return entries.flatMap(({ prizes }) => prizes.flatMap((prize) => {
|
|
211
|
-
|
|
259
|
+
// An item, compressed or not; a fresh one's tables after it, in the order of its traits (`items` maps an item's id
|
|
260
|
+
// to its decoded Item: one left out is taken as not fresh).
|
|
261
|
+
const id = prize.Item?.[0] ?? prize.Compressed?.item;
|
|
262
|
+
if (id !== undefined) {
|
|
263
|
+
const tables = traitTables(items[id]);
|
|
264
|
+
const fits = tables.length <= budget;
|
|
265
|
+
if (fits) budget -= tables.length;
|
|
266
|
+
return [meta(pda.item(project, id), false, true), ...(fits ? tables : []).map((t) => meta(pda.table(project, t), false, true))];
|
|
267
|
+
}
|
|
212
268
|
if (prize.Tokens?.from.Pool) return [meta(pda.pool(project, prize.Tokens.from.Pool[0]), false, true)];
|
|
213
269
|
const stash = prize.Nft?.stash ?? prize.Stash?.[0];
|
|
214
270
|
return stash === undefined ? [] : [meta(pda.stash(project, stash), false, true)];
|
|
@@ -248,17 +304,42 @@ function boxPoolMints(box, poolMints) {
|
|
|
248
304
|
/** Whether a box leaves its outcome to the oracle: more than one entry, or an entry gives a stash's NFT at random. */
|
|
249
305
|
export const boxRolls = (entries) => entries.length > 1 || entries.some((e) => e.prizes.some((p) => p.Stash));
|
|
250
306
|
|
|
251
|
-
/**
|
|
252
|
-
|
|
253
|
-
|
|
307
|
+
/** 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 = (item) => (item?.traits ?? []).flatMap((t) => (t.from?.Table ? [t.from.Table[0]] : []));
|
|
309
|
+
|
|
310
|
+
/** Whether a box gives a fresh item (one with traits): its units' traits are drawn by the oracle (`items` maps an
|
|
311
|
+
* item's id to its decoded Item, as `entryAccounts`). */
|
|
312
|
+
export const boxFresh = (entries, items = {}) => entries.some((e) => e.prizes.some((p) => (items[p.Item?.[0] ?? p.Compressed?.item]?.traits ?? []).length > 0));
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* What one prize a settle hands over may open or mint, at most: a collectible's Core asset with the program's plugins
|
|
316
|
+
* (about 0.004 SOL), a stash's NFT's new token account, a fresh compressed NFT's Sheet, a token account of a mint, the
|
|
317
|
+
* NFT rolled on's numbers or its stash row. A client's own figure (the program keeps no reserve): what a throwaway key
|
|
318
|
+
* that settles is funded with for each, and gives back unspent.
|
|
319
|
+
*/
|
|
320
|
+
export const SETTLE_RENT_UNIT = 6_000_000n;
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* What a settle of an order of `opens` opens of `box` may open or mint, at most, in lamports: whoever settles pays it
|
|
324
|
+
* (the order lends nothing). For each draw of each open, each prize of the entry with the most of them that opens an
|
|
325
|
+
* account (a collectible, a stash's NFT, a fresh compressed NFT's Sheet: `items` maps an item's id to its decoded
|
|
326
|
+
* Item); once an order, each source of tokens it may pay, and the fate of the NFT it rolls on; one `SETTLE_RENT_UNIT`
|
|
327
|
+
* each. A throwaway key that settles is funded with it, and gives back what it doesn't spend.
|
|
328
|
+
*/
|
|
329
|
+
export function settleRent(box, opens = 1, items = {}) {
|
|
330
|
+
const rents = (p) => Boolean(p.Item || p.Nft || p.Stash || (p.Compressed && (items[p.Compressed.item]?.traits ?? []).length));
|
|
331
|
+
const most = Math.max(0, ...box.entries.map((e) => e.prizes.filter(rents).length));
|
|
332
|
+
const sources = new Set(box.entries.flatMap((e) => e.prizes.flatMap((p) => (p.Tokens ? [JSON.stringify(p.Tokens.from)] : []))));
|
|
333
|
+
return SETTLE_RENT_UNIT * (BigInt(most * boxDraws(box) * Math.max(1, opens)) + BigInt(sources.size) + (box.subject ? 1n : 0n));
|
|
334
|
+
}
|
|
254
335
|
|
|
255
336
|
/**
|
|
256
337
|
* The nonce the `k`th roll an order won is named by when it settles: the first 8 bytes, little
|
|
257
|
-
* endian, of sha256("roll" | order | its requested slot (u64 LE) | k (
|
|
338
|
+
* endian, of sha256("roll" | order | its requested slot (u64 LE) | k (u32 LE)). Like every prize's roll, its order is at
|
|
258
339
|
* `prizeRollNonce` of it.
|
|
259
340
|
*/
|
|
260
341
|
export function rollNonce(order, requestedSlot, k) {
|
|
261
|
-
const hash = sha256(concat(utf8('roll'), addressBytes(order), uintLE(requestedSlot, 8), uintLE(k,
|
|
342
|
+
const hash = sha256(concat(utf8('roll'), addressBytes(order), uintLE(requestedSlot, 8), uintLE(k, 4)));
|
|
262
343
|
return readUintLE(hash, 0, 8);
|
|
263
344
|
}
|
|
264
345
|
|
|
@@ -272,7 +353,17 @@ export const nonceOn = (asset) => readUintLE(addressBytes(asset), 0, 8);
|
|
|
272
353
|
|
|
273
354
|
/** Whether a box's roll holds the NFT it rolls on, or moves its numbers: an entry burns it, gives it
|
|
274
355
|
* away or moves its numbers. Its order is at the NFT's nonce (`nonceOn`). */
|
|
275
|
-
export const boxHolds = (box) => (box.fates ?? []).some((f) => f.then
|
|
356
|
+
export const boxHolds = (box) => (box.fates ?? []).some((f) => f.then != null || (f.shifts ?? []).length > 0);
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* The nonce of the order a roll a sender names is made in: a craft's roll on a collectible kept (`RollOn`) and a
|
|
360
|
+
* mission's roll on the NFT leaving it. On an NFT its box holds (`boxHolds`), the NFT's own (`nonceOn`), whatever was
|
|
361
|
+
* named; otherwise `prizeRollNonce` of the named one, so no sender reaches the address of a roll an order won.
|
|
362
|
+
*/
|
|
363
|
+
export const namedRollNonce = (box, asset, named) => {
|
|
364
|
+
if (!box) throw new Error('A roll on an NFT needs its box, decoded (boxes), to know where its order is made.');
|
|
365
|
+
return boxHolds(box) ? nonceOn(asset) : prizeRollNonce(named);
|
|
366
|
+
};
|
|
276
367
|
|
|
277
368
|
/** Whether a limit for each wallet ({ total, per_window, window }) limits anything. */
|
|
278
369
|
const limits = (perWallet) => Boolean(perWallet && (Number(perWallet.total) || Number(perWallet.per_window)));
|
|
@@ -305,8 +396,8 @@ export const gateAccounts = (project, { gate, wallet, shown, listProof = [] } =
|
|
|
305
396
|
];
|
|
306
397
|
|
|
307
398
|
/**
|
|
308
|
-
* A nonce a prize route names (its `nonce` argument), from what names the prize: a grant and
|
|
309
|
-
*
|
|
399
|
+
* A nonce a prize route names (its `nonce` argument), from what names the prize: a grant, its wallet and rounds, a
|
|
400
|
+
* contest and its player, an auction and its last bid. Any nonce works (the sender picks it); the roll's order
|
|
310
401
|
* is then at `prizeRollNonce` of it (plus the place where the route says so), which no roll on an NFT is at.
|
|
311
402
|
*/
|
|
312
403
|
export const prizeNonce = (...parts) => {
|
|
@@ -345,17 +436,15 @@ function pieceMetas(p) {
|
|
|
345
436
|
* Tokens the mint (written when minted), the pool when they come from one, the winner's account of the mint;
|
|
346
437
|
* Item the item; for a collectible, its collection (the Item's), its address at the item's next serial, Metaplex Core;
|
|
347
438
|
* Nft the stash, the NFT as `piece` takes it, then a compressed NFT's proof;
|
|
348
|
-
* Roll the box, then the order its roll is made in, at `prizeRollNonce(nonce)
|
|
349
|
-
*
|
|
439
|
+
* Roll the box, then the order its roll is made in, at `prizeRollNonce(nonce)` (what the roll can give is read
|
|
440
|
+
* when it is asked: `askRollIx`);
|
|
350
441
|
* Held the lottery's own stash (`lottery`: the lottery's address), the NFT as `piece` takes it, then its proof.
|
|
351
442
|
* `poolMints` maps a pool's index to its mint (null for a pool never opened, which gives nothing: `projectMint` stands in
|
|
352
443
|
* for its mint); `items` an item's id to its decoded Item (for its next serial; `made`
|
|
353
444
|
* collectibles of the same item already made earlier in the instruction); `nft` the NFT's kind, as `piece` takes it
|
|
354
|
-
* ({ collection, tokenMetadata, compressed }).
|
|
355
|
-
* for one never written), and `rollPools` each pool its prizes name to its mint (null for one never opened;
|
|
356
|
-
* `poolMints` when not given).
|
|
445
|
+
* ({ collection, tokenMetadata, compressed }).
|
|
357
446
|
*/
|
|
358
|
-
export function prizeMetas({ project, collection, prize, winner, nonce = 0n, poolMints = {}, items = {}, nft = {}, made = 0, projectMint, programs, lottery
|
|
447
|
+
export function prizeMetas({ project, collection, prize, winner, nonce = 0n, poolMints = {}, items = {}, nft = {}, made = 0, projectMint, programs, lottery }) {
|
|
359
448
|
if (prize.Tokens) {
|
|
360
449
|
const { from } = prize.Tokens;
|
|
361
450
|
// A pool never opened (its mint given as null) gives nothing: any mint stands in for its place, the project's.
|
|
@@ -398,34 +487,47 @@ export function prizeMetas({ project, collection, prize, winner, nonce = 0n, poo
|
|
|
398
487
|
meta(BUBBLEGUM_CORE_SIGNER, false, false),
|
|
399
488
|
];
|
|
400
489
|
}
|
|
401
|
-
|
|
402
|
-
return strict ? [...roll, ...strictRollMints(project, prize.Roll[0], boxes, rollPools)] : roll;
|
|
490
|
+
return [meta(pda.box(project, prize.Roll[0]), false, false), meta(pda.order(project, winner, prizeRollNonce(nonce)), false, true)];
|
|
403
491
|
}
|
|
404
492
|
|
|
405
493
|
/**
|
|
406
|
-
* The accounts
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
* pool its prizes name to its mint, or null for a pool never opened (its address alone).
|
|
494
|
+
* The roll accounts of a box, as `open_box`, `ask_roll` and the oracle's answer take them after their own: each entry's
|
|
495
|
+
* prizes' (`entryAccounts`: an item, its tables when it is fresh and they fit the box's table budget; a pool; a stash),
|
|
496
|
+
* then each mint its tokens are in and the project's vault for it (`prizeAccounts`). `box` is the decoded LootBox (null
|
|
497
|
+
* for one never written: none). `rolls`: { items (an item's id to its decoded Item, null for none written), poolMints
|
|
498
|
+
* (a pool's index to its mint, null for one never opened), programs }.
|
|
412
499
|
*/
|
|
413
|
-
export function
|
|
500
|
+
export function rollAccounts(project, box, rolls = {}) {
|
|
414
501
|
if (!box) return [];
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
if (
|
|
419
|
-
const
|
|
420
|
-
if (!(
|
|
421
|
-
|
|
422
|
-
|
|
502
|
+
const { items = {}, poolMints = {}, programs } = /** @type {{ items?: any, poolMints?: any, programs?: any }} */ (rolls);
|
|
503
|
+
for (const p of box.entries.flatMap((e) => e.prizes)) {
|
|
504
|
+
const id = p.Item?.[0] ?? p.Compressed?.item;
|
|
505
|
+
if (id !== undefined && items[id] === undefined) throw new Error(`a roll of box ${box.box_id ?? ''} needs item ${id} (rolls.items: its decoded Item, or null for none written), for its tables`);
|
|
506
|
+
const pool = p.Tokens?.from.Pool?.[0];
|
|
507
|
+
if (pool !== undefined && !(pool in poolMints)) throw new Error(`a roll of box ${box.box_id ?? ''} needs the mint of pool ${pool} (rolls.poolMints; null for a pool never opened)`);
|
|
508
|
+
}
|
|
509
|
+
return [...entryAccounts(project, box.entries, items), ...prizeAccounts(project, box.entries, poolMints, programs)];
|
|
423
510
|
}
|
|
424
511
|
|
|
425
|
-
/**
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
512
|
+
/**
|
|
513
|
+
* What a write or a route that names boxes to roll later passes for them (`order::rollables`): each box once, in the
|
|
514
|
+
* order they first come, and nothing after it. What a roll can give is read when it is asked.
|
|
515
|
+
*/
|
|
516
|
+
export function boxesAccounts(project, ids) {
|
|
517
|
+
return [...new Set(ids)].map((id) => meta(pda.box(project, id), false, false));
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** The boxes a record names to roll later (`Def::rolls`), in order: a box's entries' rolls, a recipe's rolls and
|
|
521
|
+
* `RollOn`s, a grant's, a lottery's places'. */
|
|
522
|
+
export function defRolls(def) {
|
|
523
|
+
const [kind] = Object.keys(def ?? {});
|
|
524
|
+
const args = def?.[kind]?.args ?? {};
|
|
525
|
+
const prizes = kind === 'Box' ? (args.entries ?? []).flatMap((e) => e.prizes ?? [])
|
|
526
|
+
: kind === 'Recipe' ? (args.outputs ?? []).map((o) => o.Prize?.[0] ?? (o.RollOn ? { Roll: [o.RollOn.box_id] } : null))
|
|
527
|
+
: kind === 'Grant' ? [args.gives]
|
|
528
|
+
: kind === 'Lottery' ? (args.terms?.places ?? args.places ?? []).map((p) => (typeof p === 'object' ? p.prize : null))
|
|
529
|
+
: [];
|
|
530
|
+
return prizes.flatMap((p) => (p?.Roll ? [p.Roll[0]] : []));
|
|
429
531
|
}
|
|
430
532
|
|
|
431
533
|
/** Bubblegum's signer for its calls to Core, `["mpl_core_cpi_signer"]`. */
|
|
@@ -512,6 +614,8 @@ export function recordAddress(project, def) {
|
|
|
512
614
|
return pda.recipe(project, body.id);
|
|
513
615
|
case 'Grant':
|
|
514
616
|
return pda.grant(project, body.id);
|
|
617
|
+
case 'Table':
|
|
618
|
+
return pda.table(project, body.id);
|
|
515
619
|
default:
|
|
516
620
|
throw new Error(`Unknown record kind ${kind}`);
|
|
517
621
|
}
|
|
@@ -527,93 +631,113 @@ export function withDefaults(def) {
|
|
|
527
631
|
const body = def[kind];
|
|
528
632
|
const args = { ...body.args };
|
|
529
633
|
if (kind === 'Box' || kind === 'Lottery' || kind === 'Grant') args.gate ??= null;
|
|
530
|
-
if (kind === 'Item')
|
|
634
|
+
if (kind === 'Item') {
|
|
635
|
+
args.collection ??= NO_ADDRESS;
|
|
636
|
+
// A fresh item's traits (`Item::traits`): none for one whose units are all alike.
|
|
637
|
+
args.traits = traitsInCaps(args.traits ?? []);
|
|
638
|
+
}
|
|
531
639
|
if (kind === 'Box') {
|
|
640
|
+
stored(args.shares);
|
|
532
641
|
if (!args.currency) throw new Error('A box names the mint its price is in (`currency`).');
|
|
533
|
-
//
|
|
642
|
+
// What one open draws (`LootBox::slots`): none is one draw among every entry.
|
|
643
|
+
args.slots = (args.slots ?? []).map((s) => ({ only: [], distinct: false, share: 0, ...s }));
|
|
644
|
+
// An entry: its prizes (`prizeMetas`) and its weight. A box burns its price with a share to `Burn`.
|
|
534
645
|
if (!Array.isArray(args.entries)) throw new Error('A box needs its entries (`entries`): what its roll can give, each with its weight.');
|
|
535
|
-
args.entries = args.entries.map((e) => ({ prizes: [],
|
|
646
|
+
args.entries = args.entries.map((e) => ({ prizes: [], ...e }));
|
|
536
647
|
args.subject ??= null;
|
|
537
|
-
|
|
648
|
+
// What it does once an entry can't be given (`WhenOut`): stop selling while any entry couldn't cover the draws of
|
|
649
|
+
// a purchase and those waiting for their answer (the default), or draw among what is left ('DrawLeft').
|
|
650
|
+
args.when_out ??= 'StopSales';
|
|
651
|
+
// A fate's `then`: null sends the NFT back to its player; 'Burn', { Give: [wallet] } or { Stash: [id] }.
|
|
652
|
+
args.fates = (args.fates ?? []).map((f) => ({ ...f, then: f.then ?? null, shifts: (f.shifts ?? []).map((s) => ({ from_now: false, ...s })) }));
|
|
538
653
|
}
|
|
539
654
|
if (kind === 'Box' || kind === 'Recipe') {
|
|
540
655
|
args.window ??= NO_WINDOW;
|
|
541
656
|
args.per_wallet ??= NO_LIMIT;
|
|
542
657
|
}
|
|
543
|
-
if (kind === 'Recipe')
|
|
658
|
+
if (kind === 'Recipe') {
|
|
659
|
+
// 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 }) => ({ place, shift: { from_now: false, most: 0, ...(shift ?? flat) } }));
|
|
661
|
+
args.needs = (args.needs ?? []).map(({ place, key, test, at_least: least }) => ({ place, key, test: test ?? { AtLeast: [least ?? 0] } }));
|
|
662
|
+
}
|
|
544
663
|
if (kind === 'Box' && args.fall) args.fall = { shared: false, ...args.fall };
|
|
545
664
|
if (kind === 'Lottery') {
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
665
|
+
// Its terms (`LotteryTerms`, what a round copies at its first purchase), given in `terms` or beside `every`.
|
|
666
|
+
const { every, gate, per_wallet: perWallet, terms: given, ...flat } = args;
|
|
667
|
+
const terms = { nft_ticket: null, returns: false, min_sold: 0, ticket_mint: NO_ADDRESS, ...flat, ...given };
|
|
668
|
+
// 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((p) => (typeof p === 'object' ? { prize: null, times: 1, ...p } : { bps: p, prize: null, times: 1 }));
|
|
670
|
+
terms.shares = stored(terms.shares ?? []);
|
|
671
|
+
return { [kind]: { ...body, args: { terms, every: every ?? 0n, gate: gate ?? null, per_wallet: perWallet ?? 0 } } };
|
|
552
672
|
}
|
|
553
673
|
if (kind === 'Grant') {
|
|
554
674
|
args.key_root ??= new Uint8Array(32);
|
|
555
675
|
args.open ??= false;
|
|
676
|
+
args.shard_bits ??= 0;
|
|
556
677
|
}
|
|
557
678
|
return { [kind]: { ...body, args } };
|
|
558
679
|
}
|
|
559
680
|
|
|
560
681
|
/**
|
|
561
682
|
* The owner writes one record (`def`: { Item: { id, args } }, { Box: … }, …), at its own address. A lottery names
|
|
562
|
-
* its pot's mint, its pot (which its first write opens), its ticket mint,
|
|
563
|
-
*
|
|
564
|
-
|
|
565
|
-
|
|
683
|
+
* its pot's mint, its pot (which its first write opens), its ticket mint, both token programs and the round it waits on
|
|
684
|
+
* (`lotteryState`: the lottery as it is, null for a new one; `idleRound`: its current round's record, or round 0's once
|
|
685
|
+
* it drew once and ended: a lottery made again, edited once ended or made to repeat waits while that round runs or
|
|
686
|
+
* 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
|
|
687
|
+
* names a box to roll later passes that box after (`boxesAccounts`), which must be written: a box is written before
|
|
688
|
+
* anything names it (it may name itself).
|
|
689
|
+
*/
|
|
690
|
+
export function upsertIx({ owner, project, def: given, lotteryState = null }) {
|
|
566
691
|
const def = withDefaults(given);
|
|
567
692
|
const [kind] = Object.keys(def);
|
|
568
693
|
const { args } = def[kind];
|
|
569
694
|
let remaining = [];
|
|
695
|
+
// A collectible names its collection, read for Bubblegum's plugin (`Item::compressed`: its compressed prizes can be
|
|
696
|
+
// minted only when the collection carries it).
|
|
697
|
+
if (kind === 'Item' && args.kind === 'Collectible') remaining = [meta(args.collection, false, false)];
|
|
570
698
|
// A new sealed schedule opens its own vault: its mint, the vault and both token programs.
|
|
571
699
|
const vest = kind === 'Schedule' ? args.action?.from?.Vest?.[0] : null;
|
|
572
700
|
if (vest) {
|
|
701
|
+
// It pays a wallet, the treasury or a burn: no account it pays is named.
|
|
573
702
|
remaining = [meta(vest, false, false), meta(pda.vest(recordAddress(project, def)), false, true), meta(TOKEN_PROGRAM, false, false), meta(TOKEN_2022_PROGRAM, false, false)];
|
|
574
|
-
// A pool or a token account it pays comes next, checked now to be of the vault's mint (`open_vest`).
|
|
575
|
-
const to = args.action.to;
|
|
576
|
-
if (to?.Pool) remaining.push(meta(pda.pool(project, to.Pool[0]), false, false));
|
|
577
|
-
else if (to?.Account) remaining.push(meta(to.Account[0], false, false));
|
|
578
|
-
// A lottery's pot (the only account of the project's it may pay) comes with its lottery (`lottery`).
|
|
579
|
-
if (to?.Account && lottery) remaining.push(meta(lottery, false, false));
|
|
580
703
|
}
|
|
581
704
|
if (kind === 'Lottery') {
|
|
582
705
|
remaining = [
|
|
583
|
-
meta(args.mint, false, false),
|
|
706
|
+
meta(args.terms.mint, false, false),
|
|
584
707
|
meta(pda.pot(recordAddress(project, def)), false, true),
|
|
585
|
-
meta(args.
|
|
586
|
-
meta(pda.lotteryFunds(recordAddress(project, def)), false, true),
|
|
708
|
+
meta(paymentMint(args.terms), false, false),
|
|
587
709
|
meta(TOKEN_PROGRAM, false, false),
|
|
588
710
|
meta(TOKEN_2022_PROGRAM, false, false),
|
|
711
|
+
meta(lotteryState ? idleRound(recordAddress(project, def), lotteryState) : SYSTEM_PROGRAM, false, false),
|
|
589
712
|
];
|
|
590
|
-
// A place that holds an NFT (`Prize::Held`):
|
|
591
|
-
if (args.places.some((p) => p.prize?.Held)) remaining.push(meta(pda.lotteryStash(recordAddress(project, def)), false, false));
|
|
713
|
+
// 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((p) => p.prize?.Held || p.prize === 'Raffled')) remaining.push(meta(pda.lotteryStash(recordAddress(project, def)), false, false));
|
|
592
715
|
}
|
|
716
|
+
// Each box it names to roll later (a box's, a recipe's, a grant's, a lottery's places'), once.
|
|
717
|
+
remaining.push(...boxesAccounts(project, defRolls(def)));
|
|
593
718
|
return lootIx('upsert', { def }, { owner, project, target: recordAddress(project, def) }, { remaining });
|
|
594
719
|
}
|
|
595
720
|
|
|
596
721
|
/**
|
|
597
722
|
* The owner closes the record at `target`, and its rent comes back: a grant's with the funds it held. A lottery
|
|
598
723
|
* (`lottery`: its decoded record) sends what its pot holds to `to`, a token account of the pot's mint (the owner's own
|
|
599
|
-
* by default), and closes the pot
|
|
724
|
+
* by default), and closes the pot, once its current round has no record (one that drew once and ended: once its round 0
|
|
725
|
+
* gave out every NFT of its own stash, `idleRound`).
|
|
600
726
|
*/
|
|
601
|
-
export function closeRecordIx({ owner, project, target, lottery, schedule, programs, to,
|
|
727
|
+
export function closeRecordIx({ owner, project, target, lottery, schedule, programs, to, extra = [] }) {
|
|
602
728
|
// A sealed schedule never closes (CannotClose): its address, and its vault's, stay bound to its terms.
|
|
603
729
|
if (schedule?.action?.from?.Vest) throw new Error('A sealed schedule never closes: its vault stays bound to its terms.');
|
|
604
730
|
if (!lottery) return lootIx('upsert', { def: null }, { owner, project, target });
|
|
605
|
-
const
|
|
606
|
-
const
|
|
731
|
+
const { mint } = lottery.terms;
|
|
732
|
+
const tokenProgram = tokenProgramOf(programs, mint);
|
|
607
733
|
const remaining = [
|
|
734
|
+
meta(idleRound(target, lottery), false, false),
|
|
608
735
|
meta(pda.pot(target), false, true),
|
|
609
|
-
meta(
|
|
610
|
-
meta(to ?? ata(owner,
|
|
611
|
-
meta(pda.lotteryFunds(target), false, true),
|
|
612
|
-
meta(paymentMint, false, true),
|
|
613
|
-
meta(paymentTo ?? ata(owner, paymentMint, tokenProgramOf(programs, paymentMint)), false, true),
|
|
736
|
+
meta(mint, false, true),
|
|
737
|
+
meta(to ?? ata(owner, mint, tokenProgram), false, true),
|
|
614
738
|
meta(TOKEN_PROGRAM, false, false),
|
|
615
739
|
meta(TOKEN_2022_PROGRAM, false, false),
|
|
616
|
-
...extra,
|
|
740
|
+
...tailOf(extra),
|
|
617
741
|
];
|
|
618
742
|
return lootIx('upsert', { def: null }, { owner, project, target }, { remaining });
|
|
619
743
|
}
|
|
@@ -630,7 +754,7 @@ export function payIx({ player, project, mint, treasury, amount, shares = [], re
|
|
|
630
754
|
'pay',
|
|
631
755
|
{ amount, shares, reference: reference(ref) },
|
|
632
756
|
{ player, project, mint, treasury, treasury_token: ata(treasury, mint, tokenProgram), player_token: playerToken ?? ata(player, mint, tokenProgram) },
|
|
633
|
-
{ remaining: [...shareAccounts(project, shares, mint, { holders, programs, treasury }), ...extra], writable: sharesBurn(shares) ? ['mint'] : [] },
|
|
757
|
+
{ remaining: [...shareAccounts(project, shares, mint, { holders, programs, treasury }), ...tailOf(extra)], writable: sharesBurn(shares) ? ['mint'] : [] },
|
|
634
758
|
);
|
|
635
759
|
}
|
|
636
760
|
|
|
@@ -640,16 +764,18 @@ export function payIx({ player, project, mint, treasury, amount, shares = [], re
|
|
|
640
764
|
* prizes take tokens from to its mint (a pool's tokens won wait in the project's vault for that mint); one not given
|
|
641
765
|
* is taken to be in the box's currency, one given as null is closed. `shown`: an NFT the player shows for the box's
|
|
642
766
|
* gate or odds; `listProof`: for a list gate, the proof's nodes. `chosenRecipient`: the wallet (or, for a share with an
|
|
643
|
-
* item, the collectible, `chosen`) the buyer names for a share the box lets them choose.
|
|
644
|
-
*
|
|
767
|
+
* item, the collectible, `chosen`) the buyer names for a share the box lets them choose. Every open asks the oracle,
|
|
768
|
+
* even of a box with nothing random (which sells in one step with `buyIx`, when it rolls on no NFT).
|
|
645
769
|
* 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
|
|
646
|
-
* limits each wallet counts the purchase in the player's
|
|
770
|
+
* limits each wallet counts the purchase in the player's counts (`pda.counts`, its rent theirs).
|
|
647
771
|
*/
|
|
648
|
-
export function openBoxIx({ player, project, box, boxAddress, nonce, maxPrice, shown, listProof, subject, chosenRecipient = null, chosen = null, poolMints, playerToken, programs, extra = [] }) {
|
|
772
|
+
export function openBoxIx({ player, project, box, boxAddress, nonce, maxPrice, count = 1, items = {}, shown, listProof, subject, chosenRecipient = null, chosen = null, settler = NOBODY_KEY, poolMints, playerToken, programs, extra = [] }) {
|
|
649
773
|
const mint = box.currency;
|
|
650
774
|
poolMints = boxPoolMints(box, poolMints);
|
|
651
775
|
const tokenProgram = tokenProgramOf(programs, mint);
|
|
652
|
-
|
|
776
|
+
// `count` opens in one purchase (a box that packs), signed for `maxPrice` (the price times `count` when not given:
|
|
777
|
+
// pass the sum for a box whose price rises with each sale); `items` maps an item a prize names to its decoded Item,
|
|
778
|
+
// for a fresh one's tables (a box whose prizes are all alike needs none).
|
|
653
779
|
const address = boxAddress ?? pda.box(project, box.box_id);
|
|
654
780
|
// A box that rolls on an NFT takes it (`subject`: { asset, collection, hasAuthority, tokenMetadata, compressed }, as
|
|
655
781
|
// `piece` takes it); any other, its own address stands in.
|
|
@@ -663,7 +789,7 @@ export function openBoxIx({ player, project, box, boxAddress, nonce, maxPrice, s
|
|
|
663
789
|
const gate = [...gateAccounts(project, { gate: box.gate, wallet: player, listProof }), ...(box.subject ? on.proof : shownAccounts(project, shown))];
|
|
664
790
|
return lootIx(
|
|
665
791
|
'open_box',
|
|
666
|
-
{ nonce, max_price: maxPrice ?? box.price, chosen_recipient: chosenRecipient, version: box.version },
|
|
792
|
+
{ nonce, max_price: maxPrice ?? box.price * BigInt(count), chosen_recipient: chosenRecipient, version: box.version, count, settler },
|
|
667
793
|
{
|
|
668
794
|
loot_box: address,
|
|
669
795
|
player,
|
|
@@ -671,56 +797,68 @@ export function openBoxIx({ player, project, box, boxAddress, nonce, maxPrice, s
|
|
|
671
797
|
mint,
|
|
672
798
|
player_token: playerToken ?? ata(player, mint, tokenProgram),
|
|
673
799
|
vault: vault(project, mint, tokenProgram),
|
|
674
|
-
oracle_queue: rolled ? undefined : null,
|
|
675
|
-
program_identity: rolled ? undefined : null,
|
|
676
|
-
vrf_program: rolled ? undefined : null,
|
|
677
|
-
slot_hashes: rolled ? undefined : null,
|
|
678
800
|
chosen,
|
|
679
|
-
|
|
801
|
+
counts: limits(box.per_wallet) ? pda.counts(project, player) : null,
|
|
680
802
|
...on.accounts,
|
|
681
803
|
},
|
|
682
|
-
{ remaining: [...entryAccounts(project, box.entries), ...prizeAccounts(project, box.entries, poolMints, programs), ...gate, ...extra], writable: on.writable },
|
|
804
|
+
{ remaining: [...entryAccounts(project, box.entries, items), ...prizeAccounts(project, box.entries, poolMints, programs), ...gate, ...tailOf(extra)], writable: on.writable },
|
|
683
805
|
);
|
|
684
806
|
}
|
|
685
807
|
|
|
686
808
|
/**
|
|
687
|
-
* Buys a box that needs no roll (one entry, nothing random, no NFT to roll on: `boxRolls` false and no
|
|
688
|
-
* instruction
|
|
689
|
-
*
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
809
|
+
* Buys `count` of a box that needs no roll (one entry, nothing random, no NFT to roll on: `boxRolls` false and no
|
|
810
|
+
* subject) in one instruction, any number of draws each (a fixed pack): each at the price the one before it left, all
|
|
811
|
+
* within `maxPrice` (the price times `count` when not given: pass the sum for a box whose price rises with each sale).
|
|
812
|
+
* The price is split at once (a share may burn it) and the entry's prizes are given to the player: a fixed amount of
|
|
813
|
+
* tokens once, the whole of every draw's, and every other prize once for each draw of each sale, its accounts repeated
|
|
814
|
+
* (`prizeMetas`: `items` maps an item's id to its decoded Item, `poolMints` a pool's index to its mint, `nfts` an NFT a
|
|
815
|
+
* prize names to its kind). No order. A prize that can't be given refuses the purchase. `treasury` is the project's
|
|
816
|
+
* treasury wallet; `holders` maps a collectible a share pays to its holder; each roll the entry gives is made at the
|
|
817
|
+
* next nonce from `nonce` (`buyRolls`). `shown`/`listProof` pass the box's gate.
|
|
694
818
|
*/
|
|
695
|
-
export function buyIx({ player, project, box, boxAddress, maxPrice, treasury, chosenRecipient = null, chosen = null, nonce = 0n, shown, listProof, items = {}, poolMints = {}, nfts = {},
|
|
819
|
+
export function buyIx({ player, project, box, boxAddress, maxPrice, count = 1, treasury, chosenRecipient = null, chosen = null, nonce = 0n, shown, listProof, items = {}, poolMints = {}, nfts = {}, holders, playerToken, programs, extra = [] }) {
|
|
696
820
|
const mint = box.currency;
|
|
697
821
|
const tokenProgram = tokenProgramOf(programs, mint);
|
|
698
822
|
const address = boxAddress ?? pda.box(project, box.box_id);
|
|
699
|
-
const [entry] = box.entries;
|
|
700
823
|
const shares = chosenShares(box.shares, chosenRecipient);
|
|
701
824
|
const made = {};
|
|
702
|
-
const prizes =
|
|
825
|
+
const prizes = buyGives(box, count).flatMap(({ prize, k }) => {
|
|
703
826
|
const item = prize.Item?.[0];
|
|
704
|
-
const lines = prizeMetas({ project, prize, winner: player, nonce: toBigInt(nonce) + BigInt(k), poolMints, items, nft: nfts[prize.Nft?.nft], made: made[item] ?? 0, projectMint: mint, programs
|
|
827
|
+
const lines = prizeMetas({ project, prize, winner: player, nonce: toBigInt(nonce) + BigInt(k), poolMints, items, nft: nfts[prize.Nft?.nft], made: made[item] ?? 0, projectMint: mint, programs });
|
|
705
828
|
if (item !== undefined) made[item] = (made[item] ?? 0) + 1;
|
|
706
829
|
return lines;
|
|
707
830
|
});
|
|
708
831
|
return lootIx(
|
|
709
832
|
'buy',
|
|
710
|
-
{ max_price: maxPrice ?? box.price, chosen_recipient: chosenRecipient, version: box.version, nonce: toBigInt(nonce) },
|
|
833
|
+
{ max_price: maxPrice ?? box.price * BigInt(count), chosen_recipient: chosenRecipient, version: box.version, nonce: toBigInt(nonce), count },
|
|
711
834
|
{
|
|
712
835
|
player, project, loot_box: address, mint,
|
|
713
836
|
player_token: playerToken ?? ata(player, mint, tokenProgram),
|
|
714
837
|
treasury, treasury_token: ata(treasury, mint, tokenProgram),
|
|
715
|
-
chosen,
|
|
838
|
+
chosen, counts: limits(box.per_wallet) ? pda.counts(project, player) : null,
|
|
716
839
|
},
|
|
717
840
|
{
|
|
718
|
-
remaining: [...gateAccounts(project, { gate: box.gate, wallet: player, shown, listProof }), ...shareAccounts(project, shares, mint, { holders, programs, treasury }), ...prizes, ...extra],
|
|
719
|
-
writable:
|
|
841
|
+
remaining: [...gateAccounts(project, { gate: box.gate, wallet: player, shown, listProof }), ...shareAccounts(project, shares, mint, { holders, programs, treasury }), ...prizes, ...tailOf(extra)],
|
|
842
|
+
writable: sharesBurn(box.shares) ? ['mint'] : [],
|
|
720
843
|
},
|
|
721
844
|
);
|
|
722
845
|
}
|
|
723
846
|
|
|
847
|
+
/**
|
|
848
|
+
* What a `buy` of `count` of `box` gives, in the order it gives it (`buy.rs`): each prize of its one entry in turn, a
|
|
849
|
+
* fixed amount of tokens once (`times` the box's draws × `count`), any other prize once for each of those draws. `k`
|
|
850
|
+
* counts the gives: a roll given at `k` is made at the buy's `nonce + k` (its order at `prizeRollNonce` of it).
|
|
851
|
+
*/
|
|
852
|
+
export function buyGives(box, count = 1) {
|
|
853
|
+
const draws = boxDraws(box) * count;
|
|
854
|
+
const out = [];
|
|
855
|
+
for (const prize of box.entries[0]?.prizes ?? []) {
|
|
856
|
+
const once = Boolean(prize.Tokens);
|
|
857
|
+
for (let i = 0; i < (once ? 1 : draws); i++) out.push({ prize, k: out.length, times: once ? draws : 1 });
|
|
858
|
+
}
|
|
859
|
+
return out;
|
|
860
|
+
}
|
|
861
|
+
|
|
724
862
|
/**
|
|
725
863
|
* The oracle's answer to a box order (for a local simulation: on chain MagicBlock's oracle sends it). The accounts
|
|
726
864
|
* are the ones `open_box` handed the oracle: the order, the box, the roll's ticket (named by its first ask,
|
|
@@ -729,38 +867,64 @@ export function buyIx({ player, project, box, boxAddress, maxPrice, treasury, ch
|
|
|
729
867
|
* for a prize in a mint with a transfer hook, the hook's accounts (`extra`). The buyer's odds boosts were fixed at
|
|
730
868
|
* purchase.
|
|
731
869
|
*/
|
|
732
|
-
export function callbackRollIx({ order, orderState, box, boxAddress, randomness, project, poolMints, programs, extra = [] }) {
|
|
870
|
+
export function callbackRollIx({ order, orderState, box, boxAddress, randomness, project, poolMints, items = {}, programs, extra = [] }) {
|
|
733
871
|
const projectAddress = project ?? orderState.project;
|
|
734
872
|
const keys = [
|
|
735
873
|
meta(pda.oracleIdentity(), true, false),
|
|
736
874
|
meta(order, false, true),
|
|
737
|
-
|
|
875
|
+
// Written: the order's draws leave the box's `in_flight`.
|
|
876
|
+
meta(boxAddress ?? orderState.loot_box, false, true),
|
|
738
877
|
meta(ticketOf(order, orderState.asked_first), false, false),
|
|
739
|
-
...entryAccounts(orderState.project, box.entries),
|
|
878
|
+
...entryAccounts(orderState.project, box.entries, items),
|
|
740
879
|
];
|
|
741
880
|
const prizes = prizeAccounts(orderState.project, box.entries, boxPoolMints(box, poolMints), programs);
|
|
742
881
|
if (prizes.length) keys.push(meta(TOKEN_PROGRAM, false, false), meta(TOKEN_2022_PROGRAM, false, false), meta(projectAddress, false, false), ...prizes);
|
|
743
|
-
keys.push(...extra);
|
|
882
|
+
keys.push(...tailOf(extra));
|
|
744
883
|
return { programId: LOOT_PROGRAM, keys, data: encodeInstruction(IDL, 'callback_roll', { randomness: Uint8Array.from(randomness) }) };
|
|
745
884
|
}
|
|
746
885
|
|
|
747
886
|
/**
|
|
748
887
|
* The accounts of what an order won (`order.won`), in order, as `settle` hands each over:
|
|
749
|
-
* Item the item; for a collectible (`items[id]` its decoded Item), its collection, its address, Metaplex Core
|
|
888
|
+
* Item the item; for a collectible (`items[id]` its decoded Item), its collection, its address, Metaplex Core;
|
|
889
|
+
* for a fresh one (`drawn` set), then the table of each of its table traits
|
|
750
890
|
* Tokens the mint, the project's vault for it, the player's account of it (`prizeTo`, when given)
|
|
751
891
|
* Nft the NFT as `piece` takes it (`nfts[nft]` its kind), going to the player, then a compressed NFT's proof
|
|
752
|
-
*
|
|
892
|
+
* Compressed the item, the tree's config, the tree, the collection, Bubblegum's programs; for a fresh one, then its
|
|
893
|
+
* tables and the Sheet of the NFT it mints
|
|
753
894
|
*/
|
|
754
|
-
export function wonAccounts({
|
|
895
|
+
export function wonAccounts({ order, items = {}, nfts = {}, prizeTo, putBack = [], programs, from = 0, upto = order.won.length, tree, treeMinted = 0n, unminted = [] }) {
|
|
755
896
|
const project = order.project;
|
|
756
|
-
|
|
897
|
+
// A compressed prize is minted into `tree` (any of the project's with room), the next NFT it mints: after
|
|
898
|
+
// `treeMinted`, and the compressed prizes before it in this part that were minted. One of an item whose collection
|
|
899
|
+
// lacks Bubblegum's plugin (`unminted`: those collections) gives nothing and mints nothing: the next takes its place.
|
|
900
|
+
let minted = BigInt(treeMinted);
|
|
901
|
+
const skips = new Set(unminted);
|
|
902
|
+
return order.won.slice(from, upto).flatMap((won) => {
|
|
903
|
+
if (won.Compressed) {
|
|
904
|
+
const { item: id, drawn } = won.Compressed;
|
|
905
|
+
const address = pda.item(project, id);
|
|
906
|
+
const item = items[id];
|
|
907
|
+
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((t) => meta(pda.table(project, t), false, false)) : [];
|
|
909
|
+
if (!tree) throw new Error('settling a compressed prize needs the project tree it is minted into (tree, treeMinted)');
|
|
910
|
+
const asset = pda.compressedAsset(tree, minted);
|
|
911
|
+
if (!skips.has(item.collection)) minted += 1n;
|
|
912
|
+
return [
|
|
913
|
+
meta(address, false, true), meta(pda.treeConfig(tree), false, true), meta(tree, false, true), meta(item.collection, false, true),
|
|
914
|
+
meta(BUBBLEGUM_PROGRAM, false, false), meta(COMPRESSION_PROGRAM, false, false), meta(NOOP_PROGRAM, false, false), meta(MPL_CORE_PROGRAM, false, false), meta(BUBBLEGUM_CORE_SIGNER, false, false),
|
|
915
|
+
// A fresh one's traits are written in the project's Sheet for the NFT it mints.
|
|
916
|
+
...(drawn != null ? [...tables, meta(pda.sheet(project, asset), false, true)] : []),
|
|
917
|
+
];
|
|
918
|
+
}
|
|
757
919
|
if (won.Item) {
|
|
758
|
-
const { item: id, serial } = won.Item;
|
|
920
|
+
const { item: id, serial, drawn } = won.Item;
|
|
759
921
|
const address = pda.item(project, id);
|
|
760
922
|
const item = items[id];
|
|
761
923
|
if (!item) throw new Error(`settling item ${id} needs the item (items), for its kind and collection`);
|
|
762
924
|
if (item.kind !== 'Collectible') return [meta(address, false, true)];
|
|
763
|
-
|
|
925
|
+
// A fresh unit's traits come from the rows its tables drew at the roll.
|
|
926
|
+
const tables = drawn != null ? traitTables(item).map((t) => meta(pda.table(project, t), false, false)) : [];
|
|
927
|
+
return [meta(address, false, true), meta(item.collection, false, true), meta(pda.itemAsset(address, serial), false, true), meta(MPL_CORE_PROGRAM, false, false), ...tables];
|
|
764
928
|
}
|
|
765
929
|
if (won.Tokens) {
|
|
766
930
|
const { mint } = won.Tokens;
|
|
@@ -774,14 +938,57 @@ export function wonAccounts({ orderAddress, order, items = {}, nfts = {}, prizeT
|
|
|
774
938
|
const p = piece({ project, asset, wallet: order.player, ...nfts[asset], ...ONLY_MOVES });
|
|
775
939
|
return [...pieceMetas(p), ...p.proof];
|
|
776
940
|
}
|
|
777
|
-
|
|
941
|
+
throw new Error(`settling an order: a prize of a kind this client doesn't know (${Object.keys(won)[0]})`);
|
|
778
942
|
});
|
|
779
943
|
}
|
|
780
944
|
|
|
945
|
+
/** The calls a transaction may make, its own instructions and every call inside them (Solana's instruction trace). */
|
|
946
|
+
export const TRACE_LIMIT = 64;
|
|
947
|
+
/** The calls a settle makes before its prizes: the compute budget's, its own, and the payment's at most. */
|
|
948
|
+
const SETTLE_BASE_CALLS = 6;
|
|
949
|
+
|
|
950
|
+
/** The calls one prize makes when a settle hands it over, measured on the program (`tests/limits.rs`): a collectible's
|
|
951
|
+
* mint by Core (4, so 14 a transaction), a compressed one's by Bubblegum (5) and with its Sheet (7), tokens (2), an NFT
|
|
952
|
+
* moved (3). `items` maps an item's id to its decoded Item (a normal item makes none). */
|
|
953
|
+
export function prizeCalls(won, items = {}) {
|
|
954
|
+
if (won.Item) return won.Item.drawn == null && items[won.Item.item]?.kind === 'Normal' ? 0 : 4;
|
|
955
|
+
if (won.Compressed) return won.Compressed.drawn != null ? 7 : 5;
|
|
956
|
+
if (won.Tokens) return 2;
|
|
957
|
+
return 3;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/** Where a settle part that starts at `from` ends: as many of the order's prizes as fit the instruction trace (an
|
|
961
|
+
* NFT's move asks for the bigger heap, so at most three a part). Each part is its own transaction; anyone sends any. */
|
|
962
|
+
export function settlePartEnd(order, from = Number(order.settled ?? 0), items = {}) {
|
|
963
|
+
let calls = SETTLE_BASE_CALLS;
|
|
964
|
+
let nfts = 0;
|
|
965
|
+
let end = from;
|
|
966
|
+
while (end < order.won.length) {
|
|
967
|
+
const won = order.won[end];
|
|
968
|
+
const next = prizeCalls(won, items);
|
|
969
|
+
if (end > from && (calls + next > TRACE_LIMIT || (won.Nft && nfts >= 3))) break;
|
|
970
|
+
calls += next;
|
|
971
|
+
nfts += won.Nft ? 1 : 0;
|
|
972
|
+
end++;
|
|
973
|
+
}
|
|
974
|
+
return end;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/** How many draws one open of `box` makes: its slots' counts, or one. */
|
|
978
|
+
export const boxDraws = (box) => (box.slots?.length ? box.slots.reduce((n, s) => n + Number(s.count), 0) : 1);
|
|
979
|
+
|
|
980
|
+
/** How many settle parts an order of `opens` opens of `box` takes at most: each card, at its most calls, and with
|
|
981
|
+
* shares one more (the first call splits the payment, alone when no prize fits beside its shares: `upto` 0). */
|
|
982
|
+
export function settleParts(box, opens = 1) {
|
|
983
|
+
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((e) => e.prizes.length)) * draws * Math.max(1, opens);
|
|
985
|
+
return Math.max(1, Math.ceil(cards / Math.floor((TRACE_LIMIT - SETTLE_BASE_CALLS) / 7))) + ((box.shares ?? []).length ? 1 : 0);
|
|
986
|
+
}
|
|
987
|
+
|
|
781
988
|
/** The heap a settle that moves three or more NFTs asks for (`settleHeapFrame`): all a transaction may have, 256 KiB. */
|
|
782
989
|
export const LARGE_SETTLE_HEAP = 256 * 1024;
|
|
783
990
|
|
|
784
|
-
/** Whether an order rolled on an NFT: its `subject` names one (
|
|
991
|
+
/** Whether an order rolled on an NFT: its `subject` names one (null for none). */
|
|
785
992
|
const rolledOn = (order) => Boolean(order.subject) && order.subject !== NO_ADDRESS;
|
|
786
993
|
|
|
787
994
|
/**
|
|
@@ -810,32 +1017,50 @@ export const MAX_COMPUTE_UNITS = 1_400_000;
|
|
|
810
1017
|
* them). A limit costs nothing more when no priority fee is set.
|
|
811
1018
|
*/
|
|
812
1019
|
export function settleComputeUnits(order) {
|
|
813
|
-
|
|
1020
|
+
// 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
|
+
// many cards takes all a transaction may have.
|
|
1022
|
+
const won = order.won ?? [];
|
|
1023
|
+
const moves = won.some((w) => w.Nft || w.Compressed) || won.length > 8 || rolledOn(order);
|
|
814
1024
|
return moves ? MAX_COMPUTE_UNITS : 800_000;
|
|
815
1025
|
}
|
|
816
1026
|
|
|
1027
|
+
/** `settle`'s `upto` for its last part: past the end of any order. The last part gives what is left, pays back the share
|
|
1028
|
+
* of the draws the roll didn't make, carries out the roll on an NFT and closes the order. */
|
|
1029
|
+
export const SETTLE_LAST = 4_294_967_295;
|
|
1030
|
+
|
|
817
1031
|
/**
|
|
818
1032
|
* Carries out a rolled order: hands over what its roll set aside (`wonAccounts`: a collectible minted, the project's
|
|
819
|
-
* backend delivering a normal item from the receipt; tokens from the project's vault; an NFT taken out of its stash
|
|
820
|
-
* a
|
|
821
|
-
*
|
|
822
|
-
* is the decoded Order; `projectState` the decoded Project; `items` and `nfts` as
|
|
823
|
-
* NFTs won that the player, signing, puts back into their stash in place of taking
|
|
824
|
-
* `handBack`: the wallet the box's fate gives the NFT rolled on to
|
|
825
|
-
* (`handsBack` of it as read), so the program gives it back to the
|
|
826
|
-
* player's. `extra`: a hook's or a memo's accounts.
|
|
827
|
-
|
|
828
|
-
|
|
1033
|
+
* backend delivering a normal item from the receipt; tokens from the project's vault; an NFT taken out of its stash),
|
|
1034
|
+
* then splits the payment (a share may burn it). The order's player, its settler or the project's keys send it, for the order at `orderAddress`; `signer` pays all
|
|
1035
|
+
* the settle opens or mints (Core assets, token accounts, Sheets, a stash's row, the fate's writes: the order lends
|
|
1036
|
+
* nothing, `settleRent`). `order` is the decoded Order; `projectState` the decoded Project; `items` and `nfts` as
|
|
1037
|
+
* `wonAccounts` takes them. `putBack`: NFTs won that the player, signing, puts back into their stash in place of taking
|
|
1038
|
+
* them (one its collection won't let move). `handBack`: the wallet the box's fate gives the NFT rolled on to
|
|
1039
|
+
* (`receiverOf`) is an account of the loot program (`handsBack` of it as read), so the program gives it back to the
|
|
1040
|
+
* player, and a Token Metadata NFT's `token` is the player's. `extra`: a hook's or a memo's accounts.
|
|
1041
|
+
*
|
|
1042
|
+
* `upto`, the part (`settle.rs`): `SETTLE_LAST` (the default, past the end) is the last part, all that is left (maybe
|
|
1043
|
+
* nothing); 0 gives no prize and only splits the payment; any other gives the prizes from where the last part stopped
|
|
1044
|
+
* (`settled`) up to it, and nothing more. The first call, whichever it is, splits the payment (its shares' accounts
|
|
1045
|
+
* ride on it).
|
|
1046
|
+
*/
|
|
1047
|
+
export function settleIx({ signer, orderAddress, order, projectState, items, nfts, prizeTo, putBack, subject, handBack = false, holders, programs, upto = SETTLE_LAST, tree, treeMinted, unminted, extra = [] }) {
|
|
829
1048
|
const project = order.project;
|
|
1049
|
+
const from = Number(order.settled ?? 0);
|
|
1050
|
+
const last = Number(upto) > order.won.length;
|
|
1051
|
+
const end = last ? order.won.length : Math.max(from, Number(upto));
|
|
1052
|
+
// The payment is split once, by the first call (`drew` is 0 once it is made).
|
|
1053
|
+
const splits = Number(order.drew ?? 0) > 0;
|
|
1054
|
+
const prizes = wonAccounts({ order, items, nfts, prizeTo, putBack, programs, from, upto: end, tree, treeMinted, unminted });
|
|
830
1055
|
const tokenProgram = tokenProgramOf(programs, order.mint);
|
|
831
|
-
const burns = order.burns || sharesBurn(order.shares);
|
|
832
1056
|
// The NFT it rolled on, as the settle moves it: given to a wallet that is an account of the loot program, it goes
|
|
833
1057
|
// back to its player (`handsBack`), into the player's token account for a Token Metadata NFT.
|
|
834
|
-
|
|
835
|
-
const
|
|
1058
|
+
// A part before the last gives only its prizes (and the first, the split): no NFT rolled on (its piece a stand-in).
|
|
1059
|
+
const receiver = last ? receiverOf(order) : null;
|
|
1060
|
+
const on = last ? orderPiece(project, orderAddress, order, subject, handBack && order.fate?.then?.Give ? order.player : receiver) : standIn(orderAddress);
|
|
836
1061
|
return lootIx(
|
|
837
1062
|
'settle',
|
|
838
|
-
{},
|
|
1063
|
+
{ upto: Number(upto), prize_accounts: prizes.length },
|
|
839
1064
|
{
|
|
840
1065
|
signer,
|
|
841
1066
|
order: orderAddress,
|
|
@@ -847,10 +1072,12 @@ export function settleIx({ signer, orderAddress, order, projectState, items, nft
|
|
|
847
1072
|
treasury_token: ata(projectState.treasury, order.mint, tokenProgram),
|
|
848
1073
|
...on.accounts,
|
|
849
1074
|
receiver,
|
|
1075
|
+
// The last part of an order whose roll left a draw unmade or part given gives its share of the payment back here.
|
|
1076
|
+
player_token: BigInt(order.back ?? 0) > 0n && last ? ata(order.player, order.mint, tokenProgram) : null,
|
|
850
1077
|
},
|
|
851
1078
|
{
|
|
852
|
-
remaining: [...on.proof, ...
|
|
853
|
-
writable: [...(
|
|
1079
|
+
remaining: [...on.proof, ...prizes, ...(splits ? shareAccounts(project, order.shares, order.mint, { holders, programs, treasury: projectState.treasury }) : []), ...tailOf(extra)],
|
|
1080
|
+
writable: [...(sharesBurn(order.shares) && splits ? ['mint'] : []), ...on.writable],
|
|
854
1081
|
},
|
|
855
1082
|
);
|
|
856
1083
|
}
|
|
@@ -861,10 +1088,10 @@ export function settleIx({ signer, orderAddress, order, projectState, items, nft
|
|
|
861
1088
|
*/
|
|
862
1089
|
export function receiverOf(order) {
|
|
863
1090
|
const then = order.fate?.then;
|
|
864
|
-
if (!order.held ||
|
|
1091
|
+
if (!order.held || then == null) return null;
|
|
865
1092
|
if (then.Give) return then.Give[0];
|
|
866
1093
|
if (then.Stash) return pda.stash(order.project, then.Stash[0]);
|
|
867
|
-
return then === 'Burn'
|
|
1094
|
+
return then === 'Burn' ? INCINERATOR : null;
|
|
868
1095
|
}
|
|
869
1096
|
|
|
870
1097
|
/**
|
|
@@ -878,7 +1105,7 @@ function orderPiece(project, orderAddress, order, subject = {}, receiver) {
|
|
|
878
1105
|
// a stash keeps it where it is.
|
|
879
1106
|
const shifts = Boolean(order.fate?.shifts?.length);
|
|
880
1107
|
const then = order.fate?.then;
|
|
881
|
-
const burned = order.held &&
|
|
1108
|
+
const burned = order.held && then === 'Burn';
|
|
882
1109
|
const stays = Boolean(then?.Stash);
|
|
883
1110
|
return piece({ project, asset: order.subject, wallet: stays ? order.player : receiver ?? order.player, ...subject, moves: order.held && !stays, locks: false, writes: shifts, burns: burned, reads: shifts });
|
|
884
1111
|
}
|
|
@@ -897,8 +1124,9 @@ export const INCINERATOR = '1nc1nerator11111111111111111111111111111111';
|
|
|
897
1124
|
/**
|
|
898
1125
|
* Gives an order's payment back to its player, into their account of the mint (`signer`, anyone, opens it when it is
|
|
899
1126
|
* not there): an order whose roll failed or never came. A rolled order is settled, never refunded
|
|
900
|
-
* (`
|
|
901
|
-
* wallet is given back.
|
|
1127
|
+
* (`orderRefundable`). A purchase counted against a box's limit for each
|
|
1128
|
+
* wallet is given back. A purchase that counts its draws in its box's `in_flight` (`order.reserved`, a box that stops
|
|
1129
|
+
* selling when an entry runs out) names the box, whose count it leaves. `extra`: a hook's or a memo's accounts.
|
|
902
1130
|
*/
|
|
903
1131
|
export function refundIx({ signer, orderAddress, order, subject, programs, extra = [] }) {
|
|
904
1132
|
const project = order.project;
|
|
@@ -918,129 +1146,186 @@ export function refundIx({ signer, orderAddress, order, subject, programs, extra
|
|
|
918
1146
|
mint: order.mint,
|
|
919
1147
|
vault: vault(project, order.mint, tokenProgram),
|
|
920
1148
|
player_token: ata(order.player, order.mint, tokenProgram),
|
|
921
|
-
|
|
1149
|
+
counts: order.counted != null ? pda.counts(project, order.player) : null,
|
|
1150
|
+
// Required while the order counts draws in its box (`reserved`); an order read without the field names it too.
|
|
1151
|
+
loot_box: order.reserved == null || BigInt(order.reserved) > 0n ? order.loot_box : null,
|
|
922
1152
|
...on.accounts,
|
|
923
1153
|
},
|
|
924
|
-
{ remaining: [...on.proof, ...extra], writable: on.writable },
|
|
1154
|
+
{ remaining: [...on.proof, ...tailOf(extra)], writable: on.writable },
|
|
925
1155
|
);
|
|
926
1156
|
}
|
|
927
1157
|
|
|
928
1158
|
/**
|
|
929
|
-
*
|
|
930
|
-
* Waiting): the box's
|
|
931
|
-
*
|
|
932
|
-
*
|
|
1159
|
+
* Its player or the project's keys (`payer`, who pays the oracle) ask for the roll a mission's return, a recipe or a prize made (`order.status`
|
|
1160
|
+
* Waiting): the box's roll accounts, as `openBoxIx` passes them (`items` maps an item a prize names to its decoded
|
|
1161
|
+
* Item, for a fresh one's tables). It lands in `callback_roll`. Returns a list of instructions: first `payer` opens the
|
|
1162
|
+
* project's vault for each prize mint when it is not there (idempotent; the program refuses the ask without them,
|
|
1163
|
+
* since tokens won wait there), then the ask. A box with nothing in stock is refused (`SoldOut`) unless the order's own
|
|
1164
|
+
* player asks in a later slot than the order was made or last asked in: a route that makes a roll and asks for it in
|
|
1165
|
+
* the same transaction fails whole on an empty box, and the player keeps what the route took. The player's own ask on
|
|
1166
|
+
* an empty box ends the roll there, with no oracle: rolled with nothing (an NFT it holds goes home as it was), or failed
|
|
1167
|
+
* for its refund when its winner paid for it (an auction's roll lot). An answer that finds nothing at all in stock puts
|
|
1168
|
+
* a free roll back to waiting (`Waiting`), for a restock or for its player to end it so. Once asked, every later ask
|
|
1169
|
+
* comes in a later slot than the last (`SameSlot`, `rollAskable`).
|
|
933
1170
|
*/
|
|
934
|
-
export function askRollIx({ payer, orderAddress, order, box, poolMints, programs }) {
|
|
1171
|
+
export function askRollIx({ payer, orderAddress, order, box, settler = NOBODY_KEY, poolMints, items = {}, programs }) {
|
|
935
1172
|
poolMints = boxPoolMints(box, poolMints);
|
|
936
1173
|
const opens = prizeMints(box.entries, poolMints).map(({ mint }) => createAssociatedTokenAccountIdempotent(payer, order.project, mint, tokenProgramOf(programs, mint)));
|
|
937
|
-
const ask = lootIx('ask_roll', {}, { payer, order: orderAddress, loot_box: order.loot_box }, {
|
|
938
|
-
remaining: [...entryAccounts(order.project, box.entries), ...prizeAccounts(order.project, box.entries, poolMints, programs)],
|
|
1174
|
+
const ask = lootIx('ask_roll', { settler }, { payer, order: orderAddress, loot_box: order.loot_box, project: order.project }, {
|
|
1175
|
+
remaining: [...entryAccounts(order.project, box.entries, items), ...prizeAccounts(order.project, box.entries, poolMints, programs)],
|
|
939
1176
|
});
|
|
940
1177
|
return [...opens, ask];
|
|
941
1178
|
}
|
|
942
1179
|
|
|
943
|
-
/** The SOL a box order holds for each collectible its roll may mint (`orderEscrow`): its rent, given back unspent. The
|
|
944
|
-
* IDL's `ESCROW_LAMPORTS`. */
|
|
945
|
-
export const ESCROW_LAMPORTS = BigInt(IDL.constants.find((c) => c.name === 'ESCROW_LAMPORTS').value);
|
|
946
|
-
|
|
947
1180
|
// ---------------------------------------------------------------- out: the project's tokens move
|
|
948
1181
|
|
|
949
1182
|
/**
|
|
950
|
-
* The authority moves the project's tokens: `from` { Pool: [i] }
|
|
951
|
-
*
|
|
952
|
-
*
|
|
953
|
-
* collectible
|
|
1183
|
+
* The authority moves the project's tokens: `from` { Pool: [i] }, { Mint: [mint] } (newly minted) or { Farm: [id] } (what
|
|
1184
|
+
* that farm's rewards vault holds free, beyond what it owes its seats: a fixed amount; `farm`: the farm decoded, so its
|
|
1185
|
+
* record of ends comes when it keeps one), `to` a pool, a wallet, an account, the treasury (`treasury`, the project's
|
|
1186
|
+
* wallet), a collectible's holder or 'Burn', `amount` { Fixed: [n] } or { Bps: [bps] } of a pool. The authority opens the
|
|
1187
|
+
* account it pays into when it is not there; a collectible nobody holds gets nothing.
|
|
954
1188
|
*/
|
|
955
|
-
export function moveTokensIx({ authority, project, mint, from, to, amount, reference: ref, holders, programs, treasury, extra = [] }) {
|
|
1189
|
+
export function moveTokensIx({ authority, project, mint, from, to, amount, reference: ref, holders, programs, treasury, farm = null, extra = [] }) {
|
|
956
1190
|
const writes = Boolean(from.Mint) || to === 'Burn';
|
|
957
1191
|
return lootIx(
|
|
958
1192
|
'move_tokens',
|
|
959
1193
|
{ from, to, amount, reference: reference(ref) },
|
|
960
|
-
|
|
961
|
-
{
|
|
1194
|
+
// What a farm holds free comes out of its rewards vault, passed as the pool, with the farm.
|
|
1195
|
+
{ authority, project, mint, pool: from.Pool ? pda.pool(project, from.Pool[0]) : from.Farm ? pda.rewards(pda.farm(project, from.Farm[0])) : null, farm: from.Farm ? pda.farm(project, from.Farm[0]) : null, farm_ends: from.Farm && farm?.keeps_ends ? pda.ends(pda.farm(project, from.Farm[0])) : null },
|
|
1196
|
+
{ remaining: [...destinationAccounts(project, to, mint, { holders, programs, treasury }), ...tailOf(extra)], writable: writes ? ['mint'] : [] },
|
|
962
1197
|
);
|
|
963
1198
|
}
|
|
964
1199
|
|
|
965
1200
|
// ---------------------------------------------------------------- make: collectibles
|
|
966
1201
|
|
|
967
1202
|
/**
|
|
968
|
-
* The authority
|
|
969
|
-
*
|
|
970
|
-
*
|
|
971
|
-
* NFT the project has no authority over (a Core asset of another collection, or a Token Metadata NFT's mint):
|
|
972
|
-
* writes goes in the NFT's Sheet, or on the NFT itself when its Attributes plugin names the project.
|
|
1203
|
+
* The authority rewrites or burns a collectible, or writes a wallet's profile. `change`: { Set: [attributes] }, 'Burn'
|
|
1204
|
+
* (`owner`: whoever holds it, who gets its rent) or { Profile: [attributes] } (`asset` the wallet). `collection`: for a
|
|
1205
|
+
* Burn the project's; for a Set the asset's own (`MPL_CORE_PROGRAM` for one in none). `hasAuthority` false for a `Set`
|
|
1206
|
+
* on an NFT the project has no authority over (a Core asset of another collection, or a Token Metadata NFT's mint):
|
|
1207
|
+
* what it writes goes in the NFT's Sheet, or on the NFT itself when its Attributes plugin names the project. A `Make`
|
|
1208
|
+
* goes to `makeIx`.
|
|
973
1209
|
*/
|
|
974
|
-
export function collectibleIx({ authority, project, collection, asset, change,
|
|
975
|
-
|
|
976
|
-
const sheet = change.Set && !hasAuthority ? pda.sheet(project, asset) : null;
|
|
1210
|
+
export function collectibleIx({ authority, project, collection, asset, change, owner = null, hasAuthority = true, reference: ref, ...make }) {
|
|
1211
|
+
if (change.Make) return makeIx({ authority, project, ...change.Make, ...make, owners: make.owners ?? [owner], reference: ref });
|
|
1212
|
+
const sheet = change.Set && !hasAuthority ? pda.sheet(project, asset) : change.Profile ? pda.profile(project, asset) : null;
|
|
1213
|
+
const burn = change === 'Burn' || Boolean(change.Burn);
|
|
977
1214
|
return lootIx(
|
|
978
1215
|
'collectible',
|
|
979
1216
|
{ change, reference: reference(ref) },
|
|
980
|
-
{ authority, project, collection, asset,
|
|
981
|
-
{
|
|
1217
|
+
{ authority, project, collection, asset, owner: burn ? owner : null, sheet },
|
|
1218
|
+
{ writable: change.Set || change.Profile || collection === MPL_CORE_PROGRAM ? [] : ['collection'] },
|
|
1219
|
+
);
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
/**
|
|
1223
|
+
* The authority makes one of item `item` (`itemState`: its decoded Item, for its collection and next serial) for each
|
|
1224
|
+
* of `owners`, in serial order (an owner may come more than once), with `attributes` written over the program's own: a
|
|
1225
|
+
* collectible at `pda.itemAsset(item, serial)` (`madeAssets` says which), or with `tree`, a compressed NFT minted into
|
|
1226
|
+
* that tree of the project's (no attributes: they go in its Sheet). A switched-off item still makes; one sold out
|
|
1227
|
+
* refuses the whole call (`PrizeNotCovered`).
|
|
1228
|
+
*/
|
|
1229
|
+
export function makeIx({ authority, project, item, itemState, owners, attributes = [], tree = null, reference: ref }) {
|
|
1230
|
+
if (!itemState) throw new Error(`a make of item ${item} needs the item (itemState: its decoded Item), for its collection and next serial`);
|
|
1231
|
+
if (!owners?.length || owners.some((o) => !o)) throw new Error('A make names who gets each one (`owners`).');
|
|
1232
|
+
const prize = tree ? { Compressed: { item, tree } } : { Item: [item] };
|
|
1233
|
+
const remaining = owners.flatMap((owner, made) => [meta(owner, false, false), ...prizeMetas({ project, prize, winner: owner, items: { [item]: itemState }, made })]);
|
|
1234
|
+
return lootIx(
|
|
1235
|
+
'collectible',
|
|
1236
|
+
{ change: { Make: { item, tree, attributes } }, reference: reference(ref) },
|
|
1237
|
+
{ authority, project, collection: itemState.collection, asset: pda.item(project, item), owner: null, sheet: null },
|
|
1238
|
+
{ remaining, writable: ['collection'] },
|
|
982
1239
|
);
|
|
983
1240
|
}
|
|
984
1241
|
|
|
1242
|
+
/** The collectibles a `makeIx` of `count` mints (not compressed): at the item's next serials. */
|
|
1243
|
+
export const madeAssets = (project, item, itemState, count = 1) => Array.from({ length: count }, (_, k) => pda.itemAsset(pda.item(project, item), Number(itemState.issued) + 1 + k));
|
|
1244
|
+
|
|
985
1245
|
// ---------------------------------------------------------------- decide
|
|
986
1246
|
|
|
987
1247
|
/**
|
|
988
1248
|
* A held NFT goes to `to`: the authority's choice while the hold runs, home to its player (anyone) after; a repaid
|
|
989
1249
|
* loan's home (anyone, whenever), an unpaid one's to its lender once due. `hold` is the
|
|
990
1250
|
* decoded Hold: its address names its end (`pda.hold(asset, free_at)`), so a release sent late never lands on a later
|
|
991
|
-
* hold
|
|
1251
|
+
* hold; `address` names it when its `free_at` changed since (a repaid loan's is 0, at the address its loan named). The
|
|
1252
|
+
* NFT is any kind
|
|
992
1253
|
* (`piece`): `asset` and `collection` for a Core asset, with `tokenMetadata` for a Token Metadata NFT, or `compressed`
|
|
993
1254
|
* (after a `proveLeafIx`).
|
|
994
1255
|
*/
|
|
995
|
-
export function releaseIx({ signer, project, collection, asset, tokenMetadata: tm, compressed, hold, to, reference: ref }) {
|
|
996
|
-
|
|
1256
|
+
export function releaseIx({ signer, project, collection, asset, tokenMetadata: tm, compressed, hold, address = null, to, treasury, reference: ref, more = [] }) {
|
|
1257
|
+
// A hold whose player is the project's own address is owed to the treasury, as the project names it now (`treasury`,
|
|
1258
|
+
// the decoded Project's).
|
|
1259
|
+
if (!to && hold.player === project && !treasury) throw new Error('This hold is owed to the treasury: pass treasury (the project’s, as it is now).');
|
|
1260
|
+
const receiver = to ?? (hold.player === project ? treasury : hold.player);
|
|
997
1261
|
// Never to the project's own address, nor any account of the program, where nothing could move it again (ToTheProject).
|
|
998
1262
|
if (receiver === project) throw new Error('A held NFT can’t go to the project’s own address: name a wallet.');
|
|
999
|
-
|
|
1000
|
-
|
|
1263
|
+
// Several holds of one player's with one end, payer and rent wallet (`more`: each { asset, collection, tokenMetadata,
|
|
1264
|
+
// compressed }) go to one `to` together, in ascending order of their holds' addresses.
|
|
1265
|
+
const [first, ...rest] = ascending([{ asset, collection, tokenMetadata: tm, compressed, address }, ...more].map((n) => {
|
|
1266
|
+
const p = piece({ project, asset: n.asset, wallet: receiver, collection: n.collection, tokenMetadata: n.tokenMetadata, compressed: n.compressed, ...ONLY_MOVES });
|
|
1267
|
+
return { p, hold: n.address ?? pda.hold(p.accounts.asset, hold.free_at) };
|
|
1268
|
+
}), (x) => x.hold);
|
|
1269
|
+
const p = withPrograms(first.p, rest.map((x) => x.p));
|
|
1270
|
+
const groups = rest.flatMap((x) => [meta(x.hold, false, true), ...anotherMetas(x.p)]);
|
|
1271
|
+
// 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('release', { reference: reference(ref) }, { signer, project, ...p.accounts, hold: first.hold, payer: hold.payer, rent_to: hold.rent_to, to: receiver }, { remaining: [...p.proof, ...groups], writable: p.writable });
|
|
1001
1273
|
}
|
|
1002
1274
|
|
|
1003
1275
|
/** The mint of a contest's split prize (its `prize`, tokens): minted, or a pool's (`poolMint`). */
|
|
1004
1276
|
const splitMint = (prize, poolMint) => prize?.Tokens?.from.Mint?.[0] ?? poolMint;
|
|
1005
1277
|
|
|
1006
1278
|
/**
|
|
1007
|
-
* The authority opens contest `id` in `mint
|
|
1008
|
-
*
|
|
1009
|
-
*
|
|
1010
|
-
*
|
|
1011
|
-
*
|
|
1279
|
+
* The authority opens contest `id` in `mint` (`projectState`: the decoded Project, whose next incarnation names the
|
|
1280
|
+
* contest's list of entries). `args`: { sides, closes_at, result_by, shares, gate, deciders, min_pot, stake, prize, nft,
|
|
1281
|
+
* prize_split, cap }: `prize`, what each winning entry gets with its first claim (`prizeMetas`); `nft`, a rule an NFT
|
|
1282
|
+
* each entry stakes passes, in place of tokens (no shares then, no stake: the NFTs are the pot), each winner sharing the
|
|
1283
|
+
* losers' NFTs, at most `cap` each (null: no cap). With `args.prize_split`, its token prize is set aside whole when it
|
|
1284
|
+
* is decided, in the contest's own vault (`pda.prizeVault`, opened now at the authority's rent), and each winning entry
|
|
1012
1285
|
* takes its part by what it put in: `prizeMint` names the prize's mint when it comes from a pool.
|
|
1013
1286
|
*/
|
|
1014
|
-
export const openContestIx = ({ authority, project, id, mint, args, prizeMint }) => {
|
|
1287
|
+
export const openContestIx = ({ authority, project, projectState, id, mint, args, prizeMint }) => {
|
|
1288
|
+
stored(args.shares);
|
|
1015
1289
|
const contest = pda.contest(project, id);
|
|
1016
1290
|
const split = Boolean(args.prize_split);
|
|
1017
|
-
return lootIx('open_contest', { id, args: { deciders: [], min_pot: 0, stake: 0, prize: null, nft: null, prize_split: false, ...args, gate: args.gate ?? null } }, {
|
|
1291
|
+
return lootIx('open_contest', { id, args: { deciders: [], min_pot: 0, stake: 0, prize: null, nft: null, prize_split: false, cap: null, ...args, gate: args.gate ?? null } }, {
|
|
1018
1292
|
authority, project, mint, contest,
|
|
1019
1293
|
prize_mint: split ? splitMint(args.prize, prizeMint) : null, prize_vault: split ? pda.prizeVault(contest) : null,
|
|
1294
|
+
entries: pda.entries(contest, BigInt(projectState.incarnations) + 1n),
|
|
1295
|
+
// A split prize from a pool names the pool at its index (`WrongPool` without it; a prize mint not the pool's is
|
|
1296
|
+
// `WrongMint`).
|
|
1297
|
+
prize_pool: split && args.prize?.Tokens?.from?.Pool ? pda.pool(project, args.prize.Tokens.from.Pool[0]) : null,
|
|
1298
|
+
}, {
|
|
1299
|
+
// A prize that is a roll: its box.
|
|
1300
|
+
remaining: args.prize?.Roll ? boxesAccounts(project, [args.prize.Roll[0]]) : [],
|
|
1020
1301
|
});
|
|
1021
1302
|
};
|
|
1022
1303
|
|
|
1304
|
+
/** A contest's list of entries, from its decoded Contest. */
|
|
1305
|
+
export const contestEntries = (contestAddress, contest) => pda.entries(contestAddress, contest.incarnation);
|
|
1306
|
+
|
|
1023
1307
|
/** The prefix of a contest's verdict (`verdictBytes`). */
|
|
1024
|
-
export const VERDICT_DOMAIN = 'loot-verdict-
|
|
1308
|
+
export const VERDICT_DOMAIN = 'loot-verdict-v4';
|
|
1025
1309
|
|
|
1026
1310
|
/**
|
|
1027
|
-
* The bytes a contest's decider key signs: "loot-verdict-
|
|
1028
|
-
* side (
|
|
1029
|
-
* contest's chosen share pays. `incarnation` is the contest's (the decoded Contest's), so a verdict decides only
|
|
1030
|
-
* contest it was signed for, never one opened later at the same id. `decide` rebuilds them from its own accounts
|
|
1031
|
-
* arguments.
|
|
1311
|
+
* The bytes a contest's decider key signs: "loot-verdict-v4" | sha256(program (32) | contest (32) | incarnation (u64
|
|
1312
|
+
* LE) | side (u16 LE; 65535 calls it off) | expires (i64 LE), then | recipient (32) when the verdict names the wallet
|
|
1313
|
+
* the contest's chosen share pays). `incarnation` is the contest's (the decoded Contest's), so a verdict decides only
|
|
1314
|
+
* the contest it was signed for, never one opened later at the same id. `decide` rebuilds them from its own accounts
|
|
1315
|
+
* and arguments.
|
|
1032
1316
|
*/
|
|
1033
1317
|
export function verdictBytes({ program = LOOT_PROGRAM, contest, incarnation, side, expires, recipient = null }) {
|
|
1034
1318
|
if (incarnation == null) throw new Error('A verdict signs the contest’s incarnation (the decoded Contest’s).');
|
|
1035
1319
|
const named = recipient == null ? new Uint8Array(0) : addressBytes(recipient);
|
|
1036
|
-
return concat(utf8(VERDICT_DOMAIN), addressBytes(program), addressBytes(contest), uintLE(incarnation, 8), uintLE(side ??
|
|
1320
|
+
return concat(utf8(VERDICT_DOMAIN), sha256(concat(addressBytes(program), addressBytes(contest), uintLE(incarnation, 8), uintLE(side ?? 0xffff, 2), intLE(expires, 8), named)));
|
|
1037
1321
|
}
|
|
1038
1322
|
|
|
1039
1323
|
/**
|
|
1040
1324
|
* Names the side that won (`side`), or calls the contest off (`side: null`). `signer` is the authority; or anyone
|
|
1041
1325
|
* sending a decider's verdict (`verdict: { expires }`, taken once entries have closed): then this instruction goes
|
|
1042
1326
|
* right after the Ed25519 check of the decider's signature over `verdictBytes` (`ed25519Ix` in grant.js), and
|
|
1043
|
-
* `signer` pays the fee
|
|
1327
|
+
* `signer` pays the fee, as the authority does. A contest that is off by itself (overdue) needs no decision, and takes
|
|
1328
|
+
* none (`ResultOverdue`): `claim` closes it. The contest's incarnation names it, so a
|
|
1044
1329
|
* decision sent late never decides a contest opened later at the same id. A call-off takes no share accounts. A
|
|
1045
1330
|
* contest with a chosen share is decided naming its `recipient`, a wallet (a bounty's worker); a decider's verdict
|
|
1046
1331
|
* signs it too (`verdictBytes`).
|
|
@@ -1049,23 +1334,22 @@ export function decideIx({ signer, project, projectState, contestAddress, contes
|
|
|
1049
1334
|
const address = contestAddress ?? pda.contest(project, contest.id);
|
|
1050
1335
|
const named = recipient == null ? contest.shares : chosenShares(contest.shares, recipient);
|
|
1051
1336
|
const shares = side == null ? [] : shareAccounts(project, named, contest.mint, { holders, programs, treasury: projectState?.treasury });
|
|
1052
|
-
// A split prize is set aside when a side is named (minted, or from its pool)
|
|
1053
|
-
//
|
|
1337
|
+
// A split prize is set aside when a side is named (minted, or from its pool). A decision never closes the contest:
|
|
1338
|
+
// `claim` does, entered or not.
|
|
1054
1339
|
const split = Boolean(contest.prize_split);
|
|
1055
1340
|
const from = contest.prize?.Tokens?.from;
|
|
1056
|
-
const closes = side == null && Number(contest.entries) === 0;
|
|
1057
1341
|
return lootIx(
|
|
1058
1342
|
'decide',
|
|
1059
1343
|
{ side: side ?? null, expires: toBigInt(verdict?.expires ?? 0), reference: reference(ref), incarnation: contest.incarnation, recipient },
|
|
1060
1344
|
{
|
|
1061
|
-
signer, project, contest: address,
|
|
1345
|
+
signer, project, contest: address, mint: contest.mint, instructions: verdict ? SYSVAR_INSTRUCTIONS : null,
|
|
1062
1346
|
prize_mint: split ? splitMint(contest.prize, prizeMint) : null,
|
|
1347
|
+
// Its pool: the prize set aside from it when a side is named.
|
|
1063
1348
|
prize_pool: split && side != null && from?.Pool ? pda.pool(project, from.Pool[0]) : null,
|
|
1064
1349
|
prize_vault: split ? pda.prizeVault(address) : null,
|
|
1065
1350
|
},
|
|
1066
|
-
// The mint is written by a burn share
|
|
1067
|
-
|
|
1068
|
-
{ remaining: [...shares, ...extra], writable: [...((side == null ? closes : sharesBurn(contest.shares)) ? ['mint'] : []), ...(split && ((side != null && from?.Mint) || closes) ? ['prize_mint'] : [])] },
|
|
1351
|
+
// The mint is written by a burn share; the prize's mint when it is minted.
|
|
1352
|
+
{ remaining: [...shares, ...tailOf(extra)], writable: [...(side != null && sharesBurn(contest.shares) ? ['mint'] : []), ...(split && side != null && from?.Mint ? ['prize_mint'] : [])] },
|
|
1069
1353
|
);
|
|
1070
1354
|
}
|
|
1071
1355
|
|
|
@@ -1082,170 +1366,383 @@ export const callbackRandomIx = ({ project, reference: ref, randomness }) => ({
|
|
|
1082
1366
|
// ---------------------------------------------------------------- schedules and lotteries
|
|
1083
1367
|
|
|
1084
1368
|
/**
|
|
1085
|
-
*
|
|
1369
|
+
* The project's keys or who it pays (`payer`; a sealed burn: anyone) run a schedule that is due, and open the account it
|
|
1370
|
+
* pays into when that is not there. A run missed is missed: it pays once, for the latest run due, and the runs before it
|
|
1371
|
+
* lapse; only a sealed schedule's fixed amount pays every run due, up to `upto` runs made (`scheduleCatchesUp`).
|
|
1086
1372
|
* `poolMint`: the mint of the pool it moves from (when it moves from one). `extra`: a hook's or a memo's accounts.
|
|
1087
1373
|
*/
|
|
1088
|
-
export function runScheduleIx({ payer, project, id, schedule, poolMint, holders, programs, treasury, extra = [] }) {
|
|
1374
|
+
export function runScheduleIx({ payer, project, id, schedule, poolMint, holders, programs, treasury, upto = 0, extra = [] }) {
|
|
1089
1375
|
const { from, to } = schedule.action;
|
|
1090
1376
|
const address = pda.schedule(project, id);
|
|
1091
1377
|
// A sealed schedule pays from its own vault (`pda.vest`), passed where a pool would be.
|
|
1092
1378
|
const mint = from.Mint?.[0] ?? from.Vest?.[0] ?? poolMint;
|
|
1093
1379
|
const writes = Boolean(from.Mint) || to === 'Burn';
|
|
1094
1380
|
const pool = from.Pool ? pda.pool(project, from.Pool[0]) : from.Vest ? pda.vest(address) : null;
|
|
1381
|
+
// `upto` (the runs it has plus those due, `runsDue`; 0: no bound) has an effect only on what pays every run due.
|
|
1095
1382
|
return lootIx(
|
|
1096
1383
|
'run_schedule',
|
|
1097
|
-
{},
|
|
1384
|
+
{ upto },
|
|
1098
1385
|
{ payer, project, schedule: address, pool, mint },
|
|
1099
|
-
{ remaining: [...destinationAccounts(project, to, mint, { holders, programs, treasury }), ...extra], writable: writes ? ['mint'] : [] },
|
|
1386
|
+
{ remaining: [...destinationAccounts(project, to, mint, { holders, programs, treasury }), ...tailOf(extra)], writable: writes ? ['mint'] : [] },
|
|
1100
1387
|
);
|
|
1101
1388
|
}
|
|
1102
1389
|
|
|
1103
|
-
|
|
1390
|
+
|
|
1391
|
+
// A lottery's rounds (`accounts.js`: `decodeRound`, `partsOwed`, `commonOwed`): each round is a record of its own, opened
|
|
1392
|
+
// by its first purchase. Its ask (`draw_lottery`) fixes its money and moves the lottery on; after the answer, `pull`
|
|
1393
|
+
// pays one part at a time, in any order, and the pull that leaves nothing owed closes it. Every builder takes the
|
|
1394
|
+
// decoded records it acts on, so each is pure.
|
|
1395
|
+
|
|
1396
|
+
/** The address of a lottery's current round's record (there or not), from its decoded Lottery. */
|
|
1397
|
+
export const currentRound = (lotteryAddress, lottery) => pda.round(lotteryAddress, lottery.incarnation, lottery.round);
|
|
1104
1398
|
|
|
1105
1399
|
/**
|
|
1106
|
-
* `player` buys `count` tickets
|
|
1107
|
-
*
|
|
1108
|
-
*
|
|
1109
|
-
*
|
|
1110
|
-
*
|
|
1111
|
-
*
|
|
1112
|
-
*
|
|
1400
|
+
* `player` buys `count` tickets of lottery `id`'s current round (`lottery` decoded; `round` its decoded record, null when
|
|
1401
|
+
* nobody bought into it yet: then this purchase opens it, and the lottery's spare SOL pays its rent, or the buyer fronts
|
|
1402
|
+
* it, back at the close). It buys only at the revision the round copied (`TermsChanged` otherwise), and its round's
|
|
1403
|
+
* address names the lottery's incarnation and round (a purchase that lands as the round moves on fails, and is sent
|
|
1404
|
+
* again). A share the buyer chooses is paid now, out of the price, from the buyer's own account: to the wallet they name
|
|
1405
|
+
* (`chosenRecipient`), the holder of the collectible `chosen` (its holder in `holders`), or with no choice the project's
|
|
1406
|
+
* `treasury` wallet. A lottery whose ticket costs an NFT sells one at a time, for `nft` ({ asset, collection,
|
|
1407
|
+
* tokenMetadata, compressed }, as `piece` takes it).
|
|
1113
1408
|
*/
|
|
1114
|
-
export function buyTicketsIx({ player, project, id, lottery, mint, count,
|
|
1409
|
+
export function buyTicketsIx({ player, project, id, lottery, round = null, mint, count, shown, listProof, chosenRecipient = null, chosen = null, nft, holders = {}, treasury, playerToken, programs, extra = [] }) {
|
|
1115
1410
|
const address = pda.lottery(project, id);
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
const
|
|
1119
|
-
|
|
1411
|
+
const key = currentRound(address, lottery);
|
|
1412
|
+
// The terms the round copied, or for its first purchase the lottery's.
|
|
1413
|
+
const terms = round ? round.terms : lottery.terms;
|
|
1414
|
+
mint ??= paymentMint(terms);
|
|
1415
|
+
const paying = !terms.nft_ticket && BigInt(terms.price) > 0n;
|
|
1416
|
+
const choices = paying ? chosenShares(terms.shares.filter(chosenAtPurchase), chosenRecipient) : [];
|
|
1417
|
+
const recipients = shareAccounts(project, choices, mint, { holders, programs, treasury });
|
|
1418
|
+
const held = terms.nft_ticket ? piece({ project, wallet: player, ...nft, ...ONLY_MOVES, reads: true }) : null;
|
|
1120
1419
|
return lootIx(
|
|
1121
1420
|
'buy_tickets',
|
|
1122
|
-
{
|
|
1421
|
+
{ count, chosen_recipient: chosenRecipient, revision: round ? round.revision : lottery.revision },
|
|
1123
1422
|
{
|
|
1124
|
-
player, project, lottery: address,
|
|
1423
|
+
player, project, lottery: address, round: key,
|
|
1125
1424
|
player_token: playerToken ?? ata(player, mint, tokenProgramOf(programs, mint)),
|
|
1126
|
-
payment_vault: pda.
|
|
1127
|
-
|
|
1425
|
+
payment_vault: pda.roundTokens(key, mint), mint, chosen,
|
|
1426
|
+
counts: Number(lottery.per_wallet) ? pda.counts(project, player) : null,
|
|
1427
|
+
associated_token_program: choices.length ? ASSOCIATED_TOKEN_PROGRAM : null,
|
|
1128
1428
|
},
|
|
1129
|
-
{ remaining: [...gateAccounts(project, { gate: lottery.gate, wallet: player, shown, listProof }), ...
|
|
1429
|
+
{ remaining: [...gateAccounts(project, { gate: lottery.gate, wallet: player, shown, listProof }), ...recipients, ...(held ? [...pieceMetas(held), ...held.proof] : []), ...tailOf(extra)] },
|
|
1130
1430
|
);
|
|
1131
1431
|
}
|
|
1132
1432
|
|
|
1133
|
-
/**
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
*
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1433
|
+
/**
|
|
1434
|
+
* Anyone asks for the draw of a lottery's round once it is due (`lottery` decoded). `round`: the decoded record of the
|
|
1435
|
+
* round asked for (null when nobody bought into the current one: it moves on, or a lottery that draws once ends, with
|
|
1436
|
+
* no ask), and `roundAddress` its address when it is not the current round's (an older round still waiting for its
|
|
1437
|
+
* 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: it takes
|
|
1439
|
+
* the lottery's pot, its mint and the round's account of it, then each run of token prizes in place order, its mint and
|
|
1440
|
+
* for a pool, the pool and the round's account of the mint (`poolMints`: a pool's index to its mint). With `skipHooked`
|
|
1441
|
+
* (the project's authority only) a hooked pool or pot gives the round nothing. `extra`: hook accounts.
|
|
1442
|
+
*/
|
|
1443
|
+
export function drawLotteryIx({ payer, project, id, lottery, round = null, roundAddress = null, skipHooked = false, poolMints = {}, extra = [] }) {
|
|
1444
|
+
const address = id === undefined || id === null ? null : pda.lottery(project, id);
|
|
1445
|
+
const key = roundAddress ?? currentRound(address, lottery);
|
|
1446
|
+
const rest = [];
|
|
1447
|
+
// A round with no record moves on and a round asked before is asked again, reading none of these: they are passed
|
|
1448
|
+
// unless the round is known to have been asked.
|
|
1449
|
+
const current = !round || round.status === 'Open';
|
|
1450
|
+
if (current) {
|
|
1451
|
+
const { terms } = round ?? lottery;
|
|
1452
|
+
rest.push(meta(pda.pot(address), false, true), meta(terms.mint, false, false), meta(pda.roundTokens(key, terms.mint), false, true));
|
|
1453
|
+
for (const place of terms.places) {
|
|
1454
|
+
const from = place.prize?.Tokens?.from;
|
|
1455
|
+
if (from?.Mint) rest.push(meta(from.Mint[0], false, false));
|
|
1456
|
+
else if (from?.Pool) {
|
|
1457
|
+
const mint = poolMints[from.Pool[0]];
|
|
1458
|
+
if (!mint) throw new Error(`The ask of a round whose place gives tokens from pool ${from.Pool[0]} needs that pool's mint (poolMints).`);
|
|
1459
|
+
rest.push(meta(mint, false, false), meta(pda.pool(project, from.Pool[0]), false, true), meta(pda.roundTokens(key, mint), false, true));
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
// The lottery goes with its current round only: a round whose draw waits is asked again or cancelled without it, so a
|
|
1464
|
+
// lottery closed since never strands it.
|
|
1465
|
+
return lootIx('draw_lottery', { skip_hooked: skipHooked }, { payer, project, lottery: current ? address : null, round: key }, { remaining: [...rest, ...tailOf(extra)] });
|
|
1145
1466
|
}
|
|
1146
1467
|
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
if (recipient.kind === 2) return [];
|
|
1154
|
-
const to = recipient.kind === 1 ? { Wallet: [recipient.wallet] } : share.to?.Chosen ? 'Treasury' : share.to;
|
|
1155
|
-
return [{ ...share, to }];
|
|
1156
|
-
});
|
|
1157
|
-
}
|
|
1468
|
+
/** The oracle's answer to a round's draw (for a local simulation): the round's record is its only account. */
|
|
1469
|
+
export const callbackLotteryIx = ({ round, randomness }) => ({
|
|
1470
|
+
programId: LOOT_PROGRAM,
|
|
1471
|
+
keys: [meta(pda.oracleIdentity(), true, false), meta(round, false, true)],
|
|
1472
|
+
data: encodeInstruction(IDL, 'callback_lottery', { randomness: Uint8Array.from(randomness) }),
|
|
1473
|
+
});
|
|
1158
1474
|
|
|
1159
|
-
/**
|
|
1160
|
-
*
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
*
|
|
1169
|
-
*
|
|
1170
|
-
*
|
|
1171
|
-
*
|
|
1172
|
-
*/
|
|
1173
|
-
export function
|
|
1174
|
-
const
|
|
1175
|
-
const
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1475
|
+
/** 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
|
+
* lottery, which gets its spare back, or a buyer who fronted it; `owner` for one the lottery opened, once it is closed). */
|
|
1477
|
+
export const roundCloseMetas = (key, r, owner = null) =>
|
|
1478
|
+
r.accounts.filter((o) => o.mint !== NO_ADDRESS).flatMap((o) => [meta(pda.roundTokens(key, o.mint), false, true), meta(o.mint, false, true), meta(owner && o.by === r.lottery ? owner : o.by, false, true)]);
|
|
1479
|
+
|
|
1480
|
+
/** 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) => stash?.nfts[weighted(stream(answer, [TAG.LOTTERY, place]), stash.weights)] ?? null;
|
|
1482
|
+
|
|
1483
|
+
/**
|
|
1484
|
+
* 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
|
|
1485
|
+
* pulls take them out (`LotteryRound::assign`): the ones already taken out (`r.nfts`), then the rest as the stash
|
|
1486
|
+
* (`stash`, decoded as it is now) gives them, a named one found or one drawn by weight with the round's answer, each
|
|
1487
|
+
* taken out before the next. null for a place whose NFT isn't there (it gets nothing).
|
|
1488
|
+
*/
|
|
1489
|
+
export function stashNfts(r, stash) {
|
|
1490
|
+
const nfts = r.nfts.map((n, k) => (k < Number(r.assigned) ? (n === NO_ADDRESS ? null : n) : undefined));
|
|
1491
|
+
const rows = { nfts: [...(stash?.nfts ?? [])], weights: [...(stash?.weights ?? [])] };
|
|
1492
|
+
const take = (row) => {
|
|
1493
|
+
if (row == null || row < 0) return null;
|
|
1494
|
+
const nft = rows.nfts[row];
|
|
1495
|
+
const last = rows.nfts.length - 1;
|
|
1496
|
+
[rows.nfts[row], rows.weights[row]] = [rows.nfts[last], rows.weights[last]];
|
|
1497
|
+
rows.nfts.pop();
|
|
1498
|
+
rows.weights.pop();
|
|
1499
|
+
return nft;
|
|
1500
|
+
};
|
|
1501
|
+
const w = roundWinners(r);
|
|
1502
|
+
let [k, first] = [0, 0];
|
|
1503
|
+
for (const p of r.terms.places) {
|
|
1504
|
+
const times = Number(p.times);
|
|
1505
|
+
if (ownStash(p)) {
|
|
1506
|
+
for (let place = first; place < first + times && place < w; place++, k++) {
|
|
1507
|
+
if (nfts[k] !== undefined) continue;
|
|
1508
|
+
nfts[k] = p.prize?.Held ? take(rows.nfts.indexOf(p.prize.Held[0])) : take(weighted(stream(r.answer, [TAG.LOTTERY, place]), rows.weights));
|
|
1509
|
+
}
|
|
1510
|
+
k += Math.max(0, first + times - Math.max(first, w));
|
|
1511
|
+
}
|
|
1512
|
+
first += times;
|
|
1188
1513
|
}
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1514
|
+
return nfts.map((n) => n ?? null);
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
/** The program's rule for where a part nobody can take goes: the treasury's wallet, then its account of the mint. */
|
|
1518
|
+
const treasuryAccounts = (treasury, mint, programs) => walletAccounts(treasury, mint, programs);
|
|
1519
|
+
|
|
1520
|
+
/**
|
|
1521
|
+
* Anyone (`payer`) pulls `part` of the round at `roundAddress` (`round` decoded; `partsOwed` and `commonOwed` say what is
|
|
1522
|
+
* owed):
|
|
1523
|
+
* 'Own' entry `entry`'s own part: a refund, a no-loss ticket's price, or its NFT ticket back or where its
|
|
1524
|
+
* price sends it (`ticket` its kind as `piece` takes it; `handBack` when the wallet its price gives
|
|
1525
|
+
* 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 pot, paid into the buyer's account;
|
|
1527
|
+
* { Prize: [run] } their prize: tokens into the buyer's account, or each place's prize (`prize`: { stash, the lottery's
|
|
1528
|
+
* own stash decoded, for NFTs not taken out yet (`stashNfts`); stashes, a project stash's id to its
|
|
1529
|
+
* decoded stash; nfts, an NFT's kind by its asset; hold, NFTs made holds; most, the most places a
|
|
1530
|
+
* pull carries; and what `prizeMetas` takes: collection, poolMints, items, projectMint }), a roll's
|
|
1531
|
+
* order at `prizeRollNonce(nonce + place)` (ask it in the same transaction: `pullRolls`);
|
|
1532
|
+
* { Common: [i] } a part owed in common: share `i`, the pot's (`i` = the shares' count), a pool's prize back after a
|
|
1533
|
+
* cancel (past it), paid where it goes (`holders`, `programs`); `toTreasury` when that can't take it
|
|
1534
|
+
* (a pool never opened, the pot closed since or made again in another mint, an account gone: a
|
|
1535
|
+
* holder nobody is is found here).
|
|
1536
|
+
* The pull that leaves nothing owed closes the round (`projectState`: its treasury and owner; `lotteryClosed` when the
|
|
1537
|
+
* 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
|
+
* by any pull.
|
|
1539
|
+
*/
|
|
1540
|
+
export function pullIx({ payer, project, roundAddress: key, round: r, entry = 0, part = 'Own', projectState, ticket = {}, handBack = false, hold = false, nonce = 0n, prize = {}, holders = {}, toTreasury = false, lotteryClosed = false, programs, extra = [] }) {
|
|
1541
|
+
const rest = [];
|
|
1542
|
+
let [mint, to, settled, buyer, partial] = [null, null, 1, payer, false];
|
|
1543
|
+
const account = (m, wallet) => ata(wallet, m, tokenProgramOf(programs, m));
|
|
1544
|
+
const finished = roundFinished(r);
|
|
1545
|
+
const e = part.Common || finished ? null : r.entries[entry];
|
|
1546
|
+
if (e) buyer = e.buyer;
|
|
1547
|
+
if (finished) settled = 0;
|
|
1548
|
+
else if (part === 'Own' && e.nft !== NO_ADDRESS) {
|
|
1549
|
+
const back = r.status === 'Cancelled' || r.terms.returns;
|
|
1550
|
+
const then = back ? null : r.terms.nft_ticket?.then;
|
|
1551
|
+
if (hold) {
|
|
1552
|
+
rest.push(meta(pda.hold(e.nft, 0), false, true));
|
|
1553
|
+
if (then?.Give) rest.push(meta(then.Give[0], false, true));
|
|
1554
|
+
} else {
|
|
1555
|
+
// Where its price sends it (a stash keeps it where it is), or back to its buyer.
|
|
1556
|
+
const receiver = then ? spentTo(project, then) : null;
|
|
1557
|
+
const holder = then?.Stash || (handBack && then?.Give) ? e.buyer : receiver ?? e.buyer;
|
|
1558
|
+
const p = piece({ project, asset: e.nft, wallet: holder, ...ticket, moves: !then?.Stash, locks: false, burns: then === 'Burn' || Boolean(then?.Burn), reads: false });
|
|
1559
|
+
rest.push(...pieceMetas(p), ...p.proof, ...(receiver ? [meta(receiver, false, true)] : []));
|
|
1560
|
+
}
|
|
1561
|
+
} else if (part === 'Own') [mint, to] = [paymentMint(r.terms), account(paymentMint(r.terms), e.buyer)];
|
|
1562
|
+
else if (part.Common) {
|
|
1563
|
+
const i = Number(part.Common[0]);
|
|
1564
|
+
const n = r.terms.shares.length;
|
|
1565
|
+
let dest;
|
|
1566
|
+
if (i < n) [mint, dest] = [paymentMint(r.terms), r.terms.shares[i].to];
|
|
1567
|
+
else if (i === n) [mint, dest] = [r.terms.mint, { Account: [pda.pot(r.lottery)] }];
|
|
1568
|
+
else {
|
|
1569
|
+
const p = r.prizes[i - n - 1];
|
|
1570
|
+
[mint, dest] = [p.mint, r.terms.places[Number(p.run)].prize.Tokens.from];
|
|
1571
|
+
}
|
|
1572
|
+
rest.push(...destinationAccounts(project, dest, mint, { holders, programs, treasury: projectState?.treasury }));
|
|
1573
|
+
const nobody = dest.Holder && !holders[dest.Holder[0]];
|
|
1574
|
+
if (toTreasury || nobody) rest.push(...treasuryAccounts(projectState?.treasury, mint, programs));
|
|
1575
|
+
} else if (part.Pot || tokenRun(r, Number(part.Prize[0]))) {
|
|
1576
|
+
const run = Number((part.Pot ?? part.Prize)[0]);
|
|
1577
|
+
const p = part.Prize ? r.prizes.find((x) => Number(x.run) === run) : null;
|
|
1578
|
+
mint = p ? p.mint : r.terms.mint;
|
|
1579
|
+
to = account(mint, e.buyer);
|
|
1580
|
+
settled = placesPulled(r, entry, run, Boolean(part.Prize));
|
|
1581
|
+
} else {
|
|
1582
|
+
// Each of the entry's places in the run whose prize is not given yet, lowest first.
|
|
1583
|
+
const run = Number(part.Prize[0]);
|
|
1584
|
+
const given = r.terms.places[run].prize;
|
|
1585
|
+
const places = prizePlacesOwed(r, entry, run);
|
|
1586
|
+
const own = ownStash(r.terms.places[run]);
|
|
1587
|
+
const nfts = own ? stashNfts(r, prize.stash) : [];
|
|
1588
|
+
if (own) rest.push(meta(pda.lotteryStash(r.lottery), false, true));
|
|
1589
|
+
const handed = (asset) => (prize.hold ? [meta(pda.hold(asset, 0), false, true)] : (() => {
|
|
1590
|
+
const p = piece({ project, asset, wallet: e.buyer, ...(prize.nfts?.[asset] ?? {}), ...ONLY_MOVES });
|
|
1591
|
+
return [...pieceMetas(p), ...p.proof];
|
|
1592
|
+
})());
|
|
1593
|
+
let [carried, made, stashed] = [0, 0, false];
|
|
1594
|
+
for (const place of places) {
|
|
1595
|
+
if (own && nfts[nftSlot(r, place)] == null) continue;
|
|
1596
|
+
if (prize.most != null && carried >= prize.most) {
|
|
1597
|
+
partial = true;
|
|
1598
|
+
break;
|
|
1599
|
+
}
|
|
1600
|
+
carried += 1;
|
|
1601
|
+
if (own) rest.push(...handed(nfts[nftSlot(r, place)]));
|
|
1602
|
+
else if (given.Nft) {
|
|
1603
|
+
// A project stash's NFT, while the stash holds it: its stash, then the NFT (once: one place takes it).
|
|
1604
|
+
const { stash, nft: asset } = given.Nft;
|
|
1605
|
+
const holds = prize.stashes?.[stash] ? prize.stashes[stash].nfts.includes(asset) : true;
|
|
1606
|
+
rest.push(meta(pda.stash(project, stash), false, true));
|
|
1607
|
+
if (holds && !stashed) rest.push(...handed(asset));
|
|
1608
|
+
stashed ||= holds;
|
|
1609
|
+
} else {
|
|
1610
|
+
rest.push(...prizeMetas({ project, collection: prize.collection, prize: given, winner: e.buyer, nonce: BigInt.asUintN(64, toBigInt(nonce) + BigInt(place)), poolMints: prize.poolMints, items: prize.items, nft: prize.nfts?.[given.Item?.[0]], made, projectMint: prize.projectMint, programs }));
|
|
1611
|
+
if (given.Item) made += 1;
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
settled = places.length;
|
|
1206
1615
|
}
|
|
1207
|
-
|
|
1208
|
-
const
|
|
1209
|
-
|
|
1616
|
+
const closes = finished || (!partial && (r.status === 'Drawn' || r.status === 'Cancelled') && Number(r.owed) <= settled);
|
|
1617
|
+
const owner = lotteryClosed ? projectState?.owner : null;
|
|
1618
|
+
if (closes) rest.push(...roundCloseMetas(key, r, owner));
|
|
1619
|
+
const opener = owner && r.opener === r.lottery ? owner : r.opener;
|
|
1620
|
+
return lootIx(
|
|
1621
|
+
'pull',
|
|
1622
|
+
{ entry: part.Common || finished ? 0 : entry, part, nonce: toBigInt(nonce) },
|
|
1623
|
+
{ payer, project, round: key, buyer, mint, vault: mint ? pda.roundTokens(key, mint) : null, to, opener: closes ? opener : null, treasury: closes ? projectState?.treasury : null },
|
|
1624
|
+
{ remaining: [...rest, ...tailOf(extra)] },
|
|
1625
|
+
);
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
/** The orders the rolls a `{ Prize: [run] }` pull of entry `entry` makes (a place's prize that is a roll), each with its
|
|
1629
|
+
* box: [{ order, box }], at `prizeRollNonce(nonce + place)` for the entry's buyer. Ask each in the pull's transaction. */
|
|
1630
|
+
export function pullRolls({ project, round: r, entry, part, nonce = 0n }) {
|
|
1631
|
+
const run = Number(part?.Prize?.[0] ?? -1);
|
|
1632
|
+
const given = r.terms.places[run]?.prize;
|
|
1633
|
+
if (!given?.Roll) return [];
|
|
1634
|
+
const buyer = r.entries[entry].buyer;
|
|
1635
|
+
return prizePlacesOwed(r, entry, run).map((place) => ({ order: pda.order(project, buyer, prizeRollNonce(BigInt.asUintN(64, toBigInt(nonce) + BigInt(place)))), box: given.Roll[0] }));
|
|
1210
1636
|
}
|
|
1211
1637
|
|
|
1212
1638
|
// ---------------------------------------------------------------- farms
|
|
1213
1639
|
|
|
1214
1640
|
/**
|
|
1215
|
-
* The owner opens farm `id`: of `stakedMint`'s tokens, or of collectibles when it is null. Its rewards
|
|
1216
|
-
*
|
|
1217
|
-
*
|
|
1218
|
-
*
|
|
1219
|
-
*
|
|
1220
|
-
*
|
|
1221
|
-
* `args.
|
|
1222
|
-
*
|
|
1641
|
+
* The owner opens farm `id`: of `stakedMint`'s tokens, or of collectibles when it is null. Its rewards are `rewardsMint`'s,
|
|
1642
|
+
* in its own rewards vault (`pda.rewards(farm)`), which anything funds by a plain transfer (a share or a schedule paying
|
|
1643
|
+
* `{ Account: [vault] }`): all that lands there, shared, or with `args.rate` ({ per_day, mints, shared, halve_every }) a
|
|
1644
|
+
* fixed rate, for each seat at x1 or with `shared` the whole farm's shared by weight, halving every `halve_every`
|
|
1645
|
+
* seconds from the farm's opening (0: never), minted when a seat is paid or paid from what the vault holds free while it
|
|
1646
|
+
* lasts (a rate of 0 pays nothing: a farm that only grows or gives prizes). `args.gates`: the rules an NFT must all
|
|
1647
|
+
* pass. `args.growth`: any number of { key, per_day, level, steps, streak, profile, at_least }, each writing points (and
|
|
1648
|
+
* a level) on what is staked, or with `profile` on the staker's profile (a token seat only while it stakes `at_least`);
|
|
1649
|
+
* a tier's `earns_for`: seconds a seat earns and grows after its lock started (0: no end). `args.ends_at`: when it ends
|
|
1650
|
+
* (0: never). For a farm of collectibles, `args.lock_key`: the attribute a staked NFT's lock is written under, as the
|
|
1651
|
+
* time it ends, so recipes and rolls can move it; `args.upkeep_key`: the attribute that says until when it earns and
|
|
1652
|
+
* grows (fuel). Empty: none. `args.exit_to`: where early-exit fees go, 'Burn', { Pool: [index] } or 'Rewards' (the
|
|
1653
|
+
* farm's own vault: a farm whose staked token is its rewards'). A farm whose seats can stop earning and that pays keeps a
|
|
1654
|
+
* record of when (`farmKeepsEnds`, `pda.ends`), made here at the owner's rent; each new time costs its writer `END_BYTES`.
|
|
1223
1655
|
*/
|
|
1224
1656
|
export const openFarmIx = ({ owner, project, id, args, rewardsMint, stakedMint = null }) => {
|
|
1225
1657
|
if (!Array.isArray(args?.tiers)) throw new Error('A farm needs its lock tiers (`args.tiers`: [{ lock, bps }, …]).');
|
|
1226
|
-
const full = {
|
|
1658
|
+
const full = {
|
|
1659
|
+
gates: [], ends_at: 0, lock_key: '', upkeep_key: '', exit_to: null, ...args,
|
|
1660
|
+
rate: args.rate ? { mints: false, shared: false, halve_every: 0, ...args.rate } : null,
|
|
1661
|
+
tiers: args.tiers.map((t) => ({ prize: null, roll: null, exit_bps: 0, earns_for: 0, ...t })),
|
|
1662
|
+
growth: (args.growth ?? []).map((g) => ({ profile: false, at_least: 0n, ...g })),
|
|
1663
|
+
};
|
|
1227
1664
|
return lootIx('open_farm', { id, args: full }, {
|
|
1228
|
-
owner, project,
|
|
1665
|
+
owner, project, rewards_mint: rewardsMint, staked_mint: stakedMint,
|
|
1229
1666
|
staked_vault: stakedMint ? pda.staked(pda.farm(project, id)) : null,
|
|
1230
|
-
exit_pool: sinkPool(project, full.exit_to),
|
|
1667
|
+
exit_pool: sinkPool(project, full.exit_to),
|
|
1668
|
+
ends: farmKeepsEnds(full) ? pda.ends(pda.farm(project, id)) : null,
|
|
1669
|
+
}, {
|
|
1670
|
+
// Each box its tiers name to roll, tier by tier (its roll, then a prize that is a roll), once.
|
|
1671
|
+
remaining: boxesAccounts(project, full.tiers.flatMap((t) => [t.roll, t.prize?.Roll?.[0]].filter((x) => x != null))),
|
|
1231
1672
|
});
|
|
1232
1673
|
};
|
|
1233
1674
|
|
|
1234
|
-
/**
|
|
1675
|
+
/**
|
|
1676
|
+
* Whether a farm with these args keeps a record of when its seats stop earning (`Farm::keeps_ends`): a tier has a term
|
|
1677
|
+
* (`earns_for`) or the farm reads an upkeep (`upkeep_key`), and it pays something (no rate, or a rate above 0). The farm
|
|
1678
|
+
* and every move of it then pass the record (`WrongEnds` otherwise).
|
|
1679
|
+
*/
|
|
1680
|
+
export const farmKeepsEnds = (args) =>
|
|
1681
|
+
((args.tiers ?? []).some((t) => BigInt(t.earns_for ?? 0) > 0n) || Boolean(args.upkeep_key)) && (!args.rate || BigInt(args.rate.per_day ?? 0) > 0n);
|
|
1682
|
+
|
|
1683
|
+
/** Where a farm's fee goes (`Sink`): the pool's address for `{ Pool: [index] }`, null for a burn, its own vault or none. */
|
|
1235
1684
|
const sinkPool = (project, sink) => (sink?.Pool ? pda.pool(project, sink.Pool[0]) : null);
|
|
1236
|
-
/** The pools a farm's early-exit fees and toll go to, when they go to one (`exit_pool`, `toll_pool`). */
|
|
1237
|
-
const farmSinks = (project, farm) => ({ exit_pool: sinkPool(project, farm.exit_to), toll_pool: sinkPool(project, farm.toll?.to) });
|
|
1238
|
-
/** A toll that burns writes the rewards' mint. */
|
|
1239
|
-
const tollWrites = (farm) => (farm.toll?.to === 'Burn' ? ['rewards_mint'] : []);
|
|
1240
1685
|
|
|
1241
1686
|
const farmSide = (project, id, farm) => ({
|
|
1242
1687
|
project,
|
|
1243
1688
|
farm: pda.farm(project, id),
|
|
1244
|
-
rewards_pool: pda.pool(project, farm.rewards_pool),
|
|
1245
1689
|
rewards_mint: farm.rewards_mint,
|
|
1690
|
+
ends: farm.keeps_ends ? pda.ends(pda.farm(project, id)) : null,
|
|
1246
1691
|
});
|
|
1247
|
-
|
|
1248
|
-
|
|
1692
|
+
|
|
1693
|
+
/**
|
|
1694
|
+
* What a farm has to walk (`Farm::walk`): the times of its record of ends (`ends`, from `decodeEnds`; an empty list for none)
|
|
1695
|
+
* past its clock, up to `now` (Unix seconds, the chain's clock) or the farm's end. Every move walks all of them first, each a
|
|
1696
|
+
* little compute, so a farm idle past many ends may leave a move no room: `farmNeedsWalk`.
|
|
1697
|
+
*/
|
|
1698
|
+
export function endsToWalk(farm, ends, now) {
|
|
1699
|
+
const clock = BigInt(farm.clock ?? 0);
|
|
1700
|
+
const over = BigInt(farm.ends_at ?? 0);
|
|
1701
|
+
const stop = over > 0n && over < BigInt(now) ? over : BigInt(now);
|
|
1702
|
+
return ends.filter((e) => e.at > clock && e.at <= stop).length;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
/** How many ends `walk_farm` walks in one transaction at `WALK_COMPUTE_UNITS`: an end takes at most some 2,300 units, with a
|
|
1706
|
+
* halving rate (`js/test/farm-ends.test.mjs` measures it). */
|
|
1707
|
+
export const WALK_PER_TX = 500;
|
|
1708
|
+
/** The compute units a transaction that walks ends asks for: `walk_farm`, or a move with ends to walk first. */
|
|
1709
|
+
export const WALK_COMPUTE_UNITS = 1_400_000;
|
|
1710
|
+
/** How many ends a move can walk first at `WALK_COMPUTE_UNITS` and still do its own work: past this, the farm is walked
|
|
1711
|
+
* first (`farmNeedsWalk`). */
|
|
1712
|
+
export const MOVE_WALK_ROOM = 400;
|
|
1713
|
+
|
|
1714
|
+
/**
|
|
1715
|
+
* Whether a farm has passed more ends than a move can walk in its own transaction (`endsToWalk`, `MOVE_WALK_ROOM`): such
|
|
1716
|
+
* a move fails on compute until `walk_farm` has caught the farm up. Nothing is lost by waiting: what a seat earned by
|
|
1717
|
+
* its end is written when the farm walks past it, however late.
|
|
1718
|
+
*/
|
|
1719
|
+
export const farmNeedsWalk = (farm, ends, now) => endsToWalk(farm, ends, now) > MOVE_WALK_ROOM;
|
|
1720
|
+
|
|
1721
|
+
/**
|
|
1722
|
+
* The transactions that walk a farm up to now, oldest ends first: one `walkFarmIx` each, `perTx` ends at most, `[]` when
|
|
1723
|
+
* it has nothing to walk. Anyone may send them, in order; each is free but for its network fee.
|
|
1724
|
+
*/
|
|
1725
|
+
export function walkFarmParts({ project, id, farm, ends, now, perTx = WALK_PER_TX }) {
|
|
1726
|
+
const left = endsToWalk(farm, ends, now);
|
|
1727
|
+
const parts = [];
|
|
1728
|
+
for (let done = 0; done < left; done += perTx) parts.push(walkFarmIx({ project, id, farm, most: Math.min(perTx, left - done) }));
|
|
1729
|
+
return parts;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
/**
|
|
1733
|
+
* Credits a farm up to at most `most` of the times its seats stopped earning. Anyone may send it, signing nothing but its
|
|
1734
|
+
* network fee: it decides nothing, every move walks the same times the same way. For a farm that has passed more of them
|
|
1735
|
+
* than a move's transaction can walk (`farmNeedsWalk`, `walkFarmParts`).
|
|
1736
|
+
*/
|
|
1737
|
+
export const walkFarmIx = ({ project, id, farm, most = WALK_PER_TX }) =>
|
|
1738
|
+
lootIx('walk_farm', { most }, farmSide(project, id, farm));
|
|
1739
|
+
/** Whether a farm reads or writes its seats' players' profiles (`Farm::uses_profile`): a gate or a boost on a profile,
|
|
1740
|
+
* or a growth written on one. Its moves then pass the player's profile. */
|
|
1741
|
+
const usesProfile = (farm) => [...(farm.gates ?? []), ...(farm.boosts ?? []).map((b) => b.rule)].some((r) => r?.of === 'Profile') || (farm.growth ?? []).some((g) => g.profile);
|
|
1742
|
+
const profileOf = (project, farm, player) => (usesProfile(farm) ? pda.profile(project, player) : null);
|
|
1743
|
+
|
|
1744
|
+
/** 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 = (farm) => (farm.rate?.mints ? ['rewards_mint'] : []);
|
|
1249
1746
|
|
|
1250
1747
|
/** A token seat's accounts; `paid`: rewards may be paid (an unstake), so the player's account of them comes. */
|
|
1251
1748
|
function tokenSeat({ player, project, id, farm, programs, paid = false }) {
|
|
@@ -1259,33 +1756,70 @@ function tokenSeat({ player, project, id, farm, programs, paid = false }) {
|
|
|
1259
1756
|
mint,
|
|
1260
1757
|
player_tokens: ata(player, mint, tokenProgramOf(programs, mint)),
|
|
1261
1758
|
player_rewards: paid ? ata(player, farm.rewards_mint, tokenProgramOf(programs, farm.rewards_mint)) : null,
|
|
1262
|
-
|
|
1759
|
+
exit_pool: sinkPool(project, farm.exit_to),
|
|
1760
|
+
profile: profileOf(project, farm, player),
|
|
1263
1761
|
};
|
|
1264
1762
|
}
|
|
1265
1763
|
|
|
1266
1764
|
/**
|
|
1267
|
-
* `player` stakes `amount` at lock tier `tier`. `farm` is decoded; `extra` carries token hook/memo accounts. A
|
|
1268
|
-
*
|
|
1269
|
-
*
|
|
1765
|
+
* `player` stakes `amount` at lock tier `tier`. `farm` is decoded; `extra` carries token hook/memo accounts. A seat with
|
|
1766
|
+
* a booster on, or one whose growth the stake changes, needs a `refreshSeatIx` of it first, in the same transaction
|
|
1767
|
+
* (`stakeRefreshFirst`).
|
|
1270
1768
|
*/
|
|
1271
1769
|
export const stakeTokensIx = ({ player, project, id, farm, amount, tier = 0, programs, shown, extra = [] }) =>
|
|
1272
|
-
lootIx('stake_tokens', { amount, tier }, tokenSeat({ player, project, id, farm, programs }), { remaining: [...shownAccounts(project, shown), ...extra
|
|
1770
|
+
lootIx('stake_tokens', { amount, tier }, tokenSeat({ player, project, id, farm, programs }), { remaining: [...shownAccounts(project, shown), ...tailOf(extra)] });
|
|
1273
1771
|
|
|
1274
1772
|
/**
|
|
1275
|
-
* `player` takes `amount` out (0: only collects what is owed
|
|
1276
|
-
*
|
|
1277
|
-
*
|
|
1278
|
-
* (`unbond`), what is taken out waits that long first, earning nothing (`withdrawable`).
|
|
1773
|
+
* `player` takes `amount` out (0: only collects what is owed); `forfeit` gives up rewards that can't move. Before the
|
|
1774
|
+
* tier's lock has run, a tier with an early-exit fee (`exit_bps`) lets it leave, leaving that share of `amount` to the
|
|
1775
|
+
* farm's `exit_to`; it keeps the rewards it was credited.
|
|
1279
1776
|
*/
|
|
1280
1777
|
export const unstakeTokensIx = ({ player, project, id, farm, amount, forfeit = false, programs, extra = [] }) =>
|
|
1281
1778
|
lootIx('unstake_tokens', { amount, forfeit }, tokenSeat({ player, project, id, farm, programs, paid: !forfeit }), {
|
|
1282
|
-
remaining: extra,
|
|
1283
|
-
// An exit fee that burns writes the staked mint; a
|
|
1284
|
-
writable: [...
|
|
1779
|
+
remaining: tailOf(extra),
|
|
1780
|
+
// An exit fee that burns writes the staked mint; a farm that mints, its rewards' mint when it pays.
|
|
1781
|
+
writable: [...(forfeit ? [] : payWrites(farm)), ...(farm.exit_to === 'Burn' ? ['mint'] : [])],
|
|
1782
|
+
});
|
|
1783
|
+
|
|
1784
|
+
/**
|
|
1785
|
+
* Whether taking `amount` out of a token seat (decoded `seat`) needs a `refreshSeatIx` of it first, in the same
|
|
1786
|
+
* transaction (`RefreshFirst`): the farm grows, and the exit closes the seat or drops it below a growth's least stake, so
|
|
1787
|
+
* what it grew since its last refresh is written before it goes. Never under `forfeit`.
|
|
1788
|
+
*/
|
|
1789
|
+
export const refreshFirst = (farm, seat, amount, forfeit = false) =>
|
|
1790
|
+
!forfeit && BigInt(amount) > 0n && (farm.growth ?? []).some((g) => {
|
|
1791
|
+
const least = BigInt(g.at_least ?? 0) > 0n ? BigInt(g.at_least) : 1n;
|
|
1792
|
+
return BigInt(seat.amount) >= least && BigInt(seat.amount) - BigInt(amount) < least;
|
|
1285
1793
|
});
|
|
1286
1794
|
|
|
1287
|
-
/**
|
|
1288
|
-
|
|
1795
|
+
/**
|
|
1796
|
+
* Whether a stake of tokens at `tier` in a token seat (decoded `seat`; null for a new one), or with `compound` a
|
|
1797
|
+
* compound of what it is owed, needs a `refreshSeatIx` of it first, in the same transaction (`RefreshFirst`), at `now`
|
|
1798
|
+
* (Unix seconds, the chain's clock): a seat with a booster on (its boost is read again there, so a top-up never stakes
|
|
1799
|
+
* more at a boost its booster no longer gives), or a seat that grows (a booster's growth, or its player's profile's)
|
|
1800
|
+
* when the stake changes how: a new tier, a farm whose growth reads the amount, a seat past its term; a compound when
|
|
1801
|
+
* that growth reads the amount.
|
|
1802
|
+
* @param {any} farm
|
|
1803
|
+
* @param {any} seat
|
|
1804
|
+
* @param {{ tier?: number, now?: bigint | number, compound?: boolean }} [options]
|
|
1805
|
+
*/
|
|
1806
|
+
export function stakeRefreshFirst(farm, seat, { tier = seat?.tier, now, compound = false } = {}) {
|
|
1807
|
+
if (!seat) return false;
|
|
1808
|
+
const growth = farm.growth ?? [];
|
|
1809
|
+
const booster = seat.asset !== undefined && seat.asset !== NO_ADDRESS;
|
|
1810
|
+
const grows = BigInt(seat.amount) > 0n && growth.some((g) => g.profile || booster);
|
|
1811
|
+
const amountGrows = growth.some((g) => g.profile || BigInt(g.at_least ?? 0) > 0n);
|
|
1812
|
+
if (compound) return BigInt(seat.amount) > 0n && (booster || (grows && amountGrows));
|
|
1813
|
+
const changes = Number(tier) !== Number(seat.tier) || amountGrows || (now !== undefined && BigInt(seat.earns_until ?? 0) <= BigInt(now));
|
|
1814
|
+
return booster || (grows && changes);
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
/**
|
|
1818
|
+
* Whether a booster put on a token seat of `farm` (decoded) needs the seat refreshed first, in the same transaction
|
|
1819
|
+
* (`stake_nft`'s `RefreshFirst`): in a farm of tokens whose growth writes its players' profiles, the booster's growth
|
|
1820
|
+
* starts from now, so what the seat grew before is written first.
|
|
1821
|
+
*/
|
|
1822
|
+
export const boosterRefreshFirst = (farm) => Boolean(farm.farmed?.Tokens) && (farm.growth ?? []).some((g) => g.profile);
|
|
1289
1823
|
|
|
1290
1824
|
/**
|
|
1291
1825
|
* Whether a farm (decoded) takes a stake at lock tier `tier` at `now` (Unix seconds, the chain's clock), as the program
|
|
@@ -1335,6 +1869,7 @@ export function piece({ project, asset, wallet, collection, hasAuthority = true,
|
|
|
1335
1869
|
? { collection: c.collection ?? null, tree_config: pda.treeConfig(c.tree), leaf_delegate: delegate, bubblegum_program: BUBBLEGUM_PROGRAM, compression_program: COMPRESSION_PROGRAM, log_wrapper: NOOP_PROGRAM }
|
|
1336
1870
|
: {};
|
|
1337
1871
|
return {
|
|
1872
|
+
kind: 'compressed',
|
|
1338
1873
|
accounts: { ...NO_PIECE, asset: c.id, sheet: sheetOf(c.id), leaf: pda.leaf(c.id), tree: c.tree, ...bubblegum },
|
|
1339
1874
|
proof: c.proof.map((node) => meta(typeof node === 'string' ? node : encodeBase58(node), false, false)),
|
|
1340
1875
|
writable: calls ? ['leaf', 'tree', 'tree_config'] : [],
|
|
@@ -1350,6 +1885,7 @@ export function piece({ project, asset, wallet, collection, hasAuthority = true,
|
|
|
1350
1885
|
const calls = moves || locks;
|
|
1351
1886
|
const records = calls && pnft;
|
|
1352
1887
|
return {
|
|
1888
|
+
kind: 'tokenMetadata',
|
|
1353
1889
|
accounts: {
|
|
1354
1890
|
...NO_PIECE, asset, sheet: sheetOf(asset), metadata: tokenMetadata.metadata(asset), token,
|
|
1355
1891
|
edition: calls ? tokenMetadata.edition(asset) : null,
|
|
@@ -1372,6 +1908,7 @@ export function piece({ project, asset, wallet, collection, hasAuthority = true,
|
|
|
1372
1908
|
const inNone = !collection || collection === MPL_CORE_PROGRAM;
|
|
1373
1909
|
const calls = moves || locks || writes || burns;
|
|
1374
1910
|
return {
|
|
1911
|
+
kind: 'core',
|
|
1375
1912
|
accounts: {
|
|
1376
1913
|
...NO_PIECE, asset, sheet: hasAuthority ? null : sheetOf(asset),
|
|
1377
1914
|
collection: calls && !inNone ? collection : null, mpl_core_program: calls ? MPL_CORE_PROGRAM : null,
|
|
@@ -1381,6 +1918,41 @@ export function piece({ project, asset, wallet, collection, hasAuthority = true,
|
|
|
1381
1918
|
};
|
|
1382
1919
|
}
|
|
1383
1920
|
|
|
1921
|
+
/** The accounts a further NFT of a call of several takes (`Piece::another`), after the first NFT's: only what its kind
|
|
1922
|
+
* needs, the program's own address for one left out, then its proof. The first NFT passes every program any needs. */
|
|
1923
|
+
const ANOTHER = {
|
|
1924
|
+
core: ['asset', 'sheet', 'collection'],
|
|
1925
|
+
tokenMetadata: ['asset', 'sheet', 'metadata', 'edition', 'token', 'token_record', 'project_token', 'project_record', 'rule_set'],
|
|
1926
|
+
compressed: ['asset', 'sheet', 'collection', 'leaf', 'tree', 'tree_config', 'leaf_delegate'],
|
|
1927
|
+
};
|
|
1928
|
+
export function anotherMetas(p) {
|
|
1929
|
+
const writable = new Set([...p.writable, ...PIECE.filter((a) => a.writable).map((a) => a.name)]);
|
|
1930
|
+
return [...ANOTHER[p.kind].map((name) => (p.accounts[name] == null ? meta(LOOT_PROGRAM, false, false) : meta(p.accounts[name], false, writable.has(name)))), ...p.proof];
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
/** 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
|
+
* and Bubblegum's, by what the others are. */
|
|
1935
|
+
function withPrograms(first, others) {
|
|
1936
|
+
const kinds = new Set(others.map((p) => p.kind));
|
|
1937
|
+
const a = { ...first.accounts };
|
|
1938
|
+
if (kinds.has('core')) a.mpl_core_program ??= MPL_CORE_PROGRAM;
|
|
1939
|
+
if (kinds.has('tokenMetadata')) {
|
|
1940
|
+
a.token_metadata_program ??= TOKEN_METADATA_PROGRAM;
|
|
1941
|
+
a.spl_token_program ??= TOKEN_PROGRAM;
|
|
1942
|
+
a.spl_ata_program ??= ASSOCIATED_TOKEN_PROGRAM;
|
|
1943
|
+
a.instructions ??= SYSVAR_INSTRUCTIONS;
|
|
1944
|
+
a.rules_program ??= others.some((p) => p.accounts.rule_set && p.accounts.rule_set !== TOKEN_METADATA_PROGRAM) ? AUTH_RULES_PROGRAM : others.some((p) => p.accounts.token_record) ? TOKEN_METADATA_PROGRAM : null;
|
|
1945
|
+
}
|
|
1946
|
+
if (kinds.has('compressed')) Object.assign(a, { bubblegum_program: a.bubblegum_program ?? BUBBLEGUM_PROGRAM, compression_program: a.compression_program ?? COMPRESSION_PROGRAM, log_wrapper: a.log_wrapper ?? NOOP_PROGRAM });
|
|
1947
|
+
return { ...first, accounts: a };
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
/**
|
|
1951
|
+
* Several NFTs on one call (a hold, a stash, a farm's seats): their groups sorted by key (`keyOf`), the first the
|
|
1952
|
+
* call's own and each further one after it, in ascending order (`OutOfOrder` otherwise). Returns [first, ...rest].
|
|
1953
|
+
*/
|
|
1954
|
+
const ascending = (items, keyOf) => [...items].sort((a, b) => compareBytes(addressBytes(keyOf(a)), addressBytes(keyOf(b))));
|
|
1955
|
+
|
|
1384
1956
|
/** The account an NFT paid goes to: the wallet it is given to, the stash it goes into, or the
|
|
1385
1957
|
* incinerator for a burn (any account stands in for a Core asset, which Core burns). `then`: 'Burn', { Give: [wallet] }
|
|
1386
1958
|
* or { Stash: [id] }. */
|
|
@@ -1427,7 +1999,7 @@ export function payNftIx({ player, project, asset, to = null, collection, hasAut
|
|
|
1427
1999
|
if (to === project) throw new Error('An NFT paid can’t go to the project’s own address: name a wallet, or burn it.');
|
|
1428
2000
|
const receiver = to ?? INCINERATOR;
|
|
1429
2001
|
const p = passedOn({ project, owner: player, receiver, burns: receiver === INCINERATOR, asset, collection, hasAuthority, tokenMetadata: tm, compressed });
|
|
1430
|
-
return lootIx('pay_nft', { reference: reference(ref) }, { player, project, ...p.accounts, to: receiver }, { remaining: [...p.proof, ...extra], writable: p.writable });
|
|
2002
|
+
return lootIx('pay_nft', { reference: reference(ref) }, { player, project, ...p.accounts, to: receiver }, { remaining: [...p.proof, ...tailOf(extra)], writable: p.writable });
|
|
1431
2003
|
}
|
|
1432
2004
|
|
|
1433
2005
|
/**
|
|
@@ -1491,11 +2063,53 @@ export const TOKEN_ACCOUNT_RENT = 2_039_280n;
|
|
|
1491
2063
|
/** A route that only hands the NFT to the project or sends it on (a hold, an auction's lot, a refund): no Sheet, no write. */
|
|
1492
2064
|
const ONLY_MOVES = { moves: true, locks: false, writes: false, reads: false };
|
|
1493
2065
|
|
|
2066
|
+
/** Whether `asset` goes as its hold (`["hold", asset, 0]`, passed in its piece's place): `hold` true for every NFT a
|
|
2067
|
+
* route owes, or a list of the assets to hold. */
|
|
2068
|
+
const holds = (hold, asset) => (Array.isArray(hold) ? hold.includes(asset) : Boolean(hold));
|
|
2069
|
+
|
|
1494
2070
|
/** A piece that reads no NFT: `at` (an auction's or a seat's own address) stands in for one. */
|
|
1495
2071
|
const standIn = (at) => ({ accounts: { ...NO_PIECE, asset: at }, proof: [], writable: [] });
|
|
1496
2072
|
|
|
1497
|
-
/**
|
|
1498
|
-
|
|
2073
|
+
/**
|
|
2074
|
+
* A compressed NFT's record (`pda.leaf`) closes, its rent back to `payer`, who paid it: at any time with `payer` signing
|
|
2075
|
+
* (`signs`, the default), or by anyone once it no longer matches its tree. A route that finds no record asks for a proof
|
|
2076
|
+
* again (`NotProven`), so a client closes it in the transaction that used it (`withLeafCloses`).
|
|
2077
|
+
*/
|
|
2078
|
+
export const closeLeafIx = ({ asset, tree, payer, signs = true }) => lootIx('close_leaf', {}, { record: pda.leaf(asset), tree, payer, project: null, edition: null }, { signer: signs ? ['payer'] : [] });
|
|
2079
|
+
|
|
2080
|
+
const PROVE_LEAF = leaves(instructionDef('prove_leaf').accounts).map((a) => a.name);
|
|
2081
|
+
const CLOSE_LEAF = leaves(instructionDef('close_leaf').accounts).map((a) => a.name);
|
|
2082
|
+
const isLoot = (ix, name) => ix.programId === LOOT_PROGRAM && instructionOf(ix.data)?.name === name;
|
|
2083
|
+
|
|
2084
|
+
/**
|
|
2085
|
+
* `instructions` with a `close_leaf` after them for each compressed NFT's record a `prove_leaf` among them makes, its
|
|
2086
|
+
* prover signing as the record's payer, so the record's rent comes back in the same transaction (one closed already is
|
|
2087
|
+
* left alone).
|
|
2088
|
+
*/
|
|
2089
|
+
export function withLeafCloses(instructions) {
|
|
2090
|
+
const at = (ix, names, name) => ix.keys[names.indexOf(name)]?.pubkey;
|
|
2091
|
+
const closed = new Set(instructions.filter((ix) => isLoot(ix, 'close_leaf')).map((ix) => at(ix, CLOSE_LEAF, 'record')));
|
|
2092
|
+
const closes = [];
|
|
2093
|
+
for (const ix of instructions.filter((x) => isLoot(x, 'prove_leaf'))) {
|
|
2094
|
+
const record = at(ix, PROVE_LEAF, 'record');
|
|
2095
|
+
if (closed.has(record)) continue;
|
|
2096
|
+
closed.add(record);
|
|
2097
|
+
closes.push(closeLeafIx({ asset: at(ix, PROVE_LEAF, 'asset'), tree: at(ix, PROVE_LEAF, 'tree'), payer: at(ix, PROVE_LEAF, 'signer') }));
|
|
2098
|
+
}
|
|
2099
|
+
return closes.length ? [...instructions, ...closes] : instructions;
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
/**
|
|
2103
|
+
* Anyone closes the Sheet of an NFT that is burned: `burned` is what shows it (the Core asset's own address, burned to
|
|
2104
|
+
* one byte; a Token Metadata mint holding none; or a compressed NFT's record proving its leaf burned). Its rent goes to
|
|
2105
|
+
* the project's `treasury` wallet. A compressed NFT's (`tree`: the tree it was minted in) closes only on a record proven
|
|
2106
|
+
* empty (its data hash all zeros: a live leaf of the default owner is not burned) that still reads against that tree,
|
|
2107
|
+
* or whose tree is gone: the tree goes in the edition's place, and `burned` defaults to the NFT's record
|
|
2108
|
+
* (`pda.leaf`). A Token Metadata NFT's (`tokenMetadata`: true, or its kind) names its edition, which must be closed
|
|
2109
|
+
* too: its mint holds none, with no mint authority but its edition (`NotBurned` otherwise).
|
|
2110
|
+
*/
|
|
2111
|
+
export const closeSheetIx = ({ project, nft, tree = null, burned = tree ? pda.leaf(nft) : nft, treasury, tokenMetadata: tm = null }) =>
|
|
2112
|
+
lootIx('close_leaf', {}, { record: pda.sheet(project, nft), tree: burned, payer: treasury, project, edition: tm ? tokenMetadata.edition(nft) : tree });
|
|
1499
2113
|
|
|
1500
2114
|
/**
|
|
1501
2115
|
* Anyone proves a compressed NFT's leaf (`compressed` from `compressedFromDas`): put it before the instruction that
|
|
@@ -1531,6 +2145,7 @@ function nftSeat({ player, project, id, farm, asset, collection, hasAuthority =
|
|
|
1531
2145
|
const p = piece({ project, asset, wallet: player, collection, hasAuthority, tokenMetadata: tm, compressed, ...keeps });
|
|
1532
2146
|
const key = compressed ? compressed.id : asset;
|
|
1533
2147
|
return {
|
|
2148
|
+
kind: p.kind,
|
|
1534
2149
|
accounts: {
|
|
1535
2150
|
...farmSide(project, id, farm),
|
|
1536
2151
|
player,
|
|
@@ -1539,67 +2154,101 @@ function nftSeat({ player, project, id, farm, asset, collection, hasAuthority =
|
|
|
1539
2154
|
seat: pda.seat(farmAddress, farm.farmed.Tokens ? player : key),
|
|
1540
2155
|
// Rewards are paid only as an NFT's own seat leaves.
|
|
1541
2156
|
player_rewards: leaving && !farm.farmed.Tokens ? ata(player, farm.rewards_mint, tokenProgramOf(programs, farm.rewards_mint)) : null,
|
|
2157
|
+
profile: profileOf(project, farm, player),
|
|
1542
2158
|
},
|
|
1543
2159
|
proof: p.proof,
|
|
1544
2160
|
writable: p.writable,
|
|
1545
2161
|
};
|
|
1546
2162
|
}
|
|
1547
2163
|
|
|
2164
|
+
/** A further seat's group of a call of several (`NftSeat`): the seat, its NFT's accounts (`anotherMetas`), its box and
|
|
2165
|
+
* 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) => [meta(seat, false, true), ...anotherMetas({ ...p, proof: [] }), meta(box ?? LOOT_PROGRAM, false, false), meta(order ?? LOOT_PROGRAM, false, Boolean(order)), ...p.proof];
|
|
2167
|
+
|
|
1548
2168
|
/**
|
|
1549
2169
|
* `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
|
|
1550
2170
|
* in a farm of tokens. Any kind (`piece`): a Core asset, a Token Metadata NFT's mint with `tokenMetadata`, or
|
|
1551
2171
|
* `compressed` (after a `proveLeafIx` in the same transaction). `previous`: in a farm that watches, the last staker of
|
|
1552
|
-
* an NFT that changed hands, whose seat this takes over.
|
|
1553
|
-
*
|
|
2172
|
+
* an NFT that changed hands, whose seat this takes over (alone). In a farm of collectibles, `more` NFTs (each { asset,
|
|
2173
|
+
* collection, hasAuthority, tokenMetadata, compressed }) go in the same call at the same tier, one fee, by their seats'
|
|
2174
|
+
* addresses. `extra` carries reward token hooks/memos and Core transfer adapter accounts.
|
|
1554
2175
|
*/
|
|
1555
|
-
export const stakeNftIx = ({ player, project, id, farm, asset, collection, hasAuthority = true, tokenMetadata: tm, compressed, previous = null, tier = 0, programs, extra = [] }) => {
|
|
2176
|
+
export const stakeNftIx = ({ player, project, id, farm, asset, collection, hasAuthority = true, tokenMetadata: tm, compressed, previous = null, tier = 0, programs, more = [], extra = [] }) => {
|
|
1556
2177
|
// Its lock's end, when the farm keeps one, is written on it.
|
|
1557
2178
|
const keeps = farmKeeps(farm, { writes: Boolean(farm.lock_key) });
|
|
1558
|
-
const
|
|
1559
|
-
|
|
2179
|
+
const [first, ...rest] = ascending([{ asset, collection, hasAuthority, tokenMetadata: tm, compressed }, ...more].map((n) => nftSeat({ player, project, id, farm, ...n, previous, programs, keeps })), (x) => x.accounts.seat);
|
|
2180
|
+
const seat = withPrograms({ ...first, kind: first.kind }, rest);
|
|
2181
|
+
const groups = rest.flatMap((x) => seatGroup(x.accounts.seat, x));
|
|
2182
|
+
return lootIx('stake_nft', { tier }, seat.accounts, { remaining: [...seat.proof, ...groups, ...tailOf(extra)], writable: seat.writable });
|
|
1560
2183
|
};
|
|
1561
2184
|
|
|
1562
2185
|
/**
|
|
1563
2186
|
* `player` takes an NFT out once its lock has run: it grows on its way out (a farm with growth writes on it, or on its
|
|
1564
2187
|
* Sheet), and from its own seat takes its tier's prize. `forfeit` gives up the rewards and the prize. Leaving a tier
|
|
1565
|
-
* that rolls (`roll`: its box id), the NFT is rolled on that box, in an order
|
|
1566
|
-
* `
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
const
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
const
|
|
2188
|
+
* that rolls (`roll`: its box id, `boxes` its decoded box by id), the NFT is rolled on that box, in an order named by
|
|
2189
|
+
* `nonce` (`namedRollNonce`: the NFT's own on a box that holds it; its player or the project's keys then ask the oracle,
|
|
2190
|
+
* `askRollIx`, and settles); a box that can burn it or give it away keeps it in the project's hands until then. `more`
|
|
2191
|
+
* NFTs of the player's in the farm (each { asset, collection, hasAuthority, tokenMetadata, compressed, tier, roll }) leave
|
|
2192
|
+
* 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
|
+
* prize's at `nonce + 2i + 1` (`seatNonces` says which NFT has which). One still locked refuses the whole call.
|
|
2194
|
+
*/
|
|
2195
|
+
export const unstakeNftIx = ({ player, project, id, farm, asset, collection, hasAuthority = true, tokenMetadata: tm, compressed, forfeit = false, roll = null, nonce = 0n, tier, projectCollection, poolMints, items, nft, boxes = {}, programs, more = [], extra = [] }) => {
|
|
2196
|
+
const seats = ascending([{ asset, collection, hasAuthority, tokenMetadata: tm, compressed, tier, roll }, ...more].map((n) => {
|
|
2197
|
+
// It grows on its way out; a mission that rolls may hand it to the project.
|
|
2198
|
+
const keeps = farmKeeps(farm, { writes: Boolean(farm.growth?.length), rolls: n.roll != null });
|
|
2199
|
+
return { n, seat: nftSeat({ player, project, id, farm, ...n, programs, leaving: true, keeps }) };
|
|
2200
|
+
}), (x) => x.seat.accounts.seat);
|
|
2201
|
+
const several = seats.length > 1;
|
|
2202
|
+
// 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
|
+
// proof, a roll's order at `nonce + 2i + 1`. A forfeit takes neither the prize nor the rewards (the program reads
|
|
2204
|
+
// neither), so a call of one passes neither: the smallest way out. In a call of several each seat with a prize passes
|
|
2205
|
+
// its accounts, given or not, so the next seat's start where the program looks.
|
|
2206
|
+
const parts = seats.map(({ n, seat }, i) => {
|
|
2207
|
+
const at = BigInt.asUintN(64, toBigInt(nonce) + 2n * BigInt(i));
|
|
2208
|
+
const prize = n.tier != null && (!forfeit || several) ? farm.tiers[n.tier]?.prize : null;
|
|
2209
|
+
const prized = prize ? prizeMetas({ project, collection: projectCollection ?? n.collection, prize, winner: player, nonce: BigInt.asUintN(64, at + 1n), poolMints, items, nft, programs }) : [];
|
|
2210
|
+
const rolls = n.roll != null ? { box: pda.box(project, n.roll), order: pda.order(project, player, namedRollNonce(boxes[n.roll], seat.accounts.asset, at)) } : { box: null, order: null };
|
|
2211
|
+
return { seat, prize, prized, rolls };
|
|
2212
|
+
});
|
|
2213
|
+
const [first, ...rest] = parts;
|
|
2214
|
+
const seat = withPrograms(first.seat, rest.map((x) => x.seat));
|
|
1579
2215
|
if (forfeit) seat.accounts.player_rewards = null;
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
2216
|
+
const groups = rest.flatMap((x) => [...seatGroup(x.seat.accounts.seat, x.seat, x.rolls.box, x.rolls.order), ...x.prized]);
|
|
2217
|
+
return lootIx('unstake_nft', { forfeit, nonce }, { ...seat.accounts, loot_box: first.rolls.box, order: first.rolls.order, associated_token_program: parts.some((x) => x.prize?.Tokens) ? ASSOCIATED_TOKEN_PROGRAM : null }, {
|
|
2218
|
+
remaining: [...seat.proof, ...first.prized, ...groups, ...tailOf(extra)],
|
|
2219
|
+
writable: [...(forfeit ? [] : payWrites(farm)), ...seat.writable],
|
|
1583
2220
|
});
|
|
1584
2221
|
};
|
|
1585
2222
|
|
|
2223
|
+
/** The nonce each NFT's mission roll is named by in an `unstakeNftIx` of several (`assets`), in the order the call
|
|
2224
|
+
* takes them: the `i`th by its seat's address `nonce + 2i` (its order at `namedRollNonce` of it), its prize's roll
|
|
2225
|
+
* `nonce + 2i + 1` (at `prizeRollNonce`). */
|
|
2226
|
+
export const seatNonces = (project, id, farm, player, assets, nonce = 0n) =>
|
|
2227
|
+
Object.fromEntries(ascending(assets, (a) => pda.seat(pda.farm(project, id), farm.farmed.Tokens ? player : a)).map((a, i) => [a, toBigInt(nonce) + 2n * BigInt(i)]));
|
|
2228
|
+
|
|
1586
2229
|
/**
|
|
1587
|
-
* `player` collects what the seat of `by` owes (their own address for tokens, a collectible's asset)
|
|
2230
|
+
* `player` collects what the seat of `by` owes (their own address for tokens, a collectible's asset); with `compound`, a
|
|
2231
|
+
* token seat's is staked instead (a seat with a booster on needs a `refreshSeatIx` of it first: `stakeRefreshFirst`). `held`: in a farm
|
|
1588
2232
|
* that watches, what shows the player still holds the seat's NFT (the Core asset, their token account of a Token
|
|
1589
|
-
* Metadata NFT, or a compressed NFT's record, `pda.leaf`, with its `tree`), which such a farm refuses to go without
|
|
2233
|
+
* Metadata NFT, or a compressed NFT's record, `pda.leaf`, with its `tree`), which such a farm refuses to go without
|
|
2234
|
+
* (`NotProven`). `more` seats of the player's in the farm (each { by, held, tree }) collect in the same call, one fee,
|
|
2235
|
+
* one transfer.
|
|
1590
2236
|
*/
|
|
1591
|
-
export const harvestIx = ({ player, project, id, farm, by = player, held = null, tree = null, compound = false, programs, extra = [] }) =>
|
|
1592
|
-
|
|
2237
|
+
export const harvestIx = ({ player, project, id, farm, by = player, held = null, tree = null, compound = false, programs, more = [], extra = [] }) => {
|
|
2238
|
+
const farmAddress = pda.farm(project, id);
|
|
2239
|
+
const [first, ...rest] = ascending([{ by, held, tree }, ...more].map((x) => ({ ...x, seat: pda.seat(farmAddress, x.by) })), (x) => x.seat);
|
|
2240
|
+
const groups = rest.flatMap((x) => [meta(x.seat, false, true), meta(x.held ?? LOOT_PROGRAM, false, false), meta(x.tree ?? LOOT_PROGRAM, false, false)]);
|
|
2241
|
+
return lootIx('harvest', { compound }, {
|
|
1593
2242
|
...farmSide(project, id, farm),
|
|
1594
2243
|
player,
|
|
1595
|
-
seat:
|
|
2244
|
+
seat: first.seat,
|
|
1596
2245
|
player_rewards: ata(player, farm.rewards_mint, tokenProgramOf(programs, farm.rewards_mint)),
|
|
1597
|
-
held,
|
|
1598
|
-
tree,
|
|
2246
|
+
held: first.held,
|
|
2247
|
+
tree: first.tree,
|
|
1599
2248
|
// Compounding moves what is owed into the staked vault (a farm of tokens whose rewards are the staked token).
|
|
1600
|
-
staked_vault: compound ? pda.staked(
|
|
1601
|
-
|
|
1602
|
-
|
|
2249
|
+
staked_vault: compound ? pda.staked(farmAddress) : null,
|
|
2250
|
+
}, { remaining: [...groups, ...tailOf(extra)], writable: payWrites(farm) });
|
|
2251
|
+
};
|
|
1603
2252
|
|
|
1604
2253
|
/**
|
|
1605
2254
|
* Anyone (`signer`) writes what a seat's NFT grew (a farm with growth), then reads its boosts again. `seat` is the
|
|
@@ -1615,7 +2264,7 @@ export const refreshSeatIx = ({ signer, project, id, farm, by, seat, collection,
|
|
|
1615
2264
|
const p = seat.asset === NO_ADDRESS
|
|
1616
2265
|
? standIn(address)
|
|
1617
2266
|
: piece({ project, asset: seat.asset, wallet: holder, collection, hasAuthority, tokenMetadata: tm, compressed, ...farmKeeps(farm, { writes: Boolean(farm.growth?.length) }), moves: false, locks: false });
|
|
1618
|
-
return lootIx('refresh_seat', {}, { signer, ...farmSide(project, id, farm), seat: address, ...p.accounts }, { remaining: extra, writable:
|
|
2267
|
+
return lootIx('refresh_seat', {}, { signer, ...farmSide(project, id, farm), seat: address, ...p.accounts, profile: profileOf(project, farm, seat.player) }, { remaining: tailOf(extra), writable: p.writable });
|
|
1619
2268
|
};
|
|
1620
2269
|
|
|
1621
2270
|
// ---------------------------------------------------------------- recipes and holds
|
|
@@ -1624,19 +2273,23 @@ export const refreshSeatIx = ({ signer, project, id, farm, by, seat, collection,
|
|
|
1624
2273
|
* `player` crafts recipe `id` (`recipe` decoded, the version shown to them), `times` over. `assets`: the
|
|
1625
2274
|
* collectibles handed in, in the recipe's order (the burned, the used, and for a `Show` line the NFT shown: its address
|
|
1626
2275
|
* when it is the project's, `{ asset, collection }` for a Core NFT of another collection, `{ mint, owner }` for a Token
|
|
1627
|
-
* Metadata one
|
|
1628
|
-
*
|
|
1629
|
-
*
|
|
2276
|
+
* Metadata one, `{ compressed, tree }` for a compressed one; with `farm` (a farm's id: `{ asset, farm }` for the
|
|
2277
|
+
* project's own) when the project locks it in the player's wallet on a seat of that farm, which freezes, and a raise on
|
|
2278
|
+
* 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
|
+
* compressed }`). A collectible used that is staked in a farm holding it for the player (escrow) or locking it in their
|
|
2280
|
+
* 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`). A `Staked` line names the farm its collectible
|
|
1630
2282
|
* is staked in (`Input::Staked { item, farm }`, escrowed or frozen there): its address, or `{ asset, farm }` with that
|
|
1631
2283
|
* farm, and its seat and that farm follow it. Each item line passes its item's collection
|
|
1632
2284
|
* (`items[id].collection`, the decoded Items; `collection` when not given), written for a burn: Core writes it as it
|
|
1633
2285
|
* burns a collectible. A recipe's `raises` write on the kept ones, in their Sheet when the project has no
|
|
1634
2286
|
* authority over them. The player opens any account a line pays into that is not there. A roll it makes (a
|
|
1635
|
-
* `Prize::Roll` or a `RollOn`) passes its box
|
|
1636
|
-
*
|
|
2287
|
+
* `Prize::Roll` or a `RollOn`) passes its box and its order, nothing more; a `RollOn`'s order is where `namedRollNonce`
|
|
2288
|
+
* says, which reads the rolled box (`boxes`: a box's id to its decoded LootBox). Its player or the project's keys then ask for the roll
|
|
2289
|
+
* (`askRollIx`, in the same transaction when it fits).
|
|
1637
2290
|
* A craft makes several Core calls: raise the compute limit. `extra`: a token's hook accounts.
|
|
1638
2291
|
*/
|
|
1639
|
-
export function craftIx({ player, project, collection, id, recipe, assets = [], times = 1, nonce = 0n, holders, programs, treasury, poolMints, items = {}, nfts = {}, boxes = {},
|
|
2292
|
+
export function craftIx({ player, project, collection, id, recipe, assets = [], times = 1, nonce = 0n, holders, programs, treasury, poolMints, items = {}, nfts = {}, boxes = {}, extra = [] }) {
|
|
1640
2293
|
const remaining = [];
|
|
1641
2294
|
const given = [...assets];
|
|
1642
2295
|
const tokens = (mint) => ata(player, mint, tokenProgramOf(programs, mint));
|
|
@@ -1645,8 +2298,8 @@ export function craftIx({ player, project, collection, id, recipe, assets = [],
|
|
|
1645
2298
|
if (input.Burn) {
|
|
1646
2299
|
remaining.push(...itemLine(input.Burn.item, true));
|
|
1647
2300
|
for (let i = 0; i < input.Burn.count; i++) remaining.push(meta(take(given, 'a collectible to burn'), false, true));
|
|
1648
|
-
} else if (input.Use || input.
|
|
1649
|
-
remaining.push(...itemLine((input.Use ?? input.
|
|
2301
|
+
} else if (input.Use || input.Staked) {
|
|
2302
|
+
remaining.push(...itemLine((input.Use ?? input.Staked).item));
|
|
1650
2303
|
const used = take(given, 'a collectible to use');
|
|
1651
2304
|
// A staked one is on its seat in the farm its line names.
|
|
1652
2305
|
if (input.Staked && typeof used !== 'string' && used.farm !== undefined && Number(used.farm) !== input.Staked.farm)
|
|
@@ -1670,11 +2323,19 @@ export function craftIx({ player, project, collection, id, recipe, assets = [],
|
|
|
1670
2323
|
// with its Sheet, then its collection), a Token Metadata NFT ({ mint, owner }: its metadata, the holder's
|
|
1671
2324
|
// token account and its Sheet), or a compressed NFT ({ compressed, tree }: its record, proven earlier in the
|
|
1672
2325
|
// transaction, its tree and its Sheet). What a raise writes on is writable.
|
|
2326
|
+
// One the project locks in the player's wallet on a seat of a farm that freezes (`farm`, that farm's id) has its
|
|
2327
|
+
// seat and farm after its accounts, when a raise on it lowers a number.
|
|
2328
|
+
const place = assets.length - given.length;
|
|
1673
2329
|
const nft = take(given, 'an NFT to show');
|
|
1674
|
-
if (typeof nft === 'string') remaining.push(meta(nft, false, true), meta(input.Show.collection, false, false));
|
|
2330
|
+
if (typeof nft === 'string' || (nft.asset && nft.farm !== undefined && !('collection' in nft))) remaining.push(meta(nft.asset ?? nft, false, true), meta(input.Show.collection, false, false));
|
|
1675
2331
|
else if (nft.asset) remaining.push(meta(nft.asset, false, true), meta(pda.sheet(project, nft.asset), false, true), meta(nft.collection ?? MPL_CORE_PROGRAM, false, false));
|
|
1676
2332
|
else if (nft.compressed) remaining.push(...[nft.compressed, pda.leaf(nft.compressed), nft.tree].map((key) => meta(key, false, false)), meta(pda.sheet(project, nft.compressed), false, true));
|
|
1677
2333
|
else remaining.push(...[nft.mint, tokenMetadata.metadata(nft.mint), ata(nft.owner ?? player, nft.mint, TOKEN_PROGRAM)].map((key) => meta(key, false, false)), meta(pda.sheet(project, nft.mint), false, true));
|
|
2334
|
+
const lowers = (recipe.raises ?? []).some((raise) => Number(raise.place) === place && BigInt(raise.shift.by) < 0n);
|
|
2335
|
+
if (lowers && typeof nft !== 'string' && nft.farm !== undefined && nft.farm !== null) {
|
|
2336
|
+
const farm = pda.farm(project, nft.farm);
|
|
2337
|
+
remaining.push(meta(pda.seat(farm, nft.asset ?? nft.compressed ?? nft.mint), false, false), meta(farm, false, false));
|
|
2338
|
+
}
|
|
1678
2339
|
}
|
|
1679
2340
|
}
|
|
1680
2341
|
const made = {};
|
|
@@ -1683,16 +2344,18 @@ export function craftIx({ player, project, collection, id, recipe, assets = [],
|
|
|
1683
2344
|
// A prize (`prizeMetas`): collectibles of one item made in order, each at the next serial.
|
|
1684
2345
|
const prize = output.Prize[0];
|
|
1685
2346
|
const item = prize.Item?.[0];
|
|
1686
|
-
remaining.push(...prizeMetas({ project, collection, prize, winner: player, nonce, poolMints, items, nft: nfts[prize.Nft?.nft], made: made[item] ?? 0, programs
|
|
2347
|
+
remaining.push(...prizeMetas({ project, collection, prize, winner: player, nonce, poolMints, items, nft: nfts[prize.Nft?.nft], made: made[item] ?? 0, programs }));
|
|
1687
2348
|
if (item !== undefined) made[item] = (made[item] ?? 0) + 1;
|
|
1688
2349
|
} else if (output.RollOn) {
|
|
1689
|
-
// The box, then the order its roll is made in
|
|
1690
|
-
|
|
2350
|
+
// The box, then the order its roll is made in (`namedRollNonce` of the craft's `nonce`).
|
|
2351
|
+
const on = assets[output.RollOn.place];
|
|
2352
|
+
const order = pda.order(project, player, namedRollNonce(boxes[output.RollOn.box_id], typeof on === 'string' ? on : on?.asset ?? on?.mint ?? on?.compressed, nonce));
|
|
2353
|
+
remaining.push(meta(pda.box(project, output.RollOn.box_id), false, false), meta(order, false, true));
|
|
1691
2354
|
}
|
|
1692
2355
|
}
|
|
1693
2356
|
const address = pda.recipe(project, id);
|
|
1694
|
-
const
|
|
1695
|
-
return lootIx('craft', { version: recipe.version, times, nonce }, { player, project, recipe: address,
|
|
2357
|
+
const counts = limits(recipe.per_wallet) ? pda.counts(project, player) : null;
|
|
2358
|
+
return lootIx('craft', { version: recipe.version, times, nonce }, { player, project, recipe: address, counts }, { remaining: [...remaining, ...tailOf(extra)] });
|
|
1696
2359
|
}
|
|
1697
2360
|
|
|
1698
2361
|
function take(list, what) {
|
|
@@ -1708,9 +2371,16 @@ export const craftNewAssets = (recipe) => recipe.outputs.filter((o) => o.Prize?.
|
|
|
1708
2371
|
* asset (`collection` its own, Core's address for none), a Token Metadata NFT (`tokenMetadata`), or `compressed` (after
|
|
1709
2372
|
* a `proveLeafIx`).
|
|
1710
2373
|
*/
|
|
1711
|
-
export function holdIx({ player, project, collection, asset, tokenMetadata: tm, compressed, freeAt, reference: ref }) {
|
|
1712
|
-
|
|
1713
|
-
|
|
2374
|
+
export function holdIx({ player, project, collection, asset, tokenMetadata: tm, compressed, freeAt, reference: ref, more = [] }) {
|
|
2375
|
+
// Several NFTs (`more`: each { asset, collection, tokenMetadata, compressed }) go on holds of their own with one end, one
|
|
2376
|
+
// fee: by their holds' addresses, the first the call's own.
|
|
2377
|
+
const [first, ...rest] = ascending([{ asset, collection, tokenMetadata: tm, compressed }, ...more].map((n) => {
|
|
2378
|
+
const p = piece({ project, asset: n.asset, wallet: player, collection: n.collection, tokenMetadata: n.tokenMetadata, compressed: n.compressed, ...ONLY_MOVES });
|
|
2379
|
+
return { p, hold: pda.hold(p.accounts.asset, freeAt) };
|
|
2380
|
+
}), (x) => x.hold);
|
|
2381
|
+
const p = withPrograms(first.p, rest.map((x) => x.p));
|
|
2382
|
+
const groups = rest.flatMap((x) => [meta(x.hold, false, true), ...anotherMetas(x.p)]);
|
|
2383
|
+
return lootIx('hold', { free_at: toBigInt(freeAt), reference: reference(ref) }, { player, project, ...p.accounts, hold: first.hold }, { remaining: [...p.proof, ...groups], writable: p.writable });
|
|
1714
2384
|
}
|
|
1715
2385
|
|
|
1716
2386
|
// ---------------------------------------------------------------- stashes
|
|
@@ -1719,10 +2389,13 @@ export function holdIx({ player, project, collection, asset, tokenMetadata: tm,
|
|
|
1719
2389
|
* The project's owner or authority (`signer`) puts an NFT it holds into stash `id`, or (`out`: a wallet) takes one the
|
|
1720
2390
|
* stash holds out to that wallet. The NFT is any kind, as `piece` takes it: `collection` a Core asset's own,
|
|
1721
2391
|
* `tokenMetadata`, or `compressed` after a `proveLeafIx` in the same transaction. The signer pays what the stash grows
|
|
1722
|
-
* by, and gets back the rent it no longer needs.
|
|
1723
|
-
*
|
|
2392
|
+
* by, and gets back the rent it no longer needs. `drop`: the NFT leaves the stash and stays where it is, owed to the
|
|
2393
|
+
* treasury in its hold (`["hold", nft, 0]`, the signer paying its rent until anyone `release`s it to the treasury, one
|
|
2394
|
+
* NFT a call). `out` is never the project's own address nor another account of the program (`ToTheProject`).
|
|
1724
2395
|
*/
|
|
1725
|
-
export function stashIx({ signer, project, id, lottery = null, asset, out = null, collection, tokenMetadata: tm, compressed }) {
|
|
2396
|
+
export function stashIx({ signer, project, id, lottery = null, lotteryState = null, asset, out = null, drop = false, weight = 1, collection, tokenMetadata: tm, compressed, more = [] }) {
|
|
2397
|
+
if (drop && more.length) throw new Error('A stash drops one NFT a call.');
|
|
2398
|
+
if (drop) out = pda.hold(compressed?.id ?? asset, 0);
|
|
1726
2399
|
const to = out ?? signer;
|
|
1727
2400
|
// A lottery's own stash (`lottery`: the lottery's id): an NFT its place names leaves only as that prize, or once the
|
|
1728
2401
|
// lottery has ended; taking one out passes the lottery.
|
|
@@ -1731,10 +2404,19 @@ export function stashIx({ signer, project, id, lottery = null, asset, out = null
|
|
|
1731
2404
|
// Out to a wallet or the project's own address, never another account of the program (a record, a stash, a fee
|
|
1732
2405
|
// box), where no record names the NFT and nothing could move it again (ToTheProject): the ones this call names are
|
|
1733
2406
|
// refused before sending, and the program refuses any other (js/play only takes one out to the signer's wallet).
|
|
1734
|
-
if (out &&
|
|
1735
|
-
throw new Error('An NFT taken out of a stash can’t go to an account of the program: name a wallet, or
|
|
1736
|
-
|
|
1737
|
-
|
|
2407
|
+
if (out && !drop && [project, stash, own, pda.fees(project), pda.platform()].includes(out))
|
|
2408
|
+
throw new Error('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).');
|
|
2409
|
+
// Several NFTs (`more`: each { asset, weight, collection, tokenMetadata, compressed }) go in or out in one call, one
|
|
2410
|
+
// fee, in ascending order of their addresses; put in, each weighs its `weight` in the stash's draws.
|
|
2411
|
+
const [first, ...rest] = ascending([{ asset, weight, collection, tokenMetadata: tm, compressed }, ...more].map((n) => ({
|
|
2412
|
+
n, p: piece({ project, asset: n.asset, wallet: drop ? project : to, collection: n.collection, tokenMetadata: n.tokenMetadata, compressed: n.compressed, ...ONLY_MOVES }),
|
|
2413
|
+
})), (x) => x.p.accounts.asset);
|
|
2414
|
+
const p = withPrograms(first.p, rest.map((x) => x.p));
|
|
2415
|
+
const weights = out ? [] : [first, ...rest].map((x) => x.n.weight ?? 1);
|
|
2416
|
+
// A lottery's own stash names the lottery, and the round it waits on (`idleRound`; `lotteryState`: the decoded
|
|
2417
|
+
// Lottery; none for one never written), whose places say what it locks.
|
|
2418
|
+
const round = own && lotteryState ? idleRound(own, lotteryState) : null;
|
|
2419
|
+
return lootIx('stash', { of: own ? { Lottery: [lottery] } : { Project: [id] }, out: Boolean(out), weights }, { signer, project, stash, to, ...p.accounts, lottery: own, round }, { remaining: [...p.proof, ...rest.flatMap((x) => anotherMetas(x.p))], writable: p.writable });
|
|
1738
2420
|
}
|
|
1739
2421
|
|
|
1740
2422
|
// ---------------------------------------------------------------- contests
|
|
@@ -1743,7 +2425,7 @@ export function stashIx({ signer, project, id, lottery = null, asset, out = null
|
|
|
1743
2425
|
* `player` backs `side` with `amount`, naming the contest's incarnation (which fixes its terms). `contest` is the
|
|
1744
2426
|
* decoded Contest. A contest of NFTs (`contest.nft`) takes one NFT instead (`nft`: { asset, collection, tokenMetadata,
|
|
1745
2427
|
* compressed }, as `piece` takes it), which the contest holds until it is claimed. `shown`/`listProof` pass its gate.
|
|
1746
|
-
* `extra`: a hook's accounts.
|
|
2428
|
+
* A new entry's record is the player's to pay (not refunded). `extra`: a hook's accounts.
|
|
1747
2429
|
*/
|
|
1748
2430
|
export function enterIx({ player, project, id, contest, side, amount, shown, listProof, nft, playerToken, programs, extra = [] }) {
|
|
1749
2431
|
const address = pda.contest(project, id);
|
|
@@ -1756,85 +2438,112 @@ export function enterIx({ player, project, id, contest, side, amount, shown, lis
|
|
|
1756
2438
|
player,
|
|
1757
2439
|
project,
|
|
1758
2440
|
contest: address,
|
|
1759
|
-
|
|
2441
|
+
entries: contestEntries(address, contest),
|
|
1760
2442
|
player_token: playerToken ?? ata(player, contest.mint, tokenProgramOf(programs, contest.mint)),
|
|
1761
2443
|
mint: contest.mint,
|
|
1762
2444
|
},
|
|
1763
|
-
{ remaining: [...gateAccounts(project, { gate: contest.gate, wallet: player, shown, listProof }), ...(staked ? [...pieceMetas(staked), ...staked.proof] : []), ...extra] },
|
|
2445
|
+
{ remaining: [...gateAccounts(project, { gate: contest.gate, wallet: player, shown, listProof }), ...(staked ? [...pieceMetas(staked), ...staked.proof] : []), ...tailOf(extra)] },
|
|
1764
2446
|
);
|
|
1765
2447
|
}
|
|
1766
2448
|
|
|
1767
2449
|
/**
|
|
1768
|
-
* Anyone (`payer`) claims `player`'s entry
|
|
1769
|
-
*
|
|
1770
|
-
*
|
|
1771
|
-
*
|
|
1772
|
-
*
|
|
1773
|
-
*
|
|
1774
|
-
*
|
|
1775
|
-
*
|
|
2450
|
+
* Anyone (`payer`) claims `player`'s entry (`entries`: the contest's decoded entries, `decodeEntries`), into the
|
|
2451
|
+
* player's account of the mint, which `payer` opens when it is not there. A winner's first claim also gives the
|
|
2452
|
+
* contest's prize (`prizeMetas`: `collection`, `poolMints`, `items`, `nft`; a roll's order at `nonce`, `prizeNonce` of the
|
|
2453
|
+
* contest and the player by default), unless the player (signing) `forgo`es it. In a contest of NFTs it moves the NFTs of
|
|
2454
|
+
* the entries `nfts` names by their place in the list (the player's own by default, while the contest holds it): an
|
|
2455
|
+
* entry's own back to its player (anyone sends it); a loser's to the winner while it has picks left of its share (only
|
|
2456
|
+
* the winner sends it, and picks which: `WinnerPicks`), or to the opener once no winner has picks left. `kinds` maps an
|
|
2457
|
+
* NFT to its kind, as `piece` takes it. With `hold` (true, or a list of assets), each becomes a hold for the wallet it goes to (an NFT that can't
|
|
2458
|
+
* move now); with `end`, the winner leaves the rest of its share to the opener. The claim that leaves nothing owed
|
|
2459
|
+
* closes the contest (`projectState`: its treasury); a contest that owes nothing (nobody entered, every part claimed,
|
|
2460
|
+
* off by itself with none in) is closed by any claim, the `payer`'s. `extra`: a hook's accounts.
|
|
1776
2461
|
*/
|
|
1777
|
-
export function claimIx({ payer, project, id, contest,
|
|
2462
|
+
export function claimIx({ payer, project, projectState, id, contest, entries, player, nfts, kinds = {}, nonce, collection, poolMints, items, nft, prizeMint, forgo = false, end = false, hold = false, programs, extra = [] }) {
|
|
1778
2463
|
const address = pda.contest(project, id);
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
2464
|
+
// A contest that owes nothing (nobody entered, or every part claimed) is only closed: by any claim, entry 0, any player.
|
|
2465
|
+
const closing = BigInt(contest.owed) === 0n;
|
|
2466
|
+
player ??= payer;
|
|
2467
|
+
const i = closing ? 0 : entries.findIndex((e) => e.player === player);
|
|
2468
|
+
if (i < 0) throw new Error(`${player} has no entry in this contest.`);
|
|
2469
|
+
const me = closing ? { side: 0, bits: CLAIMED | ENDED, amount: 0n, taken: 0 } : entries[i];
|
|
2470
|
+
nonce = toBigInt(nonce ?? prizeNonce(address, player));
|
|
2471
|
+
const won = contest.verdict === 'Won' && Number(contest.winner) === Number(me.side);
|
|
2472
|
+
const first = won && !(me.bits & CLAIMED);
|
|
2473
|
+
// 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
|
+
// the contest's vault (named accounts below), not given as a prize line.
|
|
1784
2475
|
const split = Boolean(contest.prize_split);
|
|
1785
|
-
const prize = contest.prize != null &&
|
|
2476
|
+
const prize = contest.prize != null && first && !split && !forgo ? prizeMetas({ project, collection, prize: contest.prize, winner: player, nonce, poolMints, items, nft, programs }) : [];
|
|
1786
2477
|
const pMint = split ? splitMint(contest.prize, prizeMint ?? poolMints?.[contest.prize?.Tokens?.from.Pool?.[0]]) : null;
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
2478
|
+
nfts ??= !closing && contest.nft && Number(me.amount) === 1 ? [i] : [];
|
|
2479
|
+
const moved = [];
|
|
2480
|
+
let taken = Number(me.taken);
|
|
2481
|
+
for (const j of nfts) {
|
|
2482
|
+
const theirs = entries[j];
|
|
2483
|
+
let to = contest.opener;
|
|
2484
|
+
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)) [to, taken] = [player, taken + 1];
|
|
2486
|
+
moved.push(meta(theirs.player, false, true));
|
|
2487
|
+
if (holds(hold, theirs.nft)) moved.push(meta(pda.hold(theirs.nft, 0), false, true));
|
|
2488
|
+
else {
|
|
2489
|
+
const p = piece({ project, asset: theirs.nft, wallet: to, ...kinds[theirs.nft], ...ONLY_MOVES });
|
|
2490
|
+
moved.push(...pieceMetas(p), ...p.proof);
|
|
1796
2491
|
}
|
|
1797
2492
|
}
|
|
2493
|
+
// What it leaves owed: at 0 it closes the contest, burning what its pot (and a split prize's vault) still hold.
|
|
2494
|
+
const owed = BigInt(contest.owed);
|
|
2495
|
+
const left = closing ? 0n : !contest.nft ? owed - (contest.verdict === 'Won' ? (first ? BigInt(me.amount) : 0n) : 1n) : owed - BigInt(nfts.length) - (first && contest.prize ? 1n : 0n);
|
|
2496
|
+
// 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) throw new Error('This claim closes the contest: pass projectState, for its treasury.');
|
|
1798
2498
|
return lootIx(
|
|
1799
2499
|
'claim',
|
|
1800
|
-
{ nonce, forgo },
|
|
2500
|
+
{ entry: i, nonce, forgo, end, nfts },
|
|
1801
2501
|
{
|
|
1802
2502
|
payer,
|
|
1803
2503
|
project,
|
|
1804
2504
|
contest: address,
|
|
1805
|
-
|
|
2505
|
+
entries: contestEntries(address, contest),
|
|
1806
2506
|
player,
|
|
1807
2507
|
opener: contest.opener,
|
|
1808
2508
|
to: ata(player, contest.mint, tokenProgramOf(programs, contest.mint)),
|
|
1809
2509
|
mint: contest.mint,
|
|
1810
2510
|
prize_mint: pMint,
|
|
2511
|
+
// A split prize from a pool: what its vault holds at the close goes back to that pool (`WrongPool` without it).
|
|
2512
|
+
prize_pool: split && contest.prize?.Tokens?.from?.Pool ? pda.pool(project, contest.prize.Tokens.from.Pool[0]) : null,
|
|
1811
2513
|
prize_vault: split ? pda.prizeVault(address) : null,
|
|
1812
2514
|
prize_to: split ? ata(player, pMint, tokenProgramOf(programs, pMint)) : null,
|
|
2515
|
+
treasury: projectState?.treasury ?? null,
|
|
1813
2516
|
},
|
|
1814
|
-
|
|
1815
|
-
{ writable: Number(contest.entries) === 1 ? ['mint', ...(split ? ['prize_mint'] : [])] : [], remaining: [...nfts, ...prize, ...extra] },
|
|
2517
|
+
{ writable: left <= 0n ? ['mint', ...(split ? ['prize_mint'] : [])] : [], remaining: [...moved, ...prize, ...tailOf(extra)] },
|
|
1816
2518
|
);
|
|
1817
2519
|
}
|
|
1818
2520
|
|
|
1819
2521
|
// ---------------------------------------------------------------- auctions
|
|
1820
2522
|
|
|
1821
|
-
/**
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
2523
|
+
/**
|
|
2524
|
+
* What a taker signs for an auction, an offer or a listing, as the program works it out (`rail::terms`): sha256 of the
|
|
2525
|
+
* record's bytes, all but the span `[start, end)` that changes as it is taken. `record` is the account's bytes as read,
|
|
2526
|
+
* or its decoded value, written back to the same bytes (`encodeAccount`).
|
|
2527
|
+
*/
|
|
2528
|
+
function recordTerms(name, record, span) {
|
|
2529
|
+
const data = record instanceof Uint8Array ? record : encodeAccount(IDL, name, record);
|
|
2530
|
+
const [start, end] = span(data);
|
|
2531
|
+
return sha256(concat(data.subarray(0, start), data.subarray(end)));
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
/**
|
|
2535
|
+
* What a bid signs: the auction's bytes but the bid, which sits last (`high_bidder`, `high`, `last_bid_at`, `held`:
|
|
2536
|
+
* 60 bytes, and an auction of NFT bids' room for `MAX_BID_NFTS` after them). `auction`: the decoded Auction, or its
|
|
2537
|
+
* account's bytes.
|
|
2538
|
+
*/
|
|
2539
|
+
export function auctionTerms(auction) {
|
|
2540
|
+
if (auction instanceof Uint8Array) {
|
|
2541
|
+
const room = decode('Auction', auction).nft_bids ? 32 * MAX_BID_NFTS : 0;
|
|
2542
|
+
return recordTerms('Auction', auction, (data) => [data.length - 60 - room, data.length]);
|
|
2543
|
+
}
|
|
2544
|
+
// Written back with no bid's NFTs, its bytes end at the bid's 60.
|
|
2545
|
+
return recordTerms('Auction', { ...auction, gate: auction.gate ?? null, nft_bids: auction.nft_bids ?? null, held: [] }, (data) => [data.length - 60, data.length]);
|
|
2546
|
+
}
|
|
1838
2547
|
|
|
1839
2548
|
/** When bids close, as the program reckons it: `ends_at`, or `extend` seconds after the last bid when that is later. */
|
|
1840
2549
|
export const auctionEnds = (auction) => {
|
|
@@ -1843,23 +2552,26 @@ export const auctionEnds = (auction) => {
|
|
|
1843
2552
|
return late > BigInt(auction.ends_at) ? late : BigInt(auction.ends_at);
|
|
1844
2553
|
};
|
|
1845
2554
|
|
|
1846
|
-
/** The least next bid the program takes, or null when no u64 amount
|
|
2555
|
+
/** The least next bid the program takes, or null when nothing can top it (no u64 amount; a bid of NFTs past `MAX_BID_NFTS`). */
|
|
1847
2556
|
export function leastBid(auction) {
|
|
1848
2557
|
const high = BigInt(auction.high);
|
|
1849
2558
|
if (high === 0n) return BigInt(auction.reserve) > 0n ? BigInt(auction.reserve) : 1n;
|
|
1850
2559
|
const raise = (high * BigInt(auction.min_raise_bps) + 9_999n) / 10_000n;
|
|
1851
2560
|
const next = high + (raise > 0n ? raise : 1n);
|
|
1852
|
-
|
|
2561
|
+
// A bid of NFTs holds at most `MAX_BID_NFTS`: past it, nobody can top the high bid.
|
|
2562
|
+
return next <= (auction.nft_bids ? BigInt(MAX_BID_NFTS) : (1n << 64n) - 1n) ? next : null;
|
|
1853
2563
|
}
|
|
1854
2564
|
|
|
1855
2565
|
/**
|
|
1856
2566
|
* `seller` opens its auction `id` of `args.lot` ({ Item: [id] }, { Asset: [address] } or { Roll: [box id] }), alone,
|
|
1857
2567
|
* and pays its rent: a player for an NFT of the project's own collection they hold; the project's owner or authority for an
|
|
1858
2568
|
* NFT of any kind it holds (`piece`: `collection` a Core asset's own, `tokenMetadata`, or `compressed` after a
|
|
1859
|
-
* `proveLeafIx`), a new unit of its item, or a roll of one of its boxes (a mystery lot
|
|
1860
|
-
*
|
|
2569
|
+
* `proveLeafIx`), a new unit of its item, or a roll of one of its boxes (a mystery lot: the box must be written, burn
|
|
2570
|
+
* no share of its price, and make an order with the auction's shares within one call's growth: `LotRefused`). Its
|
|
2571
|
+
* address names the seller, so an id is the seller's own.
|
|
1861
2572
|
*/
|
|
1862
2573
|
export function openAuctionIx({ seller, project, collection, tokenMetadata: tm, compressed, id, mint, args }) {
|
|
2574
|
+
stored(args.shares);
|
|
1863
2575
|
const auction = pda.auction(project, seller, id);
|
|
1864
2576
|
const asset = args.lot.Asset?.[0] ?? null;
|
|
1865
2577
|
// An item lot reads no NFT: the auction's own address stands in.
|
|
@@ -1872,14 +2584,19 @@ export function openAuctionIx({ seller, project, collection, tokenMetadata: tm,
|
|
|
1872
2584
|
);
|
|
1873
2585
|
}
|
|
1874
2586
|
|
|
2587
|
+
/** The most NFTs one bid of an auction of NFT bids holds (`MAX_BID_NFTS`, not in the IDL): a bid of more fails
|
|
2588
|
+
* (`BidTooLow`), and a high bid of this many can't be topped. Its reserve is 1 to this many (`BadAuction`). */
|
|
2589
|
+
export const MAX_BID_NFTS = 3;
|
|
2590
|
+
|
|
1875
2591
|
/**
|
|
1876
2592
|
* `bidder` bids `amount` on auction `id`, signing its terms; the bid it tops goes back to the high bidder's account
|
|
1877
2593
|
* of the mint, which `bidder` opens when it is not there. `auction` is the decoded Auction. `shown`/`listProof`: what
|
|
1878
2594
|
* passes the auction's gate. An auction of NFT bids (`auction.nft_bids`) takes `nfts` instead ([{ asset, collection,
|
|
1879
2595
|
* tokenMetadata }], as `piece` takes each; never a compressed NFT: `CompressedBid`), handed to the project, and gives the high bid's NFTs
|
|
1880
|
-
* (`held`, the kinds of `auction.held`, in order) back to its bidder
|
|
2596
|
+
* (`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
|
+
* list of assets) becomes a hold for them instead, `bidder` paying its rent until its release. `extra`: a hook's accounts.
|
|
1881
2598
|
*/
|
|
1882
|
-
export function bidIx({ bidder, project, id, auction, amount, shown, listProof, nfts = [], held = [], bidderToken, programs, extra = [] }) {
|
|
2599
|
+
export function bidIx({ bidder, project, id, auction, amount, shown, listProof, nfts = [], held = [], hold = false, bidderToken, programs, extra = [] }) {
|
|
1883
2600
|
const tokenProgram = tokenProgramOf(programs, auction.mint);
|
|
1884
2601
|
const outbid = BigInt(auction.high) > 0n;
|
|
1885
2602
|
const bundle = [];
|
|
@@ -1890,13 +2607,14 @@ export function bidIx({ bidder, project, id, auction, amount, shown, listProof,
|
|
|
1890
2607
|
bundle.push(...pieceMetas(p), ...p.proof);
|
|
1891
2608
|
}
|
|
1892
2609
|
(auction.held ?? []).forEach((asset, i) => {
|
|
2610
|
+
if (holds(hold, asset)) return bundle.push(meta(pda.hold(asset, 0), false, true));
|
|
1893
2611
|
const p = piece({ project, asset, wallet: auction.high_bidder, ...held[i], ...ONLY_MOVES });
|
|
1894
2612
|
bundle.push(...pieceMetas(p), ...p.proof);
|
|
1895
2613
|
});
|
|
1896
2614
|
}
|
|
1897
2615
|
return lootIx(
|
|
1898
2616
|
'bid',
|
|
1899
|
-
{ amount: auction.nft_bids ? BigInt(nfts.length) : amount, terms: auctionTerms(auction
|
|
2617
|
+
{ amount: auction.nft_bids ? BigInt(nfts.length) : amount, terms: auctionTerms(auction) },
|
|
1900
2618
|
{
|
|
1901
2619
|
bidder,
|
|
1902
2620
|
project,
|
|
@@ -1908,7 +2626,7 @@ export function bidIx({ bidder, project, id, auction, amount, shown, listProof,
|
|
|
1908
2626
|
mint: auction.mint,
|
|
1909
2627
|
},
|
|
1910
2628
|
// The vault takes a bid in tokens; NFT bids leave it alone, unlocked.
|
|
1911
|
-
{ remaining: [...gateAccounts(project, { gate: auction.gate, wallet: bidder, shown, listProof }), ...bundle, ...extra], writable: auction.nft_bids ? [] : ['vault'] },
|
|
2629
|
+
{ remaining: [...gateAccounts(project, { gate: auction.gate, wallet: bidder, shown, listProof }), ...bundle, ...tailOf(extra)], writable: auction.nft_bids ? [] : ['vault'] },
|
|
1912
2630
|
);
|
|
1913
2631
|
}
|
|
1914
2632
|
|
|
@@ -1920,28 +2638,43 @@ export function bidIx({ bidder, project, id, auction, amount, shown, listProof,
|
|
|
1920
2638
|
* roll), however long after the end it is sent. With no bid, the lot goes home. `signer` opens any account the end pays into that is
|
|
1921
2639
|
* not there. `item` is the decoded Item of an item lot; for an NFT lot, `collection` its Core collection now (the
|
|
1922
2640
|
* project's when left out), `tokenMetadata` for a Token Metadata NFT, or `compressed` (after a `proveLeafIx`).
|
|
1923
|
-
|
|
1924
|
-
|
|
2641
|
+
* `hold` (anyone may; true for all, or a list of assets): an end that wouldn't fit otherwise (a compressed lot's long
|
|
2642
|
+
* proof, a hooked mint, many shares) makes the NFT lot a hold for whoever it goes to (its hold passed as `order`, no
|
|
2643
|
+
* 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
|
+
* then delivers each with `releaseIx`, and the end's sender gets the rent back then.
|
|
2645
|
+
*/
|
|
2646
|
+
export function endAuctionIx({ signer, project, projectState, id, auction, item = null, collection, tokenMetadata: tm, compressed, held = [], hold = false, reference: ref, nonce, holders, programs, extra = [] }) {
|
|
2647
|
+
// The lot went at an earlier end, and its seller's account couldn't take the money then (`proceeds_owed`): this end pays
|
|
2648
|
+
// it and closes the auction, naming no lot, share or winning NFT.
|
|
2649
|
+
const owed = BigInt(auction.proceeds_owed ?? 0) > 0n;
|
|
2650
|
+
const lotHeld = !owed && Boolean(auction.lot.Asset) && holds(hold, auction.lot.Asset[0]);
|
|
1925
2651
|
const tokenProgram = tokenProgramOf(programs, auction.mint);
|
|
1926
2652
|
const sold = BigInt(auction.high) > 0n;
|
|
1927
|
-
const itemId = auction.lot.Item?.[0];
|
|
1928
|
-
const made = sold && item?.kind === 'Collectible' ? pda.itemAsset(pda.item(project, itemId), auction.
|
|
1929
|
-
const asset = auction.lot.Asset?.[0] ?? made;
|
|
2653
|
+
const itemId = owed ? undefined : auction.lot.Item?.[0];
|
|
2654
|
+
const made = !owed && sold && item?.kind === 'Collectible' ? pda.itemAsset(pda.item(project, itemId), auction.pin) : null;
|
|
2655
|
+
const asset = owed ? null : auction.lot.Asset?.[0] ?? made;
|
|
1930
2656
|
const proceeds = auction.lot.Asset ? auction.seller : projectState.treasury;
|
|
1931
2657
|
const address = pda.auction(project, auction.seller, id);
|
|
1932
2658
|
const receiver = sold ? auction.high_bidder : auction.seller;
|
|
1933
|
-
// A roll lot sold: its box, and the order the winner's roll is made in.
|
|
1934
|
-
|
|
2659
|
+
// A roll lot sold: its box, and the order the winner's roll is made in. The winning bid stays in the vault as that
|
|
2660
|
+
// order's payment, split by the auction's shares when it settles (no shares' accounts here).
|
|
2661
|
+
const roll = sold && !owed ? auction.lot.Roll?.[0] : undefined;
|
|
1935
2662
|
nonce = toBigInt(nonce ?? prizeNonce(address, BigInt(auction.last_bid_at)));
|
|
1936
2663
|
// An NFT lot is sent on; a collectible sold is minted into its item's collection, which Core writes.
|
|
1937
|
-
|
|
2664
|
+
// With `hold`, an NFT lot is its address alone (no proof), and becomes a hold for whoever it goes to.
|
|
2665
|
+
const p = owed
|
|
2666
|
+
? standIn(address)
|
|
2667
|
+
: lotHeld
|
|
2668
|
+
? standIn(asset)
|
|
2669
|
+
: auction.lot.Asset
|
|
1938
2670
|
? piece({ project, asset, wallet: receiver, collection, tokenMetadata: tm, compressed, ...ONLY_MOVES })
|
|
1939
2671
|
: asset
|
|
1940
2672
|
? { ...piece({ project, asset, collection: item.collection, moves: false, locks: false, reads: false }), writable: ['collection'] }
|
|
1941
2673
|
: standIn(address);
|
|
1942
2674
|
// A winning bid of NFTs: each to where the rest of a bid in tokens would go (`held`, their kinds in order).
|
|
1943
|
-
const winning = sold && auction.nft_bids
|
|
2675
|
+
const winning = !owed && sold && auction.nft_bids
|
|
1944
2676
|
? (auction.held ?? []).flatMap((nft, i) => {
|
|
2677
|
+
if (holds(hold, nft)) return [meta(pda.hold(nft, 0), false, true)];
|
|
1945
2678
|
const q = piece({ project, asset: nft, wallet: proceeds, ...held[i], ...ONLY_MOVES });
|
|
1946
2679
|
return [...pieceMetas(q), ...q.proof];
|
|
1947
2680
|
})
|
|
@@ -1962,11 +2695,11 @@ export function endAuctionIx({ signer, project, projectState, id, auction, item
|
|
|
1962
2695
|
item: itemId !== undefined ? pda.item(project, itemId) : null,
|
|
1963
2696
|
...p.accounts,
|
|
1964
2697
|
loot_box: roll !== undefined ? pda.box(project, roll) : null,
|
|
1965
|
-
order: roll !== undefined ? pda.order(project, auction.high_bidder, prizeRollNonce(nonce)) : null,
|
|
2698
|
+
order: roll !== undefined ? pda.order(project, auction.high_bidder, prizeRollNonce(nonce)) : lotHeld ? pda.hold(asset, 0) : null,
|
|
1966
2699
|
},
|
|
1967
2700
|
{
|
|
1968
|
-
remaining: [...p.proof, ...(sold ? shareAccounts(project, auction.shares, auction.mint, { holders, programs, treasury: projectState.treasury }) : []), ...winning, ...extra],
|
|
1969
|
-
writable: [...(sold && sharesBurn(auction.shares) ? ['mint'] : []), ...(sold && !auction.nft_bids ? ['vault'] : []), ...p.writable],
|
|
2701
|
+
remaining: [...p.proof, ...(!owed && sold && roll === undefined ? shareAccounts(project, auction.shares, auction.mint, { holders, programs, treasury: projectState.treasury }) : []), ...winning, ...tailOf(extra)],
|
|
2702
|
+
writable: [...(!owed && sold && sharesBurn(auction.shares) ? ['mint'] : []), ...(sold && !auction.nft_bids ? ['vault'] : []), ...p.writable],
|
|
1970
2703
|
},
|
|
1971
2704
|
);
|
|
1972
2705
|
}
|
|
@@ -1977,11 +2710,9 @@ export function endAuctionIx({ signer, project, projectState, id, auction, item
|
|
|
1977
2710
|
// attributes, a Token Metadata or compressed NFT with none, so an attribute test fails for an NFT that doesn't carry
|
|
1978
2711
|
// the key itself (every Token Metadata or compressed one).
|
|
1979
2712
|
|
|
1980
|
-
/**
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
return sha256(concat(addressBytes(offer.mint), borsh('Rule', offer.rule), borsh('Deal', offer.deal), encodeBorsh(IDL, { vec: { defined: { name: 'Share' } } }, offer.shares)));
|
|
1984
|
-
}
|
|
2713
|
+
/** What a taker signs: the offer's bytes but `left`, its last 2 (what a take changes). `offer`: the decoded Offer, or its
|
|
2714
|
+
* account's bytes. */
|
|
2715
|
+
export const offerTerms = (offer) => recordTerms('Offer', offer, (data) => [data.length - 2, data.length]);
|
|
1985
2716
|
|
|
1986
2717
|
/** What one take of a deal pays out of its offer's money: a bid's price, a loan's amount. */
|
|
1987
2718
|
export const dealUnit = (deal) => BigInt(deal.Buy?.price ?? deal.Lend.amount);
|
|
@@ -1992,12 +2723,13 @@ export const dealUnit = (deal) => BigInt(deal.Buy?.price ?? deal.Lend.amount);
|
|
|
1992
2723
|
* lasts } }, left, shares } (shares only on a price). Its address names the maker, so an id is the maker's own.
|
|
1993
2724
|
*/
|
|
1994
2725
|
export function openOfferIx({ maker, project, id, mint, args, makerToken, programs, extra = [] }) {
|
|
2726
|
+
stored(args.shares);
|
|
1995
2727
|
const tokenProgram = tokenProgramOf(programs, mint);
|
|
1996
2728
|
return lootIx(
|
|
1997
2729
|
'open_offer',
|
|
1998
2730
|
{ id, args: { shares: [], ...args } },
|
|
1999
2731
|
{ maker, project, offer: pda.offer(project, maker, id), mint, maker_token: makerToken ?? ata(maker, mint, tokenProgram), vault: vault(project, mint, tokenProgram) },
|
|
2000
|
-
{ remaining: extra },
|
|
2732
|
+
{ remaining: tailOf(extra) },
|
|
2001
2733
|
);
|
|
2002
2734
|
}
|
|
2003
2735
|
|
|
@@ -2008,7 +2740,7 @@ export function closeOfferIx({ maker, project, id, offer, programs, extra = [] }
|
|
|
2008
2740
|
'close_offer',
|
|
2009
2741
|
{},
|
|
2010
2742
|
{ maker, project, offer: pda.offer(project, maker, id), mint: offer.mint, vault: vault(project, offer.mint, tokenProgram), maker_token: ata(maker, offer.mint, tokenProgram) },
|
|
2011
|
-
{ remaining: extra },
|
|
2743
|
+
{ remaining: tailOf(extra) },
|
|
2012
2744
|
);
|
|
2013
2745
|
}
|
|
2014
2746
|
|
|
@@ -2026,7 +2758,7 @@ export function sellToOfferIx({ seller, project, projectState, id, offer, asset,
|
|
|
2026
2758
|
{ terms: offerTerms(offer) },
|
|
2027
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) },
|
|
2028
2760
|
{
|
|
2029
|
-
remaining: [...p.proof, ...shareAccounts(project, offer.shares, offer.mint, { holders, programs, treasury: projectState?.treasury }), ...extra],
|
|
2761
|
+
remaining: [...p.proof, ...shareAccounts(project, offer.shares, offer.mint, { holders, programs, treasury: projectState?.treasury }), ...tailOf(extra)],
|
|
2030
2762
|
writable: [...(sharesBurn(offer.shares) ? ['mint'] : []), ...p.writable],
|
|
2031
2763
|
},
|
|
2032
2764
|
);
|
|
@@ -2048,38 +2780,37 @@ export function borrowIx({ borrower, project, id, offer, due, asset, collection,
|
|
|
2048
2780
|
borrower, project, offer: pda.offer(project, offer.maker, id), maker: offer.maker, hold: pda.hold(p.accounts.asset, due), ...p.accounts,
|
|
2049
2781
|
mint: offer.mint, vault: vault(project, offer.mint, tokenProgram), borrower_token: ata(borrower, offer.mint, tokenProgram),
|
|
2050
2782
|
},
|
|
2051
|
-
{ remaining: [...p.proof, ...extra], writable: p.writable },
|
|
2783
|
+
{ remaining: [...p.proof, ...tailOf(extra)], writable: p.writable },
|
|
2052
2784
|
);
|
|
2053
2785
|
}
|
|
2054
2786
|
|
|
2055
2787
|
/**
|
|
2056
2788
|
* Anyone (`payer`: the borrower, a friend) repays a loan before it is due (`hold` the decoded Hold, with its debt): the
|
|
2057
2789
|
* debt's `repay` goes to the lender, and the NFT is the borrower's again. Only money moves: anyone then sends the NFT
|
|
2058
|
-
* home with `releaseIx` (in the same transaction, or later: a repaid loan's release has no deadline). Free. `
|
|
2790
|
+
* home with `releaseIx` (in the same transaction, or later: a repaid loan's release has no deadline). Free. `repay`: the
|
|
2791
|
+
* debt the payer was shown (`hold.debt.repay` as read): any other amount is refused (`TermsChanged`). `extra`: a
|
|
2059
2792
|
* transfer hook's accounts.
|
|
2060
2793
|
*/
|
|
2061
|
-
export function repayIx({ payer, project, hold, payerToken, programs, extra = [] }) {
|
|
2794
|
+
export function repayIx({ payer, project, hold, repay, payerToken, programs, extra = [] }) {
|
|
2795
|
+
if (repay === undefined || repay === null) throw new Error('A repay names the debt the payer was shown (repay: hold.debt.repay as read).');
|
|
2062
2796
|
const { lender, mint } = hold.debt;
|
|
2063
2797
|
const tokenProgram = tokenProgramOf(programs, mint);
|
|
2064
2798
|
return lootIx(
|
|
2065
2799
|
'repay',
|
|
2066
|
-
{},
|
|
2800
|
+
{ repay: toBigInt(repay) },
|
|
2067
2801
|
{
|
|
2068
2802
|
payer, project, hold: pda.hold(hold.asset, hold.free_at), lender,
|
|
2069
2803
|
lender_token: ata(lender, mint, tokenProgram), payer_token: payerToken ?? ata(payer, mint, tokenProgram), mint,
|
|
2070
2804
|
},
|
|
2071
|
-
{ remaining: extra },
|
|
2805
|
+
{ remaining: tailOf(extra) },
|
|
2072
2806
|
);
|
|
2073
2807
|
}
|
|
2074
2808
|
|
|
2075
2809
|
// ---------------------------------------------------------------- listings
|
|
2076
2810
|
|
|
2077
|
-
/** What a taker signs:
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
const shares = encodeBorsh(IDL, { vec: { defined: { name: 'Share' } } }, listing.shares ?? []);
|
|
2081
|
-
return sha256(concat(addressBytes(listing.maker), addressBytes(listing.asset), addressBytes(listing.mint), deal, shares));
|
|
2082
|
-
}
|
|
2811
|
+
/** What a taker signs: the listing's bytes but `user`, `until` and `stopped` (bytes 129 to 170, what a rental changes).
|
|
2812
|
+
* `listing`: the decoded Listing, or its account's bytes. */
|
|
2813
|
+
export const listingTerms = (listing) => recordTerms('Listing', listing, () => [129, 170]);
|
|
2083
2814
|
|
|
2084
2815
|
/**
|
|
2085
2816
|
* `maker` (any wallet, signing alone) lists an NFT they hold, of any kind (as `piece` takes it), for sale
|
|
@@ -2090,7 +2821,7 @@ export function listingTerms(listing) {
|
|
|
2090
2821
|
*/
|
|
2091
2822
|
export function openListingIx({ maker, project, asset, mint, deal, shares = [], collection, hasAuthority, tokenMetadata: tm, compressed, extra = [] }) {
|
|
2092
2823
|
const p = piece({ project, asset, wallet: maker, collection, hasAuthority, tokenMetadata: tm, compressed, ...ONLY_MOVES });
|
|
2093
|
-
return lootIx('open_listing', { deal, shares }, { maker, project, listing: pda.listing(project, p.accounts.asset), ...p.accounts, mint }, { remaining: [...p.proof, ...extra], writable: p.writable });
|
|
2824
|
+
return lootIx('open_listing', { deal, shares: stored(shares) }, { maker, project, listing: pda.listing(project, p.accounts.asset), ...p.accounts, mint }, { remaining: [...p.proof, ...tailOf(extra)], writable: p.writable });
|
|
2094
2825
|
}
|
|
2095
2826
|
|
|
2096
2827
|
/**
|
|
@@ -2116,7 +2847,7 @@ export function takeListingIx({ taker, project, projectState, listing, periods =
|
|
|
2116
2847
|
...p.accounts, mint: listing.mint, taker_token: takerToken ?? ata(taker, listing.mint, tokenProgram),
|
|
2117
2848
|
},
|
|
2118
2849
|
{
|
|
2119
|
-
remaining: [...p.proof, ...shareAccounts(project, listing.shares ?? [], listing.mint, { holders, programs, treasury: projectState?.treasury }), ...extra],
|
|
2850
|
+
remaining: [...p.proof, ...shareAccounts(project, listing.shares ?? [], listing.mint, { holders, programs, treasury: projectState?.treasury }), ...tailOf(extra)],
|
|
2120
2851
|
writable: [...(sharesBurn(listing.shares ?? []) ? ['mint'] : []), ...p.writable],
|
|
2121
2852
|
},
|
|
2122
2853
|
);
|
|
@@ -2128,7 +2859,7 @@ export function takeListingIx({ taker, project, projectState, listing, periods =
|
|
|
2128
2859
|
*/
|
|
2129
2860
|
export function closeListingIx({ maker, project, listing, collection, hasAuthority, tokenMetadata: tm, compressed, extra = [] }) {
|
|
2130
2861
|
const p = piece({ project, asset: listing.asset, wallet: maker, collection, hasAuthority, tokenMetadata: tm, compressed, ...ONLY_MOVES });
|
|
2131
|
-
return lootIx('close_listing', {}, { maker, project, listing: pda.listing(project, listing.asset), ...p.accounts }, { remaining: [...p.proof, ...extra], writable: p.writable });
|
|
2862
|
+
return lootIx('close_listing', {}, { maker, project, listing: pda.listing(project, listing.asset), ...p.accounts }, { remaining: [...p.proof, ...tailOf(extra)], writable: p.writable });
|
|
2132
2863
|
}
|
|
2133
2864
|
|
|
2134
2865
|
// ---------------------------------------------------------------- Token Metadata NFTs
|