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