@xgamesfun/solana 0.0.0-stage → 0.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/DISCLAIMER.md +41 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +13 -0
  4. package/README.md +172 -2
  5. package/bin/xsolana.mjs +189 -0
  6. package/examples/artwork-auction.json +51 -0
  7. package/examples/blind-forge.json +128 -0
  8. package/examples/coin-flip-arena.json +108 -0
  9. package/examples/duel-arena.json +137 -0
  10. package/examples/elements-set.json +281 -0
  11. package/examples/event-badges.json +67 -0
  12. package/examples/expedition-guild.json +161 -0
  13. package/examples/fair-presale.json +48 -0
  14. package/examples/falling-drop.json +90 -0
  15. package/examples/guild-missions.json +144 -0
  16. package/examples/halving-seasons.json +112 -0
  17. package/examples/hero-trials.json +96 -0
  18. package/examples/ideas.json +789 -0
  19. package/examples/level-ladder.json +65 -0
  20. package/examples/merge-2048.json +128 -0
  21. package/examples/nft-raffle.json +33 -0
  22. package/examples/paper-hands-farm.json +56 -0
  23. package/examples/partner-vault.json +115 -0
  24. package/examples/sacrifice-raffle.json +118 -0
  25. package/examples/scorched-treasury.json +84 -0
  26. package/examples/shelter-fair.json +164 -0
  27. package/examples/spark-pity.json +153 -0
  28. package/examples/stakers-draw.json +125 -0
  29. package/examples/star-fleet.json +157 -0
  30. package/examples/starforce.json +116 -0
  31. package/examples/stud-farm.json +134 -0
  32. package/examples/team-vesting.json +70 -0
  33. package/examples/trade-in-shop.json +168 -0
  34. package/js/approve/details.mjs +262 -0
  35. package/js/approve/ed25519.mjs +36 -0
  36. package/js/approve/live-check.mjs +144 -0
  37. package/js/approve/page.html +86 -0
  38. package/js/approve/page.js +203 -0
  39. package/js/approve/server.mjs +711 -0
  40. package/js/chain/base58.js +73 -0
  41. package/js/chain/borsh.js +261 -0
  42. package/js/chain/bytes.js +141 -0
  43. package/js/chain/core.js +169 -0
  44. package/js/chain/curve.js +52 -0
  45. package/js/chain/lookup.js +273 -0
  46. package/js/chain/mint.js +96 -0
  47. package/js/chain/pda.js +66 -0
  48. package/js/chain/programs.js +102 -0
  49. package/js/chain/rpc.js +217 -0
  50. package/js/chain/sha256.js +68 -0
  51. package/js/chain/token.js +85 -0
  52. package/js/chain/token2022.js +247 -0
  53. package/js/chain/tx.js +224 -0
  54. package/js/client/accounts.js +347 -0
  55. package/js/client/compressed.js +46 -0
  56. package/js/client/grant.js +240 -0
  57. package/js/client/idl/loot.js +2 -0
  58. package/js/client/idl/loot.json +17064 -0
  59. package/js/client/index.js +9 -0
  60. package/js/client/ix.js +2156 -0
  61. package/js/client/pda.js +110 -0
  62. package/js/client/policy.js +48 -0
  63. package/js/client/sizes.js +54 -0
  64. package/js/client/traits.js +44 -0
  65. package/js/connect/connect.mjs +182 -0
  66. package/js/economy/check.mjs +1991 -0
  67. package/js/economy/cost.mjs +170 -0
  68. package/js/economy/examples.mjs +114 -0
  69. package/js/economy/explain.mjs +235 -0
  70. package/js/economy/guide.mjs +231 -0
  71. package/js/economy/legal.mjs +15 -0
  72. package/js/economy/live.mjs +107 -0
  73. package/js/economy/model.mjs +583 -0
  74. package/js/economy/plan.mjs +887 -0
  75. package/js/economy/schema.json +2923 -0
  76. package/js/economy/secrets.mjs +60 -0
  77. package/js/economy/simulate.mjs +952 -0
  78. package/js/economy/txsize.mjs +45 -0
  79. package/js/economy/validate.mjs +85 -0
  80. package/js/index.js +10 -0
  81. package/js/mcp/server.mjs +113 -0
  82. package/js/mcp/tools.mjs +479 -0
  83. package/js/play/index.js +2109 -0
  84. package/js/results/index.js +184 -0
  85. package/js/results/wire.js +90 -0
  86. package/package.json +100 -4
  87. package/release.json +8 -0
  88. package/skills/xsolana-economy/SKILL.md +105 -0
  89. package/skills/xsolana-economy/references/after-launch.md +114 -0
  90. package/skills/xsolana-economy/references/apps.md +157 -0
  91. package/skills/xsolana-economy/references/sentence-to-spec.md +172 -0
  92. package/skills/xsolana-economy/references/telling-the-person.md +93 -0
  93. package/types/chain/base58.d.ts +10 -0
  94. package/types/chain/borsh.d.ts +23 -0
  95. package/types/chain/bytes.d.ts +45 -0
  96. package/types/chain/core.d.ts +50 -0
  97. package/types/chain/curve.d.ts +1 -0
  98. package/types/chain/lookup.d.ts +191 -0
  99. package/types/chain/mint.d.ts +25 -0
  100. package/types/chain/pda.d.ts +25 -0
  101. package/types/chain/programs.d.ts +65 -0
  102. package/types/chain/rpc.d.ts +66 -0
  103. package/types/chain/sha256.d.ts +1 -0
  104. package/types/chain/token.d.ts +141 -0
  105. package/types/chain/token2022.d.ts +53 -0
  106. package/types/chain/tx.d.ts +60 -0
  107. package/types/client/accounts.d.ts +178 -0
  108. package/types/client/compressed.d.ts +29 -0
  109. package/types/client/grant.d.ts +125 -0
  110. package/types/client/idl/loot.d.ts +4467 -0
  111. package/types/client/index.d.ts +8 -0
  112. package/types/client/ix.d.ts +1687 -0
  113. package/types/client/pda.d.ts +91 -0
  114. package/types/client/policy.d.ts +23 -0
  115. package/types/client/sizes.d.ts +13 -0
  116. package/types/client/traits.d.ts +12 -0
  117. package/types/economy/check.d.mts +105 -0
  118. package/types/economy/cost.d.mts +68 -0
  119. package/types/economy/examples.d.mts +27 -0
  120. package/types/economy/explain.d.mts +19 -0
  121. package/types/economy/guide.d.mts +140 -0
  122. package/types/economy/legal.d.mts +12 -0
  123. package/types/economy/live.d.mts +109 -0
  124. package/types/economy/model.d.mts +363 -0
  125. package/types/economy/plan.d.mts +1013 -0
  126. package/types/economy/secrets.d.mts +4 -0
  127. package/types/economy/simulate.d.mts +156 -0
  128. package/types/economy/txsize.d.mts +17 -0
  129. package/types/economy/validate.d.mts +3 -0
  130. package/types/index.d.ts +9 -0
  131. package/types/mcp/tools.d.mts +627 -0
  132. package/types/play/index.d.ts +469 -0
  133. package/types/results/index.d.ts +155 -0
  134. package/types/results/wire.d.ts +122 -0
@@ -0,0 +1,2156 @@
1
+ // Every loot instruction, built from the IDL. An instruction is
2
+ // { programId: base58, keys: [{ pubkey, isSigner, isWritable }], data: Uint8Array }
3
+ // as chain/tx.js serializes it. Browser-safe, no Solana SDK.
4
+ //
5
+ // Two layers:
6
+ // - `lootIx(name, args, accounts, options)` builds any instruction from its IDL entry. Accounts are named by their
7
+ // IDL leaf name (snake_case; nested groups such as an order's `checkout` are flattened). Fixed addresses, the
8
+ // platform, the project's fee box, the program's identity and every PDA whose seeds the IDL spells out from other
9
+ // accounts and arguments fill themselves; an optional account left out is passed as the program's own address
10
+ // (Anchor's "None"); anything else missing throws, naming it.
11
+ // - One builder per instruction (`payIx`, `openBoxIx`, …) that also adds the accounts that come after the
12
+ // instruction's own (a split's destinations, a box's entries, a recipe's lines) and sets the few flags a client
13
+ // changes (a mint written only when it burns or mints). They take the decoded account they act on (`accounts.js`)
14
+ // instead of reading the chain, so they are pure.
15
+ //
16
+ // Values are the IDL's own shapes (`chain/borsh.js`): addresses base58 strings, u64/u128/i64 bigints (or numbers),
17
+ // enums 'Variant', { Variant: [values] } or { Variant: { field: value } }, structs with snake_case fields.
18
+ import { addressBytes, encodeBase58, NO_ADDRESS } from '../chain/base58.js';
19
+ import { encodeBorsh, encodeInstruction } from '../chain/borsh.js';
20
+ import { concat, intLE, readUintLE, toBigInt, uintLE, utf8 } from '../chain/bytes.js';
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
+ import { allocateWithSeedIx, createAccountWithSeedIx, initializeAccount3Ix, MINT_BYTES, TOKEN_ACCOUNT_BYTES } from '../chain/token.js';
23
+ import { createAssociatedTokenAccountIdempotent, meta, transferLamports } from '../chain/programs.js';
24
+ import { sha256 } from '../chain/sha256.js';
25
+ import IDL from './idl/loot.js';
26
+ import { lotteryOwed, lotteryStep } from './accounts.js';
27
+ import { ata, BUBBLEGUM_PROGRAM, COMPRESSION_PROGRAM, LOOT_PROGRAM, MPL_CORE_PROGRAM, NOOP_PROGRAM, pda, vault, VRF_QUEUE } from './pda.js';
28
+
29
+ // The options of a call made without any: read-only, and typed so the generated declarations take any option.
30
+ const NO_OPTIONS = Object.freeze(/** @type {Record<string, any>} */ ({}));
31
+
32
+ export { IDL, ASSOCIATED_TOKEN_PROGRAM };
33
+
34
+ /** Token Metadata: a legacy NFT's or a pNFT's metadata lives at an address of its (`METAPLEX_METADATA_PROGRAM`). */
35
+ export const TOKEN_METADATA_PROGRAM = METAPLEX_METADATA_PROGRAM;
36
+
37
+ const ZERO_REFERENCE = new Uint8Array(32);
38
+ const hashToAddress = (hash) => encodeBase58(hash);
39
+
40
+ /** 32 bytes a project names a call by (a match id, a payout's reason). Zeros when not given. */
41
+ export function reference(value) {
42
+ if (value == null) return ZERO_REFERENCE;
43
+ if (typeof value === 'string') return sha256(utf8(value));
44
+ const bytes = Uint8Array.from(value);
45
+ if (bytes.length !== 32) throw new Error('A reference is 32 bytes, or a string that is hashed to 32.');
46
+ return bytes;
47
+ }
48
+
49
+ // ---------------------------------------------------------------- any instruction
50
+
51
+ const instructionDef = (name) => {
52
+ const def = IDL.instructions.find((i) => i.name === name);
53
+ if (!def) throw new Error(`Unknown loot instruction ${name}`);
54
+ return def;
55
+ };
56
+
57
+ /** The accounts of an instruction in order, each with its group (`checkout`, `fee`, `side`, `piece`) or ''. */
58
+ function leaves(list, group = '') {
59
+ return list.flatMap((a) => (a.accounts ? leaves(a.accounts, a.name) : [{ ...a, group }]));
60
+ }
61
+
62
+ /** Accounts that are the same wherever an instruction names them. The program checks each by address, which the
63
+ * IDL doesn't carry. */
64
+ const DEFAULTS = {
65
+ platform: () => pda.platform(),
66
+ fees: (known) => known.project && pda.fees(known.project),
67
+ program_identity: () => pda.identity(),
68
+ vrf_program_identity: () => pda.oracleIdentity(),
69
+ oracle_queue: () => VRF_QUEUE,
70
+ slot_hashes: () => SYSVAR_SLOT_HASHES,
71
+ instructions: () => SYSVAR_INSTRUCTIONS,
72
+ program_data: () => pda.programData(),
73
+ token_2022_program: () => TOKEN_2022_PROGRAM,
74
+ mpl_core_program: () => MPL_CORE_PROGRAM,
75
+ item_core_program: () => MPL_CORE_PROGRAM,
76
+ token_metadata_program: () => TOKEN_METADATA_PROGRAM,
77
+ bubblegum_program: () => BUBBLEGUM_PROGRAM,
78
+ compression_program: () => COMPRESSION_PROGRAM,
79
+ log_wrapper: () => NOOP_PROGRAM,
80
+ };
81
+
82
+ const SEED_SIZES = { u8: 1, u16: 2, u32: 4, u64: 8 };
83
+
84
+ /** The bytes of one IDL seed, or null when it can't be had from what is known (it reads an account's data). */
85
+ function seedBytes(seed, known, args, def) {
86
+ if (seed.kind === 'const') return Uint8Array.from(seed.value);
87
+ if (seed.kind === 'arg') {
88
+ const arg = def.args.find((a) => a.name === seed.path);
89
+ if (!arg || !(seed.path in args)) return null;
90
+ if (arg.type === 'pubkey') return addressBytes(args[seed.path]);
91
+ if (SEED_SIZES[arg.type]) return uintLE(args[seed.path], SEED_SIZES[arg.type]);
92
+ return null;
93
+ }
94
+ if (seed.kind === 'account') {
95
+ // "side.farm" names the account `farm` of the group `side`; a path into an account's data ("order.requested_slot")
96
+ // can't be resolved here.
97
+ const parts = seed.path.split('.');
98
+ const groups = new Set(def.accounts.filter((a) => a.accounts).map((a) => a.name));
99
+ const name = parts.length === 1 ? parts[0] : groups.has(parts[0]) && parts.length === 2 ? parts[1] : null;
100
+ return name && known[name] ? addressBytes(known[name]) : null;
101
+ }
102
+ return null;
103
+ }
104
+
105
+ /**
106
+ * Any loot instruction. `accounts` names accounts by their IDL leaf name; `null` for an optional one passes "None".
107
+ * Options: `remaining` (metas after the instruction's own), `writable` and `signer` (leaf names whose flag is set
108
+ * on, beyond the IDL's), `readonly` (leaf names whose writable flag is set off).
109
+ */
110
+ export function lootIx(name, args = {}, accounts = {}, { remaining = [], writable = [], signer = [], readonly = [] } = {}) {
111
+ const def = instructionDef(name);
112
+ const list = leaves(def.accounts);
113
+ const known = {};
114
+ for (const a of list) {
115
+ if (accounts[a.name] !== undefined) known[a.name] = accounts[a.name];
116
+ else if (a.address) known[a.name] = a.address;
117
+ }
118
+ // Defaults and PDAs, until nothing more resolves (a PDA's seeds may name an account derived the same way).
119
+ for (let pass = 0; pass < 4; pass++) {
120
+ let progress = false;
121
+ for (const a of list) {
122
+ if (a.name in known) continue;
123
+ const fallback = DEFAULTS[a.name]?.(known);
124
+ if (fallback) {
125
+ known[a.name] = fallback;
126
+ progress = true;
127
+ continue;
128
+ }
129
+ if (a.pda && !a.pda.program) {
130
+ const seeds = a.pda.seeds.map((s) => seedBytes(s, known, args, def));
131
+ if (seeds.every(Boolean)) {
132
+ known[a.name] = findProgramAddress(seeds, LOOT_PROGRAM)[0];
133
+ progress = true;
134
+ }
135
+ }
136
+ }
137
+ if (!progress) break;
138
+ }
139
+ const keys = list.map((a) => {
140
+ const key = known[a.name];
141
+ if (key === null || (key === undefined && a.optional)) {
142
+ if (!a.optional) throw new Error(`${name}: ${a.name} is not optional`);
143
+ return meta(LOOT_PROGRAM, false, false);
144
+ }
145
+ if (key === undefined) throw new Error(`${name}: missing account ${a.group ? `${a.group}.` : ''}${a.name}`);
146
+ const isWritable = (a.writable === true || writable.includes(a.name)) && !readonly.includes(a.name);
147
+ return meta(key, a.signer === true || signer.includes(a.name), isWritable);
148
+ });
149
+ return { programId: LOOT_PROGRAM, keys: [...keys, ...remaining], data: encodeInstruction(IDL, name, args) };
150
+ }
151
+
152
+ // ---------------------------------------------------------------- the accounts after an instruction's own
153
+
154
+ const tokenProgramOf = (programs, mint) => programs?.[mint] ?? TOKEN_PROGRAM;
155
+
156
+ /** A wallet, then its associated token account of `mint`: where money owed to it goes. */
157
+ const walletAccounts = (wallet, mint, programs) => [meta(wallet, false, false), meta(ata(wallet, mint, tokenProgramOf(programs, mint)), false, true)];
158
+
159
+ /**
160
+ * The accounts one destination needs, in the order the program reads them: a pool, a named token account, a wallet
161
+ * and its associated token account of `mint` (the project's `treasury` wallet for `Treasury`), or whoever holds a
162
+ * collectible (its asset, then the holder and their associated token account). The program opens a wallet's account
163
+ * or a pool when it is not there, paid by whoever sends the instruction. `holders` maps an asset to its holder now;
164
+ * for one nobody holds, the default address fills the holder's two places. A buyer's choice (`Chosen`) is written as
165
+ * what was chosen first (`chosenShares`).
166
+ */
167
+ export function destinationAccounts(project, to, mint, { holders = {}, programs, treasury } = NO_OPTIONS) {
168
+ if (to === 'Burn') return [];
169
+ if (to === 'Treasury') {
170
+ if (!treasury) throw new Error('A treasury share needs the project\'s treasury wallet (pass `treasury`).');
171
+ return walletAccounts(treasury, mint, programs);
172
+ }
173
+ if (to.Pool) return [meta(pda.pool(project, to.Pool[0]), false, true)];
174
+ if (to.Account) return [meta(to.Account[0], false, true)];
175
+ if (to.Wallet) return walletAccounts(to.Wallet[0], mint, programs);
176
+ if (to.Holder) {
177
+ const asset = to.Holder[0];
178
+ const holder = holders[asset];
179
+ return [meta(asset, false, false), ...(holder ? walletAccounts(holder, mint, programs) : [meta(NO_ADDRESS, false, false), meta(NO_ADDRESS, false, false)])];
180
+ }
181
+ if (to.Chosen) return [];
182
+ throw new Error(`Unknown destination ${JSON.stringify(to)}`);
183
+ }
184
+
185
+ /** A split's destinations, in order. */
186
+ export const shareAccounts = (project, shares, mint, options) => shares.flatMap((s) => destinationAccounts(project, s.to, mint, options));
187
+
188
+ /**
189
+ * A split with the buyer's choice written in, as the program does at `open_box` and `buy_tickets`: a wallet, the
190
+ * holder of the collectible chosen (a share with an item, which needs one), or the treasury with no choice. A
191
+ * contest's decision names its wallet the same way (`decideIx`).
192
+ */
193
+ export const chosenShares = (shares, choice) =>
194
+ shares.map((s) => {
195
+ if (!s.to.Chosen) return s;
196
+ if (!choice && s.to.Chosen.item != null) throw new Error('This share pays the holder of a collectible the buyer names: name one (`chosenRecipient`, `chosen`).');
197
+ if (!choice) return { ...s, to: 'Treasury' };
198
+ return { ...s, to: s.to.Chosen.item == null ? { Wallet: [choice] } : { Holder: [choice] } };
199
+ });
200
+
201
+ /** Whether a split burns, so the mint is written: it has a burn share. */
202
+ export const sharesBurn = (shares) => shares.some((s) => s.to === 'Burn');
203
+
204
+ /**
205
+ * 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
+ * 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.
208
+ */
209
+ export function entryAccounts(project, entries) {
210
+ return entries.flatMap(({ prizes }) => prizes.flatMap((prize) => {
211
+ if (prize.Item) return [meta(pda.item(project, prize.Item[0]), false, true)];
212
+ if (prize.Tokens?.from.Pool) return [meta(pda.pool(project, prize.Tokens.from.Pool[0]), false, true)];
213
+ const stash = prize.Nft?.stash ?? prize.Stash?.[0];
214
+ return stash === undefined ? [] : [meta(pda.stash(project, stash), false, true)];
215
+ }));
216
+ }
217
+
218
+ /**
219
+ * For each mint a box's tokens are in, in the order their prizes first name them, the mint (written when the roll
220
+ * mints from it) and the project's vault for it, where tokens won wait. `poolMints` maps a pool index to its mint (a
221
+ * pool not opened yet has none and is left out).
222
+ */
223
+ export function prizeAccounts(project, entries, poolMints = {}, programs) {
224
+ return prizeMints(entries, poolMints).map(({ mint, minted }) => [meta(mint, false, minted), meta(vault(project, mint, tokenProgramOf(programs, mint)), false, true)]).flat();
225
+ }
226
+
227
+ /** The mints a box's tokens are in, in the order their prizes first name them: { mint, minted }. */
228
+ function prizeMints(entries, poolMints = {}) {
229
+ const mints = [];
230
+ for (const prize of entries.flatMap((e) => e.prizes)) {
231
+ const from = prize.Tokens?.from;
232
+ if (!from) continue;
233
+ const mint = from.Mint ? from.Mint[0] : poolMints[from.Pool[0]];
234
+ if (!mint) continue;
235
+ const seen = mints.find((m) => m.mint === mint);
236
+ if (seen) seen.minted ||= Boolean(from.Mint);
237
+ else mints.push({ mint, minted: Boolean(from.Mint) });
238
+ }
239
+ return mints;
240
+ }
241
+
242
+ /** Each pool a box's prizes name, mapped to its mint: `poolMints`'s, or the box's own currency when it isn't given. */
243
+ function boxPoolMints(box, poolMints) {
244
+ const pools = box.entries.flatMap((e) => e.prizes.flatMap((p) => (p.Tokens?.from.Pool ? [p.Tokens.from.Pool[0]] : [])));
245
+ return Object.fromEntries(pools.map((pool) => [pool, poolMints && pool in poolMints ? poolMints[pool] : box.currency]));
246
+ }
247
+
248
+ /** Whether a box leaves its outcome to the oracle: more than one entry, or an entry gives a stash's NFT at random. */
249
+ export const boxRolls = (entries) => entries.length > 1 || entries.some((e) => e.prizes.some((p) => p.Stash));
250
+
251
+ /** What an order of `box` holds for the collectibles its roll may mint: a collectible's rent for each item the entry
252
+ * with the most of them gives. */
253
+ export const orderEscrow = (box) => ESCROW_LAMPORTS * BigInt(Math.max(0, ...box.entries.map((e) => e.prizes.filter((p) => p.Item).length)));
254
+
255
+ /**
256
+ * 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 (u8)). Like every prize's roll, its order is at
258
+ * `prizeRollNonce` of it.
259
+ */
260
+ export function rollNonce(order, requestedSlot, k) {
261
+ const hash = sha256(concat(utf8('roll'), addressBytes(order), uintLE(requestedSlot, 8), uintLE(k, 1)));
262
+ return readUintLE(hash, 0, 8);
263
+ }
264
+
265
+ /**
266
+ * The nonce of a roll on an NFT whose box moves its numbers, burns it or gives it away: the first
267
+ * 8 bytes, little endian, of its address. Its player has one such roll open on it at a time: the order's address is
268
+ * taken until it settles or is refunded. `openBoxIx` makes a box's roll on one there; a mission's roll (`unstakeNftIx`)
269
+ * and a recipe's (`craftIx`) take it as their `nonce`.
270
+ */
271
+ export const nonceOn = (asset) => readUintLE(addressBytes(asset), 0, 8);
272
+
273
+ /** Whether a box's roll holds the NFT it rolls on, or moves its numbers: an entry burns it, gives it
274
+ * 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 !== 'Back' || (f.shifts ?? []).length > 0);
276
+
277
+ /** Whether a limit for each wallet ({ total, per_window, window }) limits anything. */
278
+ const limits = (perWallet) => Boolean(perWallet && (Number(perWallet.total) || Number(perWallet.per_window)));
279
+
280
+ /**
281
+ * The accounts of an NFT the player shows, for a gate or for odds: right after the instruction's other accounts.
282
+ * `shown` is a Core asset the project has authority over (its address), one it has no authority over (`{ asset }`: then
283
+ * its Sheet's address), a Token Metadata NFT (`{ mint, owner }`: its mint, its metadata, the owner's token account of
284
+ * it, its Sheet's address), or a compressed NFT (`{ compressed: assetId, tree }`: its asset id, its record, which a
285
+ * `proveLeafIx` earlier in the same transaction writes, its tree, and its Sheet's address).
286
+ */
287
+ export function shownAccounts(project, shown) {
288
+ if (!shown) return [];
289
+ if (typeof shown === 'string') return [meta(shown, false, false)];
290
+ if (shown.asset) return [meta(shown.asset, false, false), meta(pda.sheet(project, shown.asset), false, false)];
291
+ if (shown.compressed) return [shown.compressed, pda.leaf(shown.compressed), shown.tree, pda.sheet(project, shown.compressed)].map((key) => meta(key, false, false));
292
+ return [shown.mint, tokenMetadata.metadata(shown.mint), ata(shown.owner, shown.mint, TOKEN_PROGRAM), pda.sheet(project, shown.mint)].map((key) => meta(key, false, false));
293
+ }
294
+
295
+ /**
296
+ * What a player passes a gate with, right after an instruction's own accounts: a list's proof (`Gate::List`: its
297
+ * nodes as accounts, from `merkleProof` of `gateListLeaf`s), then an NFT shown (`shownAccounts`) for a gate that reads
298
+ * one, or for a box's odds.
299
+ */
300
+ export const gateAccounts = (project, { gate, wallet, shown, listProof = [] } = NO_OPTIONS) => [
301
+ ...listProof.map((node) => meta(typeof node === 'string' ? node : encodeBase58(node), false, false)),
302
+ // A stake gate reads the wallet's token seat in its farm, then the farm (its tiers' locks), where an NFT shown would be.
303
+ ...(gate?.Staked ? [meta(pda.seat(gate.Staked.farm, wallet), false, false), meta(gate.Staked.farm, false, false)] : []),
304
+ ...shownAccounts(project, shown),
305
+ ];
306
+
307
+ /**
308
+ * A nonce a prize route names (its `nonce` argument), from what names the prize: a grant and its round, a lottery's
309
+ * contribution, a contest's entry, an auction and its last bid. Any nonce works (the sender picks it); the roll's order
310
+ * is then at `prizeRollNonce` of it (plus the place where the route says so), which no roll on an NFT is at.
311
+ */
312
+ export const prizeNonce = (...parts) => {
313
+ const hash = sha256(concat(utf8('loot:prize'), ...parts.map((p) => (typeof p === 'string' ? addressBytes(p) : p instanceof Uint8Array ? p : uintLE(p, 8)))));
314
+ return readUintLE(hash, 0, 8);
315
+ };
316
+
317
+ /**
318
+ * The nonce a roll given as a prize is made at: the first 8 bytes, little endian, of
319
+ * sha256("prize" | n (u64 LE)), `n` the nonce the route names (its argument, plus the place or `k` where it says so).
320
+ * Every roll given as a prize (a grant's, a lottery place's, a contest's, an auction's roll lot, a mission's chest, a
321
+ * recipe's `Prize::Roll`, a box's prize roll) is at `["order", project, winner, prizeRollNonce(n)]`. A roll an order
322
+ * won is made at settle at `rollNonce(order, slot, k)` as it is (hashed under "roll", so no named nonce reaches it). A roll on an NFT (`nonceOn`, a recipe's `RollOn`, a mission's
323
+ * `roll`) and a box's own order are not. Not `prizeNonce`, which makes the `n` a route names from its parts.
324
+ */
325
+ export const prizeRollNonce = (n) => {
326
+ // The program adds the place or `k` with `wrapping_add`: the same here.
327
+ const hash = sha256(concat(utf8('prize'), uintLE(BigInt.asUintN(64, toBigInt(n)), 8)));
328
+ return readUintLE(hash, 0, 8);
329
+ };
330
+
331
+ /** The NFT accounts every route takes as a `Piece`, in the program's order, with the writable flags the IDL has. */
332
+ const PIECE = leaves(instructionDef('hold').accounts).filter((a) => a.group === 'piece');
333
+
334
+ /** A `piece`'s accounts as metas, in the program's order: what a prize of an NFT names after its stash. */
335
+ function pieceMetas(p) {
336
+ return PIECE.map((a) => {
337
+ const key = p.accounts[a.name];
338
+ if (key == null) return meta(LOOT_PROGRAM, false, false);
339
+ return meta(key, false, a.writable === true || p.writable.includes(a.name));
340
+ });
341
+ }
342
+
343
+ /**
344
+ * The accounts a prize takes after a route's own, given to `winner`:
345
+ * Tokens the mint (written when minted), the pool when they come from one, the winner's account of the mint;
346
+ * Item the item; for a collectible, its collection (the Item's), its address at the item's next serial, Metaplex Core;
347
+ * 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)`; on a route the player pays or
349
+ * asks for (`strict`: a buy, a craft, a grant's claim), then the box's prize mints (`rollMints`);
350
+ * Held the lottery's own stash (`lottery`: the lottery's address), the NFT as `piece` takes it, then its proof.
351
+ * `poolMints` maps a pool's index to its mint (null for a pool never opened, which gives nothing: `projectMint` stands in
352
+ * for its mint); `items` an item's id to its decoded Item (for its next serial; `made`
353
+ * collectibles of the same item already made earlier in the instruction); `nft` the NFT's kind, as `piece` takes it
354
+ * ({ collection, tokenMetadata, compressed }). A strict roll's `boxes` maps the box's id to its decoded LootBox (null
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).
357
+ */
358
+ export function prizeMetas({ project, collection, prize, winner, nonce = 0n, poolMints = {}, items = {}, nft = {}, made = 0, projectMint, programs, lottery, strict = false, boxes = {}, rollPools = poolMints }) {
359
+ if (prize.Tokens) {
360
+ const { from } = prize.Tokens;
361
+ // A pool never opened (its mint given as null) gives nothing: any mint stands in for its place, the project's.
362
+ const pool = from.Pool?.[0];
363
+ const mint = from.Mint ? from.Mint[0] : (poolMints[pool] ?? (pool in poolMints ? projectMint : undefined));
364
+ if (!mint) throw new Error(`a prize from pool ${pool} needs that pool's mint (poolMints; null for a pool never opened, with projectMint)`);
365
+ const pooled = from.Pool ? [meta(pda.pool(project, pool), false, true)] : [];
366
+ return [meta(mint, false, Boolean(from.Mint)), ...pooled, meta(ata(winner, mint, tokenProgramOf(programs, mint)), false, true)];
367
+ }
368
+ if (prize.Item) {
369
+ const id = prize.Item[0];
370
+ const item = items[id];
371
+ if (item === undefined) throw new Error(`a prize of item ${id} needs the item (items: its decoded Item, or null for none written), for its kind and next serial`);
372
+ const address = pda.item(project, id);
373
+ // A normal item, or one never written, is its address alone; a collectible is minted into its own collection.
374
+ if (!item || item.kind !== 'Collectible') return [meta(address, false, true)];
375
+ const serial = Number(item.issued) + 1 + made;
376
+ return [meta(address, false, true), meta(item.collection ?? collection, false, true), meta(pda.itemAsset(address, serial), false, true), meta(MPL_CORE_PROGRAM, false, false)];
377
+ }
378
+ if (prize.Nft) {
379
+ const { stash, nft: asset } = prize.Nft;
380
+ const p = piece({ project, asset, wallet: winner, ...nft, ...ONLY_MOVES });
381
+ return [meta(pda.stash(project, stash), false, true), ...pieceMetas(p), ...p.proof];
382
+ }
383
+ if (prize.Held) {
384
+ // An NFT a lottery's place promises, from that lottery's own stash.
385
+ if (!lottery) throw new Error('A held NFT is a lottery\'s prize: name the lottery (`lottery`, its address).');
386
+ const p = piece({ project, asset: prize.Held[0], wallet: winner, ...nft, ...ONLY_MOVES });
387
+ return [meta(pda.lotteryStash(lottery), false, true), ...pieceMetas(p), ...p.proof];
388
+ }
389
+ if (prize.Stash) throw new Error('A stash\'s NFT at random is a box entry\'s prize only: elsewhere, a roll of a box that gives it.');
390
+ if (prize.Compressed) {
391
+ // A compressed NFT of the item, minted into the project's tree, in the item's collection.
392
+ const { item: id, tree } = prize.Compressed;
393
+ const item = items[id];
394
+ if (!item) throw new Error(`a compressed prize of item ${id} needs the item (items: its decoded Item), for its collection`);
395
+ return [
396
+ meta(pda.item(project, id), false, true), meta(pda.treeConfig(tree), false, true), meta(tree, false, true), meta(item.collection, false, true),
397
+ meta(BUBBLEGUM_PROGRAM, false, false), meta(COMPRESSION_PROGRAM, false, false), meta(NOOP_PROGRAM, false, false), meta(MPL_CORE_PROGRAM, false, false),
398
+ meta(BUBBLEGUM_CORE_SIGNER, false, false),
399
+ ];
400
+ }
401
+ const roll = [meta(pda.box(project, prize.Roll[0]), false, false), meta(pda.order(project, winner, prizeRollNonce(nonce)), false, true)];
402
+ return strict ? [...roll, ...strictRollMints(project, prize.Roll[0], boxes, rollPools)] : roll;
403
+ }
404
+
405
+ /**
406
+ * The accounts the program reads after a roll's box and order, to check the roll can be asked for, on a route the
407
+ * player pays or asks for (a craft's `Prize::Roll` or `RollOn`, a buy's roll, a grant's claim of one): for each of the
408
+ * box's token prizes, in order, its pool and then, for a pool opened, the pool's mint; for tokens minted, their mint. The program refuses the whole
409
+ * transaction (PrizeHasHook) when any of those mints has a transfer hook, so the player keeps what the route takes.
410
+ * `box` is the decoded LootBox (null for one never written: no roll is made, and nothing follows); `pools` maps each
411
+ * pool its prizes name to its mint, or null for a pool never opened (its address alone).
412
+ */
413
+ export function rollMints(project, box, pools = {}) {
414
+ if (!box) return [];
415
+ return box.entries.flatMap((e) => e.prizes).flatMap((prize) => {
416
+ const from = prize.Tokens?.from;
417
+ if (from?.Mint) return [meta(from.Mint[0], false, false)];
418
+ if (!from?.Pool) return [];
419
+ const index = from.Pool[0];
420
+ if (!(index in pools)) throw new Error(`a roll of box ${box.box_id} needs the mint of pool ${index} its prizes name (rollPools; null for a pool never opened)`);
421
+ return [meta(pda.pool(project, index), false, false), ...(pools[index] ? [meta(pools[index], false, false)] : [])];
422
+ });
423
+ }
424
+
425
+ /** `rollMints` of box `id` on a strict route: `boxes` must hold it (its decoded LootBox, or null for none written). */
426
+ function strictRollMints(project, id, boxes, pools) {
427
+ if (!(id in boxes)) throw new Error(`a roll of box ${id} on a route the player pays or asks for needs the box (boxes: its decoded LootBox, or null for one never written), for its prize mints`);
428
+ return rollMints(project, boxes[id], pools);
429
+ }
430
+
431
+ /** Bubblegum's signer for its calls to Core, `["mpl_core_cpi_signer"]`. */
432
+ export const BUBBLEGUM_CORE_SIGNER = 'CbNY3JiXdXNE9tPNEk1aRZVEkWdj2v7kfJLNQwZZgpXk';
433
+
434
+ /**
435
+ * The project's owner or authority (`signer`) makes a Bubblegum v2 tree the project alone mints compressed prizes into
436
+ * (`Prize::Compressed`). `tree` is a new account of mpl-account-compression of `treeSize(maxDepth, maxBufferSize,
437
+ * canopy)` bytes, made empty first in the same transaction (a System `createAccount` owned by mpl-account-compression, the tree's keypair signing).
438
+ */
439
+ export const openTreeIx = ({ signer, project, tree, maxDepth, maxBufferSize }) =>
440
+ lootIx('open_tree', { max_depth: maxDepth, max_buffer_size: maxBufferSize }, {
441
+ signer, project, tree, tree_config: pda.treeConfig(tree), bubblegum_program: BUBBLEGUM_PROGRAM, compression_program: COMPRESSION_PROGRAM, log_wrapper: NOOP_PROGRAM,
442
+ });
443
+
444
+ /** The bytes an mpl-account-compression tree of this shape takes (its header, changelog, rightmost path and canopy). */
445
+ export function treeSize(maxDepth, maxBufferSize, canopy) {
446
+ const canopyNodes = canopy === 0 ? 0 : 2 ** (canopy + 1) - 2;
447
+ return 56 + 24 + maxBufferSize * (32 + 32 * maxDepth + 8) + (32 * maxDepth + 32 + 8) + canopyNodes * 32;
448
+ }
449
+
450
+ /** The ticket that names what an oracle request is for: sha256("ticket" | order or lottery | the slot of its first ask).
451
+ * Every request of one roll, or of one round's draw, names it. Nothing lives there. */
452
+ export const ticketOf = (requester, slot) => {
453
+ const hash = sha256(concat(utf8('ticket'), addressBytes(requester), uintLE(slot, 8)));
454
+ return hashToAddress(hash);
455
+ };
456
+
457
+
458
+ // ---------------------------------------------------------------- the platform
459
+
460
+ /** Signed by the program's upgrade authority, once. `fees`: { player, project } lamports. */
461
+ export const initPlatformIx = ({ admin, fees }) => lootIx('init_platform', { fees }, { admin });
462
+
463
+ /** `nextAdmin` signs too when it is another key. */
464
+ export const setPlatformIx = ({ admin, fees, nextAdmin = admin }) => lootIx('set_platform', { fees }, { admin, next_admin: nextAdmin });
465
+
466
+ /** The platform collects the fees waiting in each project's fee box, into `to`. */
467
+ export const collectFeesIx = ({ admin, to, projects }) =>
468
+ lootIx('collect_fees', {}, { admin, to }, { remaining: projects.map((project) => meta(pda.fees(project), false, true)) });
469
+
470
+ export const setSuspendedIx = ({ admin, project, suspended }) => lootIx('set_suspended', { suspended }, { admin, project });
471
+
472
+ // ---------------------------------------------------------------- a project's settings
473
+
474
+ /**
475
+ * Opens project `id` (any number of `owner`'s own) at `pda.project(owner, id)`. `authority` signs payouts and makes,
476
+ * `treasury` is the wallet the remainder of every split goes to (its associated token account of each mint, which the
477
+ * program opens when it is not there). `collections`: the Metaplex Core collections it has authority over (none at
478
+ * all is fine; at most 8), each one whose update authority is the owner or the project, passed after the
479
+ * instruction's own accounts. A project names no token: each mechanic names the mint it takes or pays. Only the owner
480
+ * signs; the authority's and the treasury's accounts are passed too, and the program refuses a key nobody can sign
481
+ * for that it can tell (an account of loot or of a token program, a program: `BadKey`).
482
+ */
483
+ export const createProjectIx = ({ owner, id = 0, authority, treasury, collections = [] }) =>
484
+ lootIx('create_project', { id: toBigInt(id), authority, treasury, collections }, { owner, authority, treasury, project: pda.project(owner, id) }, { remaining: collections.map((c) => meta(c, false, false)) });
485
+
486
+ /**
487
+ * `update`: { authority?, treasury?, paused?, collections? }. `collections` replaces the project's list; every one in
488
+ * it is passed after the instruction's own accounts (the program reads the new ones). The owner pays for the project
489
+ * growing with its collections, and gets back what it shrinks by. A new owner (who signs too) is optional. A new
490
+ * authority or treasury comes with the account at its key (`new_authority`, `new_treasury`), which the program checks
491
+ * as `createProjectIx`'s.
492
+ */
493
+ export const updateProjectIx = ({ owner, project, authority = null, paused = null, treasury = null, collections = null, newOwner = null }) =>
494
+ lootIx('update_project', { update: { authority, treasury, paused, collections } }, { owner, project, new_owner: newOwner, new_authority: authority, new_treasury: treasury }, { remaining: (collections ?? []).map((c) => meta(c, false, false)) });
495
+
496
+ export const openPoolIx = ({ owner, project, index, mint }) => lootIx('open_pool', { index }, { owner, project, mint, pool: pda.pool(project, index) });
497
+
498
+ /** Where a record of the owner's lives: its kind's seed, the project and its id. */
499
+ export function recordAddress(project, def) {
500
+ const [kind] = Object.keys(def);
501
+ const body = def[kind];
502
+ switch (kind) {
503
+ case 'Item':
504
+ return pda.item(project, body.id);
505
+ case 'Box':
506
+ return pda.box(project, body.id);
507
+ case 'Schedule':
508
+ return pda.schedule(project, body.id);
509
+ case 'Lottery':
510
+ return pda.lottery(project, body.id);
511
+ case 'Recipe':
512
+ return pda.recipe(project, body.id);
513
+ case 'Grant':
514
+ return pda.grant(project, body.id);
515
+ default:
516
+ throw new Error(`Unknown record kind ${kind}`);
517
+ }
518
+ }
519
+
520
+ const NO_WINDOW = { opens_at: 0n, closes_at: 0n };
521
+ const NO_LIMIT = { total: 0, per_window: 0, window: 0 };
522
+ /**
523
+ * `def` with optional fields filled in (no window, no limit for each wallet, no key root, a fall on its own clock).
524
+ */
525
+ export function withDefaults(def) {
526
+ const [kind] = Object.keys(def);
527
+ const body = def[kind];
528
+ const args = { ...body.args };
529
+ if (kind === 'Box' || kind === 'Lottery' || kind === 'Grant') args.gate ??= null;
530
+ if (kind === 'Item') args.collection ??= NO_ADDRESS;
531
+ if (kind === 'Box') {
532
+ if (!args.currency) throw new Error('A box names the mint its price is in (`currency`).');
533
+ // An entry: up to three prizes (`prizeMetas`), whether it burns the payment, and its weight.
534
+ 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: [], burns: false, ...e }));
536
+ args.subject ??= null;
537
+ args.fates = (args.fates ?? []).map((f) => ({ ...f, shifts: (f.shifts ?? []).map((s) => ({ from_now: false, ...s })) }));
538
+ }
539
+ if (kind === 'Box' || kind === 'Recipe') {
540
+ args.window ??= NO_WINDOW;
541
+ args.per_wallet ??= NO_LIMIT;
542
+ }
543
+ if (kind === 'Recipe') args.raises = (args.raises ?? []).map((r) => ({ from_now: false, ...r }));
544
+ if (kind === 'Box' && args.fall) args.fall = { shared: false, ...args.fall };
545
+ if (kind === 'Lottery') {
546
+ args.per_wallet ??= 0;
547
+ args.nft_ticket ??= null;
548
+ args.returns ??= false;
549
+ args.min_sold ??= 0;
550
+ // A place is { bps, roll }; a number is a share of the pot that rolls nothing.
551
+ args.places = (args.places ?? []).map((p) => (typeof p === 'object' ? { prize: null, ...p } : { bps: p, prize: null }));
552
+ }
553
+ if (kind === 'Grant') {
554
+ args.key_root ??= new Uint8Array(32);
555
+ args.open ??= false;
556
+ }
557
+ return { [kind]: { ...body, args } };
558
+ }
559
+
560
+ /**
561
+ * 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, its
563
+ * funds and both token programs.
564
+ */
565
+ export function upsertIx({ owner, project, def: given, lottery = null }) {
566
+ const def = withDefaults(given);
567
+ const [kind] = Object.keys(def);
568
+ const { args } = def[kind];
569
+ let remaining = [];
570
+ // A new sealed schedule opens its own vault: its mint, the vault and both token programs.
571
+ const vest = kind === 'Schedule' ? args.action?.from?.Vest?.[0] : null;
572
+ if (vest) {
573
+ 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
+ }
581
+ if (kind === 'Lottery') {
582
+ remaining = [
583
+ meta(args.mint, false, false),
584
+ meta(pda.pot(recordAddress(project, def)), false, true),
585
+ meta(args.ticket_mint === NO_ADDRESS ? args.mint : args.ticket_mint, false, false),
586
+ meta(pda.lotteryFunds(recordAddress(project, def)), false, true),
587
+ meta(TOKEN_PROGRAM, false, false),
588
+ meta(TOKEN_2022_PROGRAM, false, false),
589
+ ];
590
+ // A place that holds an NFT (`Prize::Held`): the lottery's own stash, anywhere after them.
591
+ if (args.places.some((p) => p.prize?.Held)) remaining.push(meta(pda.lotteryStash(recordAddress(project, def)), false, false));
592
+ }
593
+ return lootIx('upsert', { def }, { owner, project, target: recordAddress(project, def) }, { remaining });
594
+ }
595
+
596
+ /**
597
+ * The owner closes the record at `target`, and its rent comes back: a grant's with the funds it held. A lottery
598
+ * (`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.
600
+ */
601
+ export function closeRecordIx({ owner, project, target, lottery, schedule, programs, to, paymentTo, extra = [] }) {
602
+ // A sealed schedule never closes (CannotClose): its address, and its vault's, stay bound to its terms.
603
+ if (schedule?.action?.from?.Vest) throw new Error('A sealed schedule never closes: its vault stays bound to its terms.');
604
+ if (!lottery) return lootIx('upsert', { def: null }, { owner, project, target });
605
+ const tokenProgram = tokenProgramOf(programs, lottery.mint);
606
+ const paymentMint = lotteryPaymentMint(lottery);
607
+ const remaining = [
608
+ meta(pda.pot(target), false, true),
609
+ meta(lottery.mint, false, true),
610
+ meta(to ?? ata(owner, lottery.mint, tokenProgram), false, true),
611
+ meta(pda.lotteryFunds(target), false, true),
612
+ meta(paymentMint, false, true),
613
+ meta(paymentTo ?? ata(owner, paymentMint, tokenProgramOf(programs, paymentMint)), false, true),
614
+ meta(TOKEN_PROGRAM, false, false),
615
+ meta(TOKEN_2022_PROGRAM, false, false),
616
+ ...extra,
617
+ ];
618
+ return lootIx('upsert', { def: null }, { owner, project, target }, { remaining });
619
+ }
620
+
621
+ // ---------------------------------------------------------------- in: money comes in
622
+
623
+ /**
624
+ * `player` (or a sponsor, or the project's own keys funding a pool) pays `amount` of `mint`, split at once by `shares`.
625
+ * The rest goes to the project's `treasury` wallet. The player opens any account this pays into that is not there.
626
+ */
627
+ export function payIx({ player, project, mint, treasury, amount, shares = [], reference: ref, playerToken, holders, programs, extra = [] }) {
628
+ const tokenProgram = tokenProgramOf(programs, mint);
629
+ return lootIx(
630
+ 'pay',
631
+ { amount, shares, reference: reference(ref) },
632
+ { 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'] : [] },
634
+ );
635
+ }
636
+
637
+ /**
638
+ * Opens a loot box (`box` the decoded LootBox at `boxAddress`), in an order at `nonce` (a box whose roll moves the
639
+ * numbers of the NFT it rolls on: at that NFT's, `nonceOn`, whatever `nonce` says). `poolMints` maps each pool its
640
+ * 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
+ * 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
+ * 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. A box of one entry with
644
+ * nothing random is rolled at once and takes no oracle accounts (a box that needs no roll at all sells with `buyIx`).
645
+ * 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 receipt (its rent theirs).
647
+ */
648
+ export function openBoxIx({ player, project, box, boxAddress, nonce, maxPrice, shown, listProof, subject, chosenRecipient = null, chosen = null, poolMints, playerToken, programs, extra = [] }) {
649
+ const mint = box.currency;
650
+ poolMints = boxPoolMints(box, poolMints);
651
+ const tokenProgram = tokenProgramOf(programs, mint);
652
+ const rolled = boxRolls(box.entries);
653
+ const address = boxAddress ?? pda.box(project, box.box_id);
654
+ // A box that rolls on an NFT takes it (`subject`: { asset, collection, hasAuthority, tokenMetadata, compressed }, as
655
+ // `piece` takes it); any other, its own address stands in.
656
+ // It is held until the roll settles when an entry burns it, gives it away or moves its numbers, and the roll is at its
657
+ // nonce.
658
+ const holds = boxHolds(box);
659
+ const on = box.subject ? piece({ project, wallet: player, ...subject, moves: holds, locks: false, writes: false }) : standIn(address);
660
+ if (box.subject && holds) nonce = nonceOn(on.accounts.asset);
661
+ // A list's proof, then the NFT shown for the gate and the odds (for a box that rolls on an NFT, a compressed one's
662
+ // proof instead: that NFT is the piece).
663
+ const gate = [...gateAccounts(project, { gate: box.gate, wallet: player, listProof }), ...(box.subject ? on.proof : shownAccounts(project, shown))];
664
+ return lootIx(
665
+ 'open_box',
666
+ { nonce, max_price: maxPrice ?? box.price, chosen_recipient: chosenRecipient, version: box.version },
667
+ {
668
+ loot_box: address,
669
+ player,
670
+ project,
671
+ mint,
672
+ player_token: playerToken ?? ata(player, mint, tokenProgram),
673
+ 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
+ chosen,
679
+ receipt: limits(box.per_wallet) ? pda.receipt(address, player) : null,
680
+ ...on.accounts,
681
+ },
682
+ { remaining: [...entryAccounts(project, box.entries), ...prizeAccounts(project, box.entries, poolMints, programs), ...gate, ...extra], writable: on.writable },
683
+ );
684
+ }
685
+
686
+ /**
687
+ * Buys a box that needs no roll (one entry, nothing random, no NFT to roll on: `boxRolls` false and no subject) in one
688
+ * instruction: the price is split at once (or burned, for an entry that burns) and the entry's prizes are given to
689
+ * the player (`prizeMetas`: `items` maps an item's id to its decoded Item, `poolMints` a pool's index to its mint,
690
+ * `nfts` an NFT a prize names to its kind; a roll's `boxes` and `rollPools`, for the rolled box's prize mints). No
691
+ * order. A prize that can't be given refuses the purchase. `treasury` is
692
+ * the project's treasury wallet; `holders` maps a collectible a share pays to its holder; a roll the entry gives is
693
+ * made at `nonce` (the next one for each roll after). `shown`/`listProof` pass the box's gate.
694
+ */
695
+ export function buyIx({ player, project, box, boxAddress, maxPrice, treasury, chosenRecipient = null, chosen = null, nonce = 0n, shown, listProof, items = {}, poolMints = {}, nfts = {}, boxes = {}, rollPools = poolMints, holders, playerToken, programs, extra = [] }) {
696
+ const mint = box.currency;
697
+ const tokenProgram = tokenProgramOf(programs, mint);
698
+ const address = boxAddress ?? pda.box(project, box.box_id);
699
+ const [entry] = box.entries;
700
+ const shares = chosenShares(box.shares, chosenRecipient);
701
+ const made = {};
702
+ const prizes = entry.prizes.flatMap((prize, k) => {
703
+ 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, strict: true, boxes, rollPools });
705
+ if (item !== undefined) made[item] = (made[item] ?? 0) + 1;
706
+ return lines;
707
+ });
708
+ return lootIx(
709
+ 'buy',
710
+ { max_price: maxPrice ?? box.price, chosen_recipient: chosenRecipient, version: box.version, nonce: toBigInt(nonce) },
711
+ {
712
+ player, project, loot_box: address, mint,
713
+ player_token: playerToken ?? ata(player, mint, tokenProgram),
714
+ treasury, treasury_token: ata(treasury, mint, tokenProgram),
715
+ chosen, receipt: limits(box.per_wallet) ? pda.receipt(address, player) : null,
716
+ },
717
+ {
718
+ remaining: [...gateAccounts(project, { gate: box.gate, wallet: player, shown, listProof }), ...shareAccounts(project, shares, mint, { holders, programs, treasury }), ...prizes, ...extra],
719
+ writable: entry.burns || sharesBurn(box.shares) ? ['mint'] : [],
720
+ },
721
+ );
722
+ }
723
+
724
+ /**
725
+ * The oracle's answer to a box order (for a local simulation: on chain MagicBlock's oracle sends it). The accounts
726
+ * are the ones `open_box` handed the oracle: the order, the box, the roll's ticket (named by its first ask,
727
+ * `asked_first`, which a stale re-ask keeps), every prize's account (written); for a
728
+ * box with tokens the two token programs, the project, and each mint with its vault (a mint it mints from written);
729
+ * for a prize in a mint with a transfer hook, the hook's accounts (`extra`). The buyer's odds boosts were fixed at
730
+ * purchase.
731
+ */
732
+ export function callbackRollIx({ order, orderState, box, boxAddress, randomness, project, poolMints, programs, extra = [] }) {
733
+ const projectAddress = project ?? orderState.project;
734
+ const keys = [
735
+ meta(pda.oracleIdentity(), true, false),
736
+ meta(order, false, true),
737
+ meta(boxAddress ?? orderState.loot_box, false, false),
738
+ meta(ticketOf(order, orderState.asked_first), false, false),
739
+ ...entryAccounts(orderState.project, box.entries),
740
+ ];
741
+ const prizes = prizeAccounts(orderState.project, box.entries, boxPoolMints(box, poolMints), programs);
742
+ 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);
744
+ return { programId: LOOT_PROGRAM, keys, data: encodeInstruction(IDL, 'callback_roll', { randomness: Uint8Array.from(randomness) }) };
745
+ }
746
+
747
+ /**
748
+ * 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
750
+ * Tokens the mint, the project's vault for it, the player's account of it (`prizeTo`, when given)
751
+ * Nft the NFT as `piece` takes it (`nfts[nft]` its kind), going to the player, then a compressed NFT's proof
752
+ * Roll the box, then the order its roll is made in (`rollNonce`)
753
+ */
754
+ export function wonAccounts({ orderAddress, order, items = {}, nfts = {}, prizeTo, putBack = [], programs }) {
755
+ const project = order.project;
756
+ return order.won.flatMap((won, k) => {
757
+ if (won.Item) {
758
+ const { item: id, serial } = won.Item;
759
+ const address = pda.item(project, id);
760
+ const item = items[id];
761
+ if (!item) throw new Error(`settling item ${id} needs the item (items), for its kind and collection`);
762
+ if (item.kind !== 'Collectible') return [meta(address, false, true)];
763
+ return [meta(address, false, true), meta(item.collection, false, true), meta(pda.itemAsset(address, serial), false, true), meta(MPL_CORE_PROGRAM, false, false)];
764
+ }
765
+ if (won.Tokens) {
766
+ const { mint } = won.Tokens;
767
+ const program = tokenProgramOf(programs, mint);
768
+ return [meta(mint, false, false), meta(vault(project, mint, program), false, true), meta(prizeTo ?? ata(order.player, mint, program), false, true)];
769
+ }
770
+ if (won.Nft) {
771
+ const { stash, nft: asset } = won.Nft;
772
+ // The player, settling their own order, may put an NFT its collection won't let move back into its stash.
773
+ if (putBack.includes(asset)) return [meta(pda.stash(project, stash), false, true)];
774
+ const p = piece({ project, asset, wallet: order.player, ...nfts[asset], ...ONLY_MOVES });
775
+ return [...pieceMetas(p), ...p.proof];
776
+ }
777
+ return [meta(pda.box(project, won.Roll[0]), false, false), meta(pda.order(project, order.player, rollNonce(orderAddress, order.requested_slot, k)), false, true)];
778
+ });
779
+ }
780
+
781
+ /** The heap a settle that moves three or more NFTs asks for (`settleHeapFrame`): all a transaction may have, 256 KiB. */
782
+ export const LARGE_SETTLE_HEAP = 256 * 1024;
783
+
784
+ /** Whether an order rolled on an NFT: its `subject` names one (the default address for none). */
785
+ const rolledOn = (order) => Boolean(order.subject) && order.subject !== NO_ADDRESS;
786
+
787
+ /**
788
+ * The heap a settle of `order` asks for with Compute Budget's `RequestHeapFrame` (`requestHeapFrame`), in bytes; 0 for
789
+ * none (the 32 KiB every transaction has). A settle hands every account to each NFT's move, so one that moves three or
790
+ * more NFTs (the prizes and the NFT rolled on) can outgrow 32 KiB, for good once a payee turns on required memos
791
+ * (a known limit of the program).
792
+ *
793
+ * loot's own allocator uses the frame a transaction asks for, up to
794
+ * 256 KiB, so such a settle asks for all of it (`LARGE_SETTLE_HEAP`, about 1,000 compute units more). Every settle
795
+ * `js/play` sends for such an order asks for it, and so does the keeper's sweep: put `requestHeapFrame(heap)` before
796
+ * the settle when `settleHeapFrame(order)` is not 0. A prize step of a lottery gives one place a call and needs none.
797
+ */
798
+ export function settleHeapFrame(order) {
799
+ const moves = (order.won ?? []).filter((w) => w.Nft).length + (rolledOn(order) ? 1 : 0);
800
+ return moves >= 3 ? LARGE_SETTLE_HEAP : 0;
801
+ }
802
+
803
+ /** All the compute units a transaction may have: what a settle that moves an NFT asks for (`settleComputeUnits`). */
804
+ export const MAX_COMPUTE_UNITS = 1_400_000;
805
+
806
+ /**
807
+ * The compute limit a settle of `order` asks for. A settle that moves an NFT asks for all a transaction may have
808
+ * (1,400,000 units): up to 986,000 were measured for three pNFTs of stashes, the NFT rolled on and
809
+ * four holders' shares. Any other asks for 800,000 (tokens to the player and up to four payees, accounts opened for
810
+ * them). A limit costs nothing more when no priority fee is set.
811
+ */
812
+ export function settleComputeUnits(order) {
813
+ const moves = (order.won ?? []).some((w) => w.Nft) || rolledOn(order);
814
+ return moves ? MAX_COMPUTE_UNITS : 800_000;
815
+ }
816
+
817
+ /**
818
+ * 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 roll made for the player), then splits the payment, or burns it for an entry that burns. Anyone sends it, for the
821
+ * order at `orderAddress`; `signer` opens any account it pays into that is not there, and pays a roll's order. `order`
822
+ * is the decoded Order; `projectState` the decoded Project; `items` and `nfts` as `wonAccounts` takes them. `putBack`:
823
+ * NFTs won that the player, signing, puts back into their stash in place of taking them (one its collection won't let move).
824
+ * `handBack`: the wallet the box's fate gives the NFT rolled on to (`receiverOf`) is an account of the loot program
825
+ * (`handsBack` of it as read), so the program gives it back to the player, and a Token Metadata NFT's `token` is the
826
+ * player's. `extra`: a hook's or a memo's accounts.
827
+ */
828
+ export function settleIx({ signer, orderAddress, order, projectState, items, nfts, prizeTo, putBack, subject, handBack = false, holders, programs, extra = [] }) {
829
+ const project = order.project;
830
+ const tokenProgram = tokenProgramOf(programs, order.mint);
831
+ const burns = order.burns || sharesBurn(order.shares);
832
+ // 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
+ // back to its player (`handsBack`), into the player's token account for a Token Metadata NFT.
834
+ const receiver = receiverOf(order);
835
+ const on = orderPiece(project, orderAddress, order, subject, handBack && order.fate?.then?.Give ? order.player : receiver);
836
+ return lootIx(
837
+ 'settle',
838
+ {},
839
+ {
840
+ signer,
841
+ order: orderAddress,
842
+ player: order.player,
843
+ project,
844
+ mint: order.mint,
845
+ vault: vault(project, order.mint, tokenProgram),
846
+ treasury: projectState.treasury,
847
+ treasury_token: ata(projectState.treasury, order.mint, tokenProgram),
848
+ ...on.accounts,
849
+ receiver,
850
+ },
851
+ {
852
+ remaining: [...on.proof, ...wonAccounts({ orderAddress, order, items, nfts, prizeTo, putBack, programs }), ...shareAccounts(project, order.shares, order.mint, { holders, programs, treasury: projectState.treasury }), ...extra],
853
+ writable: [...(burns ? ['mint'] : []), ...on.writable],
854
+ },
855
+ );
856
+ }
857
+
858
+ /**
859
+ * Where a held NFT a roll was on goes, when not back to its player: the wallet its fate names, the stash it goes into,
860
+ * or the incinerator.
861
+ */
862
+ export function receiverOf(order) {
863
+ const then = order.fate?.then;
864
+ if (!order.held || !then) return null;
865
+ if (then.Give) return then.Give[0];
866
+ if (then.Stash) return pda.stash(order.project, then.Stash[0]);
867
+ return then === 'Burn' || then.Burn ? INCINERATOR : null;
868
+ }
869
+
870
+ /**
871
+ * The accounts of the NFT an order rolled on (`order.subject`, the default address for none): as its receiver holds
872
+ * it after the settle (its player, or `receiverOf`). `subject`: its kind, as `piece` takes it ({ collection,
873
+ * hasAuthority, tokenMetadata, compressed }).
874
+ */
875
+ function orderPiece(project, orderAddress, order, subject = {}, receiver) {
876
+ if (!rolledOn(order)) return standIn(orderAddress);
877
+ // The numbers its roll moves are written on it (a Core asset is burned the same way); a held one is sent on, unless
878
+ // a stash keeps it where it is.
879
+ const shifts = Boolean(order.fate?.shifts?.length);
880
+ const then = order.fate?.then;
881
+ const burned = order.held && (then === 'Burn' || Boolean(then?.Burn));
882
+ const stays = Boolean(then?.Stash);
883
+ 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
+ }
885
+
886
+ /**
887
+ * Whether an NFT the project holds, given to the wallet whose account is `account` (as read: `{ owner }`, or null for
888
+ * none), goes back to its player instead: when that account is the loot program's (the project's
889
+ * own address, a record, a stash, a fee box), where nothing could move it again. A settle's fate and an NFT lottery
890
+ * ticket's price take it as `handBack`.
891
+ */
892
+ export const handsBack = (account) => account?.owner === LOOT_PROGRAM;
893
+
894
+ /** Where an NFT that is burned but can't be (any kind but Core) is sent: an address nobody holds the key to. */
895
+ export const INCINERATOR = '1nc1nerator11111111111111111111111111111111';
896
+
897
+ /**
898
+ * Gives an order's payment back to its player, into their account of the mint (`signer`, anyone, opens it when it is
899
+ * not there): an order whose roll failed or never came. A rolled order is settled, never refunded
900
+ * (`orderRefundSlot`). A purchase counted against a box's limit for each
901
+ * wallet is given back. `extra`: a hook's or a memo's accounts.
902
+ */
903
+ export function refundIx({ signer, orderAddress, order, subject, programs, extra = [] }) {
904
+ const project = order.project;
905
+ const tokenProgram = tokenProgramOf(programs, order.mint);
906
+ // A held NFT goes back to its player.
907
+ const on = order.held && rolledOn(order)
908
+ ? piece({ project, asset: order.subject, wallet: order.player, ...subject, ...ONLY_MOVES })
909
+ : standIn(orderAddress);
910
+ return lootIx(
911
+ 'refund',
912
+ {},
913
+ {
914
+ signer,
915
+ order: orderAddress,
916
+ player: order.player,
917
+ project,
918
+ mint: order.mint,
919
+ vault: vault(project, order.mint, tokenProgram),
920
+ player_token: ata(order.player, order.mint, tokenProgram),
921
+ receipt: order.counted != null ? pda.receipt(order.loot_box, order.player) : null,
922
+ ...on.accounts,
923
+ },
924
+ { remaining: [...on.proof, ...extra], writable: on.writable },
925
+ );
926
+ }
927
+
928
+ /**
929
+ * Anyone (`payer`, who pays the oracle) asks for the roll a mission's return, a recipe or a prize made (`order.status`
930
+ * Waiting): the box's prizes' accounts, as `openBoxIx` passes them. It lands in `callback_roll`. Returns a list of
931
+ * instructions: first `payer` opens the project's vault for each prize mint when it is not there (idempotent; the
932
+ * program refuses the ask without them, since tokens won wait there), then the ask.
933
+ */
934
+ export function askRollIx({ payer, orderAddress, order, box, poolMints, programs }) {
935
+ poolMints = boxPoolMints(box, poolMints);
936
+ 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)],
939
+ });
940
+ return [...opens, ask];
941
+ }
942
+
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
+ // ---------------------------------------------------------------- out: the project's tokens move
948
+
949
+ /**
950
+ * The authority moves the project's tokens: `from` { Pool: [i] } or { Mint: [mint] } (newly minted), `to` a pool, a
951
+ * wallet, an account, the treasury (`treasury`, the project's wallet), a collectible's holder or 'Burn', `amount`
952
+ * { Fixed: [n] } or { Bps: [bps] } of a pool. The authority opens the account it pays into when it is not there; a
953
+ * collectible nobody holds gets nothing.
954
+ */
955
+ export function moveTokensIx({ authority, project, mint, from, to, amount, reference: ref, holders, programs, treasury, extra = [] }) {
956
+ const writes = Boolean(from.Mint) || to === 'Burn';
957
+ return lootIx(
958
+ 'move_tokens',
959
+ { from, to, amount, reference: reference(ref) },
960
+ { authority, project, mint, pool: from.Pool ? pda.pool(project, from.Pool[0]) : null },
961
+ { remaining: [...destinationAccounts(project, to, mint, { holders, programs, treasury }), ...extra], writable: writes ? ['mint'] : [] },
962
+ );
963
+ }
964
+
965
+ // ---------------------------------------------------------------- make: collectibles
966
+
967
+ /**
968
+ * The authority makes, rewrites or burns a collectible. `change`: { Make: { attributes } } (then `item` is the item id
969
+ * it makes one of, `asset` is a new key that signs, and `owner` receives it), { Set: [attributes] } or 'Burn'. `collection`: the project's for a Make
970
+ * or a Burn; for a Set, the asset's own (`MPL_CORE_PROGRAM` for one in none). `hasAuthority` false for a `Set` on an
971
+ * NFT the project has no authority over (a Core asset of another collection, or a Token Metadata NFT's mint): what it
972
+ * writes goes in the NFT's Sheet, or on the NFT itself when its Attributes plugin names the project.
973
+ */
974
+ export function collectibleIx({ authority, project, collection, asset, change, item = null, owner = null, hasAuthority = true, reference: ref }) {
975
+ const make = Boolean(change.Make);
976
+ const sheet = change.Set && !hasAuthority ? pda.sheet(project, asset) : null;
977
+ return lootIx(
978
+ 'collectible',
979
+ { change, reference: reference(ref) },
980
+ { authority, project, collection, asset, item: make ? pda.item(project, item) : null, owner: make ? owner : null, sheet },
981
+ { signer: make ? ['asset'] : [], writable: change.Set || collection === MPL_CORE_PROGRAM ? [] : ['collection'] },
982
+ );
983
+ }
984
+
985
+ // ---------------------------------------------------------------- decide
986
+
987
+ /**
988
+ * A held NFT goes to `to`: the authority's choice while the hold runs, home to its player (anyone) after; a repaid
989
+ * loan's home (anyone, whenever), an unpaid one's to its lender once due. `hold` is the
990
+ * decoded Hold: its address names its end (`pda.hold(asset, free_at)`), so a release sent late never lands on a later
991
+ * hold. The NFT is any kind
992
+ * (`piece`): `asset` and `collection` for a Core asset, with `tokenMetadata` for a Token Metadata NFT, or `compressed`
993
+ * (after a `proveLeafIx`).
994
+ */
995
+ export function releaseIx({ signer, project, collection, asset, tokenMetadata: tm, compressed, hold, to, reference: ref }) {
996
+ const receiver = to ?? hold.player;
997
+ // Never to the project's own address, nor any account of the program, where nothing could move it again (ToTheProject).
998
+ if (receiver === project) throw new Error('A held NFT can’t go to the project’s own address: name a wallet.');
999
+ const p = piece({ project, asset, wallet: receiver, collection, tokenMetadata: tm, compressed, ...ONLY_MOVES });
1000
+ return lootIx('release', { reference: reference(ref) }, { signer, project, ...p.accounts, hold: pda.hold(p.accounts.asset, hold.free_at), player: hold.player, to: receiver }, { remaining: p.proof, writable: p.writable });
1001
+ }
1002
+
1003
+ /** The mint of a contest's split prize (its `prize`, tokens): minted, or a pool's (`poolMint`). */
1004
+ const splitMint = (prize, poolMint) => prize?.Tokens?.from.Mint?.[0] ?? poolMint;
1005
+
1006
+ /**
1007
+ * The authority opens contest `id` in `mint`. `args`: { sides, closes_at, result_by, shares, gate, deciders,
1008
+ * min_pot, stake, prize, nft, prize_split }: `prize`, what each winning entry gets when it is claimed (`prizeMetas`);
1009
+ * `nft`, a rule an NFT each entry stakes passes, in place of tokens (no shares then, no stake: the NFTs are the pot).
1010
+ * With `args.prize_split`, its token prize is set aside whole when it is
1011
+ * decided, in the contest's own vault (`pda.prizeVault`, opened now at the authority's rent), and each winning entry
1012
+ * takes its part by what it put in: `prizeMint` names the prize's mint when it comes from a pool.
1013
+ */
1014
+ export const openContestIx = ({ authority, project, id, mint, args, prizeMint }) => {
1015
+ const contest = pda.contest(project, id);
1016
+ 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 } }, {
1018
+ authority, project, mint, contest,
1019
+ prize_mint: split ? splitMint(args.prize, prizeMint) : null, prize_vault: split ? pda.prizeVault(contest) : null,
1020
+ });
1021
+ };
1022
+
1023
+ /** The prefix of a contest's verdict (`verdictBytes`). */
1024
+ export const VERDICT_DOMAIN = 'loot-verdict-v3';
1025
+
1026
+ /**
1027
+ * The bytes a contest's decider key signs: "loot-verdict-v3" | program (32) | contest (32) | incarnation (u64 LE) |
1028
+ * side (1; 255 calls it off) | expires (i64 LE), then | recipient (32) when the verdict names the wallet the
1029
+ * contest's chosen share pays. `incarnation` is the contest's (the decoded Contest's), so a verdict decides only the
1030
+ * contest it was signed for, never one opened later at the same id. `decide` rebuilds them from its own accounts and
1031
+ * arguments.
1032
+ */
1033
+ export function verdictBytes({ program = LOOT_PROGRAM, contest, incarnation, side, expires, recipient = null }) {
1034
+ if (incarnation == null) throw new Error('A verdict signs the contest’s incarnation (the decoded Contest’s).');
1035
+ const named = recipient == null ? new Uint8Array(0) : addressBytes(recipient);
1036
+ return concat(utf8(VERDICT_DOMAIN), addressBytes(program), addressBytes(contest), uintLE(incarnation, 8), uintLE(side ?? 255, 1), intLE(expires, 8), named);
1037
+ }
1038
+
1039
+ /**
1040
+ * Names the side that won (`side`), or calls the contest off (`side: null`). `signer` is the authority; or anyone
1041
+ * sending a decider's verdict (`verdict: { expires }`, taken once entries have closed): then this instruction goes
1042
+ * right after the Ed25519 check of the decider's signature over `verdictBytes` (`ed25519Ix` in grant.js), and
1043
+ * `signer` pays the fee. Anyone writes down a contest that is off by itself. The contest's incarnation names it, so a
1044
+ * decision sent late never decides a contest opened later at the same id. A call-off takes no share accounts. A
1045
+ * contest with a chosen share is decided naming its `recipient`, a wallet (a bounty's worker); a decider's verdict
1046
+ * signs it too (`verdictBytes`).
1047
+ */
1048
+ export function decideIx({ signer, project, projectState, contestAddress, contest, side, recipient = null, verdict, reference: ref, holders, prizeMint, programs, extra = [] }) {
1049
+ const address = contestAddress ?? pda.contest(project, contest.id);
1050
+ const named = recipient == null ? contest.shares : chosenShares(contest.shares, recipient);
1051
+ 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), and its vault closes with a contest
1053
+ // called off before anyone entered.
1054
+ const split = Boolean(contest.prize_split);
1055
+ const from = contest.prize?.Tokens?.from;
1056
+ const closes = side == null && Number(contest.entries) === 0;
1057
+ return lootIx(
1058
+ 'decide',
1059
+ { side: side ?? null, expires: toBigInt(verdict?.expires ?? 0), reference: reference(ref), incarnation: contest.incarnation, recipient },
1060
+ {
1061
+ signer, project, contest: address, opener: contest.opener, mint: contest.mint, instructions: verdict ? SYSVAR_INSTRUCTIONS : null,
1062
+ prize_mint: split ? splitMint(contest.prize, prizeMint) : null,
1063
+ prize_pool: split && side != null && from?.Pool ? pda.pool(project, from.Pool[0]) : null,
1064
+ prize_vault: split ? pda.prizeVault(address) : null,
1065
+ },
1066
+ // The mint is written by a burn share, or by closing the pot of a contest nobody entered; the prize's mint when it
1067
+ // is minted, or when its vault closes.
1068
+ { remaining: [...shares, ...extra], writable: [...((side == null ? closes : sharesBurn(contest.shares)) ? ['mint'] : []), ...(split && ((side != null && from?.Mint) || closes) ? ['prize_mint'] : [])] },
1069
+ );
1070
+ }
1071
+
1072
+ /** The authority asks the oracle for a random number, for a rule of the project's own. */
1073
+ export const rollIx = ({ authority, project, reference: ref }) => lootIx('roll', { reference: reference(ref) }, { authority, project });
1074
+
1075
+ /** The oracle's answer to a `roll` (for a local simulation). */
1076
+ export const callbackRandomIx = ({ project, reference: ref, randomness }) => ({
1077
+ programId: LOOT_PROGRAM,
1078
+ keys: [meta(pda.oracleIdentity(), true, false), meta(project, false, false), meta(hashToAddress(reference(ref)), false, false)],
1079
+ data: encodeInstruction(IDL, 'callback_random', { randomness: Uint8Array.from(randomness) }),
1080
+ });
1081
+
1082
+ // ---------------------------------------------------------------- schedules and lotteries
1083
+
1084
+ /**
1085
+ * Anyone (`payer`) runs a schedule that is due, and opens the account it pays into when that is not there.
1086
+ * `poolMint`: the mint of the pool it moves from (when it moves from one). `extra`: a hook's or a memo's accounts.
1087
+ */
1088
+ export function runScheduleIx({ payer, project, id, schedule, poolMint, holders, programs, treasury, extra = [] }) {
1089
+ const { from, to } = schedule.action;
1090
+ const address = pda.schedule(project, id);
1091
+ // A sealed schedule pays from its own vault (`pda.vest`), passed where a pool would be.
1092
+ const mint = from.Mint?.[0] ?? from.Vest?.[0] ?? poolMint;
1093
+ const writes = Boolean(from.Mint) || to === 'Burn';
1094
+ const pool = from.Pool ? pda.pool(project, from.Pool[0]) : from.Vest ? pda.vest(address) : null;
1095
+ return lootIx(
1096
+ 'run_schedule',
1097
+ {},
1098
+ { payer, project, schedule: address, pool, mint },
1099
+ { remaining: [...destinationAccounts(project, to, mint, { holders, programs, treasury }), ...extra], writable: writes ? ['mint'] : [] },
1100
+ );
1101
+ }
1102
+
1103
+ export const lotteryPaymentMint = (lottery) => lottery.ticket_mint && lottery.ticket_mint !== NO_ADDRESS ? lottery.ticket_mint : lottery.mint;
1104
+
1105
+ /**
1106
+ * `player` buys `count` tickets: a contribution of their own at `nonce` (any number they have no contribution at in
1107
+ * this lottery), whose rent they pay and get back once it is finished. The ticket money waits in the lottery's
1108
+ * escrow until the draw. Whoever holds a collectible a fixed share pays is fixed now, from that collectible. It buys
1109
+ * only at the `lottery`'s incarnation and revision (`TermsChanged` once the owner edits or recreates it). A lottery
1110
+ * whose ticket costs an NFT (`lottery.nft_ticket`) sells one at a time, for `nft` ({ asset, collection, tokenMetadata,
1111
+ * compressed }, as `piece` takes it), which the project holds until the round is drawn (then it goes where its `then`
1112
+ * says, `spentTo`) or cancelled (then back to its buyer).
1113
+ */
1114
+ export function buyTicketsIx({ player, project, id, lottery, mint, count, nonce, shown, listProof, chosenRecipient = null, chosen = null, nft, playerToken, programs, extra = [] }) {
1115
+ const address = pda.lottery(project, id);
1116
+ mint ??= lotteryPaymentMint(lottery);
1117
+ const holderAssets = (lottery.shares ?? []).flatMap((s) => (s.to.Holder ? [meta(s.to.Holder[0], false, false)] : []));
1118
+ const held = lottery.nft_ticket ? piece({ project, wallet: player, ...nft, ...ONLY_MOVES, reads: true }) : null;
1119
+ const paid = held ? [...pieceMetas(held), ...held.proof] : [];
1120
+ return lootIx(
1121
+ 'buy_tickets',
1122
+ { nonce, count, chosen_recipient: chosenRecipient, incarnation: lottery.incarnation, revision: lottery.revision },
1123
+ {
1124
+ player, project, lottery: address, contribution: pda.contribution(address, player, nonce),
1125
+ player_token: playerToken ?? ata(player, mint, tokenProgramOf(programs, mint)),
1126
+ payment_vault: pda.lotteryFunds(address), mint, chosen,
1127
+ receipt: Number(lottery.per_wallet) ? pda.lotteryReceipt(address, lottery.incarnation, player) : null,
1128
+ },
1129
+ { remaining: [...gateAccounts(project, { gate: lottery.gate, wallet: player, shown, listProof }), ...holderAssets, ...paid, ...extra] },
1130
+ );
1131
+ }
1132
+
1133
+ /** Anyone asks for a lottery's draw once it is due. */
1134
+ export const drawLotteryIx = ({ payer, project, id }) => lootIx('draw_lottery', {}, { payer, lottery: pda.lottery(project, id) });
1135
+
1136
+ /** The oracle's answer to a draw (for a local simulation). Every request of a round names the round's ticket, fixed
1137
+ * at its first ask (`asked_first`): a re-ask keeps it, so the first answer to land draws the round. */
1138
+ export function callbackLotteryIx({ project, id, lottery, askedFirst = lottery?.asked_first, randomness }) {
1139
+ const address = pda.lottery(project, id);
1140
+ return {
1141
+ programId: LOOT_PROGRAM,
1142
+ keys: [meta(pda.oracleIdentity(), true, false), meta(address, false, true), meta(ticketOf(address, askedFirst), false, false)],
1143
+ data: encodeInstruction(IDL, 'callback_lottery', { randomness: Uint8Array.from(randomness) }),
1144
+ };
1145
+ }
1146
+
1147
+ export const cancelLotteryIx = ({ project, id }) => lootIx('cancel_lottery', {}, { lottery: pda.lottery(project, id) });
1148
+
1149
+ /** Where a decoded contribution's shares go: as fixed when it was bought. */
1150
+ export function lotteryContributionShares(lottery, contribution) {
1151
+ return (lottery.shares ?? []).flatMap((share, i) => {
1152
+ const recipient = contribution.recipients[i];
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
+ }
1158
+
1159
+ /**
1160
+ * Anyone (`payer`) sends a contribution's next step (`lotteryStep`; `contributionAddress`, decoded `contribution`):
1161
+ * - a drawn round gives every place's prize first: the step of a contribution that won a place with a prize gives
1162
+ * the first such prize not given yet to its buyer, one place a call (`prizeMetas`: `collection`, `poolMints`, `items`, `nfts` as it takes them; a roll's
1163
+ * order at `nonce` plus the place, `prizeNonce(contributionAddress)` by default), and nothing else moves;
1164
+ * - once every prize is given, it is routed through its shares into the pot (or its tickets burned, or its NFT ticket
1165
+ * spent); a cancelled round's, or a lottery's that returns its tickets, goes back to its buyer, never waiting;
1166
+ * - one that holds a winning ticket stays (`done`): once every contribution is finished, it pays its buyer every
1167
+ * place's share of the pot it won, into their account of the pot's mint.
1168
+ * `payer` opens any account it pays into that is not there. `treasury`: the project's treasury wallet, for a share
1169
+ * that names it. A ticket an NFT paid for passes that NFT (`ticket`: its kind, as `piece` takes it); `handBack` when the
1170
+ * wallet its price gives it to is an account of the loot program (`handsBack`), where it goes back to its buyer (a
1171
+ * Token Metadata NFT into the buyer's token account).
1172
+ */
1173
+ export function finishLotteryContributionIx({ payer, project, id, lottery, contributionAddress, contribution, treasury, nonce, collection, poolMints, items, nfts = {}, ticket = {}, handBack = false, programs, extra = [] }) {
1174
+ const address = pda.lottery(project, id);
1175
+ const step = lotteryStep(lottery, contribution) ?? (contribution.done ? 'pay' : lottery.status === 'Drawn' ? 'finish' : 'refund');
1176
+ nonce = toBigInt(nonce ?? prizeNonce(contributionAddress));
1177
+ const accounts = { payer, project, lottery: address, contribution: contributionAddress, payment_vault: pda.lotteryFunds(address), pot: pda.pot(address), player: contribution.player };
1178
+ if (step === 'prizes') {
1179
+ // One place a call: the first it won whose prize is not given yet. The places go in order, so collectibles of one
1180
+ // item are made in place order, each at the next serial.
1181
+ const [place] = lotteryOwed(lottery, contribution);
1182
+ const prize = lottery.places[place].prize;
1183
+ const prizes = prizeMetas({ project, collection, prize, winner: contribution.player, nonce: BigInt.asUintN(64, nonce + BigInt(place)), poolMints, items, nft: nfts[prize.Nft?.nft ?? prize.Held?.[0]], programs, lottery: address });
1184
+ return lootIx('finish_lottery_contribution', { nonce }, { ...accounts, to: null, mint: lottery.mint }, { remaining: [...prizes, ...extra] });
1185
+ }
1186
+ if (step === 'pay') {
1187
+ return lootIx('finish_lottery_contribution', { nonce }, { ...accounts, to: ata(contribution.player, lottery.mint, tokenProgramOf(programs, lottery.mint)), mint: lottery.mint }, { remaining: extra });
1188
+ }
1189
+ const mint = lotteryPaymentMint(lottery);
1190
+ // Drawn, its tickets go through the shares into the pot; in a lottery that returns them, back to the buyer.
1191
+ const routed = step === 'finish';
1192
+ const burns = routed && lottery.ticket_mint !== NO_ADDRESS;
1193
+ // Burning the tickets, or a share that burns, writes the mint's supply.
1194
+ const writesMint = burns || (routed && sharesBurn(lottery.shares ?? []));
1195
+ const nft = contribution.asset && contribution.asset !== NO_ADDRESS;
1196
+ let remaining = routed && !burns ? shareAccounts(project, lotteryContributionShares(lottery, contribution), mint, { programs, treasury }) : [];
1197
+ if (nft) {
1198
+ // Drawn: where its price sends it (it stays with the project in a stash); cancelled: back to its buyer.
1199
+ const then = routed ? lottery.nft_ticket?.then : null;
1200
+ const receiver = then ? spentTo(project, then) : null;
1201
+ const burned = then === 'Burn' || Boolean(then?.Burn);
1202
+ // Given to a wallet that is an account of the loot program (`handBack`), it goes back to its buyer instead.
1203
+ const holder = then?.Stash || (handBack && then?.Give) ? contribution.player : receiver ?? contribution.player;
1204
+ const p = piece({ project, asset: contribution.asset, wallet: holder, ...ticket, moves: !then?.Stash, locks: false, burns: burned, reads: false });
1205
+ remaining = [...pieceMetas(p), ...p.proof, ...(receiver ? [meta(receiver, false, Boolean(then.Stash))] : [])];
1206
+ }
1207
+ // A free ticket gives nothing back, and needs no account to give it into.
1208
+ const back = !routed && !nft && BigInt(contribution.amount ?? 0) > 0n;
1209
+ return lootIx('finish_lottery_contribution', { nonce }, { ...accounts, to: back ? ata(contribution.player, mint, tokenProgramOf(programs, mint)) : null, mint }, { remaining: [...remaining, ...extra], writable: writesMint ? ['mint'] : [] });
1210
+ }
1211
+
1212
+ // ---------------------------------------------------------------- farms
1213
+
1214
+ /**
1215
+ * The owner opens farm `id`: of `stakedMint`'s tokens, or of collectibles when it is null. Its rewards come from pool
1216
+ * `args.rewards_pool`, whose mint is `rewardsMint`: all it receives, shared, or with `args.rate` ({ per_day, mints })
1217
+ * a fixed rate, minted or from that pool (a rate of 0 pays nothing: a farm that only grows or gives prizes).
1218
+ * `args.gates`: the rules an NFT must all pass. `args.growth`: up to 4 of { key, per_day, level, steps, streak }, each
1219
+ * writing points (and a level) on what is staked; tiers' `prize` is paid from `args.prize` ({ Mint: [mint] } or
1220
+ * { Pool: [index] }), whose mint is `prizeMint`. `args.ends_at`: when it ends (0: never). For a farm of collectibles,
1221
+ * `args.lock_key`: the attribute a staked NFT's lock is written under, as the time it ends, so recipes and rolls can
1222
+ * move it; `args.upkeep_key`: the attribute that says until when it earns and grows (fuel). Empty: none.
1223
+ */
1224
+ export const openFarmIx = ({ owner, project, id, args, rewardsMint, stakedMint = null }) => {
1225
+ if (!Array.isArray(args?.tiers)) throw new Error('A farm needs its lock tiers (`args.tiers`: [{ lock, bps }, …]).');
1226
+ const full = { gates: [], rate: null, growth: [], ends_at: 0, lock_key: '', upkeep_key: '', exit_to: null, unbond: 0, toll: null, ...args, tiers: args.tiers.map((t) => ({ prize: null, roll: null, exit_bps: 0, ...t })) };
1227
+ return lootIx('open_farm', { id, args: full }, {
1228
+ owner, project, rewards_pool: pda.pool(project, args.rewards_pool), rewards_mint: rewardsMint, staked_mint: stakedMint,
1229
+ staked_vault: stakedMint ? pda.staked(pda.farm(project, id)) : null,
1230
+ exit_pool: sinkPool(project, full.exit_to), toll_pool: sinkPool(project, full.toll?.to),
1231
+ });
1232
+ };
1233
+
1234
+ /** Where a farm's fee goes (`Sink`): the pool's address for `{ Pool: [index] }`, null for a burn or none. */
1235
+ 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
+
1241
+ const farmSide = (project, id, farm) => ({
1242
+ project,
1243
+ farm: pda.farm(project, id),
1244
+ rewards_pool: pda.pool(project, farm.rewards_pool),
1245
+ rewards_mint: farm.rewards_mint,
1246
+ });
1247
+ /** What every move of a farm writes: its rewards' mint when it mints at a fixed rate, its rewards pool otherwise. */
1248
+ const farmWrites = (farm) => (farm.rate?.mints ? ['rewards_mint'] : ['rewards_pool']);
1249
+
1250
+ /** A token seat's accounts; `paid`: rewards may be paid (an unstake), so the player's account of them comes. */
1251
+ function tokenSeat({ player, project, id, farm, programs, paid = false }) {
1252
+ const mint = farm.farmed.Tokens.mint;
1253
+ const farmAddress = pda.farm(project, id);
1254
+ return {
1255
+ ...farmSide(project, id, farm),
1256
+ player,
1257
+ seat: pda.seat(farmAddress, player),
1258
+ staked_vault: pda.staked(farmAddress),
1259
+ mint,
1260
+ player_tokens: ata(player, mint, tokenProgramOf(programs, mint)),
1261
+ player_rewards: paid ? ata(player, farm.rewards_mint, tokenProgramOf(programs, farm.rewards_mint)) : null,
1262
+ ...farmSinks(project, farm),
1263
+ };
1264
+ }
1265
+
1266
+ /**
1267
+ * `player` stakes `amount` at lock tier `tier`. `farm` is decoded; `extra` carries token hook/memo accounts. A new tier
1268
+ * on a seat with a booster, in a farm with growth, needs the booster's growth written first: put a `refreshSeatIx` of
1269
+ * the seat before it, in the same transaction.
1270
+ */
1271
+ 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], writable: [...farmWrites(farm), ...tollWrites(farm)] });
1273
+
1274
+ /**
1275
+ * `player` takes `amount` out (0: only collects what is owed, and takes out tokens whose unbonding wait is over);
1276
+ * `forfeit` gives up rewards that can't move. Before the tier's lock has run, a tier with an early-exit fee
1277
+ * (`exit_bps`) lets it leave, leaving that share of `amount` to the farm's `exit_to`. In a farm that unbonds
1278
+ * (`unbond`), what is taken out waits that long first, earning nothing (`withdrawable`).
1279
+ */
1280
+ export const unstakeTokensIx = ({ player, project, id, farm, amount, forfeit = false, programs, extra = [] }) =>
1281
+ 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 toll that burns, the rewards' mint.
1284
+ writable: [...farmWrites(farm), ...tollWrites(farm), ...(farm.exit_to === 'Burn' ? ['mint'] : [])],
1285
+ });
1286
+
1287
+ /** What a token seat may take out now (Unix seconds `now`): its unbonded tokens once their wait is over, else 0. */
1288
+ export const withdrawable = (seat, now) => (BigInt(seat.leaving ?? 0) > 0n && BigInt(now) >= BigInt(seat.leaving_at) ? BigInt(seat.leaving) : 0n);
1289
+
1290
+ /**
1291
+ * Whether a farm (decoded) takes a stake at lock tier `tier` at `now` (Unix seconds, the chain's clock), as the program
1292
+ * judges it: a farm with an end (`ends_at`, 0 for never) takes none at its end or after it, nor one whose lock
1293
+ * would run past it (`FarmOver`). A stake of tokens, a booster, a new tier and a watched NFT's takeover all ask it.
1294
+ */
1295
+ export function farmOpenFor(farm, tier, now) {
1296
+ const ends = BigInt(farm.ends_at ?? 0);
1297
+ if (ends === 0n) return true;
1298
+ const t = BigInt(now);
1299
+ return t < ends && t + BigInt(farm.tiers[tier]?.lock ?? 0) <= ends;
1300
+ }
1301
+
1302
+ export const AUTH_RULES_PROGRAM = 'auth9SigNpDKz4sJJ1DfCTuZrZNSAgh9sFD3rboVmgg';
1303
+
1304
+ // ---------------------------------------------------------------- an NFT on a route (a `Piece`)
1305
+
1306
+ const NO_PIECE = {
1307
+ sheet: null, collection: null, mpl_core_program: null, metadata: null, edition: null, token: null, token_record: null, project_token: null, project_record: null,
1308
+ rule_set: null, rules_program: null, token_metadata_program: null, instructions: null, spl_token_program: null,
1309
+ spl_ata_program: null, leaf: null, tree: null, tree_config: null, leaf_delegate: null, bubblegum_program: null,
1310
+ compression_program: null, log_wrapper: null,
1311
+ };
1312
+
1313
+ /**
1314
+ * Any NFT's accounts on a route (a farm, a hold, an auction's lot): `{ accounts, proof, writable }`, the proof going
1315
+ * first among the instruction's remaining accounts, `writable` the accounts to pass writable (`lootIx`'s option). The
1316
+ * NFT is one of:
1317
+ * a Core asset (`asset`; `collection` its collection now; `hasAuthority` false for one the project has no authority
1318
+ * over, whose Sheet is passed);
1319
+ * a Token Metadata NFT (`asset` its mint, `tokenMetadata` { programmable, ruleSet } from `decodeTokenMetadata`): the
1320
+ * token account of `wallet` (its holder, or whoever the project sends it to) and the project's own;
1321
+ * a compressed NFT (`compressed`, from `compressedFromDas`), proven by a `proveLeafIx` earlier in the transaction.
1322
+ * A Token Metadata or compressed NFT passes its Sheet: the project writes nothing on it.
1323
+ * What the route does with it, so only the accounts it takes are passed (each true when left out):
1324
+ * `moves`: hands it to the project or sends it on; `locks`: locks or unlocks it in its wallet; `writes`: writes on it;
1325
+ * `burns`: burns a Core asset (false when left out); `reads`: reads its Sheet; `held`: reads it in the project's token
1326
+ * account (an escrow farm).
1327
+ */
1328
+ export function piece({ project, asset, wallet, collection, hasAuthority = true, tokenMetadata: tm, compressed: c, moves = true, locks = true, writes = true, burns = false, reads = true, held = moves }) {
1329
+ const sheetOf = (nft) => (reads || writes || burns ? pda.sheet(project, nft) : null);
1330
+ if (c) {
1331
+ // Bubblegum is called (and writes the leaf, its tree and its config) only to lock or move it.
1332
+ const calls = moves || locks;
1333
+ const delegate = calls && c.delegate !== c.owner && c.delegate !== project ? c.delegate : null;
1334
+ const bubblegum = calls
1335
+ ? { 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
+ : {};
1337
+ return {
1338
+ accounts: { ...NO_PIECE, asset: c.id, sheet: sheetOf(c.id), leaf: pda.leaf(c.id), tree: c.tree, ...bubblegum },
1339
+ proof: c.proof.map((node) => meta(typeof node === 'string' ? node : encodeBase58(node), false, false)),
1340
+ writable: calls ? ['leaf', 'tree', 'tree_config'] : [],
1341
+ };
1342
+ }
1343
+ if (tm) {
1344
+ // `tm.into`: the token account of `wallet`'s the NFT goes into when it is not their associated one (`freshNftAccount`).
1345
+ const token = tm.into ?? ata(wallet, asset, TOKEN_PROGRAM);
1346
+ const projectToken = ata(project, asset, TOKEN_PROGRAM);
1347
+ const pnft = Boolean(tm.programmable);
1348
+ // Token Metadata is called to lock it or move it; a move also takes the project's token account (and record) and the
1349
+ // ATA program, a pNFT's every call its records, rule set and the instructions sysvar.
1350
+ const calls = moves || locks;
1351
+ const records = calls && pnft;
1352
+ return {
1353
+ accounts: {
1354
+ ...NO_PIECE, asset, sheet: sheetOf(asset), metadata: tokenMetadata.metadata(asset), token,
1355
+ edition: calls ? tokenMetadata.edition(asset) : null,
1356
+ token_record: records ? tokenMetadata.record(asset, token) : null,
1357
+ project_token: held || moves ? projectToken : null,
1358
+ project_record: records && moves ? tokenMetadata.record(asset, projectToken) : null,
1359
+ rule_set: records ? tm.ruleSet ?? TOKEN_METADATA_PROGRAM : null,
1360
+ rules_program: records ? (tm.ruleSet ? AUTH_RULES_PROGRAM : TOKEN_METADATA_PROGRAM) : null,
1361
+ token_metadata_program: calls ? TOKEN_METADATA_PROGRAM : null,
1362
+ instructions: moves || records ? SYSVAR_INSTRUCTIONS : null,
1363
+ spl_token_program: calls ? TOKEN_PROGRAM : null,
1364
+ spl_ata_program: moves ? ASSOCIATED_TOKEN_PROGRAM : null,
1365
+ },
1366
+ proof: [],
1367
+ writable: [],
1368
+ };
1369
+ }
1370
+ // A Core asset: its collection now (left out for one in none: the program supplies Core's own address). Core is
1371
+ // called to move, lock, write on or burn it; only a burn writes the collection.
1372
+ const inNone = !collection || collection === MPL_CORE_PROGRAM;
1373
+ const calls = moves || locks || writes || burns;
1374
+ return {
1375
+ accounts: {
1376
+ ...NO_PIECE, asset, sheet: hasAuthority ? null : sheetOf(asset),
1377
+ collection: calls && !inNone ? collection : null, mpl_core_program: calls ? MPL_CORE_PROGRAM : null,
1378
+ },
1379
+ proof: [],
1380
+ writable: burns && !inNone ? ['collection'] : [],
1381
+ };
1382
+ }
1383
+
1384
+ /** The account an NFT paid goes to: the wallet it is given to, the stash it goes into, or the
1385
+ * incinerator for a burn (any account stands in for a Core asset, which Core burns). `then`: 'Burn', { Give: [wallet] }
1386
+ * or { Stash: [id] }. */
1387
+ export function spentTo(project, then) {
1388
+ if (then.Give) return then.Give[0];
1389
+ if (then.Stash) return pda.stash(project, then.Stash[0]);
1390
+ return INCINERATOR;
1391
+ }
1392
+
1393
+ /**
1394
+ * The accounts of an NFT `owner` pays with, after a route's own: the NFT as `piece` takes it, its proof,
1395
+ * then where it goes (`spentTo`). Given on or burned it moves straight from its owner: a Token Metadata NFT's
1396
+ * receiver's token account (and a pNFT's record of it) take the project's places; stashed, it is handed to the
1397
+ * project first. A Core asset burned writes its collection.
1398
+ */
1399
+ export function nftPaid({ project, owner, then, asset, collection, hasAuthority, tokenMetadata: tm, compressed }) {
1400
+ const to = spentTo(project, then);
1401
+ // The route reads it first (its Sheet, for one the project has no authority over: `hasAuthority` false).
1402
+ const p = then.Stash
1403
+ ? piece({ project, asset, wallet: owner, collection, hasAuthority, tokenMetadata: tm, compressed, ...ONLY_MOVES, reads: true })
1404
+ : passedOn({ project, owner, receiver: to, burns: then === 'Burn' || Boolean(then.Burn), asset, collection, hasAuthority, tokenMetadata: tm, compressed, reads: true });
1405
+ return [...pieceMetas(p), ...p.proof, meta(to, false, Boolean(then.Stash))];
1406
+ }
1407
+
1408
+ /** An NFT moving straight from `owner` to `receiver`, or burned by its owner (`burns`). */
1409
+ function passedOn({ project, owner, receiver, burns, asset, collection, hasAuthority, tokenMetadata: tm, compressed, reads = false }) {
1410
+ // `tm.into` is the receiver's account here: the NFT leaves the owner's associated one.
1411
+ const p = piece({ project, asset, wallet: owner, collection, hasAuthority, tokenMetadata: tm && { ...tm, into: undefined }, compressed, ...ONLY_MOVES, burns: burns && !tm && !compressed, reads });
1412
+ if (tm) {
1413
+ const token = tm.into ?? ata(receiver, asset, TOKEN_PROGRAM);
1414
+ p.accounts.project_token = token;
1415
+ p.accounts.project_record = tm.programmable ? tokenMetadata.record(asset, token) : null;
1416
+ }
1417
+ return p;
1418
+ }
1419
+
1420
+ /**
1421
+ * `player` pays with an NFT, any kind (as `piece` takes it): burned (`to` null: the incinerator's address goes in its
1422
+ * place), or given to the wallet `to` (the treasury, a partner). The receipt (`PaidNft`) is the record: an app admits the player when the NFT, where it went (`to`: a payment to the player's own wallet pays nothing) and the reference
1423
+ * are the ones it asked for. `extra`: Core's transfer adapters.
1424
+ */
1425
+ export function payNftIx({ player, project, asset, to = null, collection, hasAuthority, tokenMetadata: tm, compressed, reference: ref, extra = [] }) {
1426
+ // An NFT paid goes to a wallet, never to the project's own address, where nothing could ever move it (ToTheProject).
1427
+ 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
+ const receiver = to ?? INCINERATOR;
1429
+ 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 });
1431
+ }
1432
+
1433
+ /**
1434
+ * Whether a classic token account (its `{ owner, data }` as read) takes a Token Metadata NFT `mint` for `holder`: an
1435
+ * account of the Token program, of that mint, that `holder` owns, not frozen. Token Metadata refuses one another key
1436
+ * owns (a holder can hand their own associated account to another key) and a frozen one, so a route that hands the
1437
+ * NFT into it fails: then the NFT goes into a fresh account of the holder's (`freshNftAccount`).
1438
+ */
1439
+ export function takesNft(account, holder, mint) {
1440
+ if (!account || account.owner !== TOKEN_PROGRAM || account.data.length < TOKEN_ACCOUNT_BYTES) return false;
1441
+ const data = account.data;
1442
+ return encodeBase58(data.subarray(0, 32)) === mint && encodeBase58(data.subarray(32, 64)) === holder && data[108] === 1;
1443
+ }
1444
+
1445
+ /**
1446
+ * A fresh token account of `mint` that `holder` holds, which `payer` opens (and pays the rent of, the fixed
1447
+ * `TOKEN_ACCOUNT_RENT`, never an RPC's figure) at an address of its key and a seed (`addressWithSeed`): no new key
1448
+ * signs. Pass it as the NFT's `tokenMetadata.into` when the holder's associated account can't take it (`takesNft`).
1449
+ *
1450
+ * The seed names the holder and the mint (a hash of both), so each holder has their own 8 addresses (`index` 0 to 7) of
1451
+ * a payer's. `there` is the account at the address now, as read (null: nothing there): nothing there is opened with
1452
+ * `CreateAccountWithSeed`; an address that holds only lamports someone sent it (System's, no data), which
1453
+ * `CreateAccountWithSeed` refuses, is topped up to the rent (never more than `TOKEN_ACCOUNT_RENT`, whatever the RPC says
1454
+ * it holds) and allocated as the payer's seed account instead; a token account there that takes the NFT
1455
+ * needs nothing (`instructions` []). Anything else at the address (an account of the holder's that no longer takes it,
1456
+ * or lamports no account can hold: negative, or not a whole number) answers null: try the next `index`.
1457
+ */
1458
+ export function freshNftAccount({ payer, holder, mint, index = 0, there = null }) {
1459
+ const seed = freshNftSeed(holder, mint, index);
1460
+ const address = addressWithSeed(payer, seed, TOKEN_PROGRAM);
1461
+ const init = initializeAccount3Ix({ account: address, mint, owner: holder });
1462
+ if (!there) return { address, instructions: [createAccountWithSeedIx({ from: payer, address, seed, lamports: TOKEN_ACCOUNT_RENT, space: TOKEN_ACCOUNT_BYTES, owner: TOKEN_PROGRAM }), init] };
1463
+ if (takesNft(there, holder, mint)) return { address, instructions: [] };
1464
+ if (there.owner !== SYSTEM_PROGRAM || there.data.length) return null;
1465
+ // The RPC's figure only lowers the top-up, never raises it past the rent: lamports that are negative or not a whole
1466
+ // number are no account's (a lying RPC), so the address is refused (a negative figure would have the payer send
1467
+ // that much more into the holder's account).
1468
+ const lamports = there.lamports ?? 0;
1469
+ const had = typeof lamports === 'bigint' ? lamports : Number.isSafeInteger(lamports) ? BigInt(lamports) : -1n;
1470
+ if (had < 0n) return null;
1471
+ const short = had < TOKEN_ACCOUNT_RENT ? TOKEN_ACCOUNT_RENT - had : 0n;
1472
+ return {
1473
+ address,
1474
+ instructions: [
1475
+ ...(short > 0n ? [transferLamports(payer, address, short)] : []),
1476
+ allocateWithSeedIx({ base: payer, address, seed, space: TOKEN_ACCOUNT_BYTES, owner: TOKEN_PROGRAM }),
1477
+ init,
1478
+ ],
1479
+ };
1480
+ }
1481
+
1482
+ /** The seed of a holder's fresh account of a mint: `nft<index>:` and 26 base58 characters of sha256(holder | mint). */
1483
+ const freshNftSeed = (holder, mint, index) => `nft${index}:${encodeBase58(sha256(concat(addressBytes(holder), addressBytes(mint)))).slice(0, 26)}`;
1484
+
1485
+ /** How many fresh accounts a payer may open for one holder and mint (`freshNftAccount`'s `index`). */
1486
+ export const FRESH_NFT_ACCOUNTS = 8;
1487
+
1488
+ /** A token account's rent on every cluster (165 bytes). */
1489
+ export const TOKEN_ACCOUNT_RENT = 2_039_280n;
1490
+
1491
+ /** 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
+ const ONLY_MOVES = { moves: true, locks: false, writes: false, reads: false };
1493
+
1494
+ /** A piece that reads no NFT: `at` (an auction's or a seat's own address) stands in for one. */
1495
+ const standIn = (at) => ({ accounts: { ...NO_PIECE, asset: at }, proof: [], writable: [] });
1496
+
1497
+ /** Anyone closes a compressed NFT's record (`pda.leaf`) that no longer matches its tree; its rent goes to `payer`, who paid it. */
1498
+ export const closeLeafIx = ({ asset, tree, payer }) => lootIx('close_leaf', {}, { record: pda.leaf(asset), tree, payer });
1499
+
1500
+ /**
1501
+ * Anyone proves a compressed NFT's leaf (`compressed` from `compressedFromDas`): put it before the instruction that
1502
+ * uses the NFT, in the same transaction. The proof must be against the tree's current root (`StaleProof` otherwise:
1503
+ * fetch it again). Its record is read only while the tree is as it was then. Bubblegum's config of the tree comes
1504
+ * with it: only a tree Bubblegum made has one.
1505
+ */
1506
+ export const proveLeafIx = ({ signer, compressed: c }) =>
1507
+ lootIx('prove_leaf', {
1508
+ leaf: { root: c.root, data_hash: c.dataHash, creator_hash: c.creatorHash, asset_data_hash: c.assetDataHash ?? null, flags: c.flags, nonce: c.nonce },
1509
+ }, {
1510
+ signer, asset: c.id, record: pda.leaf(c.id), owner: c.owner, delegate: c.delegate, collection: c.collection ?? BUBBLEGUM_PROGRAM, tree: c.tree,
1511
+ tree_config: pda.treeConfig(c.tree),
1512
+ }, { remaining: c.proof.map((node) => meta(typeof node === 'string' ? node : encodeBase58(node), false, false)) });
1513
+
1514
+ /** The project's owner or authority vouches for NFTs' off-chain traits: `root` of a `traitsTree`, under `key`. */
1515
+ export const setTraitsIx = ({ signer, project, key, root }) => lootIx('set_traits', { key, root }, { signer, project, traits: pda.traits(project, key) });
1516
+
1517
+ /** Anyone writes `nft`'s traits ([{ key, value }]) into its Sheet in `project`, by its `proof` in the tree under `key`. */
1518
+ export const proveTraitsIx = ({ signer, project, key, nft, attributes, proof }) =>
1519
+ lootIx('prove_traits', { attributes, proof }, { signer, traits: pda.traits(project, key), nft, sheet: pda.sheet(project, nft) });
1520
+
1521
+ /** What a farm does with a seat's NFT (`piece`'s flags): kept as its custody says, written on as it grows. */
1522
+ const farmKeeps = (farm, { writes = false, rolls = false } = {}) => ({
1523
+ moves: farm.custody === 'Escrow' || rolls,
1524
+ locks: farm.custody === 'Freeze',
1525
+ writes,
1526
+ held: farm.custody === 'Escrow',
1527
+ });
1528
+
1529
+ function nftSeat({ player, project, id, farm, asset, collection, hasAuthority = true, tokenMetadata: tm, compressed, previous = null, programs, leaving = false, keeps }) {
1530
+ const farmAddress = pda.farm(project, id);
1531
+ const p = piece({ project, asset, wallet: player, collection, hasAuthority, tokenMetadata: tm, compressed, ...keeps });
1532
+ const key = compressed ? compressed.id : asset;
1533
+ return {
1534
+ accounts: {
1535
+ ...farmSide(project, id, farm),
1536
+ player,
1537
+ ...p.accounts,
1538
+ previous,
1539
+ seat: pda.seat(farmAddress, farm.farmed.Tokens ? player : key),
1540
+ // Rewards are paid only as an NFT's own seat leaves.
1541
+ player_rewards: leaving && !farm.farmed.Tokens ? ata(player, farm.rewards_mint, tokenProgramOf(programs, farm.rewards_mint)) : null,
1542
+ },
1543
+ proof: p.proof,
1544
+ writable: p.writable,
1545
+ };
1546
+ }
1547
+
1548
+ /**
1549
+ * `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
+ * in a farm of tokens. Any kind (`piece`): a Core asset, a Token Metadata NFT's mint with `tokenMetadata`, or
1551
+ * `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. `extra` carries reward token hooks/memos and Core transfer
1553
+ * adapter accounts.
1554
+ */
1555
+ export const stakeNftIx = ({ player, project, id, farm, asset, collection, hasAuthority = true, tokenMetadata: tm, compressed, previous = null, tier = 0, programs, extra = [] }) => {
1556
+ // Its lock's end, when the farm keeps one, is written on it.
1557
+ const keeps = farmKeeps(farm, { writes: Boolean(farm.lock_key) });
1558
+ const seat = nftSeat({ player, project, id, farm, asset, collection, hasAuthority, tokenMetadata: tm, compressed, previous, programs, keeps });
1559
+ return lootIx('stake_nft', { tier }, { ...seat.accounts, toll_pool: farmSinks(project, farm).toll_pool }, { remaining: [...seat.proof, ...extra], writable: [...farmWrites(farm), ...tollWrites(farm), ...seat.writable] });
1560
+ };
1561
+
1562
+ /**
1563
+ * `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
+ * 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 at `nonce` (anyone then asks the oracle,
1566
+ * `askRollIx`, and settles); a box that can burn it or give it away keeps it in the project's hands until then.
1567
+ */
1568
+ 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, programs, extra = [] }) => {
1569
+ // It grows on its way out; a mission that rolls may hand it to the project.
1570
+ const keeps = farmKeeps(farm, { writes: Boolean(farm.growth?.length), rolls: roll != null });
1571
+ const seat = nftSeat({ player, project, id, farm, asset, collection, hasAuthority, tokenMetadata: tm, compressed, programs, leaving: true, keeps });
1572
+ // Leaving a mission that rolls (its tier's `roll`, a box id): the box, and the order the roll is made in.
1573
+ const rolls = roll != null ? { loot_box: pda.box(project, roll), order: pda.order(project, player, nonce) } : { loot_box: null, order: null };
1574
+ // A mission's prize (its tier's, `tier` the seat's): its accounts after the proof, unread when it is not earned; a
1575
+ // roll's order at `nonce` plus 1. A forfeit takes neither the prize nor the rewards (the program reads neither), so it
1576
+ // passes neither: the smallest way out.
1577
+ const prize = tier != null && !forfeit ? farm.tiers[tier]?.prize : null;
1578
+ const prized = prize ? prizeMetas({ project, collection: projectCollection ?? collection, prize, winner: player, nonce: toBigInt(nonce) + 1n, poolMints, items, nft, programs }) : [];
1579
+ if (forfeit) seat.accounts.player_rewards = null;
1580
+ return lootIx('unstake_nft', { forfeit, nonce }, { ...seat.accounts, ...rolls, associated_token_program: prize?.Tokens ? ASSOCIATED_TOKEN_PROGRAM : null, toll_pool: farmSinks(project, farm).toll_pool }, {
1581
+ remaining: [...seat.proof, ...prized, ...extra],
1582
+ writable: [...farmWrites(farm), ...tollWrites(farm), ...seat.writable],
1583
+ });
1584
+ };
1585
+
1586
+ /**
1587
+ * `player` collects what the seat of `by` owes (their own address for tokens, a collectible's asset). `held`: in a farm
1588
+ * 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.
1590
+ */
1591
+ export const harvestIx = ({ player, project, id, farm, by = player, held = null, tree = null, compound = false, programs, extra = [] }) =>
1592
+ lootIx('harvest', { compound }, {
1593
+ ...farmSide(project, id, farm),
1594
+ player,
1595
+ seat: pda.seat(pda.farm(project, id), by),
1596
+ player_rewards: ata(player, farm.rewards_mint, tokenProgramOf(programs, farm.rewards_mint)),
1597
+ held,
1598
+ tree,
1599
+ // 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(pda.farm(project, id)) : null,
1601
+ toll_pool: farmSinks(project, farm).toll_pool,
1602
+ }, { remaining: extra, writable: [...farmWrites(farm), ...tollWrites(farm)] });
1603
+
1604
+ /**
1605
+ * Anyone (`signer`) writes what a seat's NFT grew (a farm with growth), then reads its boosts again. `seat` is the
1606
+ * decoded Seat of `by`. Its NFT is any kind (`piece`): a Core asset (`collection` its collection now, the project's when
1607
+ * left out; `hasAuthority` false for one the project has no authority over), a Token Metadata NFT (`tokenMetadata`), or
1608
+ * `compressed` (after a `proveLeafIx`). What shows where it is must come: a compressed NFT's proof, and a Token Metadata
1609
+ * NFT's token account that holds it, `holder`'s (its staker's, or a watched NFT's buyer's). `signer` pays for what the
1610
+ * write adds. `extra`: the reward mint's hook accounts.
1611
+ */
1612
+ export const refreshSeatIx = ({ signer, project, id, farm, by, seat, collection, hasAuthority = true, tokenMetadata: tm, compressed, holder = seat.player, extra = [] }) => {
1613
+ const address = pda.seat(pda.farm(project, id), by);
1614
+ // A token seat with no booster: its own address stands in for the NFT.
1615
+ const p = seat.asset === NO_ADDRESS
1616
+ ? standIn(address)
1617
+ : 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: [...farmWrites(farm), ...p.writable] });
1619
+ };
1620
+
1621
+ // ---------------------------------------------------------------- recipes and holds
1622
+
1623
+ /**
1624
+ * `player` crafts recipe `id` (`recipe` decoded, the version shown to them), `times` over. `assets`: the
1625
+ * collectibles handed in, in the recipe's order (the burned, the used, and for a `Show` line the NFT shown: its address
1626
+ * when it is the project's, `{ asset, collection }` for a Core NFT of another collection, `{ mint, owner }` for a Token
1627
+ * Metadata one; for a `Trade` line the NFT traded in, as `piece` takes it: `{ asset, collection, tokenMetadata,
1628
+ * compressed }`). A collectible used or renewed that is staked in a farm holding it for the player (escrow) is
1629
+ * `{ asset, farm }`, `farm` that farm's id: its seat and farm follow it. A `Staked` line names the farm its collectible
1630
+ * is staked in (`Input::Staked { item, farm }`, escrowed or frozen there): its address, or `{ asset, farm }` with that
1631
+ * farm, and its seat and that farm follow it. Each item line passes its item's collection
1632
+ * (`items[id].collection`, the decoded Items; `collection` when not given), written for a burn: Core writes it as it
1633
+ * burns a collectible. A recipe's `raises` write on the kept ones, in their Sheet when the project has no
1634
+ * 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's prize mints (`rollMints`): `boxes` maps the box's id to its decoded
1636
+ * LootBox, `rollPools` each pool its prizes name to its mint (null for one never opened; `poolMints` when not given).
1637
+ * A craft makes several Core calls: raise the compute limit. `extra`: a token's hook accounts.
1638
+ */
1639
+ export function craftIx({ player, project, collection, id, recipe, assets = [], times = 1, nonce = 0n, holders, programs, treasury, poolMints, items = {}, nfts = {}, boxes = {}, rollPools = poolMints, extra = [] }) {
1640
+ const remaining = [];
1641
+ const given = [...assets];
1642
+ const tokens = (mint) => ata(player, mint, tokenProgramOf(programs, mint));
1643
+ const itemLine = (itemId, burns = false) => [meta(pda.item(project, itemId), false, false), meta(items[itemId]?.collection ?? collection, false, burns)];
1644
+ for (const input of recipe.inputs) {
1645
+ if (input.Burn) {
1646
+ remaining.push(...itemLine(input.Burn.item, true));
1647
+ 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.Renew || input.Staked) {
1649
+ remaining.push(...itemLine((input.Use ?? input.Renew ?? input.Staked).item));
1650
+ const used = take(given, 'a collectible to use');
1651
+ // A staked one is on its seat in the farm its line names.
1652
+ if (input.Staked && typeof used !== 'string' && used.farm !== undefined && Number(used.farm) !== input.Staked.farm)
1653
+ throw new Error(`craft: the recipe takes this collectible staked in farm ${input.Staked.farm}, not farm ${used.farm}`);
1654
+ const seated = input.Staked ? { asset: typeof used === 'string' ? used : used.asset, farm: input.Staked.farm } : used;
1655
+ if (typeof seated === 'string') remaining.push(meta(seated, false, true));
1656
+ else {
1657
+ // On a seat: its seat and its farm follow it.
1658
+ const farm = pda.farm(project, seated.farm);
1659
+ remaining.push(meta(seated.asset, false, true), meta(pda.seat(farm, seated.asset), false, false), meta(farm, false, false));
1660
+ }
1661
+ } else if (input.Pay) {
1662
+ const { mint, to } = input.Pay;
1663
+ remaining.push(meta(mint, false, true), meta(tokens(mint), false, true), ...destinationAccounts(project, to, mint, { holders, programs, treasury }));
1664
+ } else if (input.Trade) {
1665
+ // Any NFT of the line's collection, traded in: burned, given on or stashed (`nftPaid`).
1666
+ const nft = take(given, 'an NFT to trade in');
1667
+ remaining.push(...nftPaid({ project, owner: player, then: input.Trade.then, ...(typeof nft === 'string' ? { asset: nft, collection: input.Trade.collection } : nft) }));
1668
+ } else if (input.Show) {
1669
+ // Any NFT, kept: one of the project's (its address), a Core asset of another collection ({ asset, collection }:
1670
+ // with its Sheet, then its collection), a Token Metadata NFT ({ mint, owner }: its metadata, the holder's
1671
+ // token account and its Sheet), or a compressed NFT ({ compressed, tree }: its record, proven earlier in the
1672
+ // transaction, its tree and its Sheet). What a raise writes on is writable.
1673
+ 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));
1675
+ 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
+ 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
+ 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));
1678
+ }
1679
+ }
1680
+ const made = {};
1681
+ for (const output of recipe.outputs) {
1682
+ if (output.Prize) {
1683
+ // A prize (`prizeMetas`): collectibles of one item made in order, each at the next serial.
1684
+ const prize = output.Prize[0];
1685
+ 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, strict: true, boxes, rollPools }));
1687
+ if (item !== undefined) made[item] = (made[item] ?? 0) + 1;
1688
+ } else if (output.RollOn) {
1689
+ // The box, then the order its roll is made in, at the craft's `nonce`, then the box's prize mints (`rollMints`).
1690
+ remaining.push(meta(pda.box(project, output.RollOn.box_id), false, false), meta(pda.order(project, player, nonce), false, true), ...strictRollMints(project, output.RollOn.box_id, boxes, rollPools ?? {}));
1691
+ }
1692
+ }
1693
+ const address = pda.recipe(project, id);
1694
+ const receipt = limits(recipe.per_wallet) ? pda.receipt(address, player) : null;
1695
+ return lootIx('craft', { version: recipe.version, times, nonce }, { player, project, recipe: address, receipt }, { remaining: [...remaining, ...extra] });
1696
+ }
1697
+
1698
+ function take(list, what) {
1699
+ if (!list.length) throw new Error(`craft: missing ${what}`);
1700
+ return list.shift();
1701
+ }
1702
+
1703
+ /** The collectibles a craft of `recipe` makes: one per `Item` prize among its outputs. */
1704
+ export const craftNewAssets = (recipe) => recipe.outputs.filter((o) => o.Prize?.[0]?.Item).length;
1705
+
1706
+ /**
1707
+ * `player` hands an NFT to the project until `freeAt` (Unix seconds; the hold's address names it, `pda.hold`): a Core
1708
+ * asset (`collection` its own, Core's address for none), a Token Metadata NFT (`tokenMetadata`), or `compressed` (after
1709
+ * a `proveLeafIx`).
1710
+ */
1711
+ export function holdIx({ player, project, collection, asset, tokenMetadata: tm, compressed, freeAt, reference: ref }) {
1712
+ const p = piece({ project, asset, wallet: player, collection, tokenMetadata: tm, compressed, ...ONLY_MOVES });
1713
+ return lootIx('hold', { free_at: toBigInt(freeAt), reference: reference(ref) }, { player, project, ...p.accounts, hold: pda.hold(p.accounts.asset, freeAt) }, { remaining: p.proof, writable: p.writable });
1714
+ }
1715
+
1716
+ // ---------------------------------------------------------------- stashes
1717
+
1718
+ /**
1719
+ * The project's owner or authority (`signer`) puts an NFT it holds into stash `id`, or (`out`: a wallet) takes one the
1720
+ * stash holds out to that wallet. The NFT is any kind, as `piece` takes it: `collection` a Core asset's own,
1721
+ * `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. Taken out with `out` the project's own address, the entry is only dropped
1723
+ * from the stash and the NFT stays where it is. `out` is never another account of the program (`ToTheProject`).
1724
+ */
1725
+ export function stashIx({ signer, project, id, lottery = null, asset, out = null, collection, tokenMetadata: tm, compressed }) {
1726
+ const to = out ?? signer;
1727
+ // A lottery's own stash (`lottery`: the lottery's id): an NFT its place names leaves only as that prize, or once the
1728
+ // lottery has ended; taking one out passes the lottery.
1729
+ const own = lottery != null ? pda.lottery(project, lottery) : null;
1730
+ const stash = own ? pda.lotteryStash(own) : pda.stash(project, id);
1731
+ // Out to a wallet or the project's own address, never another account of the program (a record, a stash, a fee
1732
+ // box), where no record names the NFT and nothing could move it again (ToTheProject): the ones this call names are
1733
+ // refused before sending, and the program refuses any other (js/play only takes one out to the signer's wallet).
1734
+ if (out && out !== project && [stash, own, pda.fees(project), pda.platform()].includes(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 the project’s own address to only drop it from the stash.');
1736
+ const p = piece({ project, asset, wallet: to, collection, tokenMetadata: tm, compressed, ...ONLY_MOVES });
1737
+ return lootIx('stash', { of: own ? { Lottery: [lottery] } : { Project: [id] }, out: Boolean(out) }, { signer, project, stash, to, ...p.accounts, lottery: own && out ? own : null }, { remaining: p.proof, writable: p.writable });
1738
+ }
1739
+
1740
+ // ---------------------------------------------------------------- contests
1741
+
1742
+ /**
1743
+ * `player` backs `side` with `amount`, naming the contest's incarnation (which fixes its terms). `contest` is the
1744
+ * decoded Contest. A contest of NFTs (`contest.nft`) takes one NFT instead (`nft`: { asset, collection, tokenMetadata,
1745
+ * compressed }, as `piece` takes it), which the contest holds until it is claimed. `shown`/`listProof` pass its gate.
1746
+ * `extra`: a hook's accounts.
1747
+ */
1748
+ export function enterIx({ player, project, id, contest, side, amount, shown, listProof, nft, playerToken, programs, extra = [] }) {
1749
+ const address = pda.contest(project, id);
1750
+ // Read for its rule (with its Sheet, for an NFT the project has no authority over), then handed to the project.
1751
+ const staked = contest.nft ? piece({ project, wallet: player, ...nft, ...ONLY_MOVES, reads: true }) : null;
1752
+ return lootIx(
1753
+ 'enter',
1754
+ { side, amount: contest.nft ? 1n : amount, incarnation: contest.incarnation },
1755
+ {
1756
+ player,
1757
+ project,
1758
+ contest: address,
1759
+ entry: pda.entry(address, player),
1760
+ player_token: playerToken ?? ata(player, contest.mint, tokenProgramOf(programs, contest.mint)),
1761
+ mint: contest.mint,
1762
+ },
1763
+ { remaining: [...gateAccounts(project, { gate: contest.gate, wallet: player, shown, listProof }), ...(staked ? [...pieceMetas(staked), ...staked.proof] : []), ...extra] },
1764
+ );
1765
+ }
1766
+
1767
+ /**
1768
+ * Anyone (`payer`) claims `player`'s entry on `side`, into the player's account of the mint, which `payer` opens when
1769
+ * it is not there (the last entry's claim closes the pot and writes the mint). A winning entry of a contest with a
1770
+ * prize (`contest.prize`) also gets it (`prizeMetas`: `collection`, `poolMints`, `items`, `nft`; a roll's order at
1771
+ * `nonce`, `prizeNonce(entry)` by default), `payer` paying what it opens. For a contest of NFTs, `entry` is the decoded
1772
+ * Entry: its own NFT (`own`, its kind as `piece` takes it) goes back to its player, or to the opener for a loser's NFT
1773
+ * no winner took; a winner, while losers' NFTs are left, also takes one (`taking`: { player, side } of a losing entry
1774
+ * that still holds one, and `kind`, its NFT's kind). While losers' NFTs are left only the winner sends their claim, and
1775
+ * picks which (`payer` the player; `WinnerPicks` otherwise). `extra`: a hook's accounts.
1776
+ */
1777
+ export function claimIx({ payer, project, id, contest, player, side, entry, own = {}, taking, nonce, collection, poolMints, items, nft, prizeMint, forgo = false, programs, extra = [] }) {
1778
+ const address = pda.contest(project, id);
1779
+ const entryAddress = pda.entry(address, player);
1780
+ nonce = toBigInt(nonce ?? prizeNonce(entryAddress));
1781
+ const won = contest.verdict === 'Won' && Number(contest.winner) === Number(side);
1782
+ // A winning entry of a contest with a prize: the prize's accounts, after the NFTs'.
1783
+ // A split prize is paid from the contest's vault (named accounts below), not given as a prize line.
1784
+ const split = Boolean(contest.prize_split);
1785
+ const prize = contest.prize != null && won && !split && !forgo ? prizeMetas({ project, collection, prize: contest.prize, winner: player, nonce, poolMints, items, nft, programs }) : [];
1786
+ const pMint = split ? splitMint(contest.prize, prizeMint ?? poolMints?.[contest.prize?.Tokens?.from.Pool?.[0]]) : null;
1787
+ const nfts = [];
1788
+ if (contest.nft && entry && entry.asset !== NO_ADDRESS) {
1789
+ const lost = contest.verdict === 'Won' && !won;
1790
+ const mine = piece({ project, asset: entry.asset, wallet: lost ? contest.opener : player, ...own, ...ONLY_MOVES });
1791
+ nfts.push(...pieceMetas(mine), ...mine.proof);
1792
+ if (won && taking) {
1793
+ const theirs = pda.entry(address, taking.player);
1794
+ const p = piece({ project, asset: taking.asset, wallet: player, ...taking.kind, ...ONLY_MOVES });
1795
+ nfts.push(meta(theirs, false, true), ...pieceMetas(p), ...p.proof);
1796
+ }
1797
+ }
1798
+ return lootIx(
1799
+ 'claim',
1800
+ { nonce, forgo },
1801
+ {
1802
+ payer,
1803
+ project,
1804
+ contest: address,
1805
+ entry: entryAddress,
1806
+ player,
1807
+ opener: contest.opener,
1808
+ to: ata(player, contest.mint, tokenProgramOf(programs, contest.mint)),
1809
+ mint: contest.mint,
1810
+ prize_mint: pMint,
1811
+ prize_vault: split ? pda.prizeVault(address) : null,
1812
+ prize_to: split ? ata(player, pMint, tokenProgramOf(programs, pMint)) : null,
1813
+ },
1814
+ // The last claim closes the pot (and a split prize's vault), writing their mints.
1815
+ { writable: Number(contest.entries) === 1 ? ['mint', ...(split ? ['prize_mint'] : [])] : [], remaining: [...nfts, ...prize, ...extra] },
1816
+ );
1817
+ }
1818
+
1819
+ // ---------------------------------------------------------------- auctions
1820
+
1821
+ /** What a bid signs: sha256(mint | Borsh(AuctionArgs)). */
1822
+ export function auctionTerms(mint, args) {
1823
+ return sha256(concat(addressBytes(mint), encodeBorsh(IDL, { defined: { name: 'AuctionArgs' } }, args)));
1824
+ }
1825
+
1826
+ /** An auction's AuctionArgs from its decoded account (what `open_auction` fixed). */
1827
+ export const auctionArgsOf = (auction) => ({
1828
+ lot: auction.lot,
1829
+ reserve: auction.reserve,
1830
+ min_raise_bps: auction.min_raise_bps,
1831
+ starts_at: auction.starts_at,
1832
+ ends_at: auction.ends_at,
1833
+ extend: auction.extend,
1834
+ shares: auction.shares,
1835
+ gate: auction.gate ?? null,
1836
+ nft_bids: auction.nft_bids ?? null,
1837
+ });
1838
+
1839
+ /** When bids close, as the program reckons it: `ends_at`, or `extend` seconds after the last bid when that is later. */
1840
+ export const auctionEnds = (auction) => {
1841
+ if (BigInt(auction.last_bid_at) === 0n) return BigInt(auction.ends_at);
1842
+ const late = BigInt(auction.last_bid_at) + BigInt(auction.extend);
1843
+ return late > BigInt(auction.ends_at) ? late : BigInt(auction.ends_at);
1844
+ };
1845
+
1846
+ /** The least next bid the program takes, or null when no u64 amount can top it. */
1847
+ export function leastBid(auction) {
1848
+ const high = BigInt(auction.high);
1849
+ if (high === 0n) return BigInt(auction.reserve) > 0n ? BigInt(auction.reserve) : 1n;
1850
+ const raise = (high * BigInt(auction.min_raise_bps) + 9_999n) / 10_000n;
1851
+ const next = high + (raise > 0n ? raise : 1n);
1852
+ return next <= (1n << 64n) - 1n ? next : null;
1853
+ }
1854
+
1855
+ /**
1856
+ * `seller` opens its auction `id` of `args.lot` ({ Item: [id] }, { Asset: [address] } or { Roll: [box id] }), alone,
1857
+ * 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
+ * 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). Its address names the seller, so an
1860
+ * id is the seller's own.
1861
+ */
1862
+ export function openAuctionIx({ seller, project, collection, tokenMetadata: tm, compressed, id, mint, args }) {
1863
+ const auction = pda.auction(project, seller, id);
1864
+ const asset = args.lot.Asset?.[0] ?? null;
1865
+ // An item lot reads no NFT: the auction's own address stands in.
1866
+ const p = asset ? piece({ project, asset, wallet: seller, collection, tokenMetadata: tm, compressed, ...ONLY_MOVES }) : standIn(auction);
1867
+ return lootIx(
1868
+ 'open_auction',
1869
+ { id, args: { ...args, gate: args.gate ?? null, nft_bids: args.nft_bids ?? null } },
1870
+ { seller, project, auction, mint, item: args.lot.Item ? pda.item(project, args.lot.Item[0]) : null, loot_box: args.lot.Roll ? pda.box(project, args.lot.Roll[0]) : null, ...p.accounts },
1871
+ { remaining: p.proof, writable: p.writable },
1872
+ );
1873
+ }
1874
+
1875
+ /**
1876
+ * `bidder` bids `amount` on auction `id`, signing its terms; the bid it tops goes back to the high bidder's account
1877
+ * of the mint, which `bidder` opens when it is not there. `auction` is the decoded Auction. `shown`/`listProof`: what
1878
+ * passes the auction's gate. An auction of NFT bids (`auction.nft_bids`) takes `nfts` instead ([{ asset, collection,
1879
+ * 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. `extra`: a hook's accounts.
1881
+ */
1882
+ export function bidIx({ bidder, project, id, auction, amount, shown, listProof, nfts = [], held = [], bidderToken, programs, extra = [] }) {
1883
+ const tokenProgram = tokenProgramOf(programs, auction.mint);
1884
+ const outbid = BigInt(auction.high) > 0n;
1885
+ const bundle = [];
1886
+ if (auction.nft_bids) {
1887
+ for (const nft of nfts) {
1888
+ if (nft.compressed) throw new Error('A bid can’t be a compressed NFT: bid Core or Token Metadata NFTs.');
1889
+ const p = piece({ project, wallet: bidder, ...nft, ...ONLY_MOVES, reads: true });
1890
+ bundle.push(...pieceMetas(p), ...p.proof);
1891
+ }
1892
+ (auction.held ?? []).forEach((asset, i) => {
1893
+ const p = piece({ project, asset, wallet: auction.high_bidder, ...held[i], ...ONLY_MOVES });
1894
+ bundle.push(...pieceMetas(p), ...p.proof);
1895
+ });
1896
+ }
1897
+ return lootIx(
1898
+ 'bid',
1899
+ { amount: auction.nft_bids ? BigInt(nfts.length) : amount, terms: auctionTerms(auction.mint, auctionArgsOf(auction)) },
1900
+ {
1901
+ bidder,
1902
+ project,
1903
+ auction: pda.auction(project, auction.seller, id),
1904
+ vault: vault(project, auction.mint, tokenProgram),
1905
+ bidder_token: bidderToken ?? ata(bidder, auction.mint, tokenProgram),
1906
+ outbid: outbid ? auction.high_bidder : null,
1907
+ outbid_token: outbid ? ata(auction.high_bidder, auction.mint, tokenProgram) : null,
1908
+ mint: auction.mint,
1909
+ },
1910
+ // 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'] },
1912
+ );
1913
+ }
1914
+
1915
+ /**
1916
+ * Anyone ends auction `id` once it is over (its seller calls off one with no bid). Sold: the lot goes to the high
1917
+ * bidder (an item that is a collectible is minted to them at `pda.itemAsset(item, serial)`; a roll of a box is made
1918
+ * for them in an order at `prizeRollNonce(nonce)`, `nonce` being `prizeNonce(auction, last_bid_at)` by default, which `signer` pays for), the price
1919
+ * through the shares, the rest to whoever owned the lot (the seller of an NFT, the treasury for the project's item or
1920
+ * 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
+ * not there. `item` is the decoded Item of an item lot; for an NFT lot, `collection` its Core collection now (the
1922
+ * project's when left out), `tokenMetadata` for a Token Metadata NFT, or `compressed` (after a `proveLeafIx`).
1923
+ */
1924
+ export function endAuctionIx({ signer, project, projectState, id, auction, item = null, collection, tokenMetadata: tm, compressed, held = [], reference: ref, nonce, holders, programs, extra = [] }) {
1925
+ const tokenProgram = tokenProgramOf(programs, auction.mint);
1926
+ 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.serial) : null;
1929
+ const asset = auction.lot.Asset?.[0] ?? made;
1930
+ const proceeds = auction.lot.Asset ? auction.seller : projectState.treasury;
1931
+ const address = pda.auction(project, auction.seller, id);
1932
+ 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
+ const roll = sold ? auction.lot.Roll?.[0] : undefined;
1935
+ nonce = toBigInt(nonce ?? prizeNonce(address, BigInt(auction.last_bid_at)));
1936
+ // An NFT lot is sent on; a collectible sold is minted into its item's collection, which Core writes.
1937
+ const p = auction.lot.Asset
1938
+ ? piece({ project, asset, wallet: receiver, collection, tokenMetadata: tm, compressed, ...ONLY_MOVES })
1939
+ : asset
1940
+ ? { ...piece({ project, asset, collection: item.collection, moves: false, locks: false, reads: false }), writable: ['collection'] }
1941
+ : standIn(address);
1942
+ // 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
1944
+ ? (auction.held ?? []).flatMap((nft, i) => {
1945
+ const q = piece({ project, asset: nft, wallet: proceeds, ...held[i], ...ONLY_MOVES });
1946
+ return [...pieceMetas(q), ...q.proof];
1947
+ })
1948
+ : [];
1949
+ return lootIx(
1950
+ 'end_auction',
1951
+ { reference: reference(ref), nonce },
1952
+ {
1953
+ signer,
1954
+ project,
1955
+ auction: address,
1956
+ seller: auction.seller,
1957
+ bidder: sold ? auction.high_bidder : auction.seller,
1958
+ mint: auction.mint,
1959
+ vault: vault(project, auction.mint, tokenProgram),
1960
+ proceeds,
1961
+ proceeds_token: ata(proceeds, auction.mint, tokenProgram),
1962
+ item: itemId !== undefined ? pda.item(project, itemId) : null,
1963
+ ...p.accounts,
1964
+ loot_box: roll !== undefined ? pda.box(project, roll) : null,
1965
+ order: roll !== undefined ? pda.order(project, auction.high_bidder, prizeRollNonce(nonce)) : null,
1966
+ },
1967
+ {
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],
1970
+ },
1971
+ );
1972
+ }
1973
+
1974
+ // ---------------------------------------------------------------- offers
1975
+ //
1976
+ // An offer's rule reads an NFT outside the project's collections without the project's Sheet: a Core NFT by its own
1977
+ // attributes, a Token Metadata or compressed NFT with none, so an attribute test fails for an NFT that doesn't carry
1978
+ // the key itself (every Token Metadata or compressed one).
1979
+
1980
+ /** The hash a taker signs, as the program rebuilds it: sha256(mint | rule | deal | shares), Borsh. `offer` is the decoded Offer. */
1981
+ export function offerTerms(offer) {
1982
+ const borsh = (name, value) => encodeBorsh(IDL, { defined: { name } }, value);
1983
+ return sha256(concat(addressBytes(offer.mint), borsh('Rule', offer.rule), borsh('Deal', offer.deal), encodeBorsh(IDL, { vec: { defined: { name: 'Share' } } }, offer.shares)));
1984
+ }
1985
+
1986
+ /** What one take of a deal pays out of its offer's money: a bid's price, a loan's amount. */
1987
+ export const dealUnit = (deal) => BigInt(deal.Buy?.price ?? deal.Lend.amount);
1988
+
1989
+ /**
1990
+ * `maker` (any wallet, signing alone) opens offer `id` in `mint`, and puts all of its money (`left` times the deal's
1991
+ * price or amount) into the project's vault now. `args`: { rule, deal: { Buy: { price } } or { Lend: { amount, repay,
1992
+ * lasts } }, left, shares } (shares only on a price). Its address names the maker, so an id is the maker's own.
1993
+ */
1994
+ export function openOfferIx({ maker, project, id, mint, args, makerToken, programs, extra = [] }) {
1995
+ const tokenProgram = tokenProgramOf(programs, mint);
1996
+ return lootIx(
1997
+ 'open_offer',
1998
+ { id, args: { shares: [], ...args } },
1999
+ { maker, project, offer: pda.offer(project, maker, id), mint, maker_token: makerToken ?? ata(maker, mint, tokenProgram), vault: vault(project, mint, tokenProgram) },
2000
+ { remaining: extra },
2001
+ );
2002
+ }
2003
+
2004
+ /** Its maker calls offer `id` off (`offer` the decoded Offer): what is left of its money goes back to them. Free. */
2005
+ export function closeOfferIx({ maker, project, id, offer, programs, extra = [] }) {
2006
+ const tokenProgram = tokenProgramOf(programs, offer.mint);
2007
+ return lootIx(
2008
+ 'close_offer',
2009
+ {},
2010
+ { 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 },
2012
+ );
2013
+ }
2014
+
2015
+ /**
2016
+ * `seller` sells an NFT that passes the bid's rule into offer `id` (`offer` the decoded Offer, its deal a `Buy`),
2017
+ * signing its terms: the NFT goes straight to the maker, the price through the shares, the rest to the seller. The NFT is
2018
+ * any kind, as `piece` takes it (`collection`, `hasAuthority` false for a Core asset of another collection,
2019
+ * `tokenMetadata`, or `compressed` after a `proveLeafIx`).
2020
+ */
2021
+ export function sellToOfferIx({ seller, project, projectState, id, offer, asset, collection, hasAuthority, tokenMetadata: tm, compressed, holders, programs, extra = [] }) {
2022
+ const tokenProgram = tokenProgramOf(programs, offer.mint);
2023
+ const p = passedOn({ project, owner: seller, receiver: offer.maker, burns: false, asset, collection, hasAuthority, tokenMetadata: tm, compressed, reads: true });
2024
+ return lootIx(
2025
+ 'sell_to_offer',
2026
+ { terms: offerTerms(offer) },
2027
+ { 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
+ {
2029
+ remaining: [...p.proof, ...shareAccounts(project, offer.shares, offer.mint, { holders, programs, treasury: projectState?.treasury }), ...extra],
2030
+ writable: [...(sharesBurn(offer.shares) ? ['mint'] : []), ...p.writable],
2031
+ },
2032
+ );
2033
+ }
2034
+
2035
+ /**
2036
+ * `borrower` borrows against an NFT that passes the offer's rule (`offer` the decoded Offer, its deal a `Lend`), due at
2037
+ * `due` (Unix seconds: after now, at most the deal's `lasts` from now), signing its terms. The NFT is held at
2038
+ * `pda.hold(asset, due)` until it is repaid (`repayIx`, then `releaseIx` sends it home) or, once due, sent to the
2039
+ * lender (`releaseIx` with `to` the lender); the loan goes to the borrower. The NFT is any kind, as `sellToOfferIx` takes it.
2040
+ */
2041
+ export function borrowIx({ borrower, project, id, offer, due, asset, collection, hasAuthority, tokenMetadata: tm, compressed, programs, extra = [] }) {
2042
+ const tokenProgram = tokenProgramOf(programs, offer.mint);
2043
+ const p = piece({ project, asset, wallet: borrower, collection, hasAuthority, tokenMetadata: tm, compressed, ...ONLY_MOVES, reads: true });
2044
+ return lootIx(
2045
+ 'borrow',
2046
+ { due: toBigInt(due), terms: offerTerms(offer) },
2047
+ {
2048
+ borrower, project, offer: pda.offer(project, offer.maker, id), maker: offer.maker, hold: pda.hold(p.accounts.asset, due), ...p.accounts,
2049
+ mint: offer.mint, vault: vault(project, offer.mint, tokenProgram), borrower_token: ata(borrower, offer.mint, tokenProgram),
2050
+ },
2051
+ { remaining: [...p.proof, ...extra], writable: p.writable },
2052
+ );
2053
+ }
2054
+
2055
+ /**
2056
+ * Anyone (`payer`: the borrower, a friend) repays a loan before it is due (`hold` the decoded Hold, with its debt): the
2057
+ * 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. `extra`: a
2059
+ * transfer hook's accounts.
2060
+ */
2061
+ export function repayIx({ payer, project, hold, payerToken, programs, extra = [] }) {
2062
+ const { lender, mint } = hold.debt;
2063
+ const tokenProgram = tokenProgramOf(programs, mint);
2064
+ return lootIx(
2065
+ 'repay',
2066
+ {},
2067
+ {
2068
+ payer, project, hold: pda.hold(hold.asset, hold.free_at), lender,
2069
+ lender_token: ata(lender, mint, tokenProgram), payer_token: payerToken ?? ata(payer, mint, tokenProgram), mint,
2070
+ },
2071
+ { remaining: extra },
2072
+ );
2073
+ }
2074
+
2075
+ // ---------------------------------------------------------------- listings
2076
+
2077
+ /** What a taker signs: sha256(maker | asset | mint | Borsh(ListingDeal) | Borsh(shares)). */
2078
+ export function listingTerms(listing) {
2079
+ const deal = encodeBorsh(IDL, { defined: { name: 'ListingDeal' } }, listing.deal);
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
+ }
2083
+
2084
+ /**
2085
+ * `maker` (any wallet, signing alone) lists an NFT they hold, of any kind (as `piece` takes it), for sale
2086
+ * (`deal: { Sell: { price } }`) or for rent (`{ Rent: { price, lasts, max_periods } }`: `price` a period of `lasts`
2087
+ * seconds, a renter taking 1 to `max_periods` at once), paid in `mint`, `shares` taken before the maker's part. The NFT
2088
+ * goes to the project until it is sold or the listing is closed. The listing's address names the NFT
2089
+ * (`pda.listing(project, asset)`), so a game finds it, and its renter, with one account read.
2090
+ */
2091
+ export function openListingIx({ maker, project, asset, mint, deal, shares = [], collection, hasAuthority, tokenMetadata: tm, compressed, extra = [] }) {
2092
+ 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 });
2094
+ }
2095
+
2096
+ /**
2097
+ * `taker` takes a listing (`listing`: the decoded Listing), signing its terms: a sale pays its price through the shares
2098
+ * to the maker and hands the NFT over; a rental of `periods` periods (1 to its `max_periods`) pays `price` each and
2099
+ * makes the taker its user until `periods × lasts` from now, refused while another runs. A rental moves no NFT, so it
2100
+ * takes none of the NFT's accounts but its address. `holders`, `programs`, `treasury` as for any split. After the
2101
+ * take's own accounts come a sale's compressed NFT's proof, then the shares' accounts, then (in `extra`) Core's
2102
+ * transfer adapters and any transfer hook's accounts, which the program finds by address after the shares'.
2103
+ */
2104
+ export function takeListingIx({ taker, project, projectState, listing, periods = 1, collection, hasAuthority, tokenMetadata: tm, compressed, holders, programs, takerToken, extra = [] }) {
2105
+ const tokenProgram = tokenProgramOf(programs, listing.mint);
2106
+ const sale = Boolean(listing.deal.Sell);
2107
+ // A rental moves nothing: a Core or Token Metadata NFT by its address; a compressed one by its record and tree
2108
+ // (`proveLeafIx` first, in the same transaction), with no proof.
2109
+ const p = sale ? piece({ project, asset: listing.asset, wallet: taker, collection, hasAuthority, tokenMetadata: tm, compressed, ...ONLY_MOVES })
2110
+ : compressed ? { ...piece({ project, asset: listing.asset, wallet: taker, compressed, ...ONLY_MOVES, moves: false }), proof: [] } : standIn(listing.asset);
2111
+ return lootIx(
2112
+ 'take_listing',
2113
+ { terms: listingTerms(listing), periods },
2114
+ {
2115
+ taker, project, listing: pda.listing(project, listing.asset), maker: listing.maker, maker_token: ata(listing.maker, listing.mint, tokenProgram),
2116
+ ...p.accounts, mint: listing.mint, taker_token: takerToken ?? ata(taker, listing.mint, tokenProgram),
2117
+ },
2118
+ {
2119
+ remaining: [...p.proof, ...shareAccounts(project, listing.shares ?? [], listing.mint, { holders, programs, treasury: projectState?.treasury }), ...extra],
2120
+ writable: [...(sharesBurn(listing.shares ?? []) ? ['mint'] : []), ...p.writable],
2121
+ },
2122
+ );
2123
+ }
2124
+
2125
+ /**
2126
+ * Its maker closes a listing (`listing` the decoded Listing): the NFT goes home. While a rental runs it only stops (no
2127
+ * renter takes it again), and the same call once the rental is over takes it home. Free.
2128
+ */
2129
+ export function closeListingIx({ maker, project, listing, collection, hasAuthority, tokenMetadata: tm, compressed, extra = [] }) {
2130
+ 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 });
2132
+ }
2133
+
2134
+ // ---------------------------------------------------------------- Token Metadata NFTs
2135
+
2136
+ const tmPda = (seeds) => findProgramAddress(seeds.map((s) => (typeof s === 'string' ? utf8(s) : s)), TOKEN_METADATA_PROGRAM)[0];
2137
+
2138
+ /**
2139
+ * Whether a classic mint's account data (`data`, 82 bytes) is an NFT's, as the program takes one:
2140
+ * exactly one token, no decimals, initialized, and no mint authority or only its edition's. A semi-fungible item's
2141
+ * mint (several units, or one that can still mint) is refused when handed in (`NotAnNft`).
2142
+ */
2143
+ export function isNftMint(mint, data) {
2144
+ if (!data || data.length !== MINT_BYTES || data[44] !== 0 || data[45] !== 1) return false;
2145
+ if (data.subarray(36, 44).some((b, i) => b !== (i === 0 ? 1 : 0))) return false;
2146
+ const tag = data.subarray(0, 4);
2147
+ if (tag.every((b) => b === 0)) return true;
2148
+ return tag[0] === 1 && tag[1] === 0 && tag[2] === 0 && tag[3] === 0 && encodeBase58(data.subarray(4, 36)) === tokenMetadata.edition(mint);
2149
+ }
2150
+
2151
+ /** A Token Metadata NFT's own addresses: its metadata, its edition, and a pNFT's record of a token account. */
2152
+ export const tokenMetadata = {
2153
+ metadata: (mint) => tmPda(['metadata', addressBytes(TOKEN_METADATA_PROGRAM), addressBytes(mint)]),
2154
+ edition: (mint) => tmPda(['metadata', addressBytes(TOKEN_METADATA_PROGRAM), addressBytes(mint), 'edition']),
2155
+ record: (mint, token) => tmPda(['metadata', addressBytes(TOKEN_METADATA_PROGRAM), addressBytes(mint), 'token_record', addressBytes(token)]),
2156
+ };