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