@xgamesfun/solana 0.0.0-stage → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) 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 +103 -0
  9. package/examples/duel-arena.json +137 -0
  10. package/examples/elements-set.json +274 -0
  11. package/examples/event-badges.json +67 -0
  12. package/examples/expedition-guild.json +154 -0
  13. package/examples/fair-presale.json +48 -0
  14. package/examples/falling-drop.json +90 -0
  15. package/examples/guild-missions.json +137 -0
  16. package/examples/halving-seasons.json +107 -0
  17. package/examples/hero-trials.json +93 -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 +44 -0
  23. package/examples/partner-vault.json +112 -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 +118 -0
  29. package/examples/star-fleet.json +150 -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 +263 -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 +726 -0
  40. package/js/chain/base58.js +73 -0
  41. package/js/chain/borsh.js +270 -0
  42. package/js/chain/bytes.js +141 -0
  43. package/js/chain/core.js +181 -0
  44. package/js/chain/curve.js +52 -0
  45. package/js/chain/lookup.js +279 -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 +219 -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 +679 -0
  55. package/js/client/compressed.js +46 -0
  56. package/js/client/grant.js +299 -0
  57. package/js/client/idl/loot.js +2 -0
  58. package/js/client/idl/loot.json +17206 -0
  59. package/js/client/index.js +10 -0
  60. package/js/client/ix.js +2887 -0
  61. package/js/client/pda.js +114 -0
  62. package/js/client/policy.js +69 -0
  63. package/js/client/random.js +70 -0
  64. package/js/client/sizes.js +297 -0
  65. package/js/client/traits.js +44 -0
  66. package/js/connect/connect.mjs +182 -0
  67. package/js/economy/check.mjs +2236 -0
  68. package/js/economy/cost.mjs +185 -0
  69. package/js/economy/examples.mjs +114 -0
  70. package/js/economy/explain.mjs +239 -0
  71. package/js/economy/guide.mjs +232 -0
  72. package/js/economy/legal.mjs +15 -0
  73. package/js/economy/live.mjs +130 -0
  74. package/js/economy/model.mjs +698 -0
  75. package/js/economy/plan.mjs +1155 -0
  76. package/js/economy/schema.json +3071 -0
  77. package/js/economy/secrets.mjs +60 -0
  78. package/js/economy/simulate.mjs +1019 -0
  79. package/js/economy/txsize.mjs +45 -0
  80. package/js/economy/validate.mjs +85 -0
  81. package/js/index.js +10 -0
  82. package/js/mcp/server.mjs +113 -0
  83. package/js/mcp/tools.mjs +485 -0
  84. package/js/play/index.js +2855 -0
  85. package/js/play/keys.js +167 -0
  86. package/js/results/index.js +184 -0
  87. package/js/results/wire.js +90 -0
  88. package/package.json +100 -4
  89. package/release.json +8 -0
  90. package/skills/xsolana-economy/SKILL.md +109 -0
  91. package/skills/xsolana-economy/references/after-launch.md +124 -0
  92. package/skills/xsolana-economy/references/apps.md +193 -0
  93. package/skills/xsolana-economy/references/sentence-to-spec.md +180 -0
  94. package/skills/xsolana-economy/references/telling-the-person.md +134 -0
  95. package/types/chain/base58.d.ts +10 -0
  96. package/types/chain/borsh.d.ts +23 -0
  97. package/types/chain/bytes.d.ts +45 -0
  98. package/types/chain/core.d.ts +52 -0
  99. package/types/chain/curve.d.ts +1 -0
  100. package/types/chain/lookup.d.ts +194 -0
  101. package/types/chain/mint.d.ts +25 -0
  102. package/types/chain/pda.d.ts +25 -0
  103. package/types/chain/programs.d.ts +65 -0
  104. package/types/chain/rpc.d.ts +68 -0
  105. package/types/chain/sha256.d.ts +1 -0
  106. package/types/chain/token.d.ts +141 -0
  107. package/types/chain/token2022.d.ts +53 -0
  108. package/types/chain/tx.d.ts +60 -0
  109. package/types/client/accounts.d.ts +318 -0
  110. package/types/client/compressed.d.ts +29 -0
  111. package/types/client/grant.d.ts +170 -0
  112. package/types/client/idl/loot.d.ts +4498 -0
  113. package/types/client/index.d.ts +9 -0
  114. package/types/client/ix.d.ts +5315 -0
  115. package/types/client/pda.d.ts +95 -0
  116. package/types/client/policy.d.ts +48 -0
  117. package/types/client/random.d.ts +26 -0
  118. package/types/client/sizes.d.ts +101 -0
  119. package/types/client/traits.d.ts +12 -0
  120. package/types/economy/check.d.mts +100 -0
  121. package/types/economy/cost.d.mts +69 -0
  122. package/types/economy/examples.d.mts +27 -0
  123. package/types/economy/explain.d.mts +19 -0
  124. package/types/economy/guide.d.mts +140 -0
  125. package/types/economy/legal.d.mts +12 -0
  126. package/types/economy/live.d.mts +124 -0
  127. package/types/economy/model.d.mts +431 -0
  128. package/types/economy/plan.d.mts +1014 -0
  129. package/types/economy/secrets.d.mts +4 -0
  130. package/types/economy/simulate.d.mts +164 -0
  131. package/types/economy/txsize.d.mts +17 -0
  132. package/types/economy/validate.d.mts +3 -0
  133. package/types/index.d.ts +9 -0
  134. package/types/mcp/tools.d.mts +635 -0
  135. package/types/play/index.d.ts +880 -0
  136. package/types/play/keys.d.ts +51 -0
  137. package/types/results/index.d.ts +155 -0
  138. package/types/results/wire.d.ts +122 -0
@@ -0,0 +1,2855 @@
1
+ // What a player does with a live economy, from an app: list the shop and the boxes, buy, open a box and collect what
2
+ // it rolled, pay, buy tickets, stake and harvest, craft, enter a contest, bid in an auction, claim a grant, see their
3
+ // collectibles.
4
+ // Browser-safe, no Solana SDK. The player's wallet signs every transaction; this module never holds a key (a craft's
5
+ // new collectibles get fresh keys made here, which sign only their own creation; a box's settle is signed by a
6
+ // throwaway key the open funded with just enough for its fee, which hands the rest back in the same transaction).
7
+ //
8
+ // import { solanaApp, walletStandardSigner } from '@xgamesfun/solana/play';
9
+ // const app = solanaApp({ spec, rpcUrl: 'https://api.devnet.solana.com', wallet: walletStandardSigner(wallet, account) });
10
+ // const items = await app.shop(); // [{ key, name, price, priceUnits, currency, left }]
11
+ // const { asset } = await app.buy('sword', { listing: items[0] }); // the price shown is the most it pays
12
+ // const { won } = await app.openBox('chest', { listing }); // waits for the roll, settles it: one prompt
13
+ // for (const c of await app.claims()) await app.claim(c); // what this player can finish: wins, payouts, refunds
14
+ // await app.claimWins({ server, token }); // prizes a results server vouched for
15
+ //
16
+ // Nothing here waits on a server. Whatever a player is owed, they can take themselves (`claims`), and public work
17
+ // that is due rides along in the next player's own transaction (an empty lottery round is moved on by the next
18
+ // ticket buyer).
19
+ //
20
+ // `spec` is the live xsolana.json (its `live` section names the project). `wallet` is { address, signTransaction(wire) →
21
+ // signed wire }: `walletStandardSigner` makes one from a Wallet Standard wallet (Phantom, Solflare, Backpack).
22
+ import { decodeBase58, encodeBase58 } from '../chain/base58.js';
23
+ import { ownedAssetFilters, parseCoreAsset, parseCoreCollection } from '../chain/core.js';
24
+ import { ASSOCIATED_TOKEN_PROGRAM, associatedTokenAddress, COMPUTE_BUDGET_PROGRAM, NATIVE_MINT, SYSVAR_SLOT_HASHES, TOKEN_2022_PROGRAM, TOKEN_PROGRAM } from '../chain/pda.js';
25
+ import { closeTokenAccount, computeUnitLimit, createAssociatedTokenAccountIdempotent, requestHeapFrame, syncNative, transferLamports, withSolWrap } from '../chain/programs.js';
26
+ import { checkCluster, notMainnet, rpcClient } from '../chain/rpc.js';
27
+ import { MEMO_PROGRAM, mintPowers, resolveTransfers } from '../chain/token2022.js';
28
+ import { compileFitting, closeTableIx, deactivateTableIx, readTable, sendFitting, sendPlan, slotHashes, tablesOf, tableStep, transactionLocks, LOOKUP_TABLE_PROGRAM } from '../chain/lookup.js';
29
+ import { compileMessage, readTransaction, wireTransaction } from '../chain/tx.js';
30
+ import * as loot from '../client/index.js';
31
+ import { browserStore, keyring } from './keys.js';
32
+
33
+ // The options of a call made without any: read-only, and typed so the generated declarations take any option.
34
+ /** A lottery's steps in `claims()`: each depends on the one before, so `claimAll` takes one of a lottery's a call. */
35
+ const LOTTERY_STEPS = new Set(['reveal', 'cancel-draw', 'pull', 'payout']);
36
+ /** The claims that are one transaction of the wallet's: `claimAll` signs them together. */
37
+ const BATCHED = new Set(['contest', 'release', 'lookup-table', 'close-leaf', ...LOTTERY_STEPS]);
38
+ const NO_OPTIONS = Object.freeze(/** @type {Record<string, any>} */ ({}));
39
+
40
+ const CHAIN = 'solana:devnet';
41
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
42
+ const MPL_CORE = loot.MPL_CORE_PROGRAM;
43
+ const NOBODY = '11111111111111111111111111111111';
44
+ /** A table made this many slots ago whose transaction never landed was never sent: its blockhashes ran out long since. */
45
+ const TABLE_UNSENT_SLOTS = 300n;
46
+ /** A compressed NFT's `prove_leaf`: its record stays good while the leaf is unchanged, so it may go in a transaction of its own first. */
47
+ const isProveLeaf = (ix) => ix.programId === loot.LOOT_PROGRAM && loot.instructionOf(ix.data)?.name === 'prove_leaf';
48
+ /** A system account must keep this much (rent for 0 bytes) or be emptied. */
49
+ const RENT_EMPTY = 890_880n;
50
+ const SIGNATURE_FEE = 5_000n;
51
+
52
+ /**
53
+ * A signer from a Wallet Standard wallet and one of its accounts: the wallet signs, the app sends. With the wallet's
54
+ * `solana:signMessage`, also `signMessage(bytes)`: the app's settle keys are made from one signature of it a session,
55
+ * so a page that closes can make them again (`recover`).
56
+ */
57
+ export function walletStandardSigner(wallet, account, chain = CHAIN) {
58
+ const feature = wallet.features?.['solana:signTransaction'];
59
+ if (!feature) throw new Error(`${wallet.name} cannot sign transactions.`);
60
+ const messages = wallet.features?.['solana:signMessage'];
61
+ return {
62
+ address: account.address,
63
+ async signTransaction(wire) {
64
+ const [out] = await feature.signTransaction({ account, transaction: wire, chain });
65
+ return out.signedTransaction;
66
+ },
67
+ ...(messages ? { async signMessage(message) {
68
+ const [out] = await messages.signMessage({ account, message });
69
+ return out.signature;
70
+ } } : {}),
71
+ };
72
+ }
73
+
74
+ /** A fresh ed25519 key (WebCrypto) that signs only its own account's creation: a crafted collectible's. */
75
+ async function freshKey() {
76
+ const pair = await globalThis.crypto.subtle.generateKey({ name: 'Ed25519' }, false, ['sign', 'verify']);
77
+ const raw = new Uint8Array(await globalThis.crypto.subtle.exportKey('raw', pair.publicKey));
78
+ return { address: encodeBase58(raw), sign: async (message) => new Uint8Array(await globalThis.crypto.subtle.sign('Ed25519', pair.privateKey, message)) };
79
+ }
80
+
81
+ /**
82
+ * An amount in whole units of a token ("12", "0.5", or a number) in its base units. Only a plain decimal is taken, with
83
+ * at most the token's decimals: "0x10", "1.2.3", "", "1e3", "-1" and an extra decimal place are refused, never read as
84
+ * some other amount.
85
+ */
86
+ export const units = (amount, decimals) => {
87
+ const text = typeof amount === 'number' && Number.isFinite(amount) && amount >= 0 ? plainNumber(amount) : typeof amount === 'string' ? amount.trim() : null;
88
+ const m = text === null ? null : /^(\d+)(?:\.(\d+))?$/.exec(text);
89
+ if (!m) throw new Error(`${typeof amount === 'string' ? JSON.stringify(amount) : String(amount)} is not an amount: write a plain number like 12 or 0.5.`);
90
+ const fraction = m[2] ?? '';
91
+ if (fraction.length > decimals && /[1-9]/.test(fraction.slice(decimals))) throw new Error(`${text} has more decimals than the token's ${decimals}.`);
92
+ return BigInt(m[1] + fraction.padEnd(decimals, '0').slice(0, decimals));
93
+ };
94
+ /** A number as plain decimal text (1e-7 → "0.0000001"). */
95
+ const plainNumber = (n) => (/e/i.test(String(n)) ? n.toFixed(20).replace(/\.?0+$/, '') : String(n));
96
+ const show = (u, decimals) => {
97
+ const s = BigInt(u).toString().padStart(decimals + 1, '0');
98
+ const f = s.slice(s.length - decimals).replace(/0+$/, '');
99
+ return `${s.slice(0, s.length - decimals)}${f ? `.${f}` : ''}`;
100
+ };
101
+
102
+ /**
103
+ * `client`, kept to devnet: a URL that names mainnet is refused at once, and before its first request goes out the
104
+ * chain's genesis block is checked, once (`checkCluster`), so an RPC whose URL hides mainnet or testnet is refused
105
+ * whatever it says. Every RPC reached over http(s) is checked: the app's own client, and one given whose `url` is
106
+ * http(s). A client with no such URL (a local chain of the caller's own, like the tests' LiteSVM) is used as it is.
107
+ */
108
+ function devnetOnly(client) {
109
+ if (typeof client.url === 'string') notMainnet(client.url);
110
+ if (!/^(https?|wss?):/i.test(String(client.url ?? ''))) return client;
111
+ let checked = null;
112
+ // A check that could not be made (the network did not answer) is made again on the next request.
113
+ const ready = () => (checked ??= checkCluster(client).catch((e) => {
114
+ checked = null;
115
+ throw e;
116
+ }));
117
+ return new Proxy(client, {
118
+ get(target, key) {
119
+ const value = Reflect.get(target, key, target);
120
+ return typeof value === 'function' ? async (...args) => {
121
+ await ready();
122
+ return value.apply(target, args);
123
+ } : value;
124
+ },
125
+ });
126
+ }
127
+
128
+ /**
129
+ * The player's side of a live economy. `rpc` may be given instead of `rpcUrl` (any object with the client's rpc
130
+ * methods: accounts, account, latestBlockhash, send, confirm, programAccounts, and call(method, params), the raw JSON
131
+ * RPC request the devnet check, the slot and the compressed-NFT reads go through). `now` is the clock auctions are read
132
+ * by (Unix seconds). Devnet only (`devnetOnly`): mainnet waits for an audit. `keyStore`: where a settle key that can't
133
+ * be made again from the wallet's signature is kept, encrypted (`browserStore()` in a browser; null keeps keys in memory
134
+ * only).
135
+ */
136
+ export function solanaApp({ spec, rpcUrl = 'https://api.devnet.solana.com', rpc = null, wallet, programs = {}, now = () => Math.floor(Date.now() / 1000), keyStore = browserStore() }) {
137
+ const live = spec?.live;
138
+ if (!live?.project) throw new Error('This economy is not live yet: its xsolana.json has no "live" section.');
139
+ if (rpc && typeof rpc.call !== 'function') throw new Error('The rpc given has no call(method, params): a custom rpc needs it (the devnet check, the slot and compressed NFTs are read through it), as well as accounts, account, latestBlockhash, send, confirm and programAccounts.');
140
+ const client = devnetOnly(rpc ?? rpcClient(notMainnet(rpcUrl)));
141
+ const project = live.project;
142
+ /** The settle keys, each named by its job's account (`keys.js`): made again after a reload (`recover`). */
143
+ const keys = wallet?.address ? keyring({ wallet, project, store: keyStore }) : null;
144
+ /** The key for a job about `name` (an order's, a hold's address): one that can be made again, or a fresh one. */
145
+ const jobKey = async (name) => (keys ? keys.key(name) : freshKey());
146
+ /** The project's Core collections: an NFT of one of them is the project's own (a project may have none). */
147
+ const collections = live.collections ?? (live.collection ? [live.collection] : []);
148
+ const ours = (collection) => collection != null && collections.includes(collection);
149
+ const tokens = new Map((spec.tokens ?? []).map((t) => [t.key, t]));
150
+ const currencyKey = spec.currency;
151
+ const mintOf = (key) => {
152
+ const t = tokens.get(key ?? currencyKey);
153
+ if (!t) throw new Error(`No token “${key}”.`);
154
+ return t.source === 'sol' ? NATIVE_MINT : spec.live.mints?.[t.key] ?? t.mint;
155
+ };
156
+ const decimalsOf = (key) => {
157
+ const t = tokens.get(key ?? currencyKey);
158
+ return t?.source === 'sol' ? 9 : t?.decimals ?? 0;
159
+ };
160
+ programs = { ...Object.fromEntries([...tokens.values()].map(t => [mintOf(t.key), t.program === 'token-2022' ? TOKEN_2022_PROGRAM : TOKEN_PROGRAM])), ...programs };
161
+ const programOf = (mint) => programs[mint] ?? TOKEN_PROGRAM;
162
+ const part = (list, key) => {
163
+ const p = (spec[list] ?? []).find((x) => x.key === key);
164
+ if (!p) throw new Error(`No ${list.replace(/s$/, '')} “${key}” in this economy.`);
165
+ return p;
166
+ };
167
+ const read = async (name, address) => {
168
+ const a = await client.account(address);
169
+ if (!a) throw new Error(`Nothing at ${address}: is the economy live on this cluster?`);
170
+ if (a.owner !== loot.LOOT_PROGRAM) throw new Error(`${address} is not a loot account.`);
171
+ return loot.decode(name, a.data);
172
+ };
173
+ /** A loot account, or null when there is none yet. */
174
+ const readMaybe = async (name, address) => {
175
+ const a = await client.account(address);
176
+ return a?.owner === loot.LOOT_PROGRAM ? loot.decode(name, a.data) : null;
177
+ };
178
+ /** The key of the token a lottery's tickets are paid in: the spec's `ticketCurrency`, or the token of its mint. */
179
+ const ticketCurrencyOf = (l, lottery) => l.ticketCurrency ?? [...tokens.keys()].find((k) => mintOf(k) === lottery.terms.mint) ?? currencyKey;
180
+ /** Farm hooks shared by all actions. Static lists resolve automatically. For dynamic hooks,
181
+ * transfers[mint] describes every actual transfer, including fee-adjusted reward credit/payout, and in a farm that
182
+ * shares its pool the move's last one when a seat gave rewards back in it (an early exit, a watched
183
+ * NFT gone): from the farm's rewards vault back to its pool, the project the authority.
184
+ * Explicit extra replaces resolution and must include all token hook/memo and Core adapter accounts. */
185
+ const farmTokens = async (mints, create, { extra, transfers = {} } = {}) => {
186
+ const selected = [...new Set(mints)];
187
+ const resolved = await Promise.all(selected.map(async (mint) => {
188
+ if (extra !== undefined) {
189
+ const account = await client.account(mint);
190
+ if (![TOKEN_PROGRAM, TOKEN_2022_PROGRAM].includes(account?.owner)) throw new Error(`There is no supported token ${mint} on this network.`);
191
+ return { mint, tokenProgram: account.owner, extraAccounts: [] };
192
+ }
193
+ try {
194
+ return { mint, ...await resolveTransfers(client, mint, transfers[mint] ?? []) };
195
+ } catch (cause) {
196
+ throw new Error(`Cannot resolve farm transfers for ${mint}: ${cause.message} Supply exact transfers keyed by mint, or explicit extra hook/memo accounts.`, { cause });
197
+ }
198
+ }));
199
+ const tokenPrograms = { ...programs, ...Object.fromEntries(resolved.map(({ mint, tokenProgram }) => [mint, tokenProgram])) };
200
+ return {
201
+ programs: tokenPrograms,
202
+ extra: extra ?? resolved.flatMap((r) => r.extraAccounts),
203
+ before: [...new Set(create)].map((mint) => createAssociatedTokenAccountIdempotent(wallet.address, wallet.address, mint, tokenPrograms[mint])),
204
+ };
205
+ };
206
+ const farmCollection = async (asset) => {
207
+ const account = await client.account(asset);
208
+ // Core tombstones and missing assets remain valid farm-cleanup paths.
209
+ return account?.owner === MPL_CORE && account.data[0] === 1
210
+ ? parseCoreAsset(account.data).collection ?? collections[0]
211
+ : collections[0];
212
+ };
213
+ /** The Core collection an asset is in, or null. */
214
+ const collectionOf = async (asset) => {
215
+ const account = await client.account(asset);
216
+ return account?.owner === MPL_CORE && account.data[0] === 1 ? parseCoreAsset(account.data).collection ?? null : null;
217
+ };
218
+ /** A Token Metadata NFT's { programmable, ruleSet } from its metadata, or null for anything else (a Core asset). */
219
+ const tokenMetadataOf = async (asset) => {
220
+ const account = await client.account(asset);
221
+ if (account?.owner !== TOKEN_PROGRAM) return null;
222
+ return loot.decodeTokenMetadata((await client.account(loot.tokenMetadata.metadata(asset)))?.data);
223
+ };
224
+ /**
225
+ * Where a Token Metadata NFT handed to `holder` goes: { into, before }. Into their associated account (`into` null)
226
+ * while it is not there (the program opens it) or takes it; when it is there but another key owns it or it is frozen,
227
+ * Token Metadata refuses it and the hand-out would fail for everyone (an outbid NFT bid, a cancelled round's ticket),
228
+ * so `payer` opens a fresh account of the holder's in the same transaction (`loot.freshNftAccount`) and it goes there.
229
+ */
230
+ async function nftInto(asset, holder, payer) {
231
+ const own = await client.account(loot.ata(holder, asset));
232
+ if (!own || loot.takesNft(own, holder, asset)) return { into: null, before: [] };
233
+ // (the rent is the fixed one for 165 bytes, never the RPC's figure: the account is the holder's)
234
+ for (let index = 0; index < loot.FRESH_NFT_ACCOUNTS; index++) {
235
+ const { address } = loot.freshNftAccount({ payer, holder, mint: asset, index });
236
+ // nothing there, only lamports someone sent there, or one opened before (a hand-out that did not land) that takes it
237
+ const fresh = loot.freshNftAccount({ payer, holder, mint: asset, index, there: await client.account(address) });
238
+ if (fresh) return { into: fresh.address, before: fresh.instructions };
239
+ }
240
+ throw new Error(`${holder}'s accounts of the NFT ${asset} can't take it (owned by another key, or frozen), and no fresh one could be opened.`);
241
+ }
242
+ /**
243
+ * What a route needs of an NFT, whatever its kind: { collection, hasAuthority, tokenMetadata, compressed, before }.
244
+ * A compressed NFT (no account at its address) is read from the RPC's DAS methods (`getAsset`, `getAssetProof`),
245
+ * which the page's RPC must offer, and proven first in the same transaction (`before`). `to`: the wallet the route
246
+ * hands it to, when it hands it to one: a Token Metadata NFT then names the account it goes into (`nftInto`), a fresh
247
+ * one opened by `payer` (the signer by default).
248
+ */
249
+ async function nftOf(asset, signer = wallet.address, { to, payer = signer } = {}) {
250
+ const account = await client.account(asset);
251
+ if (!account) {
252
+ const das = await client.call('getAsset', { id: asset }).catch(() => null);
253
+ if (das?.compression?.compressed) {
254
+ const proof = await client.call('getAssetProof', { id: asset });
255
+ const tree = await client.account(das.compression.tree);
256
+ const compressed = loot.compressedFromDas({ asset: das, proof, treeData: tree.data });
257
+ return { compressed, hasAuthority: false, before: [computeUnitLimit(1_000_000), loot.proveLeafIx({ signer, compressed })] };
258
+ }
259
+ }
260
+ const tokenMetadata = await tokenMetadataOf(asset);
261
+ // A Token Metadata mint is taken only as an NFT: one token, no decimals, none more ever minted (NotAnNft).
262
+ if (tokenMetadata && account && !loot.isNftMint(asset, account.data)) throw new Error(`${asset} is a semi-fungible item (several units, or a mint that can still mint): only an NFT is taken.`);
263
+ const { into, before } = tokenMetadata && to ? await nftInto(asset, to, payer) : { into: null, before: [] };
264
+ return {
265
+ collection: await farmCollection(asset),
266
+ hasAuthority: ours(await collectionOf(asset)),
267
+ tokenMetadata: tokenMetadata && into ? { ...tokenMetadata, into } : tokenMetadata,
268
+ before: tokenMetadata ? [computeUnitLimit(400_000), ...before] : [],
269
+ };
270
+ }
271
+ /**
272
+ * What `loot.prizeMetas` needs to give these prizes (`null`s are skipped): each pool's mint, each item (its kind, its
273
+ * collection and its next serial; null for one never written), each NFT's kind (`nfts`, and `nft` for one), the
274
+ * project's first collection, a compressed NFT's proof, which goes first in the transaction (`before`), and for a
275
+ * roll its box, decoded (`boxes`, `boxesOf`).
276
+ */
277
+ async function prizeInputs(prizes, signer = wallet.address, winner = wallet.address) {
278
+ const out = { poolMints: {}, items: {}, nfts: {}, nft: undefined, collection: collections[0], before: [] };
279
+ out.boxes = await boxesOf(prizes.filter(Boolean).flatMap((p) => (p.Roll ? [p.Roll[0]] : [])));
280
+ for (const prize of prizes.filter(Boolean)) {
281
+ const pool = prize.Tokens?.from.Pool?.[0];
282
+ if (pool !== undefined && !(pool in out.poolMints)) {
283
+ const account = await client.account(loot.pda.pool(project, pool));
284
+ // A pool never opened gives nothing; the economy's currency stands in for its mint's place.
285
+ out.poolMints[pool] = account ? encodeBase58(account.data.subarray(0, 32)) : mintOf();
286
+ }
287
+ const id = prize.Item?.[0] ?? prize.Compressed?.item;
288
+ if (id !== undefined && !(id in out.items)) out.items[id] = await readMaybe('Item', loot.pda.item(project, id));
289
+ // An NFT of a stash, or one a lottery's place holds in the lottery's own stash.
290
+ const asset = prize.Nft?.nft ?? prize.Held?.[0];
291
+ if (asset && !out.nfts[asset]) {
292
+ const { before, hasAuthority: _, ...nft } = await nftOf(asset, signer, { to: winner });
293
+ out.nfts[asset] = out.nft = nft;
294
+ out.before.push(...before);
295
+ }
296
+ }
297
+ return out;
298
+ }
299
+ /** The Sheet the project keeps of an NFT it has no authority over: { key: value } of what the project wrote. */
300
+ const sheetOf = async (nft) => {
301
+ const found = await client.account(loot.pda.sheet(project, nft));
302
+ return Object.fromEntries((loot.decodeSheet(found?.owner === loot.LOOT_PROGRAM ? found.data : null)?.attributes ?? []).map((a) => [a.key, a.value]));
303
+ };
304
+ let projectState = null;
305
+ const projectNow = async () => (projectState ??= await read('Project', project));
306
+ /** A spec auction's address: it names the seller, which xsolana.json records once it is live. */
307
+ const auctionAt = (a) => {
308
+ if (!a.seller) throw new Error(`The auction “${a.key}” has no seller in xsolana.json: publish the economy again.`);
309
+ return loot.pda.auction(project, a.seller, a.id);
310
+ };
311
+
312
+ const itemKeyOf = (id) => (spec.items ?? []).find((it) => it.id === Number(id))?.key ?? null;
313
+ /** Who holds a collectible now. */
314
+ const ownerOf = async (asset) => parseCoreAsset((await client.account(asset)).data).owner;
315
+ /** `{ asset, farm }` for a Core asset on its seat in one of the economy's farms of these custodies, or null. */
316
+ const seatedIn = async (asset, custodies = ['escrow', 'freeze']) => {
317
+ for (const f of (spec.farms ?? []).filter((x) => x.stakes === 'collectibles' && custodies.includes(x.custody ?? 'escrow'))) {
318
+ if (await readMaybe('Seat', loot.pda.seat(loot.pda.farm(project, f.id), asset))) return { asset, farm: f.id };
319
+ }
320
+ return null;
321
+ };
322
+ /** The same for one the project holds: staked in escrow. */
323
+ const escrowedIn = async (asset) => ((await ownerOf(asset).catch(() => null)) === project ? seatedIn(asset, ['escrow']) : null);
324
+ /** The same for one the project locks in the player's wallet: staked in a farm that freezes (a soulbound Core asset
325
+ * carries no lock to tell it is staked, so it goes alone). */
326
+ const lockedIn = async (asset) => {
327
+ const seated = await seatedIn(asset, ['freeze']);
328
+ if (!seated) return null;
329
+ const account = await client.account(asset);
330
+ return account?.owner === MPL_CORE && parseCoreAsset(account.data).soulbound ? null : seated;
331
+ };
332
+ /** The player's collectibles of this project (in any of its collections), each with the item it is (its "item_id" attribute). */
333
+ async function owned(owner = wallet?.address) {
334
+ const found = (await Promise.all(collections.map((c) => client.programAccounts(MPL_CORE, ownedAssetFilters(owner, c))))).flat();
335
+ return found.map(({ pubkey, account }) => {
336
+ const a = parseCoreAsset(account.data);
337
+ const itemId = a.attributes?.item_id !== undefined ? Number(a.attributes.item_id) : null;
338
+ return { asset: pubkey, ...a, itemId, item: itemId === null ? null : itemKeyOf(itemId) };
339
+ });
340
+ }
341
+ /** A token seat's refresh (`seat` decoded), with its booster's accounts (a compressed one proven first), to go first in
342
+ * a stake or a compound's transaction (`loot.stakeRefreshFirst`). */
343
+ async function refreshOf(f, farm, seat) {
344
+ const { before, ...nft } = seat.asset !== NOBODY ? await nftOf(seat.asset) : { before: [] };
345
+ return [...before, loot.refreshSeatIx({ signer: wallet.address, project, id: f.id, farm, by: seat.player, seat, ...nft })];
346
+ }
347
+ /**
348
+ * For a gate (the program's Gate: a Rule an NFT must pass, or a list of wallets), one of the player's NFTs that
349
+ * passes it, so a page needn't find it; null when nothing is to be shown. A rule naming a collection the project has no
350
+ * authority over finds the player's Core assets of it (with what the project wrote in their Sheets); a Token Metadata NFT
351
+ * is not looked for: pass it as `shown: { mint, owner }`. Throws a plain sentence when none passes, or the wallet is not on the list.
352
+ * A stake gate shows nothing: the client passes the wallet's seat in its farm; here it is only read, to say plainly
353
+ * when it holds too little or its lock is over (the program counts a stake only while its tier's lock runs, and
354
+ * only one made before this second).
355
+ */
356
+ async function passing(gate, what) {
357
+ if (!gate) return null;
358
+ // A list is passed with a proof, not an NFT (`listProofOf`).
359
+ if (gate.List) return null;
360
+ if (gate.Staked) {
361
+ const { farm, at_least: atLeast } = gate.Staked;
362
+ const seat = await readMaybe('Seat', loot.pda.seat(farm, wallet.address));
363
+ const f = (spec.farms ?? []).find((x) => loot.pda.farm(project, x.id) === farm);
364
+ const t = f ? tokens.get(f.stakes) : null;
365
+ const needs = t ? `${show(atLeast, decimalsOf(t.key))} ${t.symbol ?? t.key}` : `${atLeast} (in base units)`;
366
+ const where = f ? `“${f.name}”` : 'its farm';
367
+ if (!seat || BigInt(seat.amount) < BigInt(atLeast)) throw new Error(`You need at least ${needs} staked in ${where} to ${what}.`);
368
+ // Only a locked stake counts: its tier's lock still running.
369
+ const lock = BigInt((await read('Farm', farm)).tiers[seat.tier]?.lock ?? 0);
370
+ if (lock === 0n || BigInt(now()) >= BigInt(seat.since) + lock) throw new Error(`Your stake in ${where} counts only while it is locked: stake in a tier with a lock (staking again starts a new one) to ${what}.`);
371
+ return null;
372
+ }
373
+ if (gate.Wallets) {
374
+ if (gate.Wallets[0].includes(wallet.address)) return null;
375
+ throw new Error(`Only the wallets this names may ${what}.`);
376
+ }
377
+ // A farm's gate is a list of rules one NFT must all pass; any other gate one rule.
378
+ const rules = Array.isArray(gate) ? gate : [gate.Holds ? gate.Holds[0] : gate];
379
+ if (!rules.length) return null;
380
+ const rule = rules[0];
381
+ const atLeast = rule.test?.AtLeast?.[0];
382
+ const equals = rule.test?.Equals?.text;
383
+ const item = rule.of?.Item?.[0];
384
+ const named = rules.map((r) => r.of?.Collection?.[0]).find((c) => c && !ours(c));
385
+ const other = named ?? null;
386
+ const passes = (r, c) => {
387
+ if (r.of?.Item?.[0] !== undefined && c.itemId !== Number(r.of.Item[0])) return false;
388
+ const value = c.attributes?.[r.key];
389
+ if (r.test?.AtLeast?.[0] !== undefined) return Number(value) >= Number(r.test.AtLeast[0]);
390
+ if (r.test?.Equals?.text !== undefined) return String(value) === String(r.test.Equals.text);
391
+ return true;
392
+ };
393
+ const pass = (c) => rules.every((r) => passes(r, c));
394
+ if (other) {
395
+ const found = await client.programAccounts(MPL_CORE, ownedAssetFilters(wallet.address, other));
396
+ for (const { pubkey, account } of found) {
397
+ const a = parseCoreAsset(account.data);
398
+ if (pass({ attributes: { ...a.attributes, ...(await sheetOf(pubkey)) } })) return { asset: pubkey };
399
+ }
400
+ throw new Error(`You need an NFT of the collection ${other}${atLeast !== undefined ? ` with ${rule.key} ${atLeast} or more` : equals !== undefined ? ` whose ${rule.key} is ${equals}` : ''} to ${what}.`);
401
+ }
402
+ const found = (await owned()).find(pass);
403
+ if (found) return found.asset;
404
+ const needs = item !== undefined ? `a ${(spec.items ?? []).find((it) => it.id === Number(item))?.name ?? 'collectible of this project'}` : 'a collectible of this project';
405
+ throw new Error(`You need ${needs}${atLeast !== undefined ? ` with ${rule.key} ${atLeast} or more` : equals !== undefined ? ` whose ${rule.key} is ${equals}` : ''} to ${what}.`);
406
+ }
407
+
408
+ /**
409
+ * For a list gate (`List`: a Merkle root of wallets, `sha256("loot-list-v1" | wallet)`), the proof that the player is
410
+ * on it: `given` (as a page or a results server hands it over), or made from the wallets xsolana.json lists for the part
411
+ * (`list`, its gate's `list`). Undefined for any other gate. Throws a plain sentence when the wallet is not on it.
412
+ */
413
+ function listProofOf(gate, list, given) {
414
+ if (!gate?.List) return undefined;
415
+ if (given) return given;
416
+ if (!(list ?? []).includes(wallet.address)) throw new Error('This wallet is not on the list for this.');
417
+ // The list as the plan wrote its root: filled to a power of two (`gateList`).
418
+ return loot.gateList(list).proofOf(wallet.address);
419
+ }
420
+
421
+ /**
422
+ * SOL paid out to the player (a prize, rewards, a refund, a grant) lands in their wrapped-SOL account: close it at
423
+ * the end, so the player keeps plain SOL. When the transaction pays into it and nothing opens it first, the player
424
+ * opens it here (the program would otherwise open it, unseen), and has the rent back at the close.
425
+ */
426
+ async function unwrapAfter(instructions) {
427
+ const wrapped = associatedTokenAddress(wallet.address, NATIVE_MINT);
428
+ const opens = instructions.some((ix) => ix.programId === ASSOCIATED_TOKEN_PROGRAM && ix.keys[1]?.pubkey === wrapped);
429
+ const closes = instructions.some((ix) => ix.programId === TOKEN_PROGRAM && ix.data?.[0] === 9 && ix.keys[0]?.pubkey === wrapped);
430
+ if (closes) return instructions;
431
+ const close = closeTokenAccount(wrapped, wallet.address, wallet.address);
432
+ if (opens || (await client.account(wrapped))) return [...instructions, close];
433
+ const touches = instructions.some((ix) => ix.keys?.some((k) => k.pubkey === wrapped));
434
+ return touches ? [createAssociatedTokenAccountIdempotent(wallet.address, wallet.address, NATIVE_MINT), ...instructions, close] : instructions;
435
+ }
436
+
437
+ /**
438
+ * Build, have the wallet sign, send and confirm. Extra signers (fresh keys) sign first. Returns the signature.
439
+ * `keepWrapped`: leave the player's wrapped-SOL account open (a bid's: what is topped comes back there).
440
+ * Its size is checked before the wallet signs (`sendFitting`): a transaction too big for a legacy one goes as one v0
441
+ * transaction with a lookup table the wallet makes first (one to three more approvals, and the table's rent until the
442
+ * player closes it, which `claims()` lists); only when even that is too big does each compressed NFT's `prove_leaf`
443
+ * go alone, after the table and before the rest. A compressed NFT's record closes in the same transaction, its prover
444
+ * signing (`loot.withLeafCloses`), so its rent comes straight back.
445
+ */
446
+ /**
447
+ * The project's standing lookup tables (made at go-live, `spec.live.tables`), each as the chain holds it now: one
448
+ * deactivated (or gone) is left out, and a transaction then makes a table of its own as before.
449
+ */
450
+ let standingTables = null;
451
+ async function standing() {
452
+ standingTables ??= (async () => {
453
+ const addresses = spec.live?.tables ?? [];
454
+ const found = addresses.length ? await client.accounts(addresses) : [];
455
+ return addresses.flatMap((address, i) => {
456
+ const table = readTable(found[i] && { ...found[i], owner: found[i].owner ?? LOOKUP_TABLE_PROGRAM });
457
+ return table && table.deactivationSlot === null ? [{ address, addresses: table.addresses }] : [];
458
+ });
459
+ })();
460
+ return standingTables;
461
+ }
462
+
463
+ async function send(instructions, extra = [], { keepWrapped = false } = {}) {
464
+ if (!wallet?.address) throw new Error('Connect a wallet first.');
465
+ // A compressed NFT's record is closed in the transaction that used it, its prover signing: its rent comes back.
466
+ instructions = loot.withLeafCloses(instructions);
467
+ if (!keepWrapped) instructions = await unwrapAfter(instructions);
468
+ const sign = async (message) => {
469
+ const { signers } = readTransaction(wireTransaction(message));
470
+ const sigs = await Promise.all(signers.map(async (s) => (extra.find((k) => k.address === s) ? extra.find((k) => k.address === s).sign(message) : new Uint8Array(64))));
471
+ return wallet.signTransaction(wireTransaction(message, sigs));
472
+ };
473
+ // The project first in a table, so claims() knows a table whose later steps were never sent as this project's.
474
+ return (await sendFitting({ rpc: client, payer: wallet.address, instructions, sign, alone: isProveLeaf, first: [project], standing: await standing() }).catch(inWords)).signature;
475
+ }
476
+
477
+ const sendNow = (...args) => send(...args);
478
+
479
+ /**
480
+ * Several independent steps of the player's, each one transaction (`lists`: their instructions), signed in one wallet
481
+ * prompt when the wallet signs several at once (`wallet.signTransactions`), then sent in order; one by one otherwise,
482
+ * and for a step too big for one transaction. Returns each step's { signature } or { error }.
483
+ */
484
+ async function sendBatch(lists) {
485
+ const settled = (p) => p.then((signature) => ({ signature }), (e) => ({ error: e.message }));
486
+ let signed = null;
487
+ let messages = [];
488
+ let lastValidBlockHeight = null;
489
+ if (wallet?.signTransactions && lists.length > 1) {
490
+ const latest = await client.latestBlockhash();
491
+ lastValidBlockHeight = latest.lastValidBlockHeight;
492
+ const tables = await standing();
493
+ for (const l of lists) messages.push(compileFitting({ payer: wallet.address, blockhash: latest.blockhash, instructions: await unwrapAfter(loot.withLeafCloses(l)), tables }));
494
+ const wires = messages.filter(Boolean).map((m) => wireTransaction(m));
495
+ const back = wires.length > 1 ? await wallet.signTransactions(wires).catch(() => null) : null;
496
+ if (back) signed = messages.map((m) => (m ? back.shift() : null));
497
+ }
498
+ const out = [];
499
+ for (const [i, l] of lists.entries()) {
500
+ const wire = signed?.[i];
501
+ const confirmed = async () => {
502
+ const signature = await client.send(wire).catch(inWords);
503
+ await client.confirm?.(signature, { lastValidBlockHeight });
504
+ return signature;
505
+ };
506
+ out.push(await settled(wire ? confirmed() : send(l)));
507
+ }
508
+ return out;
509
+ }
510
+
511
+ /**
512
+ * Throws `e` with loot's reason in words in place of a bare code: a wallet whose own dry run failed often says only
513
+ * "custom program error: 0x17b5". A code is loot's when the logs name loot as the program that failed, or, with no
514
+ * logs, when it is one of loot's (Anchor's codes start at 6000; the token programs' are small).
515
+ */
516
+ function inWords(e) {
517
+ // The program logs an error's name only (its text is compiled out): the IDL's sentence for it.
518
+ const logged = e?.logs ? loot.lootError(e.logs) : null;
519
+ if (logged) throw Object.assign(new Error(`${logged.msg} (${logged.name}).`, { cause: e }), { code: logged.code, name: logged.name, logs: e.logs });
520
+ const said = String(e?.message ?? '');
521
+ const m = /custom program error: 0x([0-9a-f]+)/i.exec(said);
522
+ const failed = e?.logs?.find((line) => / failed: custom program error: 0x/i.test(line));
523
+ const known = m && (!failed || failed.includes(loot.LOOT_PROGRAM)) ? loot.ERRORS[parseInt(m[1], 16)] : null;
524
+ if (!known) throw e;
525
+ throw Object.assign(new Error(`${known.msg} (${known.name}).`, { cause: e }), { code: parseInt(m[1], 16), name: known.name, logs: e.logs });
526
+ }
527
+
528
+ /**
529
+ * Build, sign with `key` alone (it pays), send and confirm: a throwaway key's transaction. No wallet prompt. Legacy,
530
+ * or v0 with the project's standing tables (which cost it nothing); the key holds no rent for a table of its own.
531
+ */
532
+ async function sendAs(key, instructions) {
533
+ instructions = loot.withLeafCloses(instructions);
534
+ return (await sendFitting({ rpc: client, payer: key.address, instructions, sign: async (message) => wireTransaction(message, [await key.sign(message)]), table: false, standing: await standing() }).catch(inWords)).signature;
535
+ }
536
+
537
+ /**
538
+ * Refuses, in words, to stake `asset` in tier `tier` of farm `f` when its unstake (with the tier's prize, and as a
539
+ * forfeit) couldn't be sent even as a v0 transaction with a lookup table, its proof sent first (`sendPlan`).
540
+ */
541
+ async function exitFits(f, farm, asset, nft, before, tier, staking) {
542
+ const prize = farm.tiers[tier]?.prize ?? null;
543
+ const given = await prizeInputs([prize]);
544
+ // The rewards' accounts as the stake resolved them (a hook that needs each transfer named is sized without them).
545
+ const token = { ...staking, before: [createAssociatedTokenAccountIdempotent(wallet.address, wallet.address, farm.rewards_mint, staking.programs[farm.rewards_mint] ?? TOKEN_PROGRAM)] };
546
+ const exit = (forfeit) => [...before, ...(forfeit ? [] : [...given.before, ...token.before]), loot.unstakeNftIx({ player: wallet.address, project, id: f.id, farm, asset, ...nft, forfeit, roll: farm.tiers[tier]?.roll ?? null, nonce: 1n, tier, projectCollection: given.collection, poolMints: given.poolMints, items: given.items, nft: given.nft, ...token })];
547
+ for (const forfeit of [false, true]) {
548
+ const instructions = exit(forfeit).filter((x) => x.programId !== COMPUTE_BUDGET_PROGRAM);
549
+ if (!sendPlan({ payer: wallet.address, instructions: [computeUnitLimit(1_400_000), ...instructions], alone: isProveLeaf }))
550
+ throw new Error(`Not staked: this NFT couldn't come back out of “${f.name ?? f.key}”. Its unstake${forfeit ? ', even giving up the rewards,' : ''} would lock more accounts than one transaction can (${transactionLocks(wallet.address, [computeUnitLimit(1_400_000), ...instructions], { asV0: true })} with its lookup table; at most 64), so it would stay in the farm.`);
551
+ }
552
+ }
553
+
554
+ /** An NFT's kind as a route's `ticket` or `nft` takes it: without its asset. */
555
+ const kindOnly = ({ asset: _, ...kind }) => kind;
556
+
557
+ /**
558
+ * The key job rule (AGENTS.md). A step after the wallet's prompt (a settle, an ask for a roll, a release) is sent by a
559
+ * throwaway key made in memory for that one job. `keyFor(pays)` makes it, and the transfer that funds it, which goes
560
+ * in the wallet's own transaction: what its steps pay (`pays`: their fees, an ask's oracle fee, the rent of what a
561
+ * settle opens or mints, `loot.settleRent`) and the rent minimum read from the cluster, plus one spare fee so it can
562
+ * always empty itself. A key pays for an account only with lamports the wallet gave it for that, and never makes a
563
+ * proof record: then the wallet sends that step. `key`: one made already (a box's open names it before it knows how
564
+ * many settles it funds).
565
+ */
566
+ async function keyFor(pays = SIGNATURE_FEE, key = null) {
567
+ key ??= await freshKey();
568
+ return { key, fund: transferLamports(wallet.address, key.address, (await rentEmpty()) + BigInt(pays) + SIGNATURE_FEE) };
569
+ }
570
+
571
+ /** The rent minimum of an account of no data, as the cluster says it (once). A key must hold it, past its fee, to pay
572
+ * a transaction's fee: a fee payer stays rent-exempt or ends empty. */
573
+ let rentMinimum = null;
574
+ const rentEmpty = async () => (rentMinimum ??= BigInt(await client.call('getMinimumBalanceForRentExemption', [0]).catch(() => Number(RENT_EMPTY))));
575
+
576
+ /** Whether `instructions` would land as `payer` sends them now (a dry run, with the standing tables when they need
577
+ * them), and the accounts `watch` as they would be after: { ok, accounts, error }, `error` loot's when it refused
578
+ * ({ code, name, msg }, `loot.lootError`). */
579
+ async function dryRun(payer, instructions, watch = []) {
580
+ const { blockhash } = await client.latestBlockhash();
581
+ const message = compileFitting({ payer, blockhash, instructions, tables: await standing() });
582
+ if (!message) return { ok: false, accounts: [], error: null };
583
+ let error = null;
584
+ const r = await client.simulate(wireTransaction(message), { accounts: watch, replaceBlockhash: true }).catch((e) => {
585
+ error = loot.lootError(e?.logs ?? []);
586
+ return null;
587
+ });
588
+ return { ok: Boolean(r), accounts: r?.accounts ?? [], error };
589
+ }
590
+
591
+ /** A throwaway key empties itself back to the wallet: all it holds now, less its own fee. Nothing when it holds no more
592
+ * than that fee (a settle of its that failed on chain took its fee: the wallet then pays the move, one more prompt). */
593
+ async function emptyKey(key) {
594
+ const left = BigInt(await client.balance(key.address).catch(() => 0n));
595
+ if (left >= (await rentEmpty()) + SIGNATURE_FEE) await sendAs(key, [transferLamports(key.address, wallet.address, left - SIGNATURE_FEE)]).catch(() => null);
596
+ else if (left > 0n) await send([transferLamports(key.address, wallet.address, left)], [key]).catch(() => null);
597
+ // A key kept in storage is dropped once it holds nothing.
598
+ if (key.name && !(BigInt(await client.balance(key.address).catch(() => 1n)) > 0n)) keys?.forget(key.name);
599
+ }
600
+
601
+ /** A key's job: its step (`build(signer)`), simulated as the key sends it, then sent by the key, or by the wallet when
602
+ * the simulation fails (one more prompt); then the key empties itself back to the wallet. Returns the signature. */
603
+ async function keyRun(key, build) {
604
+ const steps = await build(key.address);
605
+ const signature = (await dryRun(key.address, steps)).ok ? await sendAs(key, steps).catch(() => null) : null;
606
+ const sent = signature ?? (await send(await build(wallet.address)));
607
+ await emptyKey(key);
608
+ return sent;
609
+ }
610
+
611
+ /**
612
+ * Rolls in one prompt: a step that makes orders waiting for their roll (a craft, a buy, a mission left, a grant or a
613
+ * contest prize, a lot won) asks for each in the same transaction, and a throwaway key it funds then settles each (the
614
+ * key job rule: funded with each settle's fees and what it may open or mint, `loot.settleRent`). A dry run finds them:
615
+ * the accounts the step writes that come out as orders waiting (a limit can hold a round back, so only what it would
616
+ * make). A box with nothing in stock refuses the ask, and with it the step (`SoldOut`): nothing is sent, and the
617
+ * player is told. When the step and its asks would not land together for any other reason (a missing prize vault,
618
+ * too big a transaction), the step goes alone and its rolls wait in `claims()`. Returns { signature, rolls }.
619
+ */
620
+ async function sendRolling(instructions) {
621
+ const watch = [...new Set(instructions.filter((ix) => ix.programId === loot.LOOT_PROGRAM).flatMap((ix) => ix.keys.filter((k) => k.isWritable && !k.isSigner).map((k) => k.pubkey)))];
622
+ const after = (await dryRun(wallet.address, instructions, watch)).accounts;
623
+ const waiting = watch.flatMap((address, i) => {
624
+ const a = after[i];
625
+ if (a?.owner !== loot.LOOT_PROGRAM || loot.accountKind(a.data) !== 'Order') return [];
626
+ const order = loot.decode('Order', a.data);
627
+ return order.status === 'Waiting' && order.player === wallet.address ? [[address, order]] : [];
628
+ });
629
+ if (!waiting.length) return { signature: await send(instructions), rolls: [] };
630
+ const asks = [];
631
+ const funds = [];
632
+ const burners = [];
633
+ for (const [orderAddress, order] of waiting) {
634
+ const box = await read('LootBox', order.loot_box);
635
+ // Each order its own key, named by it (it can be made again after a reload, `recover`): its settle's fee, a spare
636
+ // for its parts and its give-back, and what its settle may open or mint. The ask names it as the order's settler.
637
+ const { key, fund } = await keyFor(SIGNATURE_FEE * 3n + loot.settleRent(box, Number(order.opens ?? 1), await itemsOf(box)), await jobKey(orderAddress));
638
+ asks.push(...(await asksFor(wallet.address, orderAddress, order, key.address)));
639
+ funds.push(fund);
640
+ burners.push(key);
641
+ }
642
+ const all = [...instructions, ...asks, ...funds];
643
+ const tried = await dryRun(wallet.address, all);
644
+ if (tried.error?.name === 'SoldOut') throw Object.assign(new Error('The box this rolls has nothing left to give right now (SoldOut): nothing was sent. Try again once it is restocked.'), { code: tried.error.code, name: 'SoldOut' });
645
+ // Too heavy to be answered: a step sent alone would pay for a roll that can only fail.
646
+ if (tried.error?.name === 'PackTooLarge') throw Object.assign(new Error('The box this rolls is too heavy for the oracle\'s answer as it stands (PackTooLarge): nothing was sent.'), { code: tried.error.code, name: 'PackTooLarge' });
647
+ if (!tried.ok) return { signature: await send(instructions), rolls: [] };
648
+ const signature = await send(all);
649
+ const rolls = [];
650
+ for (const [k, [orderAddress]] of waiting.entries()) rolls.push(await collect(orderAddress, { burner: burners[k], opened: true }));
651
+ return { signature, rolls };
652
+ }
653
+
654
+ /**
655
+ * Waits for an order's roll (the oracle answers a moment after the open), then settles it. With `burner` (the
656
+ * throwaway key the open funded), the burner signs and pays the settle, and gives the rest back: no wallet prompt.
657
+ * The order names it (`settler`), so it may settle; when the player or the project did first, the burner only gives its lamports back. `putBack`: NFTs
658
+ * won that the player puts back into their stash in place of taking them (one whose collection now refuses to let it
659
+ * move); the player signs that settle.
660
+ */
661
+ async function collect(orderAddress, options = {}) {
662
+ only(options, ['timeoutMs', 'burner', 'putBack', 'opened', 'landed', 'keepBurner'], 'collect');
663
+ try {
664
+ return await collectOnce(orderAddress, options);
665
+ } catch (e) {
666
+ // The throwaway key is never left holding the player's SOL: whatever stopped the collect (a roll not in yet, a
667
+ // settle someone else sent first), it gives its lamports back before the error is said. Collecting again later
668
+ // needs no burner: the player signs that settle.
669
+ const { burner } = options;
670
+ if (!burner) throw e;
671
+ await emptyKey(burner);
672
+ if (!(await client.account(orderAddress))) return { order: orderAddress, settled: 'elsewhere', says: 'The order was settled already.' };
673
+ throw e;
674
+ }
675
+ }
676
+
677
+ async function collectOnce(orderAddress, { timeoutMs = 60_000, burner = null, putBack = [], opened = false, landed = null, keepBurner = false } = {}) {
678
+ const started = Date.now();
679
+ // `landed`: the slot one's own open landed in; the order is read from a node that has seen it.
680
+ const current = async () => {
681
+ const account = await client.account(orderAddress, 'confirmed', landed != null ? { minContextSlot: landed } : {});
682
+ return account?.owner === loot.LOOT_PROGRAM ? loot.decode('Order', account.data) : null;
683
+ };
684
+ let order = await current();
685
+ // Right after one's own confirmed open, no order is "not seen yet", never "settled already": a node of the RPC may
686
+ // lag behind the one that confirmed it. It is read again for a few seconds first.
687
+ for (let i = 0; !order && opened && i < 6; i++) {
688
+ await sleep(1000);
689
+ order = await current();
690
+ }
691
+ // A roll a mission or a recipe made waits to be asked for (or asked again, the oracle never having answered):
692
+ // its player or the project's keys ask, here the player.
693
+ if (order && loot.rollAskable(order, await slotNow(), await chainNow())) {
694
+ await askRoll(orderAddress);
695
+ order = await current();
696
+ }
697
+ while (['Pending', 'Asked'].includes(order?.status) && Date.now() - started < timeoutMs) {
698
+ await sleep(1500);
699
+ order = await current();
700
+ }
701
+ // A free roll whose answer found nothing at all in stock waits again: for a restock, or for its player to end it.
702
+ if (order?.status === 'Waiting') {
703
+ throw new Error('The box this rolls had nothing left to give when the roll landed, so the roll waits: collect it again once the box is restocked. Collected while the box is still empty, the player\'s own ask ends it with nothing.');
704
+ }
705
+ if (!order) {
706
+ if (burner && !keepBurner) await emptyKey(burner);
707
+ // One's own open still not seen after that: say so plainly, never that it was settled (claims() lists it).
708
+ if (opened) return { order: orderAddress, settled: null, says: 'The network doesn’t show this order yet: collect it again in a moment (claims() lists it).' };
709
+ return { order: orderAddress, settled: 'elsewhere', says: 'The order was settled already.' };
710
+ }
711
+ if (order.status === 'Pending') throw new Error('The oracle has not rolled yet: try collecting again in a minute (an order never rolled refunds after about 10 minutes).');
712
+ if (order.status === 'Asked') throw new Error('The oracle has not rolled yet: try collecting again in a minute (a roll never answered is asked again after about 10 minutes).');
713
+ if (order.status === 'Failed') throw new Error('The roll failed (the box changed or ran out): refund the order.');
714
+ // An order closes only in a later slot than the one it was opened in.
715
+ for (let i = 0; i < 20 && client.call; i++) {
716
+ const slot = BigInt(await client.call('getSlot', [{ commitment: 'confirmed' }]));
717
+ if (slot > order.requested_slot) break;
718
+ await sleep(400);
719
+ }
720
+ const g = await projectNow();
721
+ // What the roll set aside: each item it gave (its kind and collection) and each NFT's kind (a compressed one
722
+ // proven first).
723
+ const items = {};
724
+ for (const won of order.won) {
725
+ const id = won.Item?.item ?? won.Compressed?.item;
726
+ if (id !== undefined && !(id in items)) items[id] = await read('Item', loot.pda.item(project, id));
727
+ }
728
+ // A compressed prize is minted into the tree its box names (any of the project's with room), the next NFT it mints.
729
+ const treeOf = async () => {
730
+ if (!order.won.some((w) => w.Compressed)) return {};
731
+ const box = await read('LootBox', order.loot_box);
732
+ const tree = box.entries.flatMap((e) => e.prizes).find((p) => p.Compressed)?.Compressed.tree;
733
+ const config = (await client.account(loot.pda.treeConfig(tree)))?.data;
734
+ // An item's collection without Bubblegum's plugin mints nothing: the prizes after it take its place in the tree.
735
+ const unminted = [];
736
+ for (const c of new Set(order.won.filter((w) => w.Compressed).map((w) => items[w.Compressed.item]?.collection).filter(Boolean))) {
737
+ const data = (await client.account(c))?.data;
738
+ if (!data || !parseCoreCollection(data).bubblegum) unminted.push(c);
739
+ }
740
+ return { tree, unminted, treeMinted: config ? config.subarray(80, 88).reduce((n, b, i) => n | (BigInt(b) << BigInt(8 * i)), 0n) : 0n };
741
+ };
742
+ const holders = await holdersOf(order.shares);
743
+ // Given by its fate to a wallet that is an account of the loot program, the NFT goes back to the player.
744
+ const gives = order.held ? order.fate?.then?.Give?.[0] : undefined;
745
+ const handBack = gives !== undefined && loot.handsBack(await client.account(gives));
746
+ /** The settle's part up to `upto` (`loot.SETTLE_LAST`: all that is left; 0: the split alone) as `signer` sends it,
747
+ * and what goes before it: each compressed NFT's proof, a fresh token account. */
748
+ const settleBy = async (signer, upto = loot.SETTLE_LAST) => {
749
+ const nfts = {};
750
+ const proving = [];
751
+ const end = Math.min(order.won.length, upto);
752
+ for (const won of order.won.slice(Number(order.settled ?? 0), end)) {
753
+ if (won.Nft && !putBack.includes(won.Nft.nft)) {
754
+ // A fresh token account a Token Metadata NFT needs (its holder's own can't take it) is the wallet's to open: a
755
+ // settle with anything before it is the wallet's to send.
756
+ const { before, hasAuthority: _, ...kind } = await nftOf(won.Nft.nft, signer, { to: order.player, payer: wallet.address });
757
+ nfts[won.Nft.nft] = kind;
758
+ proving.push(...before);
759
+ }
760
+ }
761
+ // The NFT the roll was on: its kind, and for a compressed one a proof first.
762
+ const { before: subjectProof, ...subject } = order.subject && order.subject !== NOBODY ? await nftOf(order.subject, signer) : { before: [] };
763
+ const ix = loot.settleIx({ signer, orderAddress, order, projectState: g, items, nfts, putBack, subject, handBack, holders, programs, upto, ...(await treeOf()) });
764
+ return { ix, nfts, before: [...subjectProof, ...proving].filter((x) => x.programId !== COMPUTE_BUDGET_PROGRAM) };
765
+ };
766
+ // An order whose settle doesn't fit one transaction (the instruction trace, 64 accounts, 1,232 bytes) settles in
767
+ // parts, each its own transaction, from where the last stopped. The first splits the payment, its shares' accounts
768
+ // with it (alone, `upto` 0, when not even one prize fits beside them); parts that hand prizes over (`upto` where
769
+ // each stops), as many as the trace holds (`settlePartEnd`) and fewer until they fit; then a last part
770
+ // (`SETTLE_LAST`) with the NFT rolled on. The burner sends each, paying what each mints with what the open funded it
771
+ // with; the player's wallet, a part it can't.
772
+ const signatures = [];
773
+ // A key the order doesn't name can't settle it (`Order::settler`): then the player signs.
774
+ const by = burner && !putBack.length && order.settler === burner.address ? burner.address : wallet.address;
775
+ const fits = async (instructions) => Boolean(compileFitting({ payer: by, blockhash: NOBODY, instructions, tables: await standing() })) && transactionLocks(by, instructions) <= 64;
776
+ const part = async (end) => [computeUnitLimit(loot.settleComputeUnits(order)), (await settleBy(by, end)).ix];
777
+ const sendPart = async (end) => signatures.push(by === wallet.address ? await send(await part(end)) : await sendAs(burner, await part(end)));
778
+ for (;;) {
779
+ const from = Number(order.settled ?? 0);
780
+ if (from >= order.won.length) break;
781
+ let end = loot.settlePartEnd(order, from, items);
782
+ if (end >= order.won.length && (await fits(await part(loot.SETTLE_LAST)))) break;
783
+ while (end > from + 1 && !(await fits(await part(end)))) end--;
784
+ await sendPart(Number(order.drew ?? 0) > 0 && !(await fits(await part(end))) ? 0 : end);
785
+ order = await current();
786
+ if (!order) return { order: orderAddress, settled: 'elsewhere', says: 'The order was settled already.' };
787
+ }
788
+ // Room for every move the shape makes (`loot.settleComputeUnits`), and for a settle of several NFTs the bigger heap
789
+ // the program's allocator uses (`loot.settleHeapFrame`).
790
+ const heap = loot.settleHeapFrame(order);
791
+ const budget = [computeUnitLimit(loot.settleComputeUnits(order)), ...(heap ? [requestHeapFrame(heap)] : [])];
792
+ // The settle opens or mints what the order won (a collectible, a token account, a Sheet, the fate's writes), paid by
793
+ // whoever signs it: the burner, with what the open funded it with (`loot.settleRent`). It settles alone unless the
794
+ // settle needs something first (a proof, a fresh token account) or doesn't fit a legacy transaction (a lookup
795
+ // table's rent is the wallet's to pay), and only when a dry run says it lands as the burner sends it; else the
796
+ // wallet settles (one more prompt). An NFT put back into its stash (one its collection no longer lets move) is the
797
+ // player's own choice: they sign.
798
+ let signature = null;
799
+ let emptied = false;
800
+ const byBurner = burner && !putBack.length && order.settler === burner.address ? await settleBy(burner.address) : null;
801
+ const alone = byBurner && !byBurner.before.length ? [...budget, byBurner.ix] : null;
802
+ const legacy = (instructions) => Boolean(compileFitting({ payer: burner.address, blockhash: NOBODY, instructions }));
803
+ const tried = alone && legacy(alone) ? await dryRun(burner.address, alone, [burner.address]) : null;
804
+ if (tried?.ok) {
805
+ // Its last settle gives back all the burner holds after it, in the same transaction, as the dry run says it will
806
+ // hold (its fee and what the settle opened taken); when that doesn't land as one, the burner empties itself after.
807
+ const left = BigInt(tried.accounts[0]?.lamports ?? 0);
808
+ const back = !keepBurner && left > 0n ? [...alone, transferLamports(burner.address, wallet.address, left)] : null;
809
+ if (back && legacy(back) && (await dryRun(burner.address, back)).ok) signature = await sendAs(burner, back).catch(() => null);
810
+ emptied = Boolean(signature);
811
+ signature ??= await sendAs(burner, alone).catch(() => null);
812
+ }
813
+ if (!signature) {
814
+ // The wallet's settle: as a v0 transaction with a lookup table when it doesn't fit a legacy one (`send`).
815
+ const { ix, before } = await settleBy(wallet.address);
816
+ signature = await send([...budget, ...before, ix]);
817
+ }
818
+ // The burner gives back what it holds after its last settle (a purchase of several orders: after the last order's).
819
+ if (burner && !keepBurner && !emptied) await emptyKey(burner);
820
+ else if (burner && emptied) keys?.forget(burner.name);
821
+ const assets = wonAssets(order).map((a) => (putBack.includes(a) ? null : a));
822
+ return { order: orderAddress, won: order.won, assets, settled: signature, ...(signatures.length ? { parts: [...signatures, signature] } : {}), ...(assets.find(Boolean) ? { asset: assets.find(Boolean) } : {}) };
823
+ }
824
+
825
+ /**
826
+ * For each prize an order won, what it is now: the collectible minted (`["asset", item, serial]`), the NFT handed
827
+ * over; null for tokens. A normal item is the project's backend's to deliver.
828
+ */
829
+ function wonAssets(order) {
830
+ return order.won.map((won) => {
831
+ if (won.Item) return loot.pda.itemAsset(loot.pda.item(project, won.Item.item), won.Item.serial);
832
+ if (won.Nft) return won.Nft.nft;
833
+ // A compressed NFT's asset id is the tree's count when it was minted: read from its settle's receipt or the
834
+ // player's assets (`claims`), not from the order.
835
+ return null;
836
+ });
837
+ }
838
+
839
+ /** A nonce for an order no other order of the player's is at. */
840
+ const freshNonce = () => BigInt(Date.now()) * 1000n + BigInt(Math.floor(Math.random() * 1000));
841
+
842
+ /** Each box of `ids`, decoded (null for one never written), by id. */
843
+ async function boxesOf(ids) {
844
+ const out = {};
845
+ for (const id of new Set(ids)) out[id] = await readMaybe('LootBox', loot.pda.box(project, id));
846
+ return out;
847
+ }
848
+
849
+ /** Each item a box's prizes name, decoded (null for one never written), by id: a fresh one's tables go with its roll. */
850
+ async function itemsOf(box) {
851
+ const out = {};
852
+ for (const p of box.entries.flatMap((e) => e.prizes)) {
853
+ const id = p.Item?.[0] ?? p.Compressed?.item;
854
+ if (id !== undefined && !(id in out)) out[id] = await readMaybe('Item', loot.pda.item(project, id));
855
+ }
856
+ return out;
857
+ }
858
+
859
+ /** The mint of each pool a box's tokens come from (null for a pool not opened). */
860
+ async function poolMintsOf(box) {
861
+ const prizePools = [...new Set(box.entries.flatMap((e) => e.prizes).map((p) => p.Tokens?.from.Pool?.[0]).filter((p) => p !== undefined))];
862
+ const pools = prizePools.length ? await client.accounts(prizePools.map((p) => loot.pda.pool(project, p))) : [];
863
+ return Object.fromEntries(prizePools.map((p, i) => [p, pools[i] ? encodeBase58(pools[i].data.subarray(0, 32)) : null]));
864
+ }
865
+
866
+ /** The instructions that ask the oracle for the roll of `order` (at `orderAddress`), `payer` paying it. Each prize
867
+ * mint's token program is read from the mint when the app wasn't given it (`programs`): the ask opens the project's
868
+ * vault of it. */
869
+ async function asksFor(payer, orderAddress, order, settler) {
870
+ const box = await read('LootBox', order.loot_box);
871
+ const poolMints = await poolMintsOf(box);
872
+ const mints = [...Object.values(poolMints), ...box.entries.flatMap((e) => e.prizes.map((p) => p.Tokens?.from.Mint?.[0]))].filter((x) => x && !programs[x]);
873
+ const owners = mints.length ? await client.accounts(mints) : [];
874
+ const known = { ...programs, ...Object.fromEntries(mints.map((x, i) => [x, owners[i]?.owner ?? TOKEN_PROGRAM])) };
875
+ return loot.askRollIx({ payer, orderAddress, order, box, settler, poolMints, items: await itemsOf(box), programs: known });
876
+ }
877
+
878
+ /** Asks the oracle for a roll a mission, a recipe or a prize made (`Waiting`): the player pays the oracle. */
879
+ async function askRoll(orderAddress) {
880
+ return send([computeUnitLimit(400_000), ...(await asksFor(wallet.address, orderAddress, await read('Order', orderAddress)))]);
881
+ }
882
+
883
+ /**
884
+ * The most the player agreed to pay for `box`, in base units: `maxPrice` (base units), or the `listing` that
885
+ * `shop()` or `boxes()` showed them (its `priceUnits`). Never the price read now: a price the owner or other buyers
886
+ * moved since the player saw it would be paid without their say. A price above it is refused here, in words, before
887
+ * the wallet is asked.
888
+ */
889
+ function boundOf(box, name, { maxPrice, listing } = {}, key) {
890
+ if (listing && key !== undefined && listing.key !== undefined && listing.key !== key) throw new Error(`That listing is “${listing.key}”, not “${key}”.`);
891
+ const given = maxPrice ?? listing?.priceUnits;
892
+ if (given === undefined || given === null) throw new Error(`Say the most the player agreed to pay for “${name}”: pass the listing shop() or boxes() showed (listing), or maxPrice in base units. The price read now is never the bound.`);
893
+ const bound = BigInt(given);
894
+ if (BigInt(box.price) > bound) throw new Error(`The price of “${name}” is now ${show(box.price, decimalsOf(box.currencyKey))}, more than the ${show(bound, decimalsOf(box.currencyKey))} the player was shown: show them the new price first.`);
895
+ return bound;
896
+ }
897
+
898
+ /**
899
+ * Refuses an option a method doesn't take, so a misspelled or outdated one fails here instead of being ignored (a page
900
+ * passing `{ choice }` for a referrer would send every referral share to the treasury). `options`: what the method
901
+ * was given, or what is left of it once the options it takes are read; `takes`: those it takes, said in the error.
902
+ */
903
+ function only(options, takes, method) {
904
+ const unknown = Object.keys(options ?? {}).filter((k) => !takes.includes(k) && options[k] !== undefined);
905
+ if (!unknown.length) return;
906
+ const hint = unknown.includes('choice') && takes.includes('chosenRecipient') ? ' (a buyer’s choice is `chosenRecipient`)' : '';
907
+ throw new Error(`${method} doesn’t take ${unknown.map((k) => `“${k}”`).join(', ')}${hint}. It takes: ${takes.join(', ')}.`);
908
+ }
909
+ /** What `buy` and `openBox` take: a box bought at once (`buyBox`) or opened (`open`). */
910
+ const BOX_OPTIONS = ['shown', 'listProof', 'maxPrice', 'listing', 'chosenRecipient', 'on', 'oneSignature', 'count'];
911
+
912
+ /**
913
+ * Buys `count` of a box that needs no roll (a shop item, a bundle, a fixed pack: one entry, nothing random, no NFT
914
+ * rolled on) in one transaction: the price is split and every prize given at once, once for each draw of each, with
915
+ * no order to settle. A roll the box gives (`Prize::Roll`) is asked for in the same transaction and settled by a
916
+ * throwaway key after (`sendRolling`: still one prompt). `shown` (an NFT for its gate), `listProof` (for a list gate),
917
+ * `chosenRecipient` (the wallet, or collectible, a share the box lets the buyer name goes to). Returns { signature,
918
+ * assets }: for each prize given, the collectible minted or the NFT given (null for tokens or a normal item), and for
919
+ * a roll the order it is made in; with `rolls`, what each roll gave. `orNull`: a pack (more than one draw in all)
920
+ * whose purchase doesn't fit one legacy transaction, or the instruction trace, returns null, sending nothing.
921
+ */
922
+ async function buyBox(boxId, part, { shown = null, listProof, maxPrice, listing, chosenRecipient = null, count = 1, orNull = false } = {}) {
923
+ const g = await projectNow();
924
+ const box = await read('LootBox', loot.pda.box(project, boxId));
925
+ if (!Number.isInteger(count) || count < 1) throw new Error('Buy a whole number, one at least (count).');
926
+ const bound = boundOf({ ...box, currencyKey: part?.currency }, part?.name ?? `box ${boxId}`, { maxPrice, listing }, part?.key) * BigInt(count);
927
+ shown ??= await passing(box.gate, 'buy this');
928
+ listProof = listProofOf(box.gate, part?.gate?.list, listProof);
929
+ const mint = box.currency;
930
+ const [entry] = box.entries;
931
+ const given = await prizeInputs(entry.prizes);
932
+ const nonce = freshNonce();
933
+ const choosesHolder = box.shares.some((s) => s.to.Chosen && s.to.Chosen.item != null);
934
+ const holders = await holdersOf(loot.chosenShares(box.shares, chosenRecipient));
935
+ const ix = loot.buyIx({ player: wallet.address, project, box, maxPrice: bound, count, treasury: g.treasury, chosenRecipient, chosen: choosesHolder ? chosenRecipient : null, nonce, shown, listProof, items: given.items, poolMints: given.poolMints, nfts: given.nfts, holders, programs });
936
+ const made = {};
937
+ const gives = loot.buyGives(box, count);
938
+ const assets = gives.map(({ prize, k }) => {
939
+ if (prize.Item) {
940
+ const id = prize.Item[0];
941
+ const item = given.items[id];
942
+ made[id] = (made[id] ?? 0) + 1;
943
+ return item?.kind === 'Collectible' ? loot.pda.itemAsset(loot.pda.item(project, id), Number(item.issued) + made[id]) : null;
944
+ }
945
+ if (prize.Nft) return prize.Nft.nft;
946
+ if (prize.Roll) return loot.pda.order(project, wallet.address, loot.prizeRollNonce(nonce + BigInt(k)));
947
+ return null;
948
+ });
949
+ const rollsMade = gives.filter((x) => x.prize.Roll).length;
950
+ const instructions = withSolWrap(wallet.address, mint, bound, [computeUnitLimit(rollsMade || gives.length > 2 ? 1_400_000 : 600_000), ...given.before.filter((x) => x.programId !== COMPUTE_BUDGET_PROGRAM), ix]);
951
+ if (orNull && gives.length > 1) {
952
+ const fits = compileFitting({ payer: wallet.address, blockhash: NOBODY, instructions }) && transactionLocks(wallet.address, instructions) <= 64;
953
+ if (!fits || !(await dryRun(wallet.address, instructions)).ok) return null;
954
+ }
955
+ const { signature, rolls } = rollsMade ? await sendRolling(instructions) : { signature: await send(instructions), rolls: [] };
956
+ return { signature, assets, ...(assets.find(Boolean) ? { asset: assets.find(Boolean) } : {}), ...(rolls.length ? { rolls } : {}) };
957
+ }
958
+
959
+ /**
960
+ * Opens a box that rolls. `oneSignature` (the default): the same transaction funds a throwaway key with its settle's
961
+ * fee, and makes the player's account of each mint the box's tokens are in, so the settle needs no second wallet
962
+ * prompt. `chosenRecipient`: the wallet (or collectible) the player names for a share the box lets them choose (a
963
+ * referrer, a plot). `on`: the NFT a box that rolls on one takes (an NFT whose price it is, a sword to enchant).
964
+ * `listProof`: for a list gate.
965
+ */
966
+ async function open(boxId, part, { shown = null, on = null, maxPrice, listing, oneSignature = true, chosenRecipient = null, listProof, count = 1 } = {}) {
967
+ const g = await projectNow();
968
+ const box = await read('LootBox', loot.pda.box(project, boxId));
969
+ // `count` packs in one purchase: one wallet prompt, one oracle answer, each at the price the one before it left.
970
+ if (!Number.isInteger(count) || count < 1) throw new Error('Open a whole number of packs, one at least (count).');
971
+ const bound = boundOf({ ...box, currencyKey: part?.currency }, part?.name ?? `box ${boxId}`, { maxPrice, listing }, part?.key) * BigInt(count);
972
+ // Each item a prize names: a fresh one's tables go with the purchase.
973
+ const items = await itemsOf(box);
974
+ // A box that rolls on an NFT takes the one the player brings (`on`), whatever its kind.
975
+ if (box.subject && !on) throw new Error('This box rolls on an NFT: say which (on).');
976
+ const brought = box.subject ? await nftOf(on) : { before: [] };
977
+ const { before: proving, hasAuthority, ...kind } = brought;
978
+ const subject = box.subject ? { asset: on, hasAuthority, ...kind, collection: kind.collection ?? (await collectionOf(on)) ?? MPL_CORE } : undefined;
979
+ if (!box.subject) shown ??= await passing(box.gate, 'buy this');
980
+ listProof = listProofOf(box.gate, part?.gate?.list, listProof);
981
+ const mint = box.currency;
982
+ // A roll that moves the numbers of the NFT it rolls on is at that NFT's nonce: one at a time on it.
983
+ const nonce = box.subject && loot.boxHolds(box) ? loot.nonceOn(on) : freshNonce();
984
+ // Each pool's mint, and each mint tokens are minted from: what is won waits in the project's vault for its mint.
985
+ const poolMints = await poolMintsOf(box);
986
+ const minted = box.entries.flatMap((e) => e.prizes).map((p) => p.Tokens?.from.Mint?.[0]).filter(Boolean);
987
+ const prizeMints = [...new Set([...Object.values(poolMints).filter(Boolean), ...minted])];
988
+ const choosesHolder = box.shares.some((s) => s.to.Chosen && s.to.Chosen.item != null);
989
+ const chosen = chosenRecipient && choosesHolder ? chosenRecipient : null;
990
+ // A purchase of more opens than one order takes (`loot.opensAnOrder`: what one oracle answer draws, and an order's
991
+ // rent and fee within 0.01 SOL) is several orders, each its own answer, all in this one transaction: one prompt.
992
+ const each = loot.opensAnOrder(box, { items });
993
+ const orders = [];
994
+ for (let left = count, k = 0n; left > 0; left -= each, k++) {
995
+ const n = Math.min(each, left);
996
+ // Its key, named by the order as its settler (made again after a reload: `recover`), when the settle needs no prompt.
997
+ const settlerKey = oneSignature ? ((await jobKey(loot.pda.order(project, wallet.address, nonce + k))) ?? (await freshKey())) : null;
998
+ orders.push({ nonce: nonce + k, n, settlerKey, ix: loot.openBoxIx({ settler: settlerKey?.address, player: wallet.address, project, box, nonce: nonce + k, maxPrice: (bound / BigInt(count)) * BigInt(n), count: n, items, shown, listProof, subject, chosenRecipient, chosen, poolMints, programs }) });
999
+ }
1000
+ const before = [...proving, createAssociatedTokenAccountIdempotent(wallet.address, wallet.address, mint, programOf(mint))];
1001
+ // (A compressed NFT's proof brings its own compute limit, the transaction's only one.)
1002
+ let solPrize = false;
1003
+ if (oneSignature) {
1004
+ // The player opens here every account the settle will pay into (their prize's, and each wallet a share of the
1005
+ // sale pays: a referrer, a plot's holder, the treasury), so the burner's settle opens none of them.
1006
+ for (const prizeMint of prizeMints) {
1007
+ solPrize ||= prizeMint === NATIVE_MINT;
1008
+ before.push(createAssociatedTokenAccountIdempotent(wallet.address, wallet.address, prizeMint, programOf(prizeMint)));
1009
+ }
1010
+ const shares = loot.chosenShares(box.shares, chosenRecipient);
1011
+ const holders = await holdersOf(shares);
1012
+ const payees = shares.flatMap((s) => (s.to === 'Treasury' ? [g.treasury] : s.to.Wallet ? [s.to.Wallet[0]] : s.to.Holder && holders[s.to.Holder[0]] ? [holders[s.to.Holder[0]]] : []));
1013
+ const paid = new Set([g.treasury, ...payees]);
1014
+ for (const payee of paid) before.push(createAssociatedTokenAccountIdempotent(wallet.address, payee, mint, programOf(mint)));
1015
+ // A sale that pays the player themselves (the owner buying from their own shop) pays into their wrapped-SOL
1016
+ // account: it stays open for the settle too.
1017
+ solPrize ||= mint === NATIVE_MINT && paid.has(wallet.address);
1018
+ }
1019
+ // Each order's burner, named by the order (made again after a reload: `recover`), holds the fee of each settle part
1020
+ // its order may take (`settleParts`) and of its give-back, and what its settle may mint or open (`settleRent`: the
1021
+ // order lends nothing); it gives back what it doesn't use.
1022
+ const addresses = orders.map((o) => loot.pda.order(project, wallet.address, o.nonce));
1023
+ const burners = [];
1024
+ const after = [];
1025
+ if (oneSignature) {
1026
+ for (const o of orders) {
1027
+ const { key, fund } = await keyFor(SIGNATURE_FEE * BigInt(loot.settleParts(box, o.n) + 1) + loot.settleRent(box, o.n, items), o.settlerKey);
1028
+ burners.push(key);
1029
+ after.push(fund);
1030
+ }
1031
+ }
1032
+ // A SOL prize, or a share of the sale, lands in the player's wrapped-SOL account, which the prompt-less settle
1033
+ // can't open: it stays open, and the player's next transaction unwraps it.
1034
+ let instructions = withSolWrap(wallet.address, mint, BigInt(box.price) * BigInt(count), [...before, ...orders.map((o) => o.ix), ...after]);
1035
+ if (solPrize) instructions = instructions.filter((x) => !(x.programId === TOKEN_PROGRAM && x.data?.[0] === 9 && x.keys[0]?.pubkey === associatedTokenAddress(wallet.address, NATIVE_MINT)));
1036
+ const signature = await send(instructions, [], { keepWrapped: solPrize });
1037
+ // The slot it landed in: the order is read from a node that has seen it (a load-balanced RPC's node lagging
1038
+ // behind answers nothing for an order just opened).
1039
+ const landed = (await client.signatureStatus?.(signature).catch(() => null))?.slot ?? null;
1040
+ return { signature, orderAddress: addresses[0], orderAddresses: addresses, burner: burners[0] ?? null, burners, landed };
1041
+ }
1042
+
1043
+ /**
1044
+ * Holds the listing the player was shown against the one listed now: refused, in words, when none is given, when it
1045
+ * is of another NFT or another kind of deal (`Sell`, `Rent`), and when its terms differ from those listed now (the
1046
+ * program would refuse them too: TermsChanged).
1047
+ */
1048
+ async function shownListing(asset, listing, kind) {
1049
+ if (!listing) throw new Error('Say the terms the player was shown: pass the listing listing() or listings() showed (listing). The listing read now is never signed in its place.');
1050
+ if (listing.asset !== asset) throw new Error(`That listing is of ${listing.asset}, not ${asset}.`);
1051
+ if (!listing.deal?.[kind]) throw new Error(kind === 'Sell' ? 'That listing is for rent, not for sale.' : 'That listing is for sale, not for rent.');
1052
+ const listed = await readMaybe('Listing', loot.pda.listing(project, asset));
1053
+ if (!listed) throw new Error(`${asset} is not listed any more.`);
1054
+ const [was, is] = [loot.listingTerms(listing), loot.listingTerms(listed)];
1055
+ if (was.some((b, i) => b !== is[i])) throw new Error('This listing has changed since the player was shown it: show them its terms as they are now first.');
1056
+ }
1057
+
1058
+ /** A box bought at once when it needs no roll (`buyBox`: a shop item, a bundle, a fixed pack of any count), or
1059
+ * opened and collected: one that rolls, gives a fresh item (its traits are rolled), or rolls on an NFT. A fixed pack
1060
+ * too big for one transaction is opened instead (the oracle answers it like any box). */
1061
+ async function buyOrOpen(boxId, part, options) {
1062
+ const box = await read('LootBox', loot.pda.box(project, boxId));
1063
+ if (!loot.boxRolls(box.entries) && !box.subject && !loot.boxFresh(box.entries, await itemsOf(box))) {
1064
+ const { on: _on, oneSignature: _one, ...buying } = options ?? {};
1065
+ const bought = await buyBox(boxId, part, { ...buying, orNull: true });
1066
+ if (bought) return bought;
1067
+ }
1068
+ const { orderAddresses, burners, landed } = await open(boxId, part, options);
1069
+ if (orderAddresses.length === 1) return collect(orderAddresses[0], { burner: burners[0] ?? null, opened: true, landed });
1070
+ // Several orders of one purchase: each collected in turn by its own burner.
1071
+ const got = [];
1072
+ for (const [k, address] of orderAddresses.entries()) got.push(await collect(address, { burner: burners[k] ?? null, opened: true, landed }));
1073
+ return { ...got.at(-1), orders: got, won: got.flatMap((g) => g.won ?? []), assets: got.flatMap((g) => g.assets ?? []), parts: got.flatMap((g) => g.parts ?? [g.settled]) };
1074
+ }
1075
+
1076
+ /** The current slot (a later ask or a refund of a failed order needs a later slot; an order's deadlines count in seconds: `chainNow`). */
1077
+ const slotNow = async () => BigInt(await client.call('getSlot', [{ commitment: 'confirmed' }]));
1078
+ /** The slots the SlotHashes sysvar holds now (a lookup table closes once its deactivation slot has left them). */
1079
+ const recentSlots = async () => slotHashes((await client.account(SYSVAR_SLOT_HASHES))?.data);
1080
+ /** The chain's clock (Unix seconds), read from its Clock sysvar; the computer's (`now`) when it can't be read. */
1081
+ const chainNow = async () => {
1082
+ const clock = await client.account('SysvarC1ock11111111111111111111111111111111').catch(() => null);
1083
+ if (!clock?.data || clock.data.length < 40) return BigInt(now());
1084
+ const v = clock.data.subarray(32, 40).reduce((n, b, i) => n | (BigInt(b) << BigInt(8 * i)), 0n);
1085
+ return v >= 1n << 63n ? v - (1n << 64n) : v;
1086
+ };
1087
+
1088
+ /** How many ends farm `f` has passed and not walked yet (`loot.endsToWalk`), and its decoded record: 0 for one that keeps none. */
1089
+ async function endsOf(f, farm) {
1090
+ if (!farm.keeps_ends) return { left: 0, ends: [] };
1091
+ const data = (await client.account(loot.pda.ends(loot.pda.farm(project, f.id))))?.data;
1092
+ const ends = data ? loot.decodeEnds(data) : [];
1093
+ return { left: loot.endsToWalk(farm, ends, await chainNow()), ends };
1094
+ }
1095
+
1096
+ /**
1097
+ * A move of farm `f` as it can land: every move walks the ends the farm has passed first, each some compute, so one with
1098
+ * ends to walk asks for all a transaction has, and one with more than that holds is refused in words, nothing signed.
1099
+ */
1100
+ async function walking(f, farm, instructions) {
1101
+ const { left } = await endsOf(f, farm);
1102
+ if (!left) return instructions;
1103
+ if (left > loot.MOVE_WALK_ROOM) throw Object.assign(new Error(`“${f.name ?? f.key}” has passed ${left} times seats stop earning since anyone last moved it, more than one transaction can walk: walk it first (app.walkFarm: anyone may, it is free, and nothing is lost by the wait).`), { name: 'FarmNeedsWalk' });
1104
+ return [computeUnitLimit(loot.WALK_COMPUTE_UNITS), ...instructions.filter((x) => !(x.programId === COMPUTE_BUDGET_PROGRAM && x.data[0] === 2))];
1105
+ }
1106
+
1107
+ /**
1108
+ * Refuses a stake a farm would refuse (`FarmOver`) before anything is signed: a farm with an end takes none at its end
1109
+ * or after it, nor one at a tier whose lock would run past it. `f` is the spec's farm, `farm` the decoded one.
1110
+ */
1111
+ async function openFor(f, farm, tier) {
1112
+ const at = await chainNow();
1113
+ if (loot.farmOpenFor(farm, tier, at)) return;
1114
+ const ends = new Date(Number(farm.ends_at) * 1000).toISOString();
1115
+ if (at >= BigInt(farm.ends_at)) throw new Error(`“${f.name ?? f.key}” ended at ${ends}: it takes no stake any more. Stakes in it can still leave and harvest.`);
1116
+ throw new Error(`“${f.name ?? f.key}” ends at ${ends}, and lock tier ${tier} would run past it: stake at a tier with a shorter lock.`);
1117
+ }
1118
+
1119
+ /** Who holds each collectible a split's holder shares name now: { asset: holder }. */
1120
+ async function holdersOf(shares) {
1121
+ const holders = {};
1122
+ for (const s of shares) if (s.to.Holder) holders[s.to.Holder[0]] = await ownerOf(s.to.Holder[0]).catch(() => null);
1123
+ return holders;
1124
+ }
1125
+
1126
+ /**
1127
+ * A lottery and its rounds that have a record: the current one (null when nobody bought into it yet), and the two
1128
+ * before it while they wait for their draw or still pay out (the ask moves the lottery on, so a round drawing or
1129
+ * owing parts is an older one). Each { address, round } (`loot.decodeRound`).
1130
+ */
1131
+ async function roundOf(l) {
1132
+ const address = loot.pda.lottery(project, l.id);
1133
+ const lottery = await read('Lottery', address);
1134
+ const back = [1, 2].filter((k) => lottery.round >= k).map((k) => loot.pda.round(address, lottery.incarnation, lottery.round - k));
1135
+ const keys = [loot.currentRound(address, lottery), ...back];
1136
+ const found = await client.accounts(keys);
1137
+ const rounds = keys.map((key, i) => ({ address: key, round: found[i]?.owner === loot.LOOT_PROGRAM ? loot.decodeRound(found[i].data) : null }));
1138
+ return { address, lottery, current: rounds[0].round, rounds: rounds.filter((r) => r.round) };
1139
+ }
1140
+
1141
+ /**
1142
+ * The tail a round's transfers need: each mint's transfer-hook accounts (its extra-account metas, read as every
1143
+ * hooked transfer of the client reads them), once each; none for a mint with no hook. A hook whose list names each
1144
+ * transfer can't be resolved ahead and is left out (the call then fails on it).
1145
+ */
1146
+ async function hookTail(mints) {
1147
+ const out = new Map();
1148
+ for (const mint of new Set(mints.filter((x) => x && x !== NOBODY))) {
1149
+ const resolved = await resolveTransfers(client, mint).catch(() => null);
1150
+ for (const m of resolved?.extraAccounts ?? []) {
1151
+ const had = out.get(m.pubkey);
1152
+ out.set(m.pubkey, had ? { ...had, isWritable: had.isWritable || m.isWritable } : m);
1153
+ }
1154
+ }
1155
+ return [...out.values()];
1156
+ }
1157
+ /** The mints a round's ask and pulls move: its pot's, its payment's and each token run's. */
1158
+ const roundMints = (r, poolMints = {}) => [r.terms.mint, loot.paymentMint(r.terms), ...r.terms.places.map((p) => p.prize?.Tokens?.from).filter(Boolean).map((from) => (from.Pool ? poolMints[from.Pool[0]] : from.Mint?.[0] ?? from.Vest?.[0]))];
1159
+
1160
+ /** Each pool a round's places give tokens from, mapped to its mint (one never opened: the economy's currency). */
1161
+ async function roundPoolMints(terms) {
1162
+ const out = {};
1163
+ for (const p of terms.places) {
1164
+ const pool = p.prize?.Tokens?.from.Pool?.[0];
1165
+ if (pool === undefined || pool in out) continue;
1166
+ const account = await client.account(loot.pda.pool(project, pool));
1167
+ out[pool] = account ? encodeBase58(account.data.subarray(0, 32)) : mintOf();
1168
+ }
1169
+ return out;
1170
+ }
1171
+
1172
+ /** The ask for a round's draw (`round` decoded, `key` its address; null for the current round with no record). */
1173
+ async function drawStep(l, lottery, key, round, signer) {
1174
+ const terms = round?.terms ?? lottery.terms;
1175
+ const poolMints = await roundPoolMints(terms);
1176
+ const extra = await hookTail(roundMints({ terms }, poolMints));
1177
+ return [computeUnitLimit(600_000), loot.drawLotteryIx({ payer: signer, project, id: l.id, lottery, round, roundAddress: key, poolMints, extra })];
1178
+ }
1179
+
1180
+ /**
1181
+ * When a draw of round `r` (at `key`) would cancel it (an open round short of its least tickets; a waiting one past
1182
+ * the timeout), the pulls of `who`'s own parts as the cancelled round owes them (a few entries' worth), to go right
1183
+ * after the draw in its transaction; none otherwise. The round's close rides on them when they are all it owes.
1184
+ */
1185
+ async function cancelPulls(key, r, who) {
1186
+ const short = r.status === 'Open' && BigInt(r.sold) < BigInt(r.terms.min_sold ?? 0) && BigInt(now()) >= BigInt(r.terms.draws_at);
1187
+ if (!short && !loot.lotteryCancellable(r, await chainNow())) return [];
1188
+ // As the program cancels it: every entry's own part owed, and what the ask moved in owed back.
1189
+ const potOwed = BigInt(r.pot_arrived) > BigInt(r.to_pot) ? BigInt(r.pot_arrived) - BigInt(r.to_pot) : 0n;
1190
+ const pools = r.prizes.filter((p) => !p.minted && BigInt(p.arrived) > 0n).length;
1191
+ const cancelled = { ...r, status: 'Cancelled', pot_owed: potOwed, owed: Number(r.owns) + (potOwed > 0n ? 1 : 0) + pools };
1192
+ const mine = r.entries.flatMap((e, i) => (e.buyer === who && loot.partsOwed(cancelled, i).length ? [{ entry: i, parts: loot.partsOwed(cancelled, i) }] : [])).slice(0, 4);
1193
+ const settles = mine.reduce((n, { entry, parts }) => n + parts.reduce((m, p) => m + pullSettles(cancelled, entry, p), 0), 0);
1194
+ const out = [];
1195
+ for (const [k, { entry, parts }] of mine.entries()) out.push(...(await pulls(key, cancelled, entry, parts, who, { closes: settles >= cancelled.owed && k === mine.length - 1 })).instructions);
1196
+ return out;
1197
+ }
1198
+
1199
+ /** The nonce a pull of entry `i` of the round at `key` names: a place's roll is made at `prizeRollNonce(nonce + place)`. */
1200
+ const pullNonce = (key, i) => loot.prizeNonce(key, BigInt(i));
1201
+
1202
+ /** How many of a round's owed parts a pull of `part` (of entry `i`) settles. */
1203
+ const pullSettles = (r, i, part) => {
1204
+ if (part === 'Own' || part.Common) return 1;
1205
+ const run = Number((part.Pot ?? part.Prize)[0]);
1206
+ return part.Pot || loot.tokenRun(r, run) ? loot.placesPulled(r, i, run, Boolean(part.Prize)) : loot.prizePlacesOwed(r, i, run).length;
1207
+ };
1208
+
1209
+ /**
1210
+ * The pulls of `parts` of entry `i` of a round (`partsOwed`; `i` null for parts owed in common, `commonOwed`), signed by
1211
+ * `signer`, and the rolls they make ([{ order, box }], `loot.pullRolls`). With `closes`, the last pull carries the
1212
+ * round's close (when the parts it pays are the last owed).
1213
+ */
1214
+ async function pulls(key, r, i, parts, signer, { closes = false } = {}) {
1215
+ const e = i == null ? null : r.entries[i];
1216
+ const out = [];
1217
+ const rolls = [];
1218
+ const g = await projectNow();
1219
+ // A run's token prize is in its own mint: the prizes the round recorded name each.
1220
+ const extra = await hookTail([...roundMints(r), ...r.prizes.map((p) => p.mint)]);
1221
+ for (const [k, part] of parts.entries()) {
1222
+ const more = {};
1223
+ if (part === 'Own' && e.nft !== NOBODY) {
1224
+ const then = r.status === 'Cancelled' || r.terms.returns ? null : r.terms.nft_ticket?.then;
1225
+ more.handBack = Boolean(then?.Give) && loot.handsBack(await client.account(then.Give[0]));
1226
+ const to = !then || more.handBack ? e.buyer : then.Give ? then.Give[0] : undefined;
1227
+ const { before = [], hasAuthority: _, ...kind } = await nftOf(e.nft, signer, { to });
1228
+ out.push(...before);
1229
+ more.ticket = kind;
1230
+ } else if (part.Prize && !loot.tokenRun(r, Number(part.Prize[0]))) {
1231
+ // Each of the entry's places in the run: an NFT of the lottery's own stash (which one, as the pull takes them
1232
+ // out), a project stash's NFT, an item, or a roll (asked in the same transaction).
1233
+ const run = Number(part.Prize[0]);
1234
+ const given = r.terms.places[run].prize;
1235
+ const inputs = await prizeInputs([given], signer, e.buyer);
1236
+ out.push(...inputs.before);
1237
+ const stash = loot.ownStash(r.terms.places[run]) ? loot.decodeStash((await client.account(loot.pda.lotteryStash(r.lottery)))?.data ?? null) : null;
1238
+ if (stash) {
1239
+ const predicted = loot.stashNfts(r, stash);
1240
+ for (const place of loot.prizePlacesOwed(r, i, run)) {
1241
+ const nft = predicted[loot.nftSlot(r, place)];
1242
+ if (!nft || inputs.nfts[nft]) continue;
1243
+ const { before, hasAuthority: _, ...kind } = await nftOf(nft, signer, { to: e.buyer });
1244
+ inputs.nfts[nft] = kind;
1245
+ out.push(...before);
1246
+ }
1247
+ }
1248
+ const stashes = given.Nft ? { [given.Nft.stash]: loot.decodeStash((await client.account(loot.pda.stash(project, given.Nft.stash)))?.data ?? null) ?? { nfts: [] } } : {};
1249
+ more.prize = { stash, stashes, nfts: inputs.nfts, collection: inputs.collection, poolMints: inputs.poolMints, items: inputs.items, projectMint: mintOf() };
1250
+ more.nonce = pullNonce(key, i);
1251
+ rolls.push(...loot.pullRolls({ project, round: r, entry: i, part, nonce: more.nonce }));
1252
+ } else if (part.Common) {
1253
+ // Where a part owed in common goes: a share's payee (a collectible's holder now), the pot, a pool; one that can't
1254
+ // take it (a pool never opened, the pot closed since, an account gone) sends it to the treasury.
1255
+ const c = Number(part.Common[0]);
1256
+ const n = r.terms.shares.length;
1257
+ const to = c < n ? r.terms.shares[c].to : c === n ? { Account: [loot.pda.pot(r.lottery)] } : r.terms.places[Number(r.prizes[c - n - 1].run)].prize.Tokens.from;
1258
+ if (to.Holder) more.holders = await holdersOf([{ to }]);
1259
+ const target = to.Pool ? loot.pda.pool(project, to.Pool[0]) : to.Account?.[0];
1260
+ // A pot or pool made again in another mint can't take it either: the program sends it to the treasury.
1261
+ const mint = c < n ? loot.paymentMint(r.terms) : c === n ? r.terms.mint : r.prizes[c - n - 1].mint;
1262
+ const held = target ? await client.account(target) : null;
1263
+ if (target && (!held || encodeBase58(held.data.subarray(0, 32)) !== mint)) more.toTreasury = true;
1264
+ }
1265
+ const last = k === parts.length - 1;
1266
+ const round = closes && last ? { ...r, owed: pullSettles(r, i, part) } : { ...r, owed: Number.MAX_SAFE_INTEGER };
1267
+ out.push(loot.pullIx({ payer: signer, project, roundAddress: key, round, entry: i ?? 0, part, projectState: g, programs, extra, ...more }));
1268
+ }
1269
+ return { instructions: out, rolls };
1270
+ }
1271
+
1272
+ return {
1273
+ project,
1274
+ get address() {
1275
+ return wallet?.address ?? null;
1276
+ },
1277
+
1278
+ /** The shop: every item with a price (or free), its price now and what is left. */
1279
+ async shop() {
1280
+ const items = (spec.items ?? []).filter((it) => Number.isInteger(it.shopBox));
1281
+ const accounts = await client.accounts(items.flatMap((it) => [loot.pda.box(project, it.shopBox), loot.pda.item(project, it.id)]));
1282
+ return items.map((it, i) => {
1283
+ const box = accounts[2 * i] && loot.decode('LootBox', accounts[2 * i].data);
1284
+ const item = accounts[2 * i + 1] && loot.decode('Item', accounts[2 * i + 1].data);
1285
+ const dec = decimalsOf(currencyKey);
1286
+ return { key: it.key, name: it.name, price: box ? show(box.price, dec) : null, priceUnits: box?.price ?? null, currency: tokens.get(currencyKey)?.symbol ?? currencyKey, active: Boolean(box?.active && item?.active), left: item?.max_supply ? item.max_supply - item.issued : null };
1287
+ });
1288
+ },
1289
+
1290
+ /** The boxes: price now, each outcome's chance as written, and what the box does once an outcome runs out
1291
+ * (`whenOut`: 'stopSales', it isn't sold until restocked; 'drawLeft', it draws among what is left). */
1292
+ async boxes() {
1293
+ const list = spec.boxes ?? [];
1294
+ const accounts = await client.accounts(list.map((b) => loot.pda.box(project, b.id)));
1295
+ return list.map((b, i) => {
1296
+ const box = accounts[i] && loot.decode('LootBox', accounts[i].data);
1297
+ const total = box ? box.entries.reduce((n, e) => n + e.weight, 0) : 0;
1298
+ return { key: b.key, name: b.name, price: box ? show(box.price, decimalsOf(b.currency)) : null, priceUnits: box?.price ?? null, currency: tokens.get(b.currency ?? currencyKey)?.symbol, active: Boolean(box?.active), whenOut: box?.when_out === 'DrawLeft' ? 'drawLeft' : 'stopSales', outcomes: (b.outcomes ?? []).map((o, j) => ({ ...o, chancePercent: box ? (100 * box.entries[j].weight) / total : o.chance })) };
1299
+ });
1300
+ },
1301
+
1302
+ /**
1303
+ * Buys a shop item, in one transaction (`buy`: the price split and the item given at once). A listing that rolls
1304
+ * (an NFT at random, or one it rolls on) is opened and settled instead. Returns { signature, assets, asset } for a
1305
+ * purchase, or what `collect` returns for an open.
1306
+ */
1307
+ async buy(itemKey, options = {}) {
1308
+ only(options, BOX_OPTIONS, 'buy');
1309
+ const it = part('items', itemKey);
1310
+ if (!Number.isInteger(it.shopBox)) throw new Error(`“${it.name}” is not in the shop.`);
1311
+ return buyOrOpen(it.shopBox, it, options);
1312
+ },
1313
+
1314
+ /**
1315
+ * Opens a box (shows a collectible for its gate or odds when given): a box that needs no roll is bought at once
1316
+ * (`buy`); one that rolls waits for the roll and collects it, settling a pack in as many parts as it needs. `count`
1317
+ * opens several packs in one purchase: one wallet prompt and one oracle answer (`maxPrice` is a pack's).
1318
+ */
1319
+ async openBox(boxKey, options = {}) {
1320
+ only(options, BOX_OPTIONS, 'openBox');
1321
+ const b = part('boxes', boxKey);
1322
+ return buyOrOpen(b.id, b, options);
1323
+ },
1324
+
1325
+ /** Collects an order opened earlier (its roll arrived after the app stopped waiting). */
1326
+ collect,
1327
+
1328
+ /**
1329
+ * Finds the settle keys a page that closed left holding the player's SOL, and finishes their jobs: a key whose order
1330
+ * has rolled settles it (no prompt); one whose order is gone, failed or due its refund sends its SOL back; one whose
1331
+ * order still waits for its roll keeps it for later. Each key is made again from the wallet's signature (asked once
1332
+ * a session, `walletStandardSigner`'s `signMessage`), or read from the page's storage (`keyStore`) when the wallet
1333
+ * signs no messages. Returns [{ name, key, settled } | { name, key, swept } | { name, key, waiting }].
1334
+ */
1335
+ async recover() {
1336
+ if (!keys) throw new Error('Connect a wallet first.');
1337
+ const me = wallet.address;
1338
+ const tag = (t) => ({ memcmp: { offset: 0, bytes: encodeBase58(Uint8Array.of(loot.TAGS[t])) } });
1339
+ const at = (offset, bytes) => ({ memcmp: { offset, bytes } });
1340
+ const orders = new Map();
1341
+ for (const { pubkey, account } of await client.programAccounts(loot.LOOT_PROGRAM, [tag('Order'), at(1, project), at(33, me)])) orders.set(pubkey, loot.decode('Order', account.data));
1342
+ const holds = (await client.programAccounts(loot.LOOT_PROGRAM, [tag('Hold'), at(1, project), at(65, me)])).map((h) => h.pubkey);
1343
+ // Keys are named by an order of the wallet's, a hold (a loan's release), or an auction (the releases after its end).
1344
+ const names = [...new Set([...orders.keys(), ...holds, ...(spec.auctions ?? []).map((a) => auctionAt(a)), ...keys.stored()])];
1345
+ if (!names.length) return [];
1346
+ const made = (await Promise.all(names.map((name) => keys.again(name)))).filter(Boolean);
1347
+ const held = made.length ? await client.accounts(made.map((k) => k.address)) : [];
1348
+ const [slot, time] = [await slotNow(), await chainNow()];
1349
+ const out = [];
1350
+ for (const [i, key] of made.entries()) {
1351
+ const lamports = BigInt(held[i]?.lamports ?? 0);
1352
+ if (lamports === 0n) {
1353
+ keys.forget(key.name);
1354
+ continue;
1355
+ }
1356
+ const order = orders.get(key.name);
1357
+ const due = order ? loot.orderRefundable(order, slot, time) : false;
1358
+ if (order?.status === 'Rolled' && slot > BigInt(order.requested_slot) && !due) {
1359
+ const got = await collect(key.name, { burner: key }).catch((e) => ({ error: e.message }));
1360
+ out.push({ name: key.name, key: key.address, ...(got.error ? { error: got.error } : { settled: got.settled, won: got.won }) });
1361
+ } else if (order && ['Pending', 'Asked', 'Waiting'].includes(order.status) && !due) {
1362
+ out.push({ name: key.name, key: key.address, waiting: order.status });
1363
+ } else {
1364
+ await emptyKey(key);
1365
+ out.push({ name: key.name, key: key.address, swept: lamports });
1366
+ }
1367
+ }
1368
+ return out;
1369
+ },
1370
+
1371
+ /** Gives an order's payment back once its time has come (a roll that failed or never came). */
1372
+ async refund(orderAddress) {
1373
+ const order = await read('Order', orderAddress);
1374
+ // An NFT the project held for the roll goes back.
1375
+ const { before = [], hasAuthority: _, ...subject } = order.held ? await nftOf(order.subject, wallet.address, { to: order.player }) : {};
1376
+ return send([...before, loot.refundIx({ signer: wallet.address, orderAddress, order, subject, programs })]);
1377
+ },
1378
+
1379
+ /** Asks the oracle for a roll a mission, a recipe or a prize made (anyone may; the asker pays the oracle). On a box
1380
+ * with nothing in stock only the order's player may ask, a slot after it was made or last asked, and that ends the
1381
+ * roll there with nothing, no oracle (a lot its winner paid for fails, for its refund). */
1382
+ askRoll: (orderAddress) => askRoll(orderAddress),
1383
+
1384
+ /**
1385
+ * Pays a payment of the economy (an entry, an attempt, a tip). `amount`: what the player chose, in whole units, for
1386
+ * a payment with `any` (at least its `amount`). `chosenRecipient`: the wallet the player names for a share the
1387
+ * payment lets them choose (a referrer), never their own. `holders`: asset → holder, for a holder's share (read
1388
+ * now when not given).
1389
+ */
1390
+ async pay(paymentKey, { amount: chosen, holders, chosenRecipient = null, ...others } = NO_OPTIONS) {
1391
+ only(others, ['amount', 'holders', 'chosenRecipient'], 'pay');
1392
+ const p = part('payments', paymentKey);
1393
+ const g = await projectNow();
1394
+ const mint = mintOf(p.currency);
1395
+ const least = units(p.amount, decimalsOf(p.currency));
1396
+ if (chosen !== undefined && !p.any) throw new Error(`“${p.name}” has a set amount: ${p.amount}.`);
1397
+ const amount = chosen === undefined ? least : units(chosen, decimalsOf(p.currency));
1398
+ if (amount < least || amount <= 0n) throw new Error(`“${p.name}” takes at least ${p.amount}.`);
1399
+ const pools = new Map((spec.pools ?? []).map((x) => [x.key, x.index]));
1400
+ // `pay` takes its shares from the payer each time: a "chosen" share pays the wallet they name, and with none named
1401
+ // it is left out, so what it would pay goes to the treasury with the rest (as a purchase's does).
1402
+ if (chosenRecipient && chosenRecipient === wallet.address) throw new Error('A payer can’t name their own wallet for a share they choose.');
1403
+ const shares = (p.split ?? []).flatMap((s) => {
1404
+ const [kind, key] = s.to.split(':');
1405
+ const bps = Math.round(s.percent * 100);
1406
+ if (kind === 'chosen' && key) throw new Error(`“${p.name}” pays the holder of a “${key}” the payer names: a payment can’t check the collectible, so its split takes a chosen wallet only.`);
1407
+ if (kind === 'chosen') return chosenRecipient ? [{ to: { Wallet: [chosenRecipient] }, bps }] : [];
1408
+ const pot = (lotteryKey) => loot.pda.pot(loot.pda.lottery(project, part('lotteries', lotteryKey).id));
1409
+ const to = kind === 'burn' ? 'Burn' : kind === 'pool' ? { Pool: [pools.get(key)] } : kind === 'account' ? { Account: [key] } : kind === 'wallet' ? { Wallet: [key] } : kind === 'treasury' ? 'Treasury' : kind === 'lottery' ? { Account: [pot(key)] } : { Holder: [key] };
1410
+ return [{ to, bps }];
1411
+ });
1412
+ holders ??= shares.some((x) => x.to.Holder) ? await holdersOf(shares) : undefined;
1413
+ const ix = loot.payIx({ player: wallet.address, project, mint, treasury: g.treasury, amount, shares, holders, programs });
1414
+ return send(withSolWrap(wallet.address, mint, amount, [ix]));
1415
+ },
1416
+
1417
+ /**
1418
+ * Pays with one of the player's NFTs, any kind (`pay_nft`): burned (`to` null), or given to the wallet `to` (the
1419
+ * treasury, a partner), straight from the player. The receipt (`PaidNft`) is the record an app admits the player
1420
+ * by, with `reference` (32 bytes, or text the client hashes). Returns the signature.
1421
+ */
1422
+ async payNft(asset, { to = null, reference, ...others } = NO_OPTIONS) {
1423
+ only(others, ['to', 'reference'], 'payNft');
1424
+ // An NFT paid goes to a wallet: never the project's address or an account of the program (ToTheProject).
1425
+ if (to && (to === project || (await client.account(to))?.owner === loot.LOOT_PROGRAM)) throw new Error('An NFT paid can’t go to the project or an account of the program: name a wallet, or burn it.');
1426
+ const { before, ...nft } = await nftOf(asset);
1427
+ return send([...before, loot.payNftIx({ player: wallet.address, project, asset, to, ...nft, reference })]);
1428
+ },
1429
+
1430
+ /**
1431
+ * The lotteries: each one's ticket price now, its round and status, and its terms (`incarnation`, `revision`),
1432
+ * which `buyTickets` takes as the terms the player was shown. A lottery not written yet (a raffle still to open)
1433
+ * has a null price.
1434
+ */
1435
+ async lotteries() {
1436
+ const list = spec.lotteries ?? [];
1437
+ const out = [];
1438
+ for (const l of list) {
1439
+ const there = await client.account(loot.pda.lottery(project, l.id));
1440
+ // The terms on sale are the round's own copy once someone bought into it, the lottery's before.
1441
+ const { lottery, current } = there?.owner === loot.LOOT_PROGRAM ? await roundOf(l) : { lottery: null, current: null };
1442
+ const copy = current ?? lottery;
1443
+ const terms = copy?.terms;
1444
+ const currency = lottery ? ticketCurrencyOf(l, lottery) : l.ticketCurrency ?? currencyKey;
1445
+ const ended = lottery && Number(lottery.every) === 0 && lottery.round > 0;
1446
+ out.push({
1447
+ key: l.key, name: l.name, price: terms ? show(terms.price, decimalsOf(currency)) : null, priceUnits: terms?.price ?? null,
1448
+ currency: tokens.get(currency)?.symbol ?? currency, status: !lottery ? null : ended ? 'Ended' : current?.status ?? 'Open', round: lottery?.round ?? null, sold: lottery ? current?.sold ?? 0n : null,
1449
+ drawsAt: terms ? Number(terms.draws_at) : null, nftTicket: Boolean(lottery?.terms.nft_ticket), incarnation: lottery?.incarnation ?? null, revision: copy?.revision ?? null,
1450
+ });
1451
+ }
1452
+ return out;
1453
+ },
1454
+
1455
+ /**
1456
+ * Buys `count` tickets of a lottery, on the terms the player was shown: `listing`, the lottery as `lotteries()`
1457
+ * showed it (a lottery changed since, its price or anything else, is refused rather than paid), or `maxPrice`, the
1458
+ * most a ticket may cost, in base units. One of them is required: the price read now is never the bound.
1459
+ * `chosenRecipient`: the wallet (or collectible) the player names for a share the lottery lets them choose (a
1460
+ * referrer). A lottery whose ticket costs an NFT (`nft_ticket`) sells one ticket for one of the player's NFTs that
1461
+ * passes its rule: `nft`, its address (held by the project until the draw, then burned, given on or stashed; back to
1462
+ * the player if the round is cancelled). `listProof`: for a list gate.
1463
+ */
1464
+ async buyTickets(lotteryKey, count = 1, { shown, chosenRecipient = null, listProof, nft: paidWith, listing, maxPrice, ...others } = NO_OPTIONS) {
1465
+ only(others, ['shown', 'chosenRecipient', 'listProof', 'nft', 'listing', 'maxPrice'], 'buyTickets');
1466
+ const l = part('lotteries', lotteryKey);
1467
+ const name = l.name ?? lotteryKey;
1468
+ if (listing && listing.key !== undefined && listing.key !== l.key) throw new Error(`That listing is “${listing.key}”, not “${l.key}”.`);
1469
+ const given = maxPrice ?? listing?.priceUnits;
1470
+ if (given === undefined || given === null) throw new Error(`Say the terms the player was shown for “${name}”: pass the lottery lotteries() showed (listing), or maxPrice (a ticket's most, in base units). The price read now is never the bound.`);
1471
+ const { lottery: onChain, current: openRound } = await roundOf(l);
1472
+ // A round whose draw is due takes no more tickets: its draw goes first, in this same transaction (the buyer pays
1473
+ // the oracle's fee for it), and moves the lottery on, so this purchase is the next round's first. A lottery that
1474
+ // draws once has no next round.
1475
+ const due = openRound?.status === 'Open' && BigInt(now()) >= BigInt(openRound.terms.draws_at) ? openRound : null;
1476
+ if (due && Number(onChain.every) === 0) throw new Error(`The one draw of “${l.name}” is due: it takes no more tickets. Reveal it (claims() lists it).`);
1477
+ const draw = due ? (await drawStep(l, onChain, null, due, wallet.address)).filter((x) => x.programId !== COMPUTE_BUDGET_PROGRAM) : [];
1478
+ const current = due ? { ...onChain, round: onChain.round + 1 } : onChain;
1479
+ const round = due ? null : openRound;
1480
+ // The terms signed are those shown: the round's own copy once someone bought into it, the lottery's before. A
1481
+ // lottery written again or changed since is refused here, and by the program (TermsChanged) if it changes after
1482
+ // this read.
1483
+ const { terms, revision } = round ?? current;
1484
+ if (listing && (listing.incarnation == null || listing.revision == null)) throw new Error(`That listing of “${name}” names no terms (incarnation, revision): pass the lottery lotteries() showed.`);
1485
+ if (listing && (BigInt(listing.incarnation) !== BigInt(current.incarnation) || Number(listing.revision) !== Number(revision))) throw new Error(`“${name}” has changed since the player was shown it: show them the lottery as it is now first.`);
1486
+ const lottery = current;
1487
+ const currency = ticketCurrencyOf(l, lottery);
1488
+ if (BigInt(terms.price) > BigInt(given)) throw new Error(`A ticket of “${name}” now costs ${show(terms.price, decimalsOf(currency))}, more than the ${show(BigInt(given), decimalsOf(currency))} the player was shown: show them the new price first.`);
1489
+ if (Number(lottery.every) === 0 && lottery.round > 0) throw new Error(`“${l.name}” has drawn and ended.`);
1490
+ shown ??= await passing(lottery.gate, 'buy a ticket');
1491
+ listProof = listProofOf(lottery.gate, l.gate?.list, listProof);
1492
+ let nft;
1493
+ const before = [];
1494
+ if (terms.nft_ticket) {
1495
+ if (!paidWith) throw new Error(`A ticket of “${l.name}” costs an NFT: say which (nft).`);
1496
+ if (BigInt(count) !== 1n) throw new Error('An NFT pays for one ticket at a time.');
1497
+ const { before: proof, ...kind } = await nftOf(paidWith);
1498
+ nft = { asset: paidWith, ...kind };
1499
+ before.push(...proof);
1500
+ }
1501
+ const mint = loot.paymentMint(terms);
1502
+ const { shares } = terms;
1503
+ const choosesHolder = shares.some((s) => s.to.Chosen && s.to.Chosen.item != null);
1504
+ // A share the buyer chooses is paid now, from their own account: to the holder of the collectible they chose (read
1505
+ // now), the wallet they named, or with no choice the treasury.
1506
+ const holders = choosesHolder && chosenRecipient ? await holdersOf([{ to: { Holder: [chosenRecipient] } }]) : {};
1507
+ // A payment mint with a transfer hook: its accounts ride along.
1508
+ const extra = await hookTail([mint]);
1509
+ const ix = loot.buyTicketsIx({ player: wallet.address, project, id: l.id, lottery, round, mint, count: BigInt(count), shown, listProof, chosenRecipient, chosen: choosesHolder ? chosenRecipient : null, nft, holders, treasury: (await projectNow()).treasury, programs, extra });
1510
+ // An overdue round nobody bought into moves to its next date by itself when a ticket is bought.
1511
+ // A ticket paid with an NFT: its way out must land too, the NFT handed on once the round is drawn or back to its
1512
+ // buyer when it is cancelled, each sized before the NFT goes in.
1513
+ if (nft) {
1514
+ const g = await projectNow();
1515
+ const entry = { buyer: wallet.address, nft: paidWith, start: 0n, end: 1n, amount: 0n, paid: 0n };
1516
+ for (const status of ['Drawn', 'Cancelled']) {
1517
+ const r = { lottery: loot.pda.lottery(project, l.id), status, terms, accounts: [], owed: 2, opener: wallet.address, entries: [entry] };
1518
+ const exit = [computeUnitLimit(1_400_000), ...before.filter((x) => x.programId !== COMPUTE_BUDGET_PROGRAM), loot.pullIx({ payer: wallet.address, project, roundAddress: loot.currentRound(r.lottery, lottery), round: r, entry: 0, part: 'Own', projectState: g, ticket: kindOnly(nft), programs })];
1519
+ if (!sendPlan({ payer: wallet.address, instructions: exit, alone: isProveLeaf }))
1520
+ throw new Error(`Not bought: this NFT couldn't come out of “${l.name}” again (${status === 'Drawn' ? 'handed on once the round is drawn' : 'back to you if the round is cancelled'}): that transaction would lock more accounts than one can (${transactionLocks(wallet.address, exit, { asV0: true })} with its lookup table; at most 64), so it would stay with the lottery.`);
1521
+ }
1522
+ }
1523
+ const buy = [...before, ...withSolWrap(wallet.address, mint, BigInt(terms.price) * BigInt(count), [ix])];
1524
+ return send(draw.length ? [computeUnitLimit(800_000), ...draw, ...buy.filter((x) => x.programId !== COMPUTE_BUDGET_PROGRAM)] : buy);
1525
+ },
1526
+
1527
+ /**
1528
+ * Writes an NFT's traits from the project's traits tree (`traits` in the spec) into its Sheet, where rules read them:
1529
+ * anyone may, paying the Sheet's rent. `attributes` ([{ key, value }]) and `proof` come from the project's tree
1530
+ * (`loot.traitsTree`); `key` names the tree (a collection's address), the spec's first when left out. Traits the
1531
+ * Sheet already has are left as they are.
1532
+ */
1533
+ async proveTraits(nft, attributes, proof, key = spec.traits?.[0]?.collection) {
1534
+ return send([loot.proveTraitsIx({ signer: wallet.address, project, key, nft, attributes, proof })]);
1535
+ },
1536
+
1537
+ /**
1538
+ * Stakes `amount` (whole units) of a farm's token at lock tier `tier`. A farm with an end takes no stake at its end
1539
+ * or after it, nor one whose lock would run past it: refused here, before the wallet signs.
1540
+ */
1541
+ async stake(farmKey, amount, tier = 0, { shown, extra, transfers, ...others } = NO_OPTIONS) {
1542
+ only(others, ['shown', 'extra', 'transfers'], 'stake');
1543
+ const f = part('farms', farmKey);
1544
+ const farm = await read('Farm', loot.pda.farm(project, f.id));
1545
+ await openFor(f, farm, tier);
1546
+ shown ??= await passing(farm.gates, 'stake here');
1547
+ const mint = farm.farmed.Tokens.mint;
1548
+ const value = units(amount, decimalsOf(f.stakes));
1549
+ // While its lock runs, a seat moves only to a tier with no early exit, or one whose early exit costs at least as
1550
+ // much as its own (`ExitWhileLocked`): never to a way out it didn't have, or a cheaper one.
1551
+ const mine = await readMaybe('Seat', loot.pda.seat(loot.pda.farm(project, f.id), wallet.address));
1552
+ if (mine && BigInt(mine.amount) > 0n && Number(mine.tier) !== Number(tier)) {
1553
+ const [from, to] = [Number(farm.tiers[mine.tier].exit_bps), Number(farm.tiers[tier].exit_bps)];
1554
+ const locked = (await chainNow()) < BigInt(mine.since) + BigInt(farm.tiers[mine.tier].lock);
1555
+ if (locked && !(to === 0 || (from > 0 && to >= from))) throw new Error('While its lock runs, this stake can only move to a tier with no early exit, or one whose early exit costs at least as much as its own.');
1556
+ }
1557
+ const token = await farmTokens([mint], [], { extra, transfers });
1558
+ const ix = loot.stakeTokensIx({ player: wallet.address, project, id: f.id, farm, amount: value, tier, shown, ...token });
1559
+ // A seat with a booster on (its boost is read again), or one whose growth the stake changes, is refreshed first, in
1560
+ // the same transaction.
1561
+ const before = loot.stakeRefreshFirst(farm, mine, { tier, now: await chainNow() }) ? await refreshOf(f, farm, mine) : [];
1562
+ return send(await walking(f, farm, [...before, ...withSolWrap(wallet.address, mint, value, [ix])]));
1563
+ },
1564
+
1565
+ /**
1566
+ * Takes `amount` out once its lock has run (0: only rewards); `forfeit` gives up rewards that cannot move. Before the
1567
+ * lock, a tier with an early exit lets it leave for its fee, keeping what the seat was credited.
1568
+ */
1569
+ async unstake(farmKey, amount = 0, { forfeit = false, extra, transfers, ...others } = NO_OPTIONS) {
1570
+ only(others, ['forfeit', 'extra', 'transfers'], 'unstake');
1571
+ const f = part('farms', farmKey);
1572
+ const farm = await read('Farm', loot.pda.farm(project, f.id));
1573
+ const value = units(amount, decimalsOf(f.stakes));
1574
+ const mints = [...(value > 0n ? [farm.farmed.Tokens.mint] : []), ...(!forfeit ? [farm.rewards_mint] : [])];
1575
+ const token = await farmTokens(mints, mints, { extra, transfers });
1576
+ // An exit that would lose what the seat grew since its last refresh writes it first, in the same transaction.
1577
+ const by = wallet.address;
1578
+ const seat = await readMaybe('Seat', loot.pda.seat(loot.pda.farm(project, f.id), by));
1579
+ const first = [];
1580
+ if (seat && loot.refreshFirst(farm, seat, value, forfeit)) {
1581
+ const { before, ...nft } = seat.asset !== NOBODY ? await nftOf(seat.asset) : { before: [] };
1582
+ first.push(...before, loot.refreshSeatIx({ signer: wallet.address, project, id: f.id, farm, by, seat, ...nft, extra: token.extra }));
1583
+ }
1584
+ return send(await walking(f, farm, [...token.before, ...first, loot.unstakeTokensIx({ player: wallet.address, project, id: f.id, farm, amount: value, forfeit, ...token })]));
1585
+ },
1586
+
1587
+ /**
1588
+ * Stakes one of the player's NFTs in a farm (its own seat, or as the booster of their token seat), of any kind: a
1589
+ * Core asset, a Token Metadata NFT's mint (legacy or pNFT), or a compressed NFT's asset id. In a farm that watches,
1590
+ * an NFT whose last staker sold it takes their seat over. A farm with an end takes none at its end or after it.
1591
+ */
1592
+ async stakeNft(farmKey, asset, tier = 0, { extra, transfers, ...others } = NO_OPTIONS) {
1593
+ only(others, ['extra', 'transfers'], 'stakeNft');
1594
+ const f = part('farms', farmKey);
1595
+ const farm = await read('Farm', loot.pda.farm(project, f.id));
1596
+ await openFor(f, farm, tier);
1597
+ const token = await farmTokens([farm.rewards_mint], [], { extra, transfers });
1598
+ const { before, ...nft } = await nftOf(asset);
1599
+ const seat = farm.custody === 'Watch' && farm.farmed === 'Collectibles' ? await readMaybe('Seat', loot.pda.seat(loot.pda.farm(project, f.id), asset)) : null;
1600
+ const previous = seat && seat.player !== wallet.address ? seat.player : null;
1601
+ // A booster boosts a stake: a farm of tokens takes one only on a seat with tokens staked (NotFarmed). In a farm
1602
+ // whose growth writes the profile, the seat is refreshed first, in the same transaction (`boosterRefreshFirst`).
1603
+ const refresh = [];
1604
+ if (farm.farmed.Tokens) {
1605
+ const mine = await readMaybe('Seat', loot.pda.seat(loot.pda.farm(project, f.id), wallet.address));
1606
+ if (!mine || BigInt(mine.amount) === 0n) throw new Error(`Stake some ${f.stakes} in “${f.name ?? f.key}” first: a booster boosts a stake.`);
1607
+ if (loot.boosterRefreshFirst(farm)) refresh.push(...(await refreshOf(f, farm, mine)));
1608
+ }
1609
+ // Its way out must land too: its unstake from this tier, with the tier's
1610
+ // prize and as a forfeit, is sized before it goes in, and an NFT that could never come back out isn't staked.
1611
+ if (!farm.farmed.Tokens) await exitFits(f, farm, asset, nft, before, tier, token);
1612
+ return send(await walking(f, farm, [...refresh, ...before, loot.stakeNftIx({ player: wallet.address, project, id: f.id, farm, asset, ...nft, previous, tier, ...token })]));
1613
+ },
1614
+
1615
+ /**
1616
+ * Releases the collectible after its lock: it grows on its way out (a farm with `growth`), and from its own seat
1617
+ * takes its tier's prize (a mission: `prize.rs`). `forfeit` gives up rewards, and the prize, that cannot move.
1618
+ * Returns the signature, or { signature, roll } when it makes a roll (a mission's result on the NFT, or a chest),
1619
+ * which `collect` then finishes.
1620
+ */
1621
+ async unstakeNft(farmKey, asset, { forfeit = false, extra, transfers, ...others } = NO_OPTIONS) {
1622
+ only(others, ['forfeit', 'extra', 'transfers'], 'unstakeNft');
1623
+ const f = part('farms', farmKey);
1624
+ const farm = await read('Farm', loot.pda.farm(project, f.id));
1625
+ const token = await farmTokens(forfeit ? [] : [farm.rewards_mint], forfeit || farm.farmed.Tokens ? [] : [farm.rewards_mint], { extra, transfers });
1626
+ // Held in escrow, it comes back into the player's account (frozen in place, it never left it).
1627
+ const { before, ...nft } = await nftOf(asset, wallet.address, { to: farm.custody === 'Escrow' ? wallet.address : undefined });
1628
+ // Leaving a mission that rolls: its roll is made in an order of the player's, which `collect` then finishes; its
1629
+ // prize's roll (a chest) at the next nonce.
1630
+ const seat = await read('Seat', loot.pda.seat(loot.pda.farm(project, f.id), asset));
1631
+ const roll = farm.tiers[seat.tier]?.roll ?? null;
1632
+ const prize = farm.tiers[seat.tier]?.prize ?? null;
1633
+ // A forfeit takes no prize: none of its accounts (`unstakeNftIx`).
1634
+ const given = await prizeInputs([forfeit ? null : prize]);
1635
+ // A mission's roll on the NFT: on a box that holds it, at the NFT's own nonce (one at a time on it), else at
1636
+ // `prizeRollNonce` of the one named (`namedRollNonce`).
1637
+ const nonce = roll != null ? loot.nonceOn(asset) : freshNonce();
1638
+ const boxes = roll != null ? { [roll]: await read('LootBox', loot.pda.box(project, roll)) } : {};
1639
+ // A roll still at the NFT's order address blocks the leave, unless it was asked and came back with nothing to give
1640
+ // (Waiting, asked once): that one stands in for this. Any other is asked (its player or the project's keys), answered and settled first.
1641
+ if (roll != null) {
1642
+ const at = loot.pda.order(project, wallet.address, loot.namedRollNonce(boxes[roll], asset, nonce));
1643
+ const there = await readMaybe('Order', at);
1644
+ if (there && !(there.status === 'Waiting' && BigInt(there.asked_first) !== 0n))
1645
+ throw new Error(`This NFT's last mission roll (${at}) hasn't finished: it must be asked, answered and settled before the NFT can leave. claims() lists the step; collect it (app.collect), then leave.`);
1646
+ }
1647
+ const ix = loot.unstakeNftIx({ player: wallet.address, project, id: f.id, farm, asset, ...nft, forfeit, roll, nonce, tier: seat.tier, projectCollection: given.collection, poolMints: given.poolMints, items: given.items, nft: given.nft, boxes, ...token });
1648
+ const { signature, rolls } = await sendRolling(await walking(f, farm, [...before, ...given.before, ...token.before, ix]));
1649
+ const rolled = rolls.length ? { rolled: rolls[0] } : {};
1650
+ if (roll != null) return { signature, roll: loot.pda.order(project, wallet.address, loot.namedRollNonce(boxes[roll], asset, nonce)), ...rolled };
1651
+ return prize?.Roll && !forfeit ? { signature, roll: loot.pda.order(project, wallet.address, loot.prizeRollNonce(nonce + 1n)), ...rolled } : signature;
1652
+ },
1653
+
1654
+ /**
1655
+ * What farm `farmKey` has to walk: `{ ends, needsWalk, parts }`, the times its seats stop earning it has passed and not
1656
+ * yet credited up to, whether a move can no longer do them in its own transaction, and how many transactions
1657
+ * `walkFarm` would take. Every move walks them first; nothing is lost however long they wait.
1658
+ */
1659
+ async farmWalk(farmKey) {
1660
+ const f = part('farms', farmKey);
1661
+ const { left } = await endsOf(f, await read('Farm', loot.pda.farm(project, f.id)));
1662
+ return { ends: left, needsWalk: left > loot.MOVE_WALK_ROOM, parts: Math.ceil(left / loot.WALK_PER_TX) };
1663
+ },
1664
+
1665
+ /**
1666
+ * Walks a farm up to now in parts (`walk_farm`), for one that has passed more ends than a move can walk: one transaction
1667
+ * for every `loot.WALK_PER_TX` ends, oldest first. Anyone may send it, and it costs only the network fee. Returns their
1668
+ * signatures, none when there is nothing to walk.
1669
+ */
1670
+ async walkFarm(farmKey, { perTx = loot.WALK_PER_TX, ...others } = NO_OPTIONS) {
1671
+ only(others, ['perTx'], 'walkFarm');
1672
+ const f = part('farms', farmKey);
1673
+ const farm = await read('Farm', loot.pda.farm(project, f.id));
1674
+ const { ends } = await endsOf(f, farm);
1675
+ const out = [];
1676
+ for (const ix of loot.walkFarmParts({ project, id: f.id, farm, ends, now: await chainNow(), perTx })) out.push(await send([computeUnitLimit(loot.WALK_COMPUTE_UNITS), ix]));
1677
+ return out;
1678
+ },
1679
+
1680
+ /**
1681
+ * Writes what a staked collectible has grown (a farm with `growth`: its points and level, on it or in its Sheet)
1682
+ * and reads its boosts again. Anyone may send it; the player pays for what the write adds. `by`: the collectible,
1683
+ * or the player for their token seat's booster.
1684
+ */
1685
+ async refresh(farmKey, by, { extra, ...others } = NO_OPTIONS) {
1686
+ // `transfers` (a token's transfer plan) is taken and needs nothing: a refresh moves no tokens.
1687
+ only(others, ['extra', 'transfers'], 'refresh');
1688
+ const f = part('farms', farmKey);
1689
+ const farm = await read('Farm', loot.pda.farm(project, f.id));
1690
+ const seat = await read('Seat', loot.pda.seat(loot.pda.farm(project, f.id), by));
1691
+ const held = seat.asset !== NOBODY;
1692
+ const { before, ...nft } = held ? await nftOf(seat.asset) : { before: [], hasAuthority: true };
1693
+ // A refresh moves no tokens (a farm's rewards are numbers until a seat is paid): only a collectible's own tail.
1694
+ return send(await walking(f, farm, [...before, loot.refreshSeatIx({ signer: wallet.address, project, id: f.id, farm, by, seat, ...nft, extra: extra ?? [] })]));
1695
+ },
1696
+
1697
+ /**
1698
+ * Collects what a seat owes (the player's own, or one of their collectibles' seats). In a farm that watches, it
1699
+ * shows the seat's NFT is still theirs: the Core asset, or their token account of a Token Metadata NFT.
1700
+ */
1701
+ async harvest(farmKey, by = wallet.address, { compound = false, extra, transfers, ...others } = NO_OPTIONS) {
1702
+ only(others, ['compound', 'extra', 'transfers'], 'harvest');
1703
+ const f = part('farms', farmKey);
1704
+ const farm = await read('Farm', loot.pda.farm(project, f.id));
1705
+ const token = await farmTokens([farm.rewards_mint], [farm.rewards_mint], { extra, transfers });
1706
+ // In a farm that watches: what shows the seat's NFT is still the player's (a compressed one proven first).
1707
+ let [held, tree, before] = [null, null, []];
1708
+ if (farm.custody === 'Watch') {
1709
+ const { asset } = await read('Seat', loot.pda.seat(loot.pda.farm(project, f.id), by));
1710
+ if (asset !== NOBODY) {
1711
+ const nft = await nftOf(asset);
1712
+ held = nft.compressed ? loot.pda.leaf(asset) : nft.tokenMetadata ? loot.ata(wallet.address, asset) : asset;
1713
+ tree = nft.compressed?.tree ?? null;
1714
+ before = nft.compressed ? nft.before : [];
1715
+ }
1716
+ }
1717
+ // A compound stakes more: a seat with a booster on, or whose growth reads the amount, is refreshed first.
1718
+ const seat = compound ? await readMaybe('Seat', loot.pda.seat(loot.pda.farm(project, f.id), by)) : null;
1719
+ const first = loot.stakeRefreshFirst(farm, seat, { compound: true }) ? await refreshOf(f, farm, seat) : [];
1720
+ return send(await walking(f, farm, [...before, ...token.before, ...first, loot.harvestIx({ player: wallet.address, project, id: f.id, farm, by, held, tree, compound, ...token })]));
1721
+ },
1722
+
1723
+ /**
1724
+ * Adds what a token seat is owed to its stake, without restarting its lock (a farm of tokens whose rewards are the
1725
+ * staked token).
1726
+ */
1727
+ async compound(farmKey, opts = {}) {
1728
+ only(opts, ['extra', 'transfers'], 'compound');
1729
+ return this.harvest(farmKey, wallet.address, { ...opts, compound: true });
1730
+ },
1731
+
1732
+ /**
1733
+ * The player's seats in a farm (the wallet's, or `player`'s): each one's NFT (`asset`, null for a token seat with
1734
+ * no booster), what it holds staked (`amount`, in whole units of the staked token, a number for display that can
1735
+ * round past 2^53 base units; `amountUnits`, in base units exactly, a bigint; 1 for an NFT), its tier, when it
1736
+ * came in (`since`) and when its lock is over (`freeAt`, unix seconds; null when the NFT says it, `lockKey`), its
1737
+ * boost in percent, until when it earns (`earnsUntil`, unix seconds, null for no end: a tier's term, an upkeep), and
1738
+ * what it is owed now: `waiting` (whole units of the rewards, `rewards` their token) and `owedUnits` (base units). What it is owed is the program's own sum, from a refresh of the seat dry-run on the
1739
+ * network (nothing is signed or sent): what the farm released or its rate paid since its last move is in it, by the
1740
+ * farm's rounding. When the dry run fails, it is what the seat was last credited, and `counted` is false.
1741
+ * One search of the chain, and one dry run per seat: read it when the page opens and after a stake or a harvest.
1742
+ */
1743
+ async staked(farmKey, player = wallet?.address) {
1744
+ if (!player) throw new Error('Connect a wallet first.');
1745
+ const f = part('farms', farmKey);
1746
+ const farmAddress = loot.pda.farm(project, f.id);
1747
+ const farm = await read('Farm', farmAddress);
1748
+ const rewards = [...tokens.values()].find((t) => mintOf(t.key) === farm.rewards_mint);
1749
+ const nfts = f.stakes === 'collectibles';
1750
+ // A seat names its player (offset 1), not its farm: a seat of this farm is the one at its own address here.
1751
+ const found = (await client.programAccounts(loot.LOOT_PROGRAM, [
1752
+ { memcmp: { offset: 0, bytes: encodeBase58(Uint8Array.of(loot.TAGS.Seat)) } },
1753
+ { memcmp: { offset: 1, bytes: player } },
1754
+ ])).filter(({ pubkey, account }) => {
1755
+ const seat = loot.decode('Seat', account.data);
1756
+ return pubkey === loot.pda.seat(farmAddress, nfts ? seat.asset : player);
1757
+ });
1758
+ const { blockhash } = await client.latestBlockhash();
1759
+ // Who the dry run names as the refresh's signer and fee payer: the network wants one that holds SOL, whoever it is
1760
+ // (nothing is signed): the player, else the wallet looking, else the project's authority.
1761
+ let payer;
1762
+ const funded = async () => {
1763
+ for (const a of [player, wallet?.address, live.authority].filter(Boolean)) if ((await client.balance(a).catch(() => 0n)) > 0n) return a;
1764
+ return player;
1765
+ };
1766
+ const out = [];
1767
+ for (const { pubkey, account } of found) {
1768
+ let seat = loot.decode('Seat', account.data);
1769
+ const held = seat.asset !== NOBODY;
1770
+ let counted = false;
1771
+ try {
1772
+ payer ??= await funded();
1773
+ const { before, ...nft } = held ? await nftOf(seat.asset, payer) : { before: [] };
1774
+ const by = nfts ? seat.asset : player;
1775
+ const ix = loot.refreshSeatIx({ signer: payer, project, id: f.id, farm, by, seat, ...nft });
1776
+ const message = compileMessage({ payer, blockhash, instructions: [...before, ix] });
1777
+ const after = (await client.simulate(wireTransaction(message), { accounts: [pubkey], replaceBlockhash: true })).accounts[0];
1778
+ if (after?.data) [seat, counted] = [loot.decode('Seat', after.data), true];
1779
+ } catch {
1780
+ // The seat as it was last credited.
1781
+ }
1782
+ const lock = BigInt(farm.tiers[seat.tier]?.lock ?? 0);
1783
+ out.push({
1784
+ asset: held ? seat.asset : null,
1785
+ amount: nfts ? 1 : Number(show(seat.amount, decimalsOf(f.stakes))),
1786
+ amountUnits: nfts ? 1n : BigInt(seat.amount),
1787
+ tier: seat.tier,
1788
+ since: Number(seat.since),
1789
+ freeAt: farm.lock_key ? null : Number(BigInt(seat.since) + lock),
1790
+ ...(farm.lock_key ? { lockKey: farm.lock_key } : {}),
1791
+ boostPercent: Number(seat.boost_bps) / 100,
1792
+ rewards: rewards?.key ?? null,
1793
+ waiting: show(seat.owed, rewards ? decimalsOf(rewards.key) : 0),
1794
+ owedUnits: BigInt(seat.owed),
1795
+ // Until when it earns and grows (unix seconds): its tier's term or its fuel, the sooner; null for no end.
1796
+ earnsUntil: BigInt(seat.earns_until) >= loot.NO_END ? null : Number(seat.earns_until),
1797
+ counted,
1798
+ });
1799
+ }
1800
+ return out;
1801
+ },
1802
+
1803
+ /** The recipes: whether each is on, and its version now (what `craft` takes as the terms the player was shown). */
1804
+ async recipes() {
1805
+ const list = spec.recipes ?? [];
1806
+ const accounts = await client.accounts(list.map((r) => loot.pda.recipe(project, r.id)));
1807
+ return list.map((r, i) => {
1808
+ const recipe = accounts[i] && loot.decode('Recipe', accounts[i].data);
1809
+ return { key: r.key, name: r.name, active: Boolean(recipe?.active), version: recipe ? recipe.version : null };
1810
+ });
1811
+ },
1812
+
1813
+ /**
1814
+ * Crafts a recipe, handing in `assets` (the collectibles it burns or uses, in the recipe's order). `version`: the
1815
+ * recipe's version the page showed the player (`recipes()`, or the listing itself as `listing`), so a recipe the
1816
+ * owner changed since is refused rather than crafted on terms the player never saw. It is required: the version
1817
+ * read now is never signed in its place. `extra`: the tail a Core collection's plugins need when the craft moves or
1818
+ * rolls on a Core NFT (sent after the Memo program's account, as on every other Core move).
1819
+ */
1820
+ async craft(recipeKey, assets = [], times = 1, { version, listing, extra = [], ...others } = NO_OPTIONS) {
1821
+ only(others, ['version', 'listing', 'extra'], 'craft');
1822
+ const r = part('recipes', recipeKey);
1823
+ version ??= listing?.version ?? undefined;
1824
+ if (version === undefined || version === null) throw new Error(`Say which version of “${r.name ?? recipeKey}” the player was shown: pass the listing recipes() showed (listing), or its version. The version read now is never signed in its place.`);
1825
+ const current = await read('Recipe', loot.pda.recipe(project, r.id));
1826
+ const recipe = { ...current, version };
1827
+ const pays = recipe.inputs.filter((i) => i.Pay).map((i) => i.Pay);
1828
+ const holders = await holdersOf(pays.map((p) => ({ to: p.to })));
1829
+ const treasury = (await projectNow()).treasury;
1830
+ // An NFT a `show` line takes, by its address: as the project's, a Core asset of another collection, or a Token
1831
+ // Metadata mint, which the craft reads differently.
1832
+ // Where the collectibles of input `i` start among those handed in.
1833
+ const at = (i) => recipe.inputs.slice(0, i).reduce((n, x) => n + (x.Burn?.count ?? (x.Use || x.Show || x.Staked || x.Trade ? 1 : 0)), 0);
1834
+ const shownAt = new Set(recipe.inputs.flatMap((input, i) => (input.Show ? [at(i)] : [])));
1835
+ // An NFT a `trade` line takes in, any kind: read for its kind (a compressed one proven first).
1836
+ const tradedAt = new Set(recipe.inputs.flatMap((input, i) => (input.Trade ? [at(i)] : [])));
1837
+ const proving = [];
1838
+ // A collectible used or renewed that is staked in escrow, in the project's hands: its farm, found by its seat.
1839
+ const keptAt = new Set(recipe.inputs.flatMap((input, i) => (input.Use ? [at(i)] : [])));
1840
+ // One a `staked` line takes: on its seat in the farm the line names, escrowed or frozen there.
1841
+ const stakedAt = new Map(recipe.inputs.flatMap((input, i) => (input.Staked ? [[at(i), input.Staked.farm]] : [])));
1842
+ // The places a raise lowers a number on.
1843
+ const lowered = new Set((recipe.raises ?? []).filter((raise) => BigInt(raise.shift.by) < 0n).map((raise) => Number(raise.place)));
1844
+ const handed = await Promise.all(assets.map(async (nft, i) => {
1845
+ if (stakedAt.has(i) && typeof nft === 'string') {
1846
+ const farm = stakedAt.get(i);
1847
+ if (!(await readMaybe('Seat', loot.pda.seat(loot.pda.farm(project, farm), nft)))) {
1848
+ const name = (spec.farms ?? []).find((f) => f.id === farm)?.name ?? `farm ${farm}`;
1849
+ throw new Error(`${nft} is not staked in “${name}”, where this recipe takes it.`);
1850
+ }
1851
+ return { asset: nft, farm };
1852
+ }
1853
+ if (keptAt.has(i) && typeof nft === 'string') return (await escrowedIn(nft)) ?? (await lockedIn(nft)) ?? nft;
1854
+ if (tradedAt.has(i) && typeof nft === 'string') {
1855
+ const { before, ...kind } = await nftOf(nft);
1856
+ proving.push(...before);
1857
+ return { asset: nft, ...kind };
1858
+ }
1859
+ if (!shownAt.has(i) || typeof nft !== 'string') return nft;
1860
+ // Shown on a seat of a farm that freezes, with a raise lowering a number on it: its seat and farm go with it.
1861
+ const seated = lowered.has(i) ? await lockedIn(nft) : null;
1862
+ const on = seated ? { farm: seated.farm } : {};
1863
+ const account = await client.account(nft);
1864
+ if (account?.owner !== MPL_CORE) return { mint: nft, owner: wallet.address, ...on };
1865
+ const theirs = await collectionOf(nft);
1866
+ return ours(theirs) ? (seated ? { asset: nft, ...on } : nft) : { asset: nft, collection: theirs, ...on };
1867
+ }));
1868
+ // A recipe that rolls makes its roll in an order of the player's, which `collect` then finishes.
1869
+ const rolls = recipe.outputs.some((o) => o.RollOn || o.Prize?.[0]?.Roll);
1870
+ // A roll on a collectible kept is named by that collectible's nonce (`nonceOn`): on a box that holds it, its
1871
+ // order is there, one at a time on it (`namedRollNonce`).
1872
+ const rollOn = recipe.outputs.find((o) => o.RollOn)?.RollOn;
1873
+ const nonce = rollOn ? loot.nonceOn(typeof assets[rollOn.place] === 'string' ? assets[rollOn.place] : assets[rollOn.place].asset) : freshNonce();
1874
+ // A roll on a kept collectible (`RollOn`) passes its box's prize mints as a roll given does.
1875
+ const given = await prizeInputs(recipe.outputs.map((o) => o.Prize?.[0] ?? (o.RollOn ? { Roll: [o.RollOn.box_id] } : null)));
1876
+ // Each item a line takes passes its collection.
1877
+ const items = { ...given.items };
1878
+ for (const input of recipe.inputs) {
1879
+ const id = (input.Burn ?? input.Use ?? input.Renew ?? input.Staked)?.item;
1880
+ if (id !== undefined && !(id in items)) items[id] = await read('Item', loot.pda.item(project, id));
1881
+ }
1882
+ // The collectibles it makes, each at its item's next serial.
1883
+ const counts = {};
1884
+ const made = recipe.outputs.flatMap((o) => {
1885
+ const id = o.Prize?.[0]?.Item?.[0];
1886
+ if (id === undefined || items[id]?.kind !== 'Collectible') return [];
1887
+ counts[id] = (counts[id] ?? 0) + 1;
1888
+ return [loot.pda.itemAsset(loot.pda.item(project, id), Number(items[id].issued) + counts[id])];
1889
+ });
1890
+ const before = [...proving, ...given.before].filter((x) => x.programId !== COMPUTE_BUDGET_PROGRAM);
1891
+ let instructions = [computeUnitLimit(rolls ? 1_400_000 : 1_000_000), ...before, loot.craftIx({ player: wallet.address, project, collection: collections[0], id: r.id, recipe, assets: handed, times, nonce, holders, programs, treasury, poolMints: given.poolMints, items, nfts: given.nfts, boxes: given.boxes, extra })];
1892
+ // Fuel bought for a staked collectible counts once its seat is refreshed: in the same transaction.
1893
+ for (const raise of recipe.raises ?? []) {
1894
+ const asset = assets[raise.place];
1895
+ if (typeof asset !== 'string') continue;
1896
+ for (const f of (spec.farms ?? []).filter((x) => x.upkeepAttribute === raise.shift.key)) {
1897
+ const seat = await readMaybe('Seat', loot.pda.seat(loot.pda.farm(project, f.id), asset));
1898
+ if (!seat) continue;
1899
+ const farm = await read('Farm', loot.pda.farm(project, f.id));
1900
+ const { before, ...nft } = await nftOf(asset);
1901
+ instructions.push(...before, loot.refreshSeatIx({ signer: wallet.address, project, id: f.id, farm, by: asset, seat, ...nft }));
1902
+ }
1903
+ }
1904
+ for (const p of pays) instructions = withSolWrap(wallet.address, p.mint, p.amount * BigInt(times), instructions);
1905
+ // A roll on a collectible kept is at its nonce; a prize's roll at `prizeRollNonce` of the craft's. Asked for in the
1906
+ // craft's own transaction and settled by a key after: one prompt.
1907
+ const on = rollOn && (typeof assets[rollOn.place] === 'string' ? assets[rollOn.place] : assets[rollOn.place].asset);
1908
+ const roll = rolls ? loot.pda.order(project, wallet.address, rollOn ? loot.namedRollNonce(given.boxes[rollOn.box_id], on, nonce) : loot.prizeRollNonce(nonce)) : null;
1909
+ const { signature, rolls: rolled } = await sendRolling(instructions);
1910
+ return { signature, made, ...(roll ? { roll } : {}), ...(rolled.length ? { rolled: rolled[0] } : {}) };
1911
+ },
1912
+
1913
+ /**
1914
+ * Hands an NFT of any kind to the project until a time (a hold): `seconds` from now, or `{ until }` (Unix seconds).
1915
+ * The hold's address names its end. Returns { signature, freeAt }.
1916
+ */
1917
+ async hold(asset, seconds) {
1918
+ const freeAt = BigInt(typeof seconds === 'object' ? seconds.until : now() + Number(seconds));
1919
+ const { before, hasAuthority: _, ...nft } = await nftOf(asset);
1920
+ const signature = await send([...before, loot.holdIx({ player: wallet.address, project, asset, ...nft, collection: nft.collection ?? (await collectionOf(asset)) ?? MPL_CORE, freeAt })]);
1921
+ return { signature, freeAt };
1922
+ },
1923
+
1924
+ /**
1925
+ * A lender's offer to borrow against: `maker`'s offer number `id` in this project. Returns { address, offer, mint,
1926
+ * amount, repay, lasts, risks }: `risks`, in words, each power over the loan's token that could make repaying fail
1927
+ * (its freeze authority, a Token-2022 pause, a transfer hook, a permanent delegate) and who holds it. The program
1928
+ * takes such tokens, and a lender who holds one can stop every repayment and take the NFT once it is due: show the
1929
+ * risks before the player borrows.
1930
+ */
1931
+ async loanOffer(maker, id) {
1932
+ const address = loot.pda.offer(project, maker, id);
1933
+ const offer = await read('Offer', address);
1934
+ if (!offer.deal.Lend) throw new Error(`The offer ${address} buys NFTs; it lends nothing.`);
1935
+ const account = await client.account(offer.mint);
1936
+ if (!account) throw new Error(`The loan's token ${offer.mint} is not on this cluster.`);
1937
+ const lender = (who) => (who === maker ? ' That is the lender: they could keep you from repaying and take your NFT when the loan is due.' : '');
1938
+ const risks = mintPowers(account).map((p) => {
1939
+ if (p.power === 'freeze') return { ...p, says: `${p.holder} can freeze accounts of the loan's token, which makes repaying fail.${lender(p.holder)}` };
1940
+ if (p.power === 'pause') return { ...p, says: `${p.holder ?? 'Its pause authority'} can pause every transfer of the loan's token, which makes repaying fail${p.paused ? ' (it is paused now)' : ''}.${lender(p.holder)}` };
1941
+ if (p.power === 'hook') return { ...p, says: `Every transfer of the loan's token runs the program ${p.holder}${p.authority ? `, which ${p.authority} can change` : ''}; it can refuse a repayment.${lender(p.authority)}` };
1942
+ return { ...p, says: `${p.holder} can move anyone's tokens of the loan's token, the ones you would repay with too.${lender(p.holder)}` };
1943
+ });
1944
+ const { amount, repay, lasts } = offer.deal.Lend;
1945
+ return { address, offer, mint: offer.mint, program: account.owner, amount, repay, lasts, risks };
1946
+ },
1947
+
1948
+ /**
1949
+ * Borrows against the player's NFT `asset` from `maker`'s offer `id`: the NFT is held until the loan is repaid, or
1950
+ * goes to the lender once it is due (`due`, Unix seconds; by default the longest the offer allows, less a minute).
1951
+ * A loan whose token someone could freeze, pause, hook or move (`loanOffer`'s `risks`) is refused unless
1952
+ * `acceptRisks` is true: the page shows the player who holds that power first. `extra`: a transfer hook's accounts.
1953
+ */
1954
+ async borrow(maker, id, asset, { due, acceptRisks = false, extra = [], ...others } = NO_OPTIONS) {
1955
+ only(others, ['due', 'acceptRisks', 'extra'], 'borrow');
1956
+ const loan = await this.loanOffer(maker, id);
1957
+ if (loan.risks.length && acceptRisks !== true) throw new Error(`Repaying this loan can be blocked. ${loan.risks.map((r) => r.says).join(' ')} Borrow only if the player accepts this: pass acceptRisks: true.`);
1958
+ const lasts = BigInt(loan.lasts);
1959
+ due = BigInt(due ?? BigInt(now()) + lasts - (lasts > 120n ? 60n : 0n));
1960
+ const { before, ...nft } = await nftOf(asset);
1961
+ const collection = nft.collection ?? (await collectionOf(asset)) ?? MPL_CORE;
1962
+ const loanPrograms = { ...programs, [loan.mint]: loan.program };
1963
+ const ix = loot.borrowIx({ borrower: wallet.address, project, id, offer: loan.offer, due, asset, ...nft, collection, programs: loanPrograms, extra });
1964
+ const signature = await send([...before, createAssociatedTokenAccountIdempotent(wallet.address, wallet.address, loan.mint, loan.program), ix]);
1965
+ return { signature, due, hold: loot.pda.hold(asset, due) };
1966
+ },
1967
+
1968
+ /**
1969
+ * The loan on `asset` (held until `due`), as a page shows it before anyone repays: { lender, mint, repay (base units),
1970
+ * due, paid }. `repay` is what `repay` then takes as the amount shown. Null when it holds no loan.
1971
+ */
1972
+ async loan(asset, due) {
1973
+ const hold = await readMaybe('Hold', loot.pda.hold(asset, due));
1974
+ if (!hold?.debt) return null;
1975
+ return { lender: hold.debt.lender, mint: hold.debt.mint, repay: BigInt(hold.debt.repay), due: Number(hold.free_at), borrower: hold.player };
1976
+ },
1977
+
1978
+ /**
1979
+ * Repays the loan on `asset` (held until `due`), the player's own or a friend's, then sends the NFT home to its
1980
+ * borrower. `repay`: the debt the payer was shown (`loan()`'s, in base units); the program refuses any other
1981
+ * (`TermsChanged`). The repay goes in the wallet's transaction alone, with what it opens (the borrower's Token
1982
+ * Metadata account), so a release that fails never undoes a repay that must land before the loan is due; the same
1983
+ * transaction funds a throwaway key that sends the release (anyone may), a pNFT's or a compressed NFT's too: the key
1984
+ * is funded for what that release opens (a pNFT's token account and record, a compressed NFT's proof record, which
1985
+ * comes back in the same transaction). Returns { signature, released }.
1986
+ */
1987
+ async repay(asset, due, { repay, extra = [], ...others } = NO_OPTIONS) {
1988
+ only(others, ['repay', 'extra'], 'repay');
1989
+ const hold = await read('Hold', loot.pda.hold(asset, due));
1990
+ if (!hold.debt) throw new Error('This NFT is held for no loan left to repay.');
1991
+ if (repay === undefined || repay === null) throw new Error('Say the debt the payer was shown (repay, from loan()): the amount read now is never paid in its place.');
1992
+ if (BigInt(repay) !== BigInt(hold.debt.repay)) throw new Error(`This loan now asks ${hold.debt.repay}, not the ${repay} shown: show the payer the loan as it is now first.`);
1993
+ const { before, hasAuthority: _, ...nft } = await nftOf(asset, wallet.address, { to: hold.player });
1994
+ const collection = nft.collection ?? (await collectionOf(asset)) ?? MPL_CORE;
1995
+ // A pNFT's or a compressed NFT's release opens something (a token account and record, a proof record): its key
1996
+ // is funded for it, and makes its own proof and accounts as it sends the release.
1997
+ const heavy = before.some(isProveLeaf) || Boolean(nft.tokenMetadata?.programmable);
1998
+ const opens = heavy ? [] : before.filter((x) => x.programId !== COMPUTE_BUDGET_PROGRAM);
1999
+ const at = loot.pda.hold(asset, due);
2000
+ const { key, fund } = await keyFor(heavy ? loot.SETTLE_RENT_UNIT + SIGNATURE_FEE : SIGNATURE_FEE, await jobKey(at));
2001
+ const repaid = loot.repayIx({ payer: wallet.address, project, hold, repay, programs, extra });
2002
+ const signature = await send(withSolWrap(wallet.address, hold.debt.mint, BigInt(hold.debt.repay), [...opens, repaid, fund]));
2003
+ // Repaid, the hold is a plain one that is over (no debt, `free_at` 0) at the address its loan named.
2004
+ const release = async (signer) => {
2005
+ const { before: first, hasAuthority: _h, ...kind } = heavy ? await nftOf(asset, signer, { to: hold.player }) : { before: [], ...nft };
2006
+ return [...first, loot.releaseIx({ signer, project, asset, ...kind, collection, hold: { ...hold, debt: null, free_at: 0 }, address: at })];
2007
+ };
2008
+ const released = await keyRun(key, release);
2009
+ return { signature, released };
2010
+ },
2011
+
2012
+ /**
2013
+ * Backs `side` of contest number `id` (the authority opens contests while the project runs) with `amount`; a contest
2014
+ * of NFTs takes one of the player's NFTs that passes its rule instead (`nft`, its address), held until it is
2015
+ * claimed. `listProof`: for a list gate.
2016
+ */
2017
+ async enter(id, side, amount, { shown, listProof, nft: staked, ...others } = NO_OPTIONS) {
2018
+ only(others, ['shown', 'listProof', 'nft'], 'enter');
2019
+ const contest = await read('Contest', loot.pda.contest(project, id));
2020
+ shown ??= await passing(contest.gate, 'enter');
2021
+ listProof = listProofOf(contest.gate, null, listProof);
2022
+ if (contest.nft) {
2023
+ if (!staked) throw new Error('This contest takes an NFT: say which (nft).');
2024
+ const { before, ...kind } = await nftOf(staked);
2025
+ return send([...before, loot.enterIx({ player: wallet.address, project, id, contest, side, amount: 1n, shown, listProof, nft: { asset: staked, ...kind }, programs })]);
2026
+ }
2027
+ const t = [...tokens.values()].find((x) => mintOf(x.key) === contest.mint);
2028
+ const value = units(amount, t?.source === 'sol' ? 9 : t?.decimals ?? 0);
2029
+ return send(withSolWrap(wallet.address, contest.mint, value, [loot.enterIx({ player: wallet.address, project, id, contest, side, amount: value, shown, listProof, programs })]));
2030
+ },
2031
+
2032
+ /**
2033
+ * The auctions: each one's lot, high bid, the least the next bid must be, when it ends and how long is left, and
2034
+ * whether the player leads. One no longer on chain has ended. `waiting`: SOL that came back to the player's
2035
+ * wrapped-SOL account from a bid that was topped (`unwrap` turns it into plain SOL).
2036
+ */
2037
+ async auctions() {
2038
+ const list = spec.auctions ?? [];
2039
+ const accounts = await client.accounts(list.map((a) => auctionAt(a)));
2040
+ const at = BigInt(now());
2041
+ const wrapped = wallet?.address ? await client.account(associatedTokenAddress(wallet.address, NATIVE_MINT)) : null;
2042
+ const waiting = wrapped ? show(new DataView(wrapped.data.buffer, wrapped.data.byteOffset).getBigUint64(64, true), 9) : '0';
2043
+ return list.map((a, i) => {
2044
+ const x = accounts[i] && loot.decode('Auction', accounts[i].data);
2045
+ const base = { key: a.key, name: a.name, lot: a.lot, currency: tokens.get(a.currency ?? currencyKey)?.symbol ?? a.currency ?? currencyKey };
2046
+ if (!x) return { ...base, ended: true };
2047
+ // Bids of NFTs count NFTs.
2048
+ const dec = x.nft_bids ? 0 : decimalsOf(a.currency);
2049
+ const ends = loot.auctionEnds(x);
2050
+ const bid = x.high > 0n;
2051
+ const nextBid = loot.leastBid(x);
2052
+ return {
2053
+ ...base,
2054
+ ended: false,
2055
+ reserve: show(x.reserve, dec),
2056
+ high: bid ? show(x.high, dec) : null,
2057
+ highBidder: bid ? x.high_bidder : null,
2058
+ leading: bid && x.high_bidder === wallet?.address,
2059
+ nextBid: nextBid === null ? null : show(nextBid, dec),
2060
+ nextBidUnits: nextBid,
2061
+ startsAt: Number(x.starts_at),
2062
+ endsAt: Number(ends),
2063
+ secondsLeft: Number(ends > at ? ends - at : 0n),
2064
+ open: nextBid !== null && at >= x.starts_at && at < ends,
2065
+ over: at >= ends,
2066
+ // The lot went at its end, and its seller's account couldn't take the money then: the next end pays it and closes it.
2067
+ ...(BigInt(x.proceeds_owed) > 0n ? { delivered: true, proceedsOwed: show(x.proceeds_owed, dec) } : {}),
2068
+ // A bid of NFTs: a picker of 1 to `maxNfts` (and at least `nextBid`) of the player's NFTs.
2069
+ ...(x.nft_bids ? { nftBids: true, held: x.held, maxNfts: loot.MAX_BID_NFTS } : {}),
2070
+ ...(tokens.get(a.currency ?? currencyKey)?.source === 'sol' ? { waiting } : {}),
2071
+ };
2072
+ });
2073
+ },
2074
+
2075
+ /**
2076
+ * Bids `amount` (whole units) on an auction. The bid it tops goes back to its bidder in the same transaction
2077
+ * (the program opens their account again if they closed it, paid by this bidder). In SOL, only what the player's
2078
+ * wrapped-SOL account does not hold already is wrapped, and the account stays open: if this bid is topped, it comes
2079
+ * back there.
2080
+ */
2081
+ async bid(auctionKey, amount, { shown, listProof, nfts, ...others } = NO_OPTIONS) {
2082
+ only(others, ['shown', 'listProof', 'nfts'], 'bid');
2083
+ const a = part('auctions', auctionKey);
2084
+ const auction = await read('Auction', auctionAt(a));
2085
+ shown ??= await passing(auction.gate, 'bid');
2086
+ listProof = listProofOf(auction.gate, a.gate?.list, listProof);
2087
+ // An auction of NFT bids: a bid is a bundle of the player's NFTs (`nfts`, their addresses, or `amount` read as
2088
+ // them), and the bid it tops goes back to its bidder in the same transaction.
2089
+ if (auction.nft_bids) {
2090
+ const bundle = nfts ?? (Array.isArray(amount) ? amount : null);
2091
+ if (!bundle?.length) throw new Error('This auction takes NFTs as bids: say which (nfts).');
2092
+ if (bundle.length > loot.MAX_BID_NFTS) throw new Error(`A bid holds at most ${loot.MAX_BID_NFTS} NFTs.`);
2093
+ if (auction.held.length >= loot.MAX_BID_NFTS) throw new Error(`The high bid holds ${loot.MAX_BID_NFTS} NFTs, the most a bid holds: it can't be topped.`);
2094
+ const before = [];
2095
+ const kindOf = async (asset, to) => {
2096
+ const { before: proof, ...kind } = await nftOf(asset, wallet.address, { to });
2097
+ before.push(...proof);
2098
+ return { asset, ...kind };
2099
+ };
2100
+ const bid = [];
2101
+ for (const asset of bundle) bid.push(await kindOf(asset));
2102
+ // The bid it tops goes back to its bidder: into a fresh account of theirs when their own can't take it.
2103
+ const held = [];
2104
+ for (const asset of auction.held) held.push(await kindOf(asset, auction.high_bidder));
2105
+ const ix = loot.bidIx({ bidder: wallet.address, project, id: a.id, auction, shown, listProof, nfts: bid, held, programs });
2106
+ return send([computeUnitLimit(800_000), ...before.filter((x) => x.programId !== COMPUTE_BUDGET_PROGRAM), ix]);
2107
+ }
2108
+ const mint = auction.mint;
2109
+ const value = units(amount, decimalsOf(a.currency));
2110
+ const instructions = [];
2111
+ if (mint === NATIVE_MINT) {
2112
+ const wrapped = associatedTokenAddress(wallet.address, NATIVE_MINT);
2113
+ const have = await client.account(wrapped);
2114
+ const held = have ? new DataView(have.data.buffer, have.data.byteOffset).getBigUint64(64, true) : 0n;
2115
+ instructions.push(createAssociatedTokenAccountIdempotent(wallet.address, wallet.address, NATIVE_MINT));
2116
+ if (value > held) instructions.push(transferLamports(wallet.address, wrapped, value - held), syncNative(wrapped));
2117
+ }
2118
+ // A Token-2022 account can ask for a memo on every transfer into it: the Memo program rides along.
2119
+ const memo = programOf(mint) === TOKEN_2022_PROGRAM ? [{ pubkey: MEMO_PROGRAM, isSigner: false, isWritable: false }] : [];
2120
+ instructions.push(loot.bidIx({ bidder: wallet.address, project, id: a.id, auction, amount: value, shown, listProof, programs, extra: memo }));
2121
+ return send(instructions, [], { keepWrapped: true });
2122
+ },
2123
+
2124
+ /**
2125
+ * Ends an auction that is over (anyone can; the winner usually does): the lot to the highest bidder, a new
2126
+ * collectible minted for them at the address its item and serial name, however long after the end it is sent. A
2127
+ * roll lot makes the winner's roll of its box (`order`), which `collect` asks for and settles. An end too big for one
2128
+ * transaction makes each NFT a hold and sends each home with a release after it (`releases`).
2129
+ */
2130
+ async endAuction(auctionKey, { holders, ...others } = NO_OPTIONS) {
2131
+ only(others, ['holders'], 'endAuction');
2132
+ const a = part('auctions', auctionKey);
2133
+ const auction = await read('Auction', auctionAt(a));
2134
+ const g = await projectNow();
2135
+ // The lot went at an earlier end: this one pays what its seller is owed and closes the auction.
2136
+ if (BigInt(auction.proceeds_owed) > 0n) {
2137
+ const memo = programOf(auction.mint) === TOKEN_2022_PROGRAM ? [{ pubkey: MEMO_PROGRAM, isSigner: false, isWritable: false }] : [];
2138
+ return { signature: await send([loot.endAuctionIx({ signer: wallet.address, project, projectState: g, id: a.id, auction, programs, extra: memo })]), owedPaid: auction.proceeds_owed };
2139
+ }
2140
+ const sold = auction.high > 0n;
2141
+ const item = auction.lot.Item ? await read('Item', loot.pda.item(project, auction.lot.Item[0])) : null;
2142
+ const made = sold && item?.kind === 'Collectible' ? loot.pda.itemAsset(loot.pda.item(project, auction.lot.Item[0]), auction.pin) : null;
2143
+ const memo = programOf(auction.mint) === TOKEN_2022_PROGRAM ? [{ pubkey: MEMO_PROGRAM, isSigner: false, isWritable: false }] : [];
2144
+ holders ??= sold ? await holdersOf(auction.shares) : {};
2145
+ const lot = auction.lot.Asset?.[0];
2146
+ // The lot goes to its winner (back to its seller, unsold); a winning bid of NFTs to whoever the rest of a bid in
2147
+ // tokens would go to: the seller of an NFT, the treasury for a new item.
2148
+ const { before, hasAuthority: _, ...nft } = lot ? await nftOf(lot, wallet.address, { to: sold ? auction.high_bidder : auction.seller }) : { before: [] };
2149
+ const collection = lot && !nft.compressed && !nft.tokenMetadata ? (await collectionOf(lot)) ?? MPL_CORE : undefined;
2150
+ const proceeds = auction.lot.Asset ? auction.seller : g.treasury;
2151
+ const held = [];
2152
+ if (sold && auction.nft_bids) {
2153
+ for (const asset of auction.held) {
2154
+ const { before: proof, ...kind } = await nftOf(asset, wallet.address, { to: proceeds });
2155
+ before.push(...proof);
2156
+ held.push(kind);
2157
+ }
2158
+ }
2159
+ const ix = loot.endAuctionIx({ signer: wallet.address, project, projectState: g, id: a.id, auction, item, ...nft, collection, held, holders, programs, extra: memo });
2160
+ let all = [computeUnitLimit(800_000), ...before.filter((x) => x.programId !== COMPUTE_BUDGET_PROGRAM), ix];
2161
+ // An end that can't fit one transaction (a compressed lot's long proof, a hooked mint, many shares) makes each NFT
2162
+ // a hold for whoever it goes to, then sends each home with its own release: anyone may, and the rent comes back.
2163
+ const holds = lot || (sold && auction.nft_bids) ? !sendPlan({ payer: wallet.address, instructions: all, alone: isProveLeaf }) : false;
2164
+ if (holds) {
2165
+ // The end's own transaction funds a throwaway key that sends each release after it (anyone may): each one's
2166
+ // fees, and what it may open (a pNFT's token account and record, a compressed NFT's proof record). One the key
2167
+ // can't send, the wallet does.
2168
+ const outs = [...(lot ? [[lot, sold ? auction.high_bidder : auction.seller]] : []), ...(sold && auction.nft_bids ? auction.held.map((x) => [x, proceeds]) : [])];
2169
+ const { key, fund } = await keyFor(BigInt(outs.length) * (2n * SIGNATURE_FEE + loot.SETTLE_RENT_UNIT), await jobKey(auctionAt(a)));
2170
+ all = [computeUnitLimit(800_000), loot.endAuctionIx({ signer: wallet.address, project, projectState: g, id: a.id, auction, item, ...nft, collection, held, hold: true, holders, programs, extra: memo }), fund];
2171
+ const signature = await send(all);
2172
+ const releases = [];
2173
+ for (const [asset, to] of outs) {
2174
+ const release = async (signer) => {
2175
+ const hold = await read('Hold', loot.pda.hold(asset, 0));
2176
+ const { before: proof, hasAuthority: _h, ...kind } = await nftOf(asset, signer, { to });
2177
+ return [...proof, loot.releaseIx({ signer, project, asset, ...kind, collection: kind.compressed || kind.tokenMetadata ? undefined : (await collectionOf(asset)) ?? MPL_CORE, hold, to })];
2178
+ };
2179
+ const steps = await release(key.address);
2180
+ const byKey = (await dryRun(key.address, steps)).ok ? await sendAs(key, steps).catch(() => null) : null;
2181
+ releases.push(byKey ?? (await send(await release(wallet.address))));
2182
+ }
2183
+ await emptyKey(key);
2184
+ return { signature, releases, ...(sold ? { winner: auction.high_bidder } : {}) };
2185
+ }
2186
+ // A roll lot sold: the winner's roll, made at the default nonce, waits for its winner or the project's keys to ask it (`collect`); the winner
2187
+ // ending it asks in the same transaction and a key settles it.
2188
+ const roll = sold && auction.lot.Roll ? loot.pda.order(project, auction.high_bidder, loot.prizeRollNonce(loot.prizeNonce(auctionAt(a), BigInt(auction.last_bid_at)))) : null;
2189
+ const { signature, rolls } = roll && auction.high_bidder === wallet.address ? await sendRolling(all) : { signature: await send(all), rolls: [] };
2190
+ return { signature, ...(sold ? { winner: auction.high_bidder } : {}), ...(made ? { asset: made } : {}), ...(roll ? { order: roll } : {}), ...(rolls.length ? { rolled: rolls[0] } : {}) };
2191
+ },
2192
+
2193
+ /**
2194
+ * Runs a schedule that is due (an emission into a pool, a rent or a drip): the project's owner or authority, or who it
2195
+ * pays, may (a sealed burn: anyone), and nothing is paid to whoever sends it but the network fee. A run missed is missed:
2196
+ * this pays once, for the latest run due, and the runs before it lapse; only a sealed fixed amount pays every run due. `holders`: asset → holder, for a schedule that pays a collectible's holder.
2197
+ * Returns null when it isn't due yet.
2198
+ */
2199
+ async runSchedule(scheduleKey, { holders, most, ...others } = NO_OPTIONS) {
2200
+ only(others, ['holders', 'most'], 'runSchedule');
2201
+ const sc = part('schedules', scheduleKey);
2202
+ const address = loot.pda.schedule(project, sc.id);
2203
+ const schedule = await read('Schedule', address);
2204
+ const from = schedule.action.from;
2205
+ // A sealed schedule's vault: past its last run, whatever arrived since is due once more.
2206
+ const vault = from.Vest ? await client.account(loot.pda.vest(address)) : null;
2207
+ const held = vault ? vault.data.subarray(64, 72).reduce((n, b, i) => n | (BigInt(b) << BigInt(8 * i)), 0n) : 0n;
2208
+ // Every run due by the chain's clock (`most`: at most that many of a sealed fixed amount's, the only one paid run by
2209
+ // run; a long backlog of those takes a few calls).
2210
+ const due = Math.min(loot.runsDue(schedule, await chainNow(), held), most ?? Infinity);
2211
+ if (!due) return null;
2212
+ const poolMint = from.Pool ? encodeBase58((await client.account(loot.pda.pool(project, from.Pool[0]))).data.subarray(0, 32)) : null;
2213
+ holders ??= await holdersOf([{ to: schedule.action.to }]);
2214
+ const treasury = (await projectNow()).treasury;
2215
+ // A sealed fixed amount's backlog is a step for each halving: room for the longest the program takes a call.
2216
+ return send([computeUnitLimit(1_400_000), loot.runScheduleIx({ payer: wallet.address, project, id: sc.id, schedule, poolMint, holders, programs, treasury, upto: Number(schedule.runs) + due })]);
2217
+ },
2218
+
2219
+ // ---------------------------------------------------------------- listings
2220
+
2221
+ /**
2222
+ * Lists one of the player's NFTs (any kind) for sale at `price` (whole units of `currency`, the economy's by
2223
+ * default), `shares` taken first (a royalty, a platform's cut). The project holds it until it sells or is unlisted.
2224
+ */
2225
+ async listForSale(asset, { price, currency, shares = [], ...others } = NO_OPTIONS) {
2226
+ only(others, ['price', 'currency', 'shares'], 'listForSale');
2227
+ const mint = mintOf(currency);
2228
+ const { before, ...nft } = await nftOf(asset);
2229
+ return send([...before, loot.openListingIx({ maker: wallet.address, project, asset, mint, deal: { Sell: { price: units(price, decimalsOf(currency)) } }, shares, ...nft })]);
2230
+ },
2231
+
2232
+ /**
2233
+ * Lists one of the player's NFTs for rent: `price` (whole units) a period of `lasts` seconds, a renter taking 1 to
2234
+ * `maxPeriods` at once. Nobody can end a rental early; the next renter takes it once one is over, until it is unlisted.
2235
+ */
2236
+ async listForRent(asset, { price, lasts, maxPeriods = 1, currency, shares = [], ...others } = NO_OPTIONS) {
2237
+ only(others, ['price', 'lasts', 'maxPeriods', 'currency', 'shares'], 'listForRent');
2238
+ const mint = mintOf(currency);
2239
+ const { before, ...nft } = await nftOf(asset);
2240
+ return send([...before, loot.openListingIx({ maker: wallet.address, project, asset, mint, deal: { Rent: { price: units(price, decimalsOf(currency)), lasts, max_periods: maxPeriods } }, shares, ...nft })]);
2241
+ },
2242
+
2243
+ /** The listing of `asset` (one account read), or null: { listing, asset, maker, mint, deal, renter, until }. */
2244
+ async listing(asset) {
2245
+ const listing = await readMaybe('Listing', loot.pda.listing(project, asset));
2246
+ if (!listing) return null;
2247
+ return { listing: loot.pda.listing(project, asset), ...listing, renter: loot.renterOf(listing, await chainNow()) };
2248
+ },
2249
+
2250
+ /** Who rents `asset` now (one account read, then a check that the project still holds it), or null. */
2251
+ async rentedTo(asset) {
2252
+ const listing = await readMaybe('Listing', loot.pda.listing(project, asset));
2253
+ const renter = loot.renterOf(listing, await chainNow());
2254
+ if (!renter) return null;
2255
+ const account = await client.account(asset);
2256
+ const holds = account?.owner === MPL_CORE ? parseCoreAsset(account.data).owner === project : true;
2257
+ return holds ? renter : null;
2258
+ },
2259
+
2260
+ /** The project's listings, a maker's (`maker`) or all: one search of the program's accounts. */
2261
+ async listings({ maker, ...others } = NO_OPTIONS) {
2262
+ only(others, ['maker'], 'listings');
2263
+ const filters = [{ memcmp: { offset: 0, bytes: encodeBase58(Uint8Array.of(loot.TAGS.Listing)) } }, { memcmp: { offset: 1, bytes: project } }, ...(maker ? [{ memcmp: { offset: 33, bytes: maker } }] : [])];
2264
+ const time = await chainNow();
2265
+ return (await client.programAccounts(loot.LOOT_PROGRAM, filters)).map(({ pubkey, account }) => {
2266
+ const listing = loot.decode('Listing', account.data);
2267
+ return { listing: pubkey, ...listing, renter: loot.renterOf(listing, time) };
2268
+ });
2269
+ },
2270
+
2271
+ /**
2272
+ * Buys a listed NFT at the terms the player saw: `listing`, as `listing()` or `listings()` showed it. It is
2273
+ * required: the terms signed are those, never the listing read now, so a price or a share changed since is refused.
2274
+ */
2275
+ async buyListing(asset, { listing, ...others } = NO_OPTIONS) {
2276
+ only(others, ['listing'], 'buyListing');
2277
+ await shownListing(asset, listing, 'Sell');
2278
+ const { before, ...nft } = await nftOf(asset, wallet.address, { to: wallet.address });
2279
+ const holders = await holdersOf(listing.shares ?? []);
2280
+ return send([...before, ...withSolWrap(wallet.address, listing.mint, listing.deal.Sell.price, [loot.takeListingIx({ taker: wallet.address, project, projectState: await projectNow(), listing, holders, programs, ...nft })])]);
2281
+ },
2282
+
2283
+ /**
2284
+ * Rents a listed NFT for `periods` periods, at the terms the player saw: `listing`, as `listing()` or `listings()`
2285
+ * showed it (required, as for `buyListing`).
2286
+ */
2287
+ async rentListing(asset, { periods = 1, listing, ...others } = NO_OPTIONS) {
2288
+ only(others, ['periods', 'listing'], 'rentListing');
2289
+ await shownListing(asset, listing, 'Rent');
2290
+ const holders = await holdersOf(listing.shares ?? []);
2291
+ const total = BigInt(listing.deal.Rent.price) * BigInt(periods);
2292
+ // A compressed NFT is read by its record, proven first in the same transaction.
2293
+ const { before, compressed } = await nftOf(asset);
2294
+ return send([...(compressed ? before : []), ...withSolWrap(wallet.address, listing.mint, total, [loot.takeListingIx({ taker: wallet.address, project, projectState: await projectNow(), listing, periods, compressed, holders, programs })])]);
2295
+ },
2296
+
2297
+ /** Takes a listed NFT home (its maker). While a rental runs it only stops the listing; call again once it is over. */
2298
+ async unlist(asset) {
2299
+ const listing = await read('Listing', loot.pda.listing(project, asset));
2300
+ const { before, ...nft } = await nftOf(asset, wallet.address, { to: wallet.address });
2301
+ return send([...before, loot.closeListingIx({ maker: wallet.address, project, listing, ...nft })]);
2302
+ },
2303
+
2304
+ // ---------------------------------------------------------------- raffles
2305
+
2306
+ /**
2307
+ * The project's owner raffles an NFT they hold, as lottery `id`: the NFT goes into the lottery's own stash, where
2308
+ * nothing but the lottery can take it while a place names it; the winner gets it before any ticket money moves.
2309
+ * `price` (whole units of `currency`) a ticket, `drawsAt` (Unix seconds) its one draw, `minSold` the least tickets
2310
+ * (fewer: the round is cancelled and refunded), `shares` where the ticket money goes (the seller's own wallet by
2311
+ * default: the treasury). A seller runs it in a project of their own, whose owner they are.
2312
+ */
2313
+ async raffle(id, nft, { price, currency, drawsAt, minSold = 0, perWallet = 0, shares = [{ to: 'Treasury', bps: 10_000 }], ...others } = NO_OPTIONS) {
2314
+ only(others, ['price', 'currency', 'drawsAt', 'minSold', 'perWallet', 'shares'], 'raffle');
2315
+ const kind = await nftOf(nft);
2316
+ const { before, hasAuthority: _, ...piece } = kind;
2317
+ const lotteryState = await readMaybe('Lottery', loot.pda.lottery(project, id));
2318
+ await send([...before, loot.stashIx({ signer: wallet.address, project, lottery: id, lotteryState, asset: nft, ...piece })]);
2319
+ const args = {
2320
+ price: units(price, decimalsOf(currency)), mint: mintOf(currency), shares, places: [{ bps: 0, prize: { Held: [nft] } }],
2321
+ ticket_mint: NOBODY, every: 0n, draws_at: BigInt(drawsAt), gate: null, per_wallet: perWallet, min_sold: BigInt(minSold),
2322
+ };
2323
+ return send([loot.upsertIx({ owner: wallet.address, project, def: { Lottery: { id, args } }, lotteryState })]);
2324
+ },
2325
+
2326
+ /** The raffle's owner takes the NFT back out of lottery `id`'s own stash once nothing promises it (the lottery ended, or no place names it). */
2327
+ async unraffle(id, nft) {
2328
+ const { before, hasAuthority: _, ...piece } = await nftOf(nft, wallet.address, { to: wallet.address });
2329
+ const lotteryState = await readMaybe('Lottery', loot.pda.lottery(project, id));
2330
+ return send([...before, loot.stashIx({ signer: wallet.address, project, lottery: id, lotteryState, asset: nft, out: wallet.address, ...piece })]);
2331
+ },
2332
+
2333
+ /** Turns what waits in the player's wrapped-SOL account (a bid that was topped) into plain SOL. */
2334
+ async unwrap() {
2335
+ const wrapped = associatedTokenAddress(wallet.address, NATIVE_MINT);
2336
+ if (!(await client.account(wrapped))) return null;
2337
+ return send([closeTokenAccount(wrapped, wallet.address, wallet.address)]);
2338
+ },
2339
+
2340
+ /**
2341
+ * Claims a grant for the connected wallet: from the approved list (the proof comes from xsolana.json, or is given:
2342
+ * `proof`, `personHash` and the `round` the list pays, as a results server's list answers them), or with a voucher
2343
+ * a server signed ({ publicKey, signature, incarnation, round and amount, or rounds: [{ id, amount }], expires, personHash }). A list in xsolana.json names
2344
+ * no round (zeros). `amount` (a voucher's, or `options.amount` for a list's leaf): the tokens this claim gives, in
2345
+ * base units, up to the grant's own, for a grant of a fixed amount of tokens (a reward by rank); absent or 0 gives
2346
+ * the grant's prize as it is. A grant that gives a roll of a box makes the wallet's roll, which `claims()` then
2347
+ * lists and `collect` asks for and settles.
2348
+ */
2349
+ async claimGrant(grantKey, options = {}) {
2350
+ only(options, ['voucher', 'proof', 'personHash', 'round', 'amount', 'shown', 'listProof'], 'claimGrant');
2351
+ let { voucher = null, proof: given = null, personHash: listPerson = null, round: listRound = null, amount: listAmount = 0n, shown } = options;
2352
+ const gspec = part('grants', grantKey);
2353
+ const address = loot.pda.grant(project, gspec.id);
2354
+ const grant = await read('Grant', address);
2355
+ shown ??= await passing(grant.gate, 'claim this');
2356
+ const gateProof = listProofOf(grant.gate, gspec.gate?.list, options.listProof);
2357
+ // What the grant has paid this wallet: its count of claims (an open claim's roll is named by it), and the rounds
2358
+ // paid (one paid already is skipped).
2359
+ const bits = grant.shard_bits;
2360
+ const [claimsShard, roundsShard] = await client.accounts([loot.pda.claims(address, grant.incarnation, loot.shardOf(loot.limitKey('wallet', wallet.address), bits)), loot.pda.rounds(address, grant.incarnation, loot.walletShard(grant, wallet.address))]);
2361
+ const count = loot.readLimit(loot.readListAccount(claimsShard), loot.limitKey('wallet', wallet.address)).count;
2362
+ const paidRounds = loot.readListAccount(roundsShard);
2363
+ // What its prize needs: a pool's mint, the item, an NFT's kind (a compressed one proven first).
2364
+ const prized = await prizeInputs([grant.gives]);
2365
+ const pool = grant.gives.Tokens?.from.Pool?.[0];
2366
+ const poolMint = pool !== undefined ? prized.poolMints[pool] : null;
2367
+ const item = prized.items[grant.gives.Item?.[0] ?? grant.gives.Compressed?.item] ?? null;
2368
+ const instructions = [...prized.before];
2369
+ let proof = [];
2370
+ const keys = gspec.keys ?? [];
2371
+ if (voucher) {
2372
+ if (voucher.incarnation == null || BigInt(voucher.incarnation) !== grant.incarnation) throw new Error('This voucher belongs to a different grant incarnation.');
2373
+ const message = loot.voucherBytes({ grant: address, incarnation: grant.incarnation, wallet: wallet.address, personHash: voucher.personHash, rounds: voucher.rounds, round: voucher.round, expires: voucher.expires, amount: BigInt(voucher.amount ?? 0) });
2374
+ const signature = typeof voucher.signature === 'string' ? decodeBase58(voucher.signature) : voucher.signature;
2375
+ instructions.push(loot.ed25519Ix({ publicKey: voucher.publicKey, signature, message }));
2376
+ // A key under the grant's key root (a promo code's) proves it is one of them.
2377
+ const at = keys.indexOf(voucher.publicKey);
2378
+ proof = given ?? (at >= 0 ? loot.merkleProof(keys.map((k) => loot.keyLeaf(k)), at) : []);
2379
+ } else if (given) {
2380
+ proof = given;
2381
+ } else if ((gspec.list ?? []).length) {
2382
+ // An entry is an address (the grant's amount, round 0), or { wallet, amount, round } (whole units of the grant's
2383
+ // token): a wallet listed under several rounds claims each once (`options.round`, a number; the first by default).
2384
+ const list = gspec.list.map((e) => (typeof e === 'string' ? { wallet: e } : e));
2385
+ const mint = grant.gives.Tokens?.from.Mint?.[0] ?? poolMint;
2386
+ const decimals = mint && list.some((e) => e.amount !== undefined) ? (await client.account(mint))?.data?.[44] ?? 0 : 0;
2387
+ const leaves = list.map((e) => loot.listLeaf(e.wallet, null, loot.listRound(e.round ?? 0), e.amount !== undefined ? units(e.amount, decimals) : 0n));
2388
+ const at = list.findIndex((e) => e.wallet === wallet.address && (typeof listRound !== 'number' || (e.round ?? 0) === listRound));
2389
+ if (at < 0) throw new Error('This wallet is not on the list for this grant.');
2390
+ proof = loot.merkleProof(leaves, at);
2391
+ listRound = loot.listRound(list[at].round ?? 0);
2392
+ if (list[at].amount !== undefined) listAmount = units(list[at].amount, decimals);
2393
+ } else if ((gspec.vouchers ?? []).length || keys.length) {
2394
+ throw new Error('This grant pays a wallet a voucher key signs for: pass the voucher.');
2395
+ }
2396
+ // What this claim gives: the amount the voucher or the list's leaf names (0: the grant's prize as it is).
2397
+ const amount = BigInt((voucher ? voucher.amount : listAmount) ?? 0);
2398
+ const most = grant.gives.Tokens?.amount;
2399
+ if (amount > 0n && (most === undefined || amount > BigInt(most))) throw new Error(`This claim names ${amount} tokens, more than this grant gives${most === undefined ? ' (it gives no fixed amount of tokens)' : ` (${most})`}.`);
2400
+ instructions.push(loot.claimGrantIx({ project, id: gspec.id, grant, wallet: wallet.address, signer: wallet.address, count, rounds: voucher?.rounds, round: voucher?.round ?? listRound, expires: voucher?.expires, personHash: voucher?.personHash ?? listPerson, amount, proof, paidRounds, poolMint, item, nft: prized.nft, shown, listProof: gateProof, programs, voucherKey: voucher?.publicKey }));
2401
+ // A claim's records and prizes can pass the 200,000 compute units a transaction has by default.
2402
+ const all = [computeUnitLimit(grant.gives.Roll ? 1_400_000 : loot.CLAIM_COMPUTE_UNITS), ...instructions.filter((x) => x.programId !== COMPUTE_BUDGET_PROGRAM)];
2403
+ if (!grant.gives.Roll) return send(all);
2404
+ // A roll's orders are asked for in the claim's own transaction and settled by a key after: one prompt.
2405
+ const { signature, rolls } = await sendRolling(all);
2406
+ return rolls.length ? { signature, rolls } : signature;
2407
+ },
2408
+
2409
+ /**
2410
+ * Takes every prize a results server vouched for this wallet's wins, or put on a list (`server`: its URL;
2411
+ * a results server), page by page. Each pays only this wallet, within the grant's limits. Returns [{ round, grant,
2412
+ * signature }] for each claim made, and [{ round, grant, error }] for one that failed, without stopping the rest;
2413
+ * wins already claimed are skipped (the program pays a wallet each round once per grant incarnation).
2414
+ *
2415
+ * `token`: the player's play-host sign-in for this game. A drop that gives NFTs from a stash gives one only at a
2416
+ * signed-in claim, first come first served (the server answers `R-SIGN-IN` to a voucher request with no sign-in, so
2417
+ * a stranger can't hand them out): with the token such a win is claimed through `POST /v1/claims`; without it, it is
2418
+ * [{ round, grant, error }].
2419
+ */
2420
+ async claimWins({ server, token = null, fetchImpl = globalThis.fetch, ...others } = NO_OPTIONS) {
2421
+ only(others, ['server', 'token', 'fetchImpl'], 'claimWins');
2422
+ const base = server.replace(/\/$/, '');
2423
+ const ask = async (path, init) => {
2424
+ const r = await fetchImpl(path.startsWith('http') ? path : `${base}${path}`, init);
2425
+ // The status first: a proxy's error page is no JSON, and says nothing a player can act on but its status.
2426
+ const body = await r.json().catch(() => null);
2427
+ if (!r.ok) throw Object.assign(new Error(body?.error?.why ?? `The results server answered ${r.status}.`), { code: body?.error?.code, status: r.status });
2428
+ if (body === null || typeof body !== 'object') throw new Error(`The results server answered ${r.status} with no JSON.`);
2429
+ return body;
2430
+ };
2431
+ const get = (path) => ask(path);
2432
+ // Where a list's proof is, as the server named it: a path on the server itself, or a whole URL on the server's own
2433
+ // origin. Any other place is refused before anything is fetched, so a server's answer can't send the page to
2434
+ // another host. A server given as a path (such as "/results") is on the page's own origin.
2435
+ const proofAt = (proof) => {
2436
+ const said = String(proof ?? '');
2437
+ let origin = null;
2438
+ let url = null;
2439
+ try {
2440
+ const home = new URL(`${base}/`, globalThis.location?.href);
2441
+ origin = home.origin;
2442
+ url = new URL(said.startsWith('/') && !said.startsWith('//') ? `${home.href.replace(/\/$/, '')}${said}` : said, home);
2443
+ } catch {
2444
+ // not a URL at all: refused below
2445
+ }
2446
+ if (!url || origin === null || origin === 'null' || url.origin !== origin) throw new Error(`The results server named a list's proof at “${said}”, which is not on the server itself (${origin ?? base}): it was not fetched.`);
2447
+ return url.href;
2448
+ };
2449
+ // Each value in a query is encoded: a round or a page the server named can't change the request's path.
2450
+ const q = (value) => encodeURIComponent(String(value));
2451
+ // A win's vouchers; a stash drop's NFT not given yet: the signed-in claim's, those for this wallet
2452
+ const vouchersOf = async (round) => {
2453
+ try {
2454
+ return (await get(`/v1/vouchers?game=${q(project)}&round=${q(round)}&wallet=${q(wallet.address)}`)).vouchers;
2455
+ } catch (e) {
2456
+ if (e.code !== 'R-SIGN-IN' || !token) throw e;
2457
+ const { vouchers } = await ask('/v1/claims', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ game: project, round, token }) });
2458
+ return vouchers.filter((v) => v.wallet === wallet.address);
2459
+ }
2460
+ };
2461
+ const claimed = async (grantId, incarnation, round) => {
2462
+ const address = loot.pda.grant(project, grantId);
2463
+ const grant = await read('Grant', address);
2464
+ if (incarnation != null && BigInt(incarnation) !== BigInt(grant.incarnation)) return false;
2465
+ const shard = await client.account(loot.pda.rounds(address, grant.incarnation, loot.walletShard(grant, wallet.address)));
2466
+ return loot.roundPaid(loot.readListAccount(shard), wallet.address, round);
2467
+ };
2468
+ const out = [];
2469
+ const take = async (round, grant, claim) => {
2470
+ try {
2471
+ const r = await claim();
2472
+ out.push({ round, grant, ...(typeof r === 'string' ? { signature: r } : r) });
2473
+ } catch (e) {
2474
+ out.push({ round, grant, error: e.message });
2475
+ }
2476
+ };
2477
+ // Page by page, until a page names no next one (null, or left out) or one already read: a server that leaves it
2478
+ // out, or names the same page again, must not have it read again and again.
2479
+ let page = { next: '' };
2480
+ const pagesRead = new Set();
2481
+ for (let first = true; first || (page.next && !pagesRead.has(page.next)); first = false) {
2482
+ pagesRead.add(page.next);
2483
+ page = await get(`/v1/wins?game=${q(project)}&wallet=${q(wallet.address)}${page.next ? `&before=${q(page.next)}` : ''}`);
2484
+ for (const win of page.wins.filter((x) => x.grant !== null && x.grant !== undefined)) {
2485
+ // one win that fails (its vouchers refused, a sign-in run out, a list not there) is its own error line, and
2486
+ // the rest go on (a refused POST /v1/claims must not stop every later win)
2487
+ try {
2488
+ for (const v of await vouchersOf(win.round)) {
2489
+ const grantKey = (spec.grants ?? []).find((x) => x.id === v.grant)?.key;
2490
+ if (!grantKey) continue;
2491
+ if (v.list) {
2492
+ // Paid by a list the owner wrote from the draw: its proof names the round.
2493
+ const list = await get(proofAt(v.proof));
2494
+ const mine = list.winners.find((w) => w.wallet === wallet.address);
2495
+ if (!mine || (await claimed(v.grant, null, list.round))) continue;
2496
+ await take(win.round, v.grant, () => this.claimGrant(grantKey, { proof: mine.proof, personHash: mine.personHash, round: list.round, amount: mine.amount ?? 0 }));
2497
+ } else if (v.signature && !(await claimed(v.grant, v.incarnation, v.round))) {
2498
+ await take(win.round, v.grant, () => this.claimGrant(grantKey, { voucher: v }));
2499
+ }
2500
+ }
2501
+ } catch (e) {
2502
+ out.push({ round: win.round, grant: win.grant, error: e.message });
2503
+ }
2504
+ }
2505
+ }
2506
+ return out;
2507
+ },
2508
+
2509
+ /**
2510
+ * What this player can finish now, themselves: a box to settle or refund, a contest entry to claim, a held
2511
+ * collectible to take home, an auction they won (or one off by the clock: their bid back), a lottery place they
2512
+ * won, a draw that is due (anyone may reveal it, or ask again when the oracle never answered; it takes 0.0008
2513
+ * SOL), and a drawn lottery's unpaid places, which anyone may pay so the next round can open. [{ kind, says, … }];
2514
+ * pass one to `claim`.
2515
+ */
2516
+ async claims() {
2517
+ const me = wallet.address;
2518
+ const out = [];
2519
+ const tag = (t) => ({ memcmp: { offset: 0, bytes: encodeBase58(Uint8Array.of(loot.TAGS[t])) } });
2520
+ const at = (offset, bytes) => ({ memcmp: { offset, bytes } });
2521
+ const slot = await slotNow();
2522
+ const time = BigInt(now());
2523
+ const chainTime = await chainNow();
2524
+ for (const { pubkey, account } of await client.programAccounts(loot.LOOT_PROGRAM, [tag('Order'), at(1, project), at(33, me)])) {
2525
+ const o = loot.decode('Order', account.data);
2526
+ const opened = BigInt(o.requested_slot);
2527
+ if (loot.orderRefundable(o, slot, chainTime)) out.push({ kind: 'refund', order: pubkey, says: 'Your payment back for an order that never finished.' });
2528
+ else if (o.status === 'Rolled' && slot > opened) out.push({ kind: 'settle', order: pubkey, says: 'What your box rolled.' });
2529
+ else if (loot.rollAskable(o, slot, chainTime)) out.push({ kind: 'settle', order: pubkey, says: 'A roll you won (a mission’s, a recipe’s, a prize’s): ask for it and see what it gives (one whose box has nothing left to give waits until it has).' });
2530
+ }
2531
+ for (const { pubkey, account } of await client.programAccounts(loot.LOOT_PROGRAM, [tag('Contest'), at(1, project)])) {
2532
+ const contest = loot.decode('Contest', account.data);
2533
+ const off = loot.contestOff(contest, time);
2534
+ if (!(off || contest.verdict === 'Won')) continue;
2535
+ const list = await client.account(loot.contestEntries(pubkey, contest));
2536
+ const entry = list?.owner === loot.LOOT_PROGRAM ? loot.decodeEntries(list.data).find((e) => e.player === me) : null;
2537
+ if (!entry) continue;
2538
+ const won = !off && Number(contest.winner) === Number(entry.side);
2539
+ const first = won && !(entry.bits & loot.CLAIMED);
2540
+ // Owed: its money back (off) or its part of the pot (won), its own NFT while the contest holds it, a winner's
2541
+ // prize with its first claim, and the losers' NFTs its share still lets it pick. A loser of a contest of tokens
2542
+ // is owed nothing, and never sends a transaction.
2543
+ const picks = won && contest.nft && !(entry.bits & loot.ENDED) && Number(entry.taken) < loot.shareOf(contest, entry);
2544
+ const owed = (contest.nft ? Number(entry.amount) === 1 && (off || won) : BigInt(entry.amount) > 0n && (off || first)) || first || picks;
2545
+ if (!owed) continue;
2546
+ const says = off ? 'Your entry back: the contest is off.' : contest.nft ? `Your NFT back${picks ? ', and your pick of the losers’' : ''}.` : contest.prize != null ? 'Your share of the pot, and your prize.' : 'Your share of the pot.';
2547
+ out.push({ kind: 'contest', id: contest.id, side: entry.side, says });
2548
+ }
2549
+ // The player's holds; the project's treasury wallet's also those owed to the treasury (a hold whose player is the
2550
+ // project's own address: a dropped stash NFT, a lottery's or an auction's NFT owed to it).
2551
+ const g = await projectNow();
2552
+ const holdsOf = [me, ...(g.treasury === me ? [project] : [])];
2553
+ const holds = (await Promise.all(holdsOf.map((who) => client.programAccounts(loot.LOOT_PROGRAM, [tag('Hold'), at(1, project), at(65, who)])))).flat();
2554
+ for (const { pubkey, account } of holds) {
2555
+ const hold = loot.decode('Hold', account.data);
2556
+ // A repaid loan is a plain hold that is over (no debt, `free_at` 0): its NFT goes home whenever. An unpaid one's
2557
+ // is the lender's once due.
2558
+ if (!hold.debt && time >= BigInt(hold.free_at)) out.push({ kind: 'release', asset: hold.asset, hold: pubkey, says: hold.player === project ? 'An NFT owed to the treasury: send it there.' : 'Your collectible back: its hold is over.' });
2559
+ }
2560
+ // A loan this wallet made that is overdue and unpaid: its NFT is the lender's to take.
2561
+ for (const { pubkey, account } of await client.programAccounts(loot.LOOT_PROGRAM, [tag('Hold'), at(1, project)])) {
2562
+ const hold = loot.decode('Hold', account.data);
2563
+ if (hold.debt?.lender === me && time >= BigInt(hold.free_at)) out.push({ kind: 'release', asset: hold.asset, hold: pubkey, says: 'A loan you made is overdue and unpaid: take its NFT.' });
2564
+ }
2565
+ // A compressed NFT's proof record this wallet paid for and left open: closing it brings its rent back.
2566
+ for (const { account } of await client.programAccounts(loot.LOOT_PROGRAM, [tag('Proven')])) {
2567
+ const leaf = (() => {
2568
+ try {
2569
+ return loot.decodeProven(account.data);
2570
+ } catch {
2571
+ return null;
2572
+ }
2573
+ })();
2574
+ if (leaf?.payer === me) out.push({ kind: 'close-leaf', asset: leaf.asset, tree: leaf.tree, says: 'A compressed NFT’s proof record you paid for: close it, and its rent comes back.' });
2575
+ }
2576
+ const auctions = spec.auctions ?? [];
2577
+ const found = await client.accounts(auctions.map((a) => auctionAt(a)));
2578
+ auctions.forEach((a, i) => {
2579
+ const x = found[i] && loot.decode('Auction', found[i].data);
2580
+ if (!x || time < loot.auctionEnds(x)) return;
2581
+ // The lot went at its end, and its seller's account couldn't take the money then: only the money is left to pay.
2582
+ if (BigInt(x.proceeds_owed) > 0n) {
2583
+ if (x.seller === me) out.push({ kind: 'auction', key: a.key, says: `The winner of “${a.name}” has the lot, and your account couldn’t take the money then: end it again once it can, and it is paid to you.` });
2584
+ return;
2585
+ }
2586
+ if (x.high_bidder === me) out.push({ kind: 'auction', key: a.key, says: `You won “${a.name}”: take it.` });
2587
+ // The seller's own auction, over: ending it pays them (or brings an unsold lot home).
2588
+ else if (x.seller === me) out.push({ kind: 'auction', key: a.key, says: BigInt(x.high) > 0n ? `Your auction “${a.name}” is over: end it, and the winning bid is paid to you.` : `Your auction “${a.name}” ended with no bid: take your lot back.` });
2589
+ });
2590
+ for (const l of spec.lotteries ?? []) {
2591
+ // A lottery the page names but nobody has written yet (a raffle still to open) has nothing to finish.
2592
+ if (!(await client.account(loot.pda.lottery(project, l.id)))) continue;
2593
+ const { current, rounds } = await roundOf(l);
2594
+ const bought = (r) => r.entries.some((e) => e.buyer === me);
2595
+ if (current?.status === 'Open' && time >= BigInt(current.terms.draws_at)) {
2596
+ const short = BigInt(current.sold) < BigInt(current.terms.min_sold ?? 0);
2597
+ out.push({ kind: 'reveal', key: l.key, says: short ? `“${l.name}” sold fewer tickets than it needs: call it off${bought(current) ? ' and take your ticket payment back, in one step' : ''}.` : `The draw of “${l.name}” is due: reveal the winners.` });
2598
+ }
2599
+ for (const { address: round, round: r } of rounds) {
2600
+ // The ask moved the lottery on: a round waiting for the oracle is an older one, asked again by its address,
2601
+ // and after the timeout the next ask cancels it.
2602
+ if (loot.lotteryCancellable(r, chainTime)) out.push({ kind: 'cancel-draw', key: l.key, round, says: `The oracle never answered the draw of “${l.name}”: call it off${bought(r) ? ' and take your ticket payment back, in one step' : ', so every ticket is refunded'}.` });
2603
+ else if (loot.lotteryDrawStuck(r, chainTime)) out.push({ kind: 'reveal', key: l.key, round, says: `The oracle never answered the draw of “${l.name}”: ask it again.` });
2604
+ if (r.status !== 'Drawn' && r.status !== 'Cancelled') continue;
2605
+ // What this wallet is owed: a refund, its NFT ticket, its places' parts of the pot and their prizes.
2606
+ const mine = r.entries.flatMap((e, i) => {
2607
+ const parts = e.buyer === me ? loot.partsOwed(r, i) : [];
2608
+ return parts.length ? [{ entry: i, parts }] : [];
2609
+ });
2610
+ if (mine.length) out.push({ kind: 'pull', key: l.key, round, pulls: mine, says: r.status === 'Cancelled' ? `Recover your ticket payment for “${l.name}”.` : `Take what you are owed in “${l.name}”.` });
2611
+ // Anyone may pay everyone else what they are owed, and the parts owed in common (about 0.002 SOL of account
2612
+ // rent for each payee who has no account for it yet, which stays with them): the last pull closes the round,
2613
+ // and its rent goes back to whoever fronted it (the lottery, or its first buyer).
2614
+ const others = r.entries.flatMap((e, i) => (loot.partsOwed(r, i).length && !mine.some((m) => m.entry === i) ? [i] : []));
2615
+ const fronted = r.opener === me;
2616
+ if (others.length || loot.commonOwed(r).length || (loot.roundFinished(r) && !mine.length))
2617
+ out.push({ kind: 'payout', key: l.key, round, says: fronted ? `Pay everyone of “${l.name}” what they are owed: the round then closes, and your deposit for it comes back.` : `Pay the others of “${l.name}” what they are owed, so its round can close.` });
2618
+ }
2619
+ }
2620
+ // A farm idle past more ends than a move can walk: anyone may walk it, in parts.
2621
+ for (const f of spec.farms ?? []) {
2622
+ const farm = await readMaybe('Farm', loot.pda.farm(project, f.id));
2623
+ if (!farm?.keeps_ends) continue;
2624
+ const { left } = await endsOf(f, farm);
2625
+ if (left > loot.MOVE_WALK_ROOM) out.push({ kind: 'walk', key: f.key, says: `“${f.name ?? f.key}” must be caught up before anyone can move it: ${left} seats’ end times to walk, in ${Math.ceil(left / loot.WALK_PER_TX)} transactions. It is free but for the network fee, and nothing is lost by the wait.` });
2626
+ }
2627
+ // A sealed schedule that pays this wallet: its runs that are due (vesting nobody can take back).
2628
+ for (const sc of spec.schedules ?? []) {
2629
+ const schedule = await readMaybe('Schedule', loot.pda.schedule(project, sc.id));
2630
+ if (!schedule) continue;
2631
+ if (!schedule.action.from.Vest) {
2632
+ // Any other schedule that is due: it waits for the project's keys or whoever it pays.
2633
+ const state = await projectNow();
2634
+ const to = schedule.action.to;
2635
+ if ([state.owner, state.authority].includes(me) || to.Wallet?.[0] === me || (to.Treasury !== undefined && state.treasury === me)) {
2636
+ if (loot.runsDue(schedule, chainTime) > 0) out.push({ kind: 'schedule', key: sc.key, says: `“${sc.name ?? sc.key}” is due to run: send it.` });
2637
+ }
2638
+ continue;
2639
+ }
2640
+ if (schedule.action.to.Wallet?.[0] !== me) continue;
2641
+ const vault = await client.account(loot.pda.vest(loot.pda.schedule(project, sc.id)));
2642
+ const held = vault ? vault.data.subarray(64, 72).reduce((n, b, i) => n | (BigInt(b) << BigInt(8 * i)), 0n) : 0n;
2643
+ if (loot.runsDue(schedule, chainTime, held) > 0) out.push({ kind: 'vest', key: sc.key, says: `What has vested of “${sc.name ?? sc.key}”: claim it.` });
2644
+ }
2645
+ // The player's listings whose rental is over: their NFT can go home (or wait for the next renter).
2646
+ const mineListed = await client.programAccounts(loot.LOOT_PROGRAM, [tag('Listing'), at(1, project), at(33, me)]);
2647
+ for (const { account } of mineListed) {
2648
+ const listing = loot.decode('Listing', account.data);
2649
+ if (listing.deal.Rent && listing.user !== NOBODY && !loot.renterOf(listing, chainTime)) out.push({ kind: 'unlist', asset: listing.asset, says: listing.stopped ? 'The rental of your stopped listing is over: take your NFT home.' : 'A rental of yours is over: take your NFT home, or leave it listed for the next renter.' });
2650
+ }
2651
+ // The lookup tables a transaction of this player's too big for a legacy one made (`send`): each is deactivated by
2652
+ // the transaction it was made for, and its rent comes back once it has cooled down (about 513 slots). One of this
2653
+ // project's whose transaction was never sent (the wallet declined it) is retired first. A table that names no
2654
+ // account of this project is another app's: left alone.
2655
+ const mineTables = await client.programAccounts(LOOKUP_TABLE_PROGRAM, tablesOf(me)).catch(() => []);
2656
+ // A table closes once its deactivation slot has left the recent slot hashes (skipped slots keep it there longer).
2657
+ const recent = mineTables.length ? await recentSlots() : [];
2658
+ for (const { pubkey, account } of mineTables) {
2659
+ const table = readTable({ ...account, owner: account.owner ?? LOOKUP_TABLE_PROGRAM });
2660
+ // The project's standing tables are never a transaction's own: never retired here.
2661
+ if (!table || table.authority !== me || (spec.live?.tables ?? []).includes(pubkey)) continue;
2662
+ const step = tableStep(table, slot, recent);
2663
+ const rent = show(BigInt(account.lamports ?? 0), 9);
2664
+ if (step === 'close') out.push({ kind: 'lookup-table', table: pubkey, says: `${rent} SOL of rent back from a lookup table a large transaction of yours used.` });
2665
+ else if (step === 'active' && table.addresses[0] === project && slot > table.lastExtendedSlot + TABLE_UNSENT_SLOTS)
2666
+ out.push({ kind: 'lookup-table', table: pubkey, says: `A lookup table made for a large transaction of yours that was never sent: retire it, and its ${rent} SOL of rent comes back about two minutes later.` });
2667
+ }
2668
+ return out;
2669
+ },
2670
+
2671
+ /**
2672
+ * Finishes one of `claims()`, signed by this player. Returns the signature. A winner of a contest of NFTs picks
2673
+ * which loser's NFT they take (`{ take: asset }`, the NFT of a losing entry that still holds one; the first found by default):
2674
+ * only the winner can, while any are left.
2675
+ */
2676
+ async claim(c, { take, build = false, ...others } = NO_OPTIONS) {
2677
+ only(others, ['take', 'build'], 'claim');
2678
+ const me = wallet.address;
2679
+ // `build`: its instructions, unsent, for a step that is one transaction of the wallet's (`claimAll` signs several
2680
+ // at once); null for one that is not (a roll to wait for, a refund, a step of another flow).
2681
+ if (build && !BATCHED.has(c.kind)) return null;
2682
+ const send = build ? async (instructions) => instructions : sendNow;
2683
+ const g = await projectNow();
2684
+ if (c.kind === 'settle' || c.kind === 'refund') {
2685
+ // A settle key this order's open funded (made again from this session's signature, or stored): it settles with
2686
+ // no prompt, or gives its SOL back before the refund.
2687
+ const key = keys ? await keys.again(c.order, { ask: false }) : null;
2688
+ const funded = key && BigInt(await client.balance(key.address).catch(() => 0n)) > 0n ? key : null;
2689
+ if (c.kind === 'settle') return (await collect(c.order, funded ? { burner: funded } : {})).settled;
2690
+ if (funded) await emptyKey(funded);
2691
+ return this.refund(c.order);
2692
+ }
2693
+ if (c.kind === 'contest') {
2694
+ const contestAddress = loot.pda.contest(project, c.id);
2695
+ const contest = await read('Contest', contestAddress);
2696
+ const entries = loot.decodeEntries((await client.account(loot.contestEntries(contestAddress, contest))).data);
2697
+ const i = entries.findIndex((e) => e.player === me);
2698
+ const own = entries[i];
2699
+ const won = contest.verdict === 'Won' && Number(contest.winner) === Number(own.side);
2700
+ const given = await prizeInputs([won && !(own.bits & loot.CLAIMED) ? contest.prize : null]);
2701
+ // A contest of NFTs: the entry's own NFT while the contest holds it, and for a winner the losers' NFTs its share
2702
+ // lets it pick (`take`: one asset, or several; the first left by default), as many as fit.
2703
+ const nfts = [];
2704
+ const kinds = {};
2705
+ if (contest.nft) {
2706
+ const kindOf = async (asset, to) => {
2707
+ const { before, hasAuthority: _, ...kind } = await nftOf(asset, me, { to });
2708
+ given.before.push(...before);
2709
+ kinds[asset] = kind;
2710
+ };
2711
+ if (Number(own.amount) === 1) {
2712
+ nfts.push(i);
2713
+ await kindOf(own.nft, me);
2714
+ }
2715
+ const share = won && !(own.bits & loot.ENDED) ? loot.shareOf(contest, own) - Number(own.taken) : 0;
2716
+ const left = entries.map((e, j) => ({ e, j })).filter(({ e }) => Number(e.side) !== Number(contest.winner) && Number(e.amount) === 1);
2717
+ const wanted = take ? [take].flat() : [];
2718
+ const picked = (wanted.length ? left.filter(({ e }) => wanted.includes(e.nft)) : left).slice(0, Math.min(share, 4));
2719
+ if (wanted.length && picked.length < Math.min(wanted.length, share)) throw new Error(`${wanted.join(', ')} is not a loser’s NFT left in this contest for you to pick.`);
2720
+ for (const { e, j } of picked) {
2721
+ nfts.push(j);
2722
+ await kindOf(e.nft, me);
2723
+ }
2724
+ }
2725
+ const claimed = [computeUnitLimit(800_000), ...given.before.filter((x) => x.programId !== COMPUTE_BUDGET_PROGRAM), loot.claimIx({ payer: me, project, projectState: g, id: c.id, contest, entries, player: me, nfts: contest.nft ? nfts : undefined, kinds, collection: given.collection, poolMints: given.poolMints, items: given.items, nft: given.nft, programs })];
2726
+ // A prize that rolls: asked for in the claim's own transaction and settled by a key after (one prompt).
2727
+ return contest.prize?.Roll && !build ? (await sendRolling(claimed)).signature : send(claimed);
2728
+ }
2729
+ if (c.kind === 'release') {
2730
+ const hold = await read('Hold', c.hold);
2731
+ const { before, hasAuthority: _, ...nft } = await nftOf(c.asset, me, { to: me });
2732
+ // An overdue loan's NFT goes to its lender.
2733
+ return send([...before, loot.releaseIx({ signer: me, project, asset: c.asset, ...nft, collection: (await collectionOf(c.asset)) ?? MPL_CORE, hold, address: c.hold, treasury: g.treasury, ...(hold.debt ? { to: hold.debt.lender } : {}) })]);
2734
+ }
2735
+ if (c.kind === 'auction') return (await this.endAuction(c.key)).signature;
2736
+ if (c.kind === 'close-leaf') return send([loot.closeLeafIx({ asset: c.asset, tree: c.tree, payer: me })]);
2737
+ if (c.kind === 'walk') return this.walkFarm(c.key);
2738
+ if (c.kind === 'vest' || c.kind === 'schedule') return this.runSchedule(c.key);
2739
+ if (c.kind === 'unlist') return this.unlist(c.asset);
2740
+ if (c.kind === 'lookup-table') {
2741
+ const table = readTable(await client.account(c.table));
2742
+ const step = table && tableStep(table, await slotNow(), await recentSlots());
2743
+ if (!step || step === 'cooling') return null;
2744
+ return send([step === 'close' ? closeTableIx({ table: c.table, authority: me }) : deactivateTableIx({ table: c.table, authority: me })], [], { keepWrapped: true });
2745
+ }
2746
+ if (!LOTTERY_STEPS.has(c.kind)) throw new Error(`Nothing to do for a claim of kind ${c.kind}.`);
2747
+ const l = part('lotteries', c.key);
2748
+ // An older round, named by its address, needs no lottery: one closed since never strands it.
2749
+ const lottery = c.round ? await readMaybe('Lottery', loot.pda.lottery(project, l.id)) : await read('Lottery', loot.pda.lottery(project, l.id));
2750
+ const key = c.round ?? loot.currentRound(loot.pda.lottery(project, l.id), lottery);
2751
+ const found = await client.account(key);
2752
+ const r = found?.owner === loot.LOOT_PROGRAM ? loot.decodeRound(found.data) : null;
2753
+ // The ask: the current round's once due, or an older round's again (after the timeout, it cancels the round).
2754
+ if (c.kind === 'reveal' || c.kind === 'cancel-draw') {
2755
+ const steps = await drawStep(l, lottery, c.round ?? null, r, me);
2756
+ // A draw that cancels the round (short of its least tickets, or the oracle never answered by the timeout)
2757
+ // refunds in the same transaction: this wallet's own parts are pulled right after it.
2758
+ const refunds = r ? await cancelPulls(key, r, me) : [];
2759
+ if (refunds.length) {
2760
+ const all = [computeUnitLimit(1_400_000), ...[...steps, ...refunds].filter((x) => x.programId !== COMPUTE_BUDGET_PROGRAM)];
2761
+ if ((await dryRun(me, all)).ok) return build ? all : send(all);
2762
+ }
2763
+ return build ? steps : send(steps);
2764
+ }
2765
+ if (!r) return null;
2766
+ // Pulls: this wallet's own parts, or (`payout`) everyone's and the parts owed in common, a few to a transaction;
2767
+ // when they are the last owed, the last pull closes the round. A round that owes nothing is only closed.
2768
+ // A named stash NFT that is gone refuses a stranger's pull: only its winner or a project key passes it over.
2769
+ const projectKey = me === g.owner || me === g.authority;
2770
+ const stranger = async (i, p) => {
2771
+ const named = p.Prize ? r.terms.places[Number(p.Prize[0])].prize?.Nft : null;
2772
+ if (!named || projectKey || r.entries[i].buyer === me) return false;
2773
+ const stash = loot.decodeStash((await client.account(loot.pda.stash(project, named.stash)))?.data ?? null);
2774
+ return !stash?.nfts.includes(named.nft);
2775
+ };
2776
+ const theirs = [];
2777
+ if (c.kind !== 'pull') for (const i of r.entries.keys()) {
2778
+ const parts = [];
2779
+ for (const p of loot.partsOwed(r, i)) if (!(await stranger(i, p))) parts.push(p);
2780
+ if (parts.length) theirs.push({ entry: i, parts });
2781
+ }
2782
+ const owed = c.kind === 'pull' ? c.pulls : [...loot.commonOwed(r).map((p) => ({ entry: null, parts: [p] })), ...theirs];
2783
+ if (!owed.length) {
2784
+ if (!loot.roundFinished(r)) return null;
2785
+ const close = [loot.pullIx({ payer: me, project, roundAddress: key, round: r, projectState: g, programs })];
2786
+ return build ? close : send(close);
2787
+ }
2788
+ const batch = owed.slice(0, 4);
2789
+ const settles = batch.reduce((n, { entry, parts }) => n + parts.reduce((m, p) => m + pullSettles(r, entry, p), 0), 0);
2790
+ const closes = settles >= Number(r.owed);
2791
+ const instructions = [computeUnitLimit(1_400_000)];
2792
+ const rolls = [];
2793
+ for (const [k, { entry, parts }] of batch.entries()) {
2794
+ const built = await pulls(key, r, entry, parts, me, { closes: closes && k === batch.length - 1 });
2795
+ instructions.push(...built.instructions.filter((x) => x.programId !== COMPUTE_BUDGET_PROGRAM));
2796
+ rolls.push(...built.rolls);
2797
+ }
2798
+ // A place's roll is asked for in the pull's own transaction (catalog P1): the winner's own, with a key that settles
2799
+ // it after (one prompt); someone else's, asked for when the box can give, else left for its winner to ask.
2800
+ if (!rolls.length) return build ? instructions : send(instructions);
2801
+ if (c.kind === 'pull' && !build) return (await sendRolling(instructions)).signature;
2802
+ const asks = [];
2803
+ for (const { order, box } of rolls) asks.push(...(await asksFor(me, order, { project, loot_box: loot.pda.box(project, box) })));
2804
+ const all = [...instructions, ...asks];
2805
+ const asked = (await dryRun(me, all)).ok ? all : instructions;
2806
+ return build ? asked : send(asked);
2807
+ },
2808
+
2809
+ /**
2810
+ * Everything in `list` (by default `claims()`) taken in as few prompts as it can: each step that is one transaction
2811
+ * of the wallet's is built first and the lot signed in one prompt when the wallet signs several at once
2812
+ * (`wallet.signTransactions`); a lottery's steps depend on each other, so one of each lottery a call. The rest (a roll
2813
+ * to settle, a refund, an auction to end…) go one by one after. Returns [{ claim, signature } | { claim, error }].
2814
+ */
2815
+ async claimAll(list) {
2816
+ list ??= await this.claims();
2817
+ const [built, alone, lotteries] = [[], [], new Set()];
2818
+ const out = [];
2819
+ for (const c of list) {
2820
+ if (!BATCHED.has(c.kind)) {
2821
+ alone.push(c);
2822
+ continue;
2823
+ }
2824
+ const lottery = LOTTERY_STEPS.has(c.kind) ? c.key : null;
2825
+ if (lottery !== null && lotteries.has(lottery)) continue;
2826
+ const instructions = await this.claim(c, { build: true }).catch((e) => (out.push({ claim: c, error: e.message }), null));
2827
+ if (!instructions?.length) continue;
2828
+ if (lottery !== null) lotteries.add(lottery);
2829
+ built.push([c, instructions]);
2830
+ }
2831
+ const sent = await sendBatch(built.map(([, instructions]) => instructions));
2832
+ out.push(...built.map(([claim], i) => ({ claim, ...sent[i] })));
2833
+ for (const claim of alone) out.push(await this.claim(claim).then((signature) => ({ claim, signature }), (e) => ({ claim, error: e.message })));
2834
+ return out;
2835
+ },
2836
+
2837
+ /** The player's collectibles of this project: [{ asset, item, itemId, name, uri, attributes, frozen }]. `item` is its key in the spec. */
2838
+ collectibles: owned,
2839
+
2840
+ /** What the player holds of each of the economy's tokens, in whole units. */
2841
+ async balances(owner = wallet?.address) {
2842
+ const out = {};
2843
+ for (const t of tokens.values()) {
2844
+ const mint = mintOf(t.key);
2845
+ if (t.source === 'sol') out[t.key] = show(await client.balance(owner), 9);
2846
+ else if (mint) {
2847
+ const a = await client.account(loot.ata(owner, mint, programOf(mint)));
2848
+ out[t.key] = a ? show(new DataView(a.data.buffer, a.data.byteOffset).getBigUint64(64, true), t.decimals ?? 0) : '0';
2849
+ }
2850
+ }
2851
+ return out;
2852
+ },
2853
+ };
2854
+ }
2855
+