@xgamesfun/solana 0.25.3 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +145 -228
- package/js/approve/details.mjs +52 -2
- package/js/approve/live-check.mjs +0 -15
- package/js/approve/server.mjs +5 -4
- package/js/chain/token2022.js +3 -2
- package/js/client/accounts.js +67 -25
- package/js/client/grant.js +7 -2
- package/js/client/idl/loot.js +1 -1
- package/js/client/idl/loot.json +714 -182
- package/js/client/ix.js +617 -113
- package/js/client/sizes.js +35 -3
- package/js/economy/check.mjs +655 -242
- package/js/economy/cost.mjs +2 -4
- package/js/economy/explain.mjs +16 -2
- package/js/economy/guide.mjs +12 -12
- package/js/economy/model.mjs +133 -14
- package/js/economy/plan.mjs +36 -11
- package/js/economy/review.mjs +50 -8
- package/js/economy/schema.json +73 -29
- package/js/economy/simulate.mjs +116 -13
- package/js/mcp/tools.mjs +2 -2
- package/js/play/index.js +847 -296
- package/package.json +1 -1
- package/release.json +5 -5
- package/skills/xsolana-economy/SKILL.md +12 -4
- package/skills/xsolana-economy/references/after-launch.md +1 -1
- package/skills/xsolana-economy/references/apps.md +11 -2
- package/skills/xsolana-economy/references/sentence-to-spec.md +3 -3
- package/skills/xsolana-economy/references/telling-the-person.md +21 -4
- package/skills/xsolana-economy/references/trust-and-cheating.md +15 -5
- package/types/chain/token2022.d.ts +3 -2
- package/types/client/accounts.d.ts +34 -11
- package/types/client/idl/loot.d.ts +25 -4
- package/types/client/ix.d.ts +217 -45
- package/types/economy/check.d.mts +5 -0
- package/types/economy/model.d.mts +75 -12
- package/types/economy/plan.d.mts +22 -1
- package/types/economy/simulate.d.mts +12 -1
- package/types/mcp/server.d.mts +12 -1
- package/types/mcp/tools.d.mts +12 -1
- package/types/play/index.d.ts +210 -144
package/js/client/ix.js
CHANGED
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
nftSlot,
|
|
53
53
|
ownStash,
|
|
54
54
|
paymentMint,
|
|
55
|
+
placesIn,
|
|
55
56
|
placesPulled,
|
|
56
57
|
potIdOf,
|
|
57
58
|
prizePlacesOwed,
|
|
@@ -492,6 +493,25 @@ export const gateAccounts = (project, { gate, wallet, shown, listProof = [] } =
|
|
|
492
493
|
...shownAccounts(project, shown),
|
|
493
494
|
];
|
|
494
495
|
|
|
496
|
+
/** Whether a rule reads the player's profile (`Of::Profile`), not an NFT. */
|
|
497
|
+
export const readsProfile = rule => rule?.of === 'Profile' || rule?.of?.Profile !== undefined;
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Whether a box's gate or odds read the player's profile (`profile` true) or an NFT (false), as the program's
|
|
501
|
+
* `LootBox::reads` tells: a gate on a rule, then each odds rule. A box may read both.
|
|
502
|
+
*/
|
|
503
|
+
export const boxReads = (box, profile) =>
|
|
504
|
+
[...(box.gate?.Holds ? [box.gate.Holds[0]] : []), ...(box.odds ?? []).map(o => o.boost.rule)].some(
|
|
505
|
+
rule => readsProfile(rule) === profile,
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* What a player shows a gate with (`rules::shown_for`): their own profile, `["profile", project, wallet]`, for a gate on
|
|
510
|
+
* a rule that reads one; the NFT `shown` otherwise.
|
|
511
|
+
*/
|
|
512
|
+
export const shownFor = (project, gate, wallet, shown) =>
|
|
513
|
+
gate?.Holds && readsProfile(gate.Holds[0]) ? pda.profile(project, wallet) : shown;
|
|
514
|
+
|
|
495
515
|
/**
|
|
496
516
|
* A nonce a prize route names (its `nonce` argument), from what names the prize: a grant, its wallet and rounds, a
|
|
497
517
|
* contest and its player, an auction and its last bid. Any nonce works (the sender picks it); the roll's order
|
|
@@ -867,7 +887,18 @@ export function withDefaults(def) {
|
|
|
867
887
|
if (kind === 'Lottery') {
|
|
868
888
|
// Its terms (`LotteryTerms`, what a round copies at its first purchase), given in `terms` or beside `every`.
|
|
869
889
|
const { every, gate, per_wallet: perWallet, terms: given, ...flat } = args;
|
|
870
|
-
|
|
890
|
+
// Sales close `closes_before` seconds before the draw (0: at the draw). `time_weighted` (a lottery that returns its
|
|
891
|
+
// tickets only): a purchase counts for its tickets times the seconds from it to the close of sales.
|
|
892
|
+
const terms = {
|
|
893
|
+
nft_ticket: null,
|
|
894
|
+
returns: false,
|
|
895
|
+
min_sold: 0,
|
|
896
|
+
ticket_mint: NO_ADDRESS,
|
|
897
|
+
closes_before: 0n,
|
|
898
|
+
time_weighted: false,
|
|
899
|
+
...flat,
|
|
900
|
+
...given,
|
|
901
|
+
};
|
|
871
902
|
// Where its places are paid from is the project's to name, never a default: `{ Pot: [id] }`, a pot kept from round
|
|
872
903
|
// to round, or `{ Round: { rest } }`, each round's own ticket money with what its places leave going to `rest`.
|
|
873
904
|
const pays = terms.pays_from;
|
|
@@ -877,9 +908,13 @@ export function withDefaults(def) {
|
|
|
877
908
|
);
|
|
878
909
|
if (pays.Pot && !(Number(pays.Pot[0]) >= 0 && Number(pays.Pot[0]) < MAX_POTS))
|
|
879
910
|
throw new Error(`A lottery's pot id is below ${MAX_POTS}.`);
|
|
880
|
-
// A run of places alike is { bps, prize, times }; a number is one place's share of the pot that gives
|
|
911
|
+
// A run of places alike is { bps, prize, when_short, times }; a number is one place's share of the pot that gives
|
|
912
|
+
// nothing else. `when_short`: what a place of tokens from a pool gets when the pool holds less than the places with
|
|
913
|
+
// a winner win: 'InOrder' (the first places in full, the default), 'InProportion' or 'Nothing'.
|
|
881
914
|
terms.places = (terms.places ?? []).map(p =>
|
|
882
|
-
typeof p === 'object'
|
|
915
|
+
typeof p === 'object'
|
|
916
|
+
? { prize: null, when_short: 'InOrder', times: 1, ...p }
|
|
917
|
+
: { bps: p, prize: null, when_short: 'InOrder', times: 1 },
|
|
883
918
|
);
|
|
884
919
|
terms.shares = stored(terms.shares ?? []);
|
|
885
920
|
return { [kind]: { ...body, args: { terms, every: every ?? 0n, gate: gate ?? null, per_wallet: perWallet ?? 0 } } };
|
|
@@ -892,11 +927,16 @@ export function withDefaults(def) {
|
|
|
892
927
|
return { [kind]: { ...body, args } };
|
|
893
928
|
}
|
|
894
929
|
|
|
930
|
+
/** The most "one of" texts a lottery's NFT ticket rule holds: the oracle's answer reads it (`lottery.rs`). */
|
|
931
|
+
export const MAX_TICKET_TEXTS = 170;
|
|
932
|
+
/** The most places of a lottery that give NFTs of its own stash (`MOST_OWN`): the oracle's answer loads their list. */
|
|
933
|
+
export const MAX_OWN_STASH_PLACES = 256;
|
|
934
|
+
|
|
895
935
|
/**
|
|
896
936
|
* The owner writes one record (`def`: { Item: { id, args } }, { Box: … }, …), at its own address. A lottery names
|
|
897
937
|
* its mint, the pot its terms pay from (which a write opens when it isn't open; pot 0's address for a lottery whose
|
|
898
938
|
* rounds pay from their own money, where any account will do), its ticket mint, both token programs and the round it waits on
|
|
899
|
-
* (`lotteryState`: the lottery as it is, null for a new one; `idleRound`: its current round's record, or round
|
|
939
|
+
* (`lotteryState`: the lottery as it is, null for a new one; `idleRound`: its current round's record, or its last round's once
|
|
900
940
|
* it drew once and ended: a lottery made again, edited once ended or made to repeat waits while that round runs or
|
|
901
941
|
* still owes NFTs of its own stash), then its own stash when a place holds or draws an NFT of it. A box, a recipe, a grant or a lottery that
|
|
902
942
|
* names a box to roll later passes that box after (`boxesAccounts`), which must be written: a box is written before
|
|
@@ -913,15 +953,30 @@ export function upsertIx({ owner, project, def: given, lotteryState = null }) {
|
|
|
913
953
|
// A new sealed schedule opens its own vault: its mint, the vault and both token programs.
|
|
914
954
|
const vest = kind === 'Schedule' ? args.action?.from?.Vest?.[0] : null;
|
|
915
955
|
if (vest) {
|
|
916
|
-
// It pays a wallet, the treasury or a burn
|
|
956
|
+
// It pays a wallet, the treasury or a burn. A wallet it pays follows the vault's four: the program refuses one
|
|
957
|
+
// that is an account of its own (`BadMove`: every run would pass it over).
|
|
958
|
+
const payee = args.action.to?.Wallet?.[0];
|
|
917
959
|
remaining = [
|
|
918
960
|
meta(vest, false, false),
|
|
919
961
|
meta(pda.vest(recordAddress(project, def)), false, true),
|
|
920
962
|
meta(TOKEN_PROGRAM, false, false),
|
|
921
963
|
meta(TOKEN_2022_PROGRAM, false, false),
|
|
964
|
+
...(payee ? [meta(payee, false, false)] : []),
|
|
922
965
|
];
|
|
923
966
|
}
|
|
924
967
|
if (kind === 'Lottery') {
|
|
968
|
+
// The oracle's answer reads the round on the default 32 KiB of memory: an NFT ticket's rule holds at most 170 "one
|
|
969
|
+
// of" texts, and a raffle at most 256 places from its own stash (BadLottery past either).
|
|
970
|
+
const texts = args.terms.nft_ticket?.rule?.test?.OneOf?.texts?.length ?? 0;
|
|
971
|
+
if (texts > MAX_TICKET_TEXTS)
|
|
972
|
+
throw new Error(
|
|
973
|
+
`A lottery's NFT ticket rule holds at most ${MAX_TICKET_TEXTS} "one of" texts; this one has ${texts} (BadLottery).`,
|
|
974
|
+
);
|
|
975
|
+
const own = args.terms.places.filter(ownStash).reduce((n, p) => n + Number(p.times ?? 1), 0);
|
|
976
|
+
if (own > MAX_OWN_STASH_PLACES)
|
|
977
|
+
throw new Error(
|
|
978
|
+
`A lottery gives at most ${MAX_OWN_STASH_PLACES} places from its own stash; this one gives ${own} (BadLottery).`,
|
|
979
|
+
);
|
|
925
980
|
remaining = [
|
|
926
981
|
meta(args.terms.mint, false, false),
|
|
927
982
|
meta(pda.lotteryPot(recordAddress(project, def), potIdOf(args.terms) ?? 0), false, true),
|
|
@@ -943,7 +998,7 @@ export function upsertIx({ owner, project, def: given, lotteryState = null }) {
|
|
|
943
998
|
* The owner closes the record at `target`, and its rent comes back: a grant's with the funds it held. A lottery
|
|
944
999
|
* (`lottery`: its decoded record) sends what every pot it opened holds (`Lottery::pots`) to `to`, a token account of its
|
|
945
1000
|
* mint (the owner's own by default), and closes each, once its current round has no record (one that drew once and
|
|
946
|
-
* ended: once its round
|
|
1001
|
+
* ended: once its last round gave out every NFT of its own stash, `idleRound`), and once the round its `owing` names has
|
|
947
1002
|
* moved in what it owes its pot (pull it first, `Part::Common`). One that never opened a pot names only those rounds.
|
|
948
1003
|
*/
|
|
949
1004
|
export function closeRecordIx({ owner, project, target, lottery, schedule, programs, to, extra = [] }) {
|
|
@@ -1016,7 +1071,8 @@ export function payIx({
|
|
|
1016
1071
|
* numbers of the NFT it rolls on: at that NFT's, `nonceOn`, whatever `nonce` says). `poolMints` maps each pool its
|
|
1017
1072
|
* prizes take tokens from to its mint (a pool's tokens won wait in the project's vault for that mint); one not given
|
|
1018
1073
|
* 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
|
|
1019
|
-
* gate or odds;
|
|
1074
|
+
* gate or odds; the player's profile goes before it whenever the box's gate or odds read one (`boxReads`); `listProof`:
|
|
1075
|
+
* for a list gate, the proof's nodes. `chosenRecipient`: the wallet (or, for a share with an
|
|
1020
1076
|
* item, the collectible, `chosen`) the buyer names for a share the box lets them choose. Every open asks the oracle,
|
|
1021
1077
|
* even of a box with nothing random (which sells in one step with `buyIx`, when it rolls on no NFT).
|
|
1022
1078
|
* 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
|
|
@@ -1032,6 +1088,7 @@ export function openBoxIx({
|
|
|
1032
1088
|
count = 1,
|
|
1033
1089
|
items = {},
|
|
1034
1090
|
shown,
|
|
1091
|
+
profile,
|
|
1035
1092
|
listProof,
|
|
1036
1093
|
subject,
|
|
1037
1094
|
chosenRecipient = null,
|
|
@@ -1058,11 +1115,13 @@ export function openBoxIx({
|
|
|
1058
1115
|
? piece({ project, wallet: player, ...subject, moves: holds, locks: false, writes: false })
|
|
1059
1116
|
: standIn(address);
|
|
1060
1117
|
if (box.subject && holds) nonce = nonceOn(on.accounts.asset);
|
|
1061
|
-
// A list's proof
|
|
1062
|
-
//
|
|
1118
|
+
// A list's proof (or a stake gate's seat and farm); then the player's own profile, for a box whose gate or odds read
|
|
1119
|
+
// one (`profile`: its address, the player's by default); then the NFT shown, for one whose gate or odds read an NFT
|
|
1120
|
+
// (for a box that rolls on an NFT, a compressed one's proof instead: that NFT is the piece).
|
|
1063
1121
|
const gate = [
|
|
1064
1122
|
...gateAccounts(project, { gate: box.gate, wallet: player, listProof }),
|
|
1065
|
-
...(box
|
|
1123
|
+
...(boxReads(box, true) ? [meta(profile ?? pda.profile(project, player), false, false)] : []),
|
|
1124
|
+
...(box.subject ? on.proof : boxReads(box, false) ? shownAccounts(project, shown) : []),
|
|
1066
1125
|
];
|
|
1067
1126
|
return lootIx(
|
|
1068
1127
|
'open_box',
|
|
@@ -1173,7 +1232,13 @@ export function buyIx({
|
|
|
1173
1232
|
},
|
|
1174
1233
|
{
|
|
1175
1234
|
remaining: [
|
|
1176
|
-
|
|
1235
|
+
// Only the gate's own account: the player's profile for a gate that reads one, else the NFT shown.
|
|
1236
|
+
...gateAccounts(project, {
|
|
1237
|
+
gate: box.gate,
|
|
1238
|
+
wallet: player,
|
|
1239
|
+
shown: shownFor(project, box.gate, player, shown),
|
|
1240
|
+
listProof,
|
|
1241
|
+
}),
|
|
1177
1242
|
...shareAccounts(project, shares, mint, { holders, programs, treasury }),
|
|
1178
1243
|
...prizes,
|
|
1179
1244
|
...tailOf(extra),
|
|
@@ -1248,6 +1313,8 @@ export function callbackRollIx({
|
|
|
1248
1313
|
* Item the item; for a collectible (`items[id]` its decoded Item), its collection, its address, Metaplex Core;
|
|
1249
1314
|
* for a fresh one (`drawn` set), then the table of each of its table traits
|
|
1250
1315
|
* Tokens the mint, the project's vault for it, the player's account of it (`prizeTo`, when given)
|
|
1316
|
+
* `giveUp`: the places in `won` of the prizes the player, settling their own order, gives up (tokens they can't receive
|
|
1317
|
+
* stay in the project's vault; a collectible's serial is voided): the vault, or the item, goes in the receiver's place.
|
|
1251
1318
|
* Nft the NFT as `piece` takes it (`nfts[nft]` its kind), going to the player, then a compressed NFT's proof
|
|
1252
1319
|
* Compressed the item, the tree's config, the tree, the collection, Bubblegum's programs; for a fresh one, then its
|
|
1253
1320
|
* tables and the Sheet of the NFT it mints
|
|
@@ -1258,6 +1325,7 @@ export function wonAccounts({
|
|
|
1258
1325
|
nfts = {},
|
|
1259
1326
|
prizeTo,
|
|
1260
1327
|
putBack = [],
|
|
1328
|
+
giveUp = [],
|
|
1261
1329
|
programs,
|
|
1262
1330
|
from = 0,
|
|
1263
1331
|
upto = order.won.length,
|
|
@@ -1271,7 +1339,11 @@ export function wonAccounts({
|
|
|
1271
1339
|
// lacks Bubblegum's plugin (`unminted`: those collections) gives nothing and mints nothing: the next takes its place.
|
|
1272
1340
|
let minted = BigInt(treeMinted);
|
|
1273
1341
|
const skips = new Set(unminted);
|
|
1274
|
-
|
|
1342
|
+
// The player, settling their own order, gives up the prizes at these places in `won` (tokens: the project's vault in
|
|
1343
|
+
// their account's place, so they stay there; a collectible: the item in its address's place, its serial voided).
|
|
1344
|
+
const givesUp = new Set(giveUp.map(Number));
|
|
1345
|
+
return order.won.slice(from, upto).flatMap((won, k) => {
|
|
1346
|
+
const up = givesUp.has(from + k);
|
|
1275
1347
|
if (won.Compressed) {
|
|
1276
1348
|
const { item: id, drawn } = won.Compressed;
|
|
1277
1349
|
const address = pda.item(project, id);
|
|
@@ -1307,7 +1379,7 @@ export function wonAccounts({
|
|
|
1307
1379
|
return [
|
|
1308
1380
|
meta(address, false, true),
|
|
1309
1381
|
meta(item.collection, false, true),
|
|
1310
|
-
meta(pda.itemAsset(address, serial), false, true),
|
|
1382
|
+
meta(up ? address : pda.itemAsset(address, serial), false, true),
|
|
1311
1383
|
meta(MPL_CORE_PROGRAM, false, false),
|
|
1312
1384
|
...tables,
|
|
1313
1385
|
];
|
|
@@ -1315,10 +1387,11 @@ export function wonAccounts({
|
|
|
1315
1387
|
if (won.Tokens) {
|
|
1316
1388
|
const { mint } = won.Tokens;
|
|
1317
1389
|
const program = tokenProgramOf(programs, mint);
|
|
1390
|
+
const kept = vault(project, mint, program);
|
|
1318
1391
|
return [
|
|
1319
1392
|
meta(mint, false, false),
|
|
1320
|
-
meta(
|
|
1321
|
-
meta(prizeTo ?? ata(order.player, mint, program), false, true),
|
|
1393
|
+
meta(kept, false, true),
|
|
1394
|
+
meta(up ? kept : (prizeTo ?? ata(order.player, mint, program)), false, true),
|
|
1322
1395
|
];
|
|
1323
1396
|
}
|
|
1324
1397
|
if (won.Nft) {
|
|
@@ -1428,14 +1501,18 @@ export const SETTLE_LAST = 4_294_967_295;
|
|
|
1428
1501
|
* the settle opens or mints (Core assets, token accounts, Sheets, a stash's row, the fate's writes: the order lends
|
|
1429
1502
|
* nothing, `settleRent`). `order` is the decoded Order; `projectState` the decoded Project; `items` and `nfts` as
|
|
1430
1503
|
* `wonAccounts` takes them. `putBack`: NFTs won that the player, signing, puts back into their stash in place of taking
|
|
1431
|
-
* them (one its collection won't let move). `
|
|
1504
|
+
* them (one its collection won't let move). `giveUp`: the places in `won` of tokens or collectibles the player,
|
|
1505
|
+
* signing, gives up because they can't receive them (an account its issuer froze, a collection that refuses the mint),
|
|
1506
|
+
* so the rest of the order is not held up: tokens given up stay in the project's vault (`PrizeForgone` says which).
|
|
1507
|
+
* `handBack`: the wallet the box's fate gives the NFT rolled on to
|
|
1432
1508
|
* (`receiverOf`) is an account of the loot program (`handsBack` of it as read), so the program gives it back to the
|
|
1433
1509
|
* player, and a Token Metadata NFT's `token` is the player's. `extra`: a hook's or a memo's accounts.
|
|
1434
1510
|
*
|
|
1435
1511
|
* `upto`, the part (`settle.rs`): `SETTLE_LAST` (the default, past the end) is the last part, all that is left (maybe
|
|
1436
1512
|
* nothing); 0 gives no prize and only splits the payment; any other gives the prizes from where the last part stopped
|
|
1437
1513
|
* (`settled`) up to it, and nothing more. The first call, whichever it is, splits the payment (its shares' accounts
|
|
1438
|
-
* ride on it)
|
|
1514
|
+
* ride on it whenever `order.drew` is above 0, a roll that gave nothing included), and moves the numbers on an NFT
|
|
1515
|
+
* rolled on that the project does not hold (that NFT's accounts ride on it, not on the last part).
|
|
1439
1516
|
*/
|
|
1440
1517
|
export function settleIx({
|
|
1441
1518
|
signer,
|
|
@@ -1446,6 +1523,7 @@ export function settleIx({
|
|
|
1446
1523
|
nfts,
|
|
1447
1524
|
prizeTo,
|
|
1448
1525
|
putBack,
|
|
1526
|
+
giveUp,
|
|
1449
1527
|
subject,
|
|
1450
1528
|
handBack = false,
|
|
1451
1529
|
holders,
|
|
@@ -1468,6 +1546,7 @@ export function settleIx({
|
|
|
1468
1546
|
nfts,
|
|
1469
1547
|
prizeTo,
|
|
1470
1548
|
putBack,
|
|
1549
|
+
giveUp,
|
|
1471
1550
|
programs,
|
|
1472
1551
|
from,
|
|
1473
1552
|
upto: end,
|
|
@@ -1478,9 +1557,11 @@ export function settleIx({
|
|
|
1478
1557
|
const tokenProgram = tokenProgramOf(programs, order.mint);
|
|
1479
1558
|
// The NFT it rolled on, as the settle moves it: given to a wallet that is an account of the loot program, it goes
|
|
1480
1559
|
// back to its player (`handsBack`), into the player's token account for a Token Metadata NFT.
|
|
1481
|
-
//
|
|
1560
|
+
// One the project holds goes with the last part; one it doesn't hold has its numbers moved by the first call (the one
|
|
1561
|
+
// that splits), its Sheet with it when they go there. Any other part's piece is a stand-in.
|
|
1562
|
+
const carries = order.held ? last : splits;
|
|
1482
1563
|
const receiver = last ? receiverOf(order) : null;
|
|
1483
|
-
const on =
|
|
1564
|
+
const on = carries
|
|
1484
1565
|
? orderPiece(project, orderAddress, order, subject, handBack && order.fate?.then?.Give ? order.player : receiver)
|
|
1485
1566
|
: standIn(orderAddress);
|
|
1486
1567
|
return lootIx(
|
|
@@ -1611,11 +1692,32 @@ export function refundIx({ signer, orderAddress, order, subject, programs, extra
|
|
|
1611
1692
|
* a free roll back to waiting (`Waiting`), for a restock or for its player to end it so. Once asked, every later ask
|
|
1612
1693
|
* comes in a later slot than the last (`SameSlot`, `rollAskable`).
|
|
1613
1694
|
*/
|
|
1614
|
-
export function askRollIx({
|
|
1695
|
+
export function askRollIx({
|
|
1696
|
+
payer,
|
|
1697
|
+
orderAddress,
|
|
1698
|
+
order,
|
|
1699
|
+
box,
|
|
1700
|
+
settler = NOBODY_KEY,
|
|
1701
|
+
poolMints,
|
|
1702
|
+
items = {},
|
|
1703
|
+
programs,
|
|
1704
|
+
shown = null,
|
|
1705
|
+
first = firstAsk(order, box),
|
|
1706
|
+
}) {
|
|
1615
1707
|
poolMints = boxPoolMints(box, poolMints);
|
|
1616
1708
|
const opens = prizeMints(box.entries, poolMints).map(({ mint }) =>
|
|
1617
1709
|
createAssociatedTokenAccountIdempotent(payer, order.project, mint, tokenProgramOf(programs, mint)),
|
|
1618
1710
|
);
|
|
1711
|
+
// A roll's odds are fixed at its first ask: then its player's profile, when the box's odds read one, and an NFT its
|
|
1712
|
+
// player shows, when they read an NFT and the roll is on none. A later ask reads nothing.
|
|
1713
|
+
const reads = askReads(box, order);
|
|
1714
|
+
const player = order.player ?? payer;
|
|
1715
|
+
const odds = first
|
|
1716
|
+
? [
|
|
1717
|
+
...(reads.profile ? [meta(pda.profile(order.project, player), false, false)] : []),
|
|
1718
|
+
...(reads.nft ? shownAccounts(order.project, shown) : []),
|
|
1719
|
+
]
|
|
1720
|
+
: [];
|
|
1619
1721
|
const ask = lootIx(
|
|
1620
1722
|
'ask_roll',
|
|
1621
1723
|
{ settler },
|
|
@@ -1624,12 +1726,71 @@ export function askRollIx({ payer, orderAddress, order, box, settler = NOBODY_KE
|
|
|
1624
1726
|
remaining: [
|
|
1625
1727
|
...entryAccounts(order.project, box.entries, items),
|
|
1626
1728
|
...prizeAccounts(order.project, box.entries, poolMints, programs),
|
|
1729
|
+
...odds,
|
|
1627
1730
|
],
|
|
1628
1731
|
},
|
|
1629
1732
|
);
|
|
1630
1733
|
return [...opens, ask];
|
|
1631
1734
|
}
|
|
1632
1735
|
|
|
1736
|
+
/**
|
|
1737
|
+
* What a roll's first ask reads for its box's odds (`ask_roll`): `profile`, the order's player's profile, when any odds
|
|
1738
|
+
* rule of the box reads a profile; `nft`, an NFT the player shows, when any reads an NFT and the roll is on none (neither
|
|
1739
|
+
* the box nor the order names one). Unlike a purchase, a gate is not read at the ask.
|
|
1740
|
+
*/
|
|
1741
|
+
export function askReads(box, order = {}) {
|
|
1742
|
+
const rules = (box.odds ?? []).map(o => o.boost.rule);
|
|
1743
|
+
const onNft = Boolean(box.subject) || (Boolean(order.subject) && order.subject !== NO_ADDRESS);
|
|
1744
|
+
return {
|
|
1745
|
+
profile: rules.some(r => r.of === 'Profile' || r.of?.Profile !== undefined),
|
|
1746
|
+
nft: !onNft && rules.some(r => !(r.of === 'Profile' || r.of?.Profile !== undefined)),
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
/**
|
|
1751
|
+
* Whether the next ask of `order` is its first, the one that reads the odds' accounts: it was never asked
|
|
1752
|
+
* (`asked_first` 0, or an order not made yet), or its box was edited since (the edit cleared what it read).
|
|
1753
|
+
*/
|
|
1754
|
+
export const firstAsk = (order, box) =>
|
|
1755
|
+
!order.asked_first ||
|
|
1756
|
+
BigInt(order.asked_first) === 0n ||
|
|
1757
|
+
(order.box_version != null && order.box_version !== box.version);
|
|
1758
|
+
|
|
1759
|
+
/**
|
|
1760
|
+
* How much each NFT of `nfts` would raise a box's odds at a roll's first ask, by the box's rules on an NFT, and the one
|
|
1761
|
+
* that raises the most: { best, raised }. `nfts`: [{ asset, itemId, attributes }] as read (a Core collectible of the
|
|
1762
|
+
* project's: its item and its attributes). `raised` maps each asset to the basis points its rules add in all. A rule
|
|
1763
|
+
* on time (`Ahead`, `Passed`, `Within`, `Older`) or on another collection counts nothing here; pass such an NFT by name.
|
|
1764
|
+
*/
|
|
1765
|
+
export function bestShown(box, nfts) {
|
|
1766
|
+
const value = (nft, key) => (key === 'item_id' ? nft.itemId : nft.attributes?.[key]);
|
|
1767
|
+
const passes = (rule, nft) => {
|
|
1768
|
+
if (rule.of?.Item !== undefined && Number(nft.itemId) !== Number(rule.of.Item[0])) return false;
|
|
1769
|
+
if (rule.of?.Collection !== undefined || rule.of === 'Profile' || rule.of?.Profile !== undefined) return false;
|
|
1770
|
+
const v = value(nft, rule.key);
|
|
1771
|
+
const t = rule.test;
|
|
1772
|
+
if (t === 'Any' || t?.Any !== undefined) return true;
|
|
1773
|
+
if (t?.AtLeast) return v !== undefined && Number(v) >= Number(t.AtLeast[0]);
|
|
1774
|
+
if (t?.AtMost) return v !== undefined && Number(v) <= Number(t.AtMost[0]);
|
|
1775
|
+
if (t?.OneOf) return v !== undefined && t.OneOf.texts.map(String).includes(String(v));
|
|
1776
|
+
return false;
|
|
1777
|
+
};
|
|
1778
|
+
const raise = (boost, nft) => {
|
|
1779
|
+
const e = boost.effect;
|
|
1780
|
+
if (e.Flat) return BigInt(e.Flat[0]);
|
|
1781
|
+
const points = BigInt(Math.max(0, Math.floor(Number(value(nft, boost.rule.key)) || 0)));
|
|
1782
|
+
return BigInt(e.PerPoint.bps) * (points < BigInt(e.PerPoint.most) ? points : BigInt(e.PerPoint.most));
|
|
1783
|
+
};
|
|
1784
|
+
const raised = {};
|
|
1785
|
+
let best = null;
|
|
1786
|
+
for (const nft of nfts) {
|
|
1787
|
+
const total = (box.odds ?? []).reduce((n, o) => n + (passes(o.boost.rule, nft) ? raise(o.boost, nft) : 0n), 0n);
|
|
1788
|
+
raised[nft.asset] = total;
|
|
1789
|
+
if (total > 0n && (best === null || total > raised[best])) best = nft.asset;
|
|
1790
|
+
}
|
|
1791
|
+
return { best, raised };
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1633
1794
|
// ---------------------------------------------------------------- out: the project's tokens move
|
|
1634
1795
|
|
|
1635
1796
|
/**
|
|
@@ -1711,23 +1872,63 @@ export function collectibleIx({
|
|
|
1711
1872
|
);
|
|
1712
1873
|
}
|
|
1713
1874
|
|
|
1875
|
+
/** The program's own keys on a collectible (`asset::FIXED`): which item it is. A make never writes them. */
|
|
1876
|
+
const FIXED_KEYS = ['item_id', 'rarity', 'serial'];
|
|
1877
|
+
|
|
1878
|
+
/**
|
|
1879
|
+
* The attributes a make writes, as the program patches them onto nothing (`write_attributes`): the program's own keys
|
|
1880
|
+
* dropped, a repeated key once with its last value, an empty value taking the key off.
|
|
1881
|
+
*/
|
|
1882
|
+
export function madeAttributes(attributes = []) {
|
|
1883
|
+
const out = [];
|
|
1884
|
+
for (const a of attributes) {
|
|
1885
|
+
if (FIXED_KEYS.includes(a.key)) continue;
|
|
1886
|
+
const at = out.findIndex(x => x.key === a.key);
|
|
1887
|
+
if (a.value === '') {
|
|
1888
|
+
if (at >= 0) out.splice(at, 1);
|
|
1889
|
+
} else if (at >= 0) out[at] = a;
|
|
1890
|
+
else out.push(a);
|
|
1891
|
+
}
|
|
1892
|
+
return out;
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1714
1895
|
/**
|
|
1715
1896
|
* The authority makes one of item `item` (`itemState`: its decoded Item, for its collection and next serial) for each
|
|
1716
1897
|
* of `owners`, in serial order (an owner may come more than once), with `attributes` written over the program's own: a
|
|
1717
1898
|
* collectible at `pda.itemAsset(item, serial)` (`madeAssets` says which), or with `tree`, a compressed NFT minted into
|
|
1718
|
-
* that tree of the project's
|
|
1719
|
-
*
|
|
1899
|
+
* that tree of the project's, whose attributes go in its Sheet (`["sheet", project, asset id]`, after its 9 prize
|
|
1900
|
+
* accounts): `treeMinted` is how many the tree has minted, so the first is `pda.compressedAsset(tree, treeMinted)`. A
|
|
1901
|
+
* switched-off item still makes; one sold out refuses the whole call (`PrizeNotCovered`).
|
|
1720
1902
|
*/
|
|
1721
|
-
export function makeIx({
|
|
1903
|
+
export function makeIx({
|
|
1904
|
+
authority,
|
|
1905
|
+
project,
|
|
1906
|
+
item,
|
|
1907
|
+
itemState,
|
|
1908
|
+
owners,
|
|
1909
|
+
attributes = [],
|
|
1910
|
+
tree = null,
|
|
1911
|
+
treeMinted,
|
|
1912
|
+
reference: ref,
|
|
1913
|
+
}) {
|
|
1722
1914
|
if (!itemState)
|
|
1723
1915
|
throw new Error(
|
|
1724
1916
|
`a make of item ${item} needs the item (itemState: its decoded Item), for its collection and next serial`,
|
|
1725
1917
|
);
|
|
1726
1918
|
if (!owners?.length || owners.some(o => !o)) throw new Error('A make names who gets each one (`owners`).');
|
|
1727
1919
|
const prize = tree ? { Compressed: { item, tree } } : { Item: [item] };
|
|
1920
|
+
// A compressed one with attributes: its Sheet, at the asset id it is minted at.
|
|
1921
|
+
const sheets = Boolean(tree) && madeAttributes(attributes).length > 0;
|
|
1922
|
+
if (sheets && treeMinted == null)
|
|
1923
|
+
throw new Error(
|
|
1924
|
+
"A make of compressed NFTs with attributes writes each one's Sheet: pass how many the tree has minted (`treeMinted`).",
|
|
1925
|
+
);
|
|
1728
1926
|
const remaining = owners.flatMap((owner, made) => [
|
|
1729
1927
|
meta(owner, false, false),
|
|
1730
1928
|
...prizeMetas({ project, prize, winner: owner, items: { [item]: itemState }, made }),
|
|
1929
|
+
...(sheets
|
|
1930
|
+
? [meta(pda.sheet(project, pda.compressedAsset(tree, BigInt(treeMinted) + BigInt(made))), false, true)]
|
|
1931
|
+
: []),
|
|
1731
1932
|
]);
|
|
1732
1933
|
return lootIx(
|
|
1733
1934
|
'collectible',
|
|
@@ -2068,7 +2269,12 @@ export function buyTicketsIx({
|
|
|
2068
2269
|
},
|
|
2069
2270
|
{
|
|
2070
2271
|
remaining: [
|
|
2071
|
-
...gateAccounts(project, {
|
|
2272
|
+
...gateAccounts(project, {
|
|
2273
|
+
gate: lottery.gate,
|
|
2274
|
+
wallet: player,
|
|
2275
|
+
shown: shownFor(project, lottery.gate, player, shown),
|
|
2276
|
+
listProof,
|
|
2277
|
+
}),
|
|
2072
2278
|
...recipients,
|
|
2073
2279
|
...(held ? [...pieceMetas(held), ...held.proof] : []),
|
|
2074
2280
|
...tailOf(extra),
|
|
@@ -2213,6 +2419,38 @@ export function stashNfts(r, stash) {
|
|
|
2213
2419
|
return nfts.map(n => n ?? null);
|
|
2214
2420
|
}
|
|
2215
2421
|
|
|
2422
|
+
/** The most of the lottery's own stash's NFTs one pull takes out for its places (`ASSIGN_DRAWS`), counted in draws: a
|
|
2423
|
+
* named NFT's search counts one, and one more for each `ASSIGN_ROWS_A_DRAW` rows of the stash. A pull after carries on. */
|
|
2424
|
+
export const ASSIGN_DRAWS = 64;
|
|
2425
|
+
export const ASSIGN_ROWS_A_DRAW = 1_000;
|
|
2426
|
+
|
|
2427
|
+
/**
|
|
2428
|
+
* How many of a drawn round's places that win an NFT of the lottery's own stash have theirs out of the stash after one
|
|
2429
|
+
* more pull for a place up to `upto` (`LotteryRound::assign`): `r.assigned`, plus as many as one pull takes, in place
|
|
2430
|
+
* order. `stash` is the lottery's own stash, decoded as it is now (its rows count for a named NFT's search). A pull
|
|
2431
|
+
* stops at a place whose NFT is not out yet, so it carries accounts only for places below this.
|
|
2432
|
+
*/
|
|
2433
|
+
export function pullAssigns(r, stash, upto) {
|
|
2434
|
+
const rows = BigInt(stash?.nfts?.length ?? 0);
|
|
2435
|
+
const end = Math.min(roundWinners(r), Number(upto) + 1);
|
|
2436
|
+
let [k, first, work, assigned] = [0, 0, 0n, Number(r.assigned)];
|
|
2437
|
+
for (const p of r.terms.places) {
|
|
2438
|
+
const times = Number(p.times);
|
|
2439
|
+
const run = [first, first + times];
|
|
2440
|
+
first += times;
|
|
2441
|
+
if (!ownStash(p)) continue;
|
|
2442
|
+
for (let place = run[0]; place < run[1]; place++, k++) {
|
|
2443
|
+
if (place >= end) return assigned;
|
|
2444
|
+
if (k !== assigned) continue;
|
|
2445
|
+
const cost = p.prize?.Held ? 1n + rows / BigInt(ASSIGN_ROWS_A_DRAW) : 1n;
|
|
2446
|
+
if (work > 0n && work + cost > BigInt(ASSIGN_DRAWS)) return assigned;
|
|
2447
|
+
work += cost;
|
|
2448
|
+
assigned += 1;
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
return assigned;
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2216
2454
|
/** The program's rule for where a part nobody can take goes: the treasury's wallet, then its account of the mint. */
|
|
2217
2455
|
const treasuryAccounts = (treasury, mint, programs) => walletAccounts(treasury, mint, programs);
|
|
2218
2456
|
|
|
@@ -2233,9 +2471,15 @@ const treasuryAccounts = (treasury, mint, programs) => walletAccounts(treasury,
|
|
|
2233
2471
|
* cancel (past it), paid where it goes (`holders`, `programs`); `toTreasury` when that can't take it
|
|
2234
2472
|
* (a pool never opened, the pot closed since or made again in another mint, an account gone: a
|
|
2235
2473
|
* holder nobody is is found here).
|
|
2236
|
-
* The pull that leaves nothing owed closes the round (`projectState`: its treasury and owner
|
|
2237
|
-
*
|
|
2238
|
-
*
|
|
2474
|
+
* The pull that leaves nothing owed closes the round (`projectState`: its treasury and owner). While the lottery is
|
|
2475
|
+
* there, the close names it for the rent it lent; once it is gone (`lotteryClosed`), that rent goes to the project's
|
|
2476
|
+
* owner, and only the owner's own closing pull may send it: anyone else's leaves the close off and the round waits. A
|
|
2477
|
+
* round that owes nothing and waits only for its close is closed by any pull. A place's prize that is not tokens is
|
|
2478
|
+
* given as far as `prize.most` places a pull (a run of rolls: two accounts a place; size it from a dry run), and the
|
|
2479
|
+
* lottery's own stash's NFTs as far as one pull takes them out (`pullAssigns`): a pull that stops short carries no
|
|
2480
|
+
* close, and the next carries on. Every pull of one entry's places names the same `nonce`: a place's roll is made at
|
|
2481
|
+
* `prizeRollNonce(nonce + place)`, so a later pull with another nonce can land on an order an earlier pull made and fail
|
|
2482
|
+
* ("already in use"). js/play names `prizeNonce(round, entry)` for every pull of an entry, whoever sends it.
|
|
2239
2483
|
*/
|
|
2240
2484
|
export function pullIx({
|
|
2241
2485
|
payer,
|
|
@@ -2312,6 +2556,11 @@ export function pullIx({
|
|
|
2312
2556
|
const places = prizePlacesOwed(r, entry, run);
|
|
2313
2557
|
const own = ownStash(r.terms.places[run]);
|
|
2314
2558
|
const nfts = own ? stashNfts(r, prize.stash) : [];
|
|
2559
|
+
// A pull takes out at most `ASSIGN_DRAWS` of the stash's NFTs, up to the entry's last place in the run, and stops at
|
|
2560
|
+
// a place whose NFT is not out yet: accounts only for the places out after it (`pullAssigns`).
|
|
2561
|
+
const out = own
|
|
2562
|
+
? pullAssigns(r, prize.stash, placesIn(r, run, r.entries[entry].start, r.entries[entry].end).at(-1) ?? 0)
|
|
2563
|
+
: 0;
|
|
2315
2564
|
if (own) rest.push(meta(pda.lotteryStash(r.lottery), false, true));
|
|
2316
2565
|
const handed = asset =>
|
|
2317
2566
|
prize.hold
|
|
@@ -2322,6 +2571,10 @@ export function pullIx({
|
|
|
2322
2571
|
})();
|
|
2323
2572
|
let [carried, made, stashed] = [0, 0, false];
|
|
2324
2573
|
for (const place of places) {
|
|
2574
|
+
if (own && nftSlot(r, place) >= out) {
|
|
2575
|
+
partial = true;
|
|
2576
|
+
break;
|
|
2577
|
+
}
|
|
2325
2578
|
if (own && nfts[nftSlot(r, place)] == null) continue;
|
|
2326
2579
|
if (prize.most != null && carried >= prize.most) {
|
|
2327
2580
|
partial = true;
|
|
@@ -2357,8 +2610,13 @@ export function pullIx({
|
|
|
2357
2610
|
}
|
|
2358
2611
|
settled = places.length;
|
|
2359
2612
|
}
|
|
2613
|
+
// Rent the lottery lent goes back to it, or once it is closed, to the project's owner, and only when the owner sends
|
|
2614
|
+
// the close: anyone else's closing pull of such a round leaves the close off, and the round waits for the owner.
|
|
2615
|
+
const lent = r.opener === r.lottery || r.accounts.some(o => o.mint !== NO_ADDRESS && o.by === r.lottery);
|
|
2616
|
+
const waitsForOwner = lotteryClosed && lent && payer !== projectState?.owner;
|
|
2360
2617
|
const closes =
|
|
2361
|
-
|
|
2618
|
+
!waitsForOwner &&
|
|
2619
|
+
(finished || (!partial && (r.status === 'Drawn' || r.status === 'Cancelled') && Number(r.owed) <= settled));
|
|
2362
2620
|
const owner = lotteryClosed ? projectState?.owner : null;
|
|
2363
2621
|
if (closes) rest.push(...roundCloseMetas(key, r, owner));
|
|
2364
2622
|
const opener = owner && r.opener === r.lottery ? owner : r.opener;
|
|
@@ -2381,13 +2639,17 @@ export function pullIx({
|
|
|
2381
2639
|
}
|
|
2382
2640
|
|
|
2383
2641
|
/** The orders the rolls a `{ Prize: [run] }` pull of entry `entry` makes (a place's prize that is a roll), each with its
|
|
2384
|
-
* box: [{ order, box }], at `prizeRollNonce(nonce + place)` for the entry's buyer
|
|
2385
|
-
|
|
2642
|
+
* box: [{ order, box }], at `prizeRollNonce(nonce + place)` for the entry's buyer, the first `most` of its places owed
|
|
2643
|
+
* when the pull carries no more (`pullIx`'s `prize.most`). Ask each in the pull's transaction. Every pull of one entry's
|
|
2644
|
+
* places names the same `nonce` (a different one can land on an order an earlier pull made, "already in use"): js/play
|
|
2645
|
+
* names `prizeNonce(round, entry)` for all of them. */
|
|
2646
|
+
export function pullRolls({ project, round: r, entry, part, nonce = 0n, most = null }) {
|
|
2386
2647
|
const run = Number(part?.Prize?.[0] ?? -1);
|
|
2387
2648
|
const given = r.terms.places[run]?.prize;
|
|
2388
2649
|
if (!given?.Roll) return [];
|
|
2389
2650
|
const buyer = r.entries[entry].buyer;
|
|
2390
|
-
|
|
2651
|
+
const places = prizePlacesOwed(r, entry, run);
|
|
2652
|
+
return (most == null ? places : places.slice(0, most)).map(place => ({
|
|
2391
2653
|
order: pda.order(project, buyer, prizeRollNonce(BigInt.asUintN(64, toBigInt(nonce) + BigInt(place)))),
|
|
2392
2654
|
box: given.Roll[0],
|
|
2393
2655
|
}));
|
|
@@ -3233,7 +3495,9 @@ export const stakeNftIx = ({
|
|
|
3233
3495
|
|
|
3234
3496
|
/**
|
|
3235
3497
|
* `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
|
|
3236
|
-
* Sheet), and from its own seat takes its tier's prize. `forfeit` gives up the rewards and the prize.
|
|
3498
|
+
* Sheet), and from its own seat takes its tier's prize. `forfeit` gives up the rewards and the prize. `needPrize` (true
|
|
3499
|
+
* by default): a mission prize that can't be given refuses the unstake (`PrizeNotCovered`) and the seat stays; false
|
|
3500
|
+
* lets the NFT go home without it (`Unseated.prize_missed`). Leaving a tier
|
|
3237
3501
|
* that rolls (`roll`: its box id, `boxes` its decoded box by id), the NFT is rolled on that box, in an order named by
|
|
3238
3502
|
* `nonce` (`namedRollNonce`: the NFT's own on a box that holds it; its player or the project's keys then ask the oracle,
|
|
3239
3503
|
* `askRollIx`, and settles); a box that can burn it or give it away keeps it in the project's hands until then. `more`
|
|
@@ -3252,6 +3516,7 @@ export const unstakeNftIx = ({
|
|
|
3252
3516
|
tokenMetadata: tm,
|
|
3253
3517
|
compressed,
|
|
3254
3518
|
forfeit = false,
|
|
3519
|
+
needPrize = true,
|
|
3255
3520
|
roll = null,
|
|
3256
3521
|
nonce = 0n,
|
|
3257
3522
|
tier,
|
|
@@ -3314,7 +3579,7 @@ export const unstakeNftIx = ({
|
|
|
3314
3579
|
]);
|
|
3315
3580
|
return lootIx(
|
|
3316
3581
|
'unstake_nft',
|
|
3317
|
-
{ forfeit, nonce },
|
|
3582
|
+
{ forfeit, nonce, need_prize: needPrize },
|
|
3318
3583
|
{
|
|
3319
3584
|
...seat.accounts,
|
|
3320
3585
|
loot_box: first.rolls.box,
|
|
@@ -3459,9 +3724,14 @@ export const refreshSeatIx = ({
|
|
|
3459
3724
|
* (`items[id].collection`, the decoded Items; `collection` when not given), written for a burn: Core writes it as it
|
|
3460
3725
|
* burns a collectible. A recipe's `raises` write on the kept ones, in their Sheet when the project has no
|
|
3461
3726
|
* authority over them. The player opens any account a line pays into that is not there. A roll it makes (a
|
|
3462
|
-
* `Prize::Roll` or a `RollOn`) passes its box and its order, nothing more
|
|
3463
|
-
*
|
|
3464
|
-
*
|
|
3727
|
+
* `Prize::Roll` or a `RollOn`) passes its box and its order, nothing more. A recipe may give several rolls: each is at
|
|
3728
|
+
* the craft's `nonce` plus the rolls before it in the recipe's order (`craftRollNonces`); a `RollOn`'s order is where
|
|
3729
|
+
* `namedRollNonce` says of that, which reads the rolled box (`boxes`: a box's id to its decoded LootBox). Its player or
|
|
3730
|
+
* the project's keys then ask for the roll (`askRollIx`, in the same transaction when it fits).
|
|
3731
|
+
* A payment line nobody can take goes to the treasury (`Crafted.to_treasury`): a collectible nobody holds or the project
|
|
3732
|
+
* holds (`holders`: who holds each now, as read when building; if it changes hands before the craft lands, the craft
|
|
3733
|
+
* fails cleanly), the project's own address, or an address `lootAccounts` names (wallets read as accounts of the loot
|
|
3734
|
+
* program). The treasury's wallet and its account then follow that line's accounts (`treasury`).
|
|
3465
3735
|
* A craft makes several Core calls: raise the compute limit. `extra`: a token's hook accounts.
|
|
3466
3736
|
*/
|
|
3467
3737
|
export function craftIx({
|
|
@@ -3480,6 +3750,7 @@ export function craftIx({
|
|
|
3480
3750
|
items = {},
|
|
3481
3751
|
nfts = {},
|
|
3482
3752
|
boxes = {},
|
|
3753
|
+
lootAccounts = [],
|
|
3483
3754
|
extra = [],
|
|
3484
3755
|
}) {
|
|
3485
3756
|
const remaining = [];
|
|
@@ -3527,10 +3798,16 @@ export function craftIx({
|
|
|
3527
3798
|
}
|
|
3528
3799
|
} else if (input.Pay) {
|
|
3529
3800
|
const { mint, to } = input.Pay;
|
|
3801
|
+
if (!treasury && paysNowhere(project, to, { holders, lootAccounts }))
|
|
3802
|
+
throw new Error(
|
|
3803
|
+
"craft: a payment line nobody can take goes to the project's treasury: pass its wallet (`treasury`).",
|
|
3804
|
+
);
|
|
3530
3805
|
remaining.push(
|
|
3531
3806
|
meta(mint, false, true),
|
|
3532
3807
|
meta(tokens(mint), false, true),
|
|
3533
3808
|
...destinationAccounts(project, to, mint, { holders, programs, treasury }),
|
|
3809
|
+
// A line nobody can take goes to the treasury: its wallet and its account follow the line's own.
|
|
3810
|
+
...(paysNowhere(project, to, { holders, lootAccounts }) ? treasuryAccounts(treasury, mint, programs) : []),
|
|
3534
3811
|
);
|
|
3535
3812
|
} else if (input.Trade) {
|
|
3536
3813
|
// Any NFT of the line's collection, traded in: burned, given on or stashed (`nftPaid`).
|
|
@@ -3583,6 +3860,10 @@ export function craftIx({
|
|
|
3583
3860
|
}
|
|
3584
3861
|
}
|
|
3585
3862
|
const made = {};
|
|
3863
|
+
// Each roll the recipe gives (a `Prize::Roll` or a `RollOn`) is at the craft's nonce plus the rolls before it, in the
|
|
3864
|
+
// recipe's order: the first at `nonce`, the next at `nonce + 1`.
|
|
3865
|
+
let rolls = 0n;
|
|
3866
|
+
const nextRoll = () => BigInt.asUintN(64, toBigInt(nonce) + rolls++);
|
|
3586
3867
|
for (const output of recipe.outputs) {
|
|
3587
3868
|
if (output.Prize) {
|
|
3588
3869
|
// A prize (`prizeMetas`): collectibles of one item made in order, each at the next serial.
|
|
@@ -3594,7 +3875,7 @@ export function craftIx({
|
|
|
3594
3875
|
collection,
|
|
3595
3876
|
prize,
|
|
3596
3877
|
winner: player,
|
|
3597
|
-
nonce,
|
|
3878
|
+
nonce: prize.Roll ? nextRoll() : nonce,
|
|
3598
3879
|
poolMints,
|
|
3599
3880
|
items,
|
|
3600
3881
|
nft: nfts[prize.Nft?.nft],
|
|
@@ -3604,7 +3885,8 @@ export function craftIx({
|
|
|
3604
3885
|
);
|
|
3605
3886
|
if (item !== undefined) made[item] = (made[item] ?? 0) + 1;
|
|
3606
3887
|
} else if (output.RollOn) {
|
|
3607
|
-
// The box, then the order its roll is made in (`namedRollNonce` of
|
|
3888
|
+
// The box, then the order its roll is made in (`namedRollNonce` of its nonce: on an NFT its box holds, that NFT's
|
|
3889
|
+
// own, though it still counts in the numbering).
|
|
3608
3890
|
const on = assets[output.RollOn.place];
|
|
3609
3891
|
const order = pda.order(
|
|
3610
3892
|
project,
|
|
@@ -3612,7 +3894,7 @@ export function craftIx({
|
|
|
3612
3894
|
namedRollNonce(
|
|
3613
3895
|
boxes[output.RollOn.box_id],
|
|
3614
3896
|
typeof on === 'string' ? on : (on?.asset ?? on?.mint ?? on?.compressed),
|
|
3615
|
-
|
|
3897
|
+
nextRoll(),
|
|
3616
3898
|
),
|
|
3617
3899
|
);
|
|
3618
3900
|
remaining.push(meta(pda.box(project, output.RollOn.box_id), false, false), meta(order, false, true));
|
|
@@ -3628,6 +3910,33 @@ export function craftIx({
|
|
|
3628
3910
|
);
|
|
3629
3911
|
}
|
|
3630
3912
|
|
|
3913
|
+
/**
|
|
3914
|
+
* Whether a payment to `to` finds nobody to take it (`rail::Target::Nowhere`), so a recipe's line goes to the treasury:
|
|
3915
|
+
* a collectible nobody holds, or the project holds (`holders`: who holds each now); a wallet that is the project's own
|
|
3916
|
+
* address or an account of the loot program (`lootAccounts`, as read). A pool or a token account always takes it.
|
|
3917
|
+
*/
|
|
3918
|
+
export function paysNowhere(project, to, { holders = {}, lootAccounts = [] } = NO_OPTIONS) {
|
|
3919
|
+
const program = wallet => wallet === project || lootAccounts.includes(wallet);
|
|
3920
|
+
if (to?.Wallet) return program(to.Wallet[0]);
|
|
3921
|
+
if (to?.Holder) {
|
|
3922
|
+
const holder = holders[to.Holder[0]];
|
|
3923
|
+
return !holder || holder === NO_ADDRESS || program(holder);
|
|
3924
|
+
}
|
|
3925
|
+
return false;
|
|
3926
|
+
}
|
|
3927
|
+
|
|
3928
|
+
/**
|
|
3929
|
+
* The nonce each roll a craft of `recipe` makes is named at, in the recipe's outputs' order: [{ output, nonce }], the
|
|
3930
|
+
* first at `nonce`, each next one more. A `RollOn` on an NFT its box holds is made at that NFT's own nonce
|
|
3931
|
+
* (`namedRollNonce`), but counts all the same.
|
|
3932
|
+
*/
|
|
3933
|
+
export function craftRollNonces(recipe, nonce = 0n) {
|
|
3934
|
+
let k = 0n;
|
|
3935
|
+
return recipe.outputs.flatMap((output, i) =>
|
|
3936
|
+
output.RollOn || output.Prize?.[0]?.Roll ? [{ output: i, nonce: BigInt.asUintN(64, toBigInt(nonce) + k++) }] : [],
|
|
3937
|
+
);
|
|
3938
|
+
}
|
|
3939
|
+
|
|
3631
3940
|
function take(list, what) {
|
|
3632
3941
|
if (!list.length) throw new Error(`craft: missing ${what}`);
|
|
3633
3942
|
return list.shift();
|
|
@@ -3754,6 +4063,55 @@ export function stashIx({
|
|
|
3754
4063
|
);
|
|
3755
4064
|
}
|
|
3756
4065
|
|
|
4066
|
+
/**
|
|
4067
|
+
* How many NFTs of one kind one `stash` call puts in, measured on the program (`tests/v026_stash_batch.rs`): in a legacy
|
|
4068
|
+
* transaction, with a lookup table, and with a lookup table and the most memory asked for. A compressed NFT needs its
|
|
4069
|
+
* proof in the same transaction (`proveLeafIx`): one a call.
|
|
4070
|
+
*/
|
|
4071
|
+
export const STASH_BATCH = Object.freeze({
|
|
4072
|
+
own: [19, 26, 29],
|
|
4073
|
+
core: [10, 24, 27],
|
|
4074
|
+
tokenMetadata: [3, 7, 7],
|
|
4075
|
+
programmable: [2, 4, 4],
|
|
4076
|
+
compressed: [1, 1, 1],
|
|
4077
|
+
});
|
|
4078
|
+
|
|
4079
|
+
/** The kind of an NFT as `stashIx` takes it, for `STASH_BATCH`: the project's own Core collectible ('own'), another
|
|
4080
|
+
* collection's Core NFT ('core', `hasAuthority` false), a Token Metadata NFT ('tokenMetadata', or 'programmable' for a
|
|
4081
|
+
* pNFT) or a compressed one ('compressed'). */
|
|
4082
|
+
export const stashKind = n =>
|
|
4083
|
+
n.compressed
|
|
4084
|
+
? 'compressed'
|
|
4085
|
+
: n.tokenMetadata
|
|
4086
|
+
? n.tokenMetadata.programmable
|
|
4087
|
+
? 'programmable'
|
|
4088
|
+
: 'tokenMetadata'
|
|
4089
|
+
: n.hasAuthority === false
|
|
4090
|
+
? 'core'
|
|
4091
|
+
: 'own';
|
|
4092
|
+
|
|
4093
|
+
/**
|
|
4094
|
+
* A long list of NFTs to put in a stash (each { asset, weight, collection, hasAuthority, tokenMetadata, compressed }, as
|
|
4095
|
+
* `stashIx` takes them), split into calls of one kind each, as many a call as the measured sizes allow (`STASH_BATCH`):
|
|
4096
|
+
* `lookupTable` when the transactions carry one, `heap` when they also ask for the most memory. Returns the calls, each
|
|
4097
|
+
* a list of NFTs: the first is a call's own (`asset`…), the rest its `more`. Each call is one project fee.
|
|
4098
|
+
*/
|
|
4099
|
+
export function stashBatches(nfts, { lookupTable = false, heap = false } = NO_OPTIONS) {
|
|
4100
|
+
const column = lookupTable ? (heap ? 2 : 1) : 0;
|
|
4101
|
+
const byKind = new Map();
|
|
4102
|
+
for (const n of nfts) {
|
|
4103
|
+
const kind = stashKind(n);
|
|
4104
|
+
if (!byKind.has(kind)) byKind.set(kind, []);
|
|
4105
|
+
byKind.get(kind).push(n);
|
|
4106
|
+
}
|
|
4107
|
+
const calls = [];
|
|
4108
|
+
for (const [kind, list] of byKind) {
|
|
4109
|
+
const size = STASH_BATCH[kind][column];
|
|
4110
|
+
for (let i = 0; i < list.length; i += size) calls.push(list.slice(i, i + size));
|
|
4111
|
+
}
|
|
4112
|
+
return calls;
|
|
4113
|
+
}
|
|
4114
|
+
|
|
3757
4115
|
// ---------------------------------------------------------------- contests
|
|
3758
4116
|
|
|
3759
4117
|
/**
|
|
@@ -3792,7 +4150,12 @@ export function enterIx({
|
|
|
3792
4150
|
},
|
|
3793
4151
|
{
|
|
3794
4152
|
remaining: [
|
|
3795
|
-
...gateAccounts(project, {
|
|
4153
|
+
...gateAccounts(project, {
|
|
4154
|
+
gate: contest.gate,
|
|
4155
|
+
wallet: player,
|
|
4156
|
+
shown: shownFor(project, contest.gate, player, shown),
|
|
4157
|
+
listProof,
|
|
4158
|
+
}),
|
|
3796
4159
|
...(staked ? [...pieceMetas(staked), ...staked.proof] : []),
|
|
3797
4160
|
...tailOf(extra),
|
|
3798
4161
|
],
|
|
@@ -3968,11 +4331,13 @@ export function leastBid(auction) {
|
|
|
3968
4331
|
|
|
3969
4332
|
/**
|
|
3970
4333
|
* `seller` opens its auction `id` of `args.lot` ({ Item: [id] }, { Asset: [address] } or { Roll: [box id] }), alone,
|
|
3971
|
-
* and pays its rent:
|
|
3972
|
-
*
|
|
3973
|
-
*
|
|
3974
|
-
*
|
|
3975
|
-
* address names the seller, so an id
|
|
4334
|
+
* and pays its rent: anyone for an NFT of any kind and any collection they hold (`piece`: `collection` a Core asset's
|
|
4335
|
+
* own, `tokenMetadata`, or `compressed` after a `proveLeafIx`); the project's owner or authority also for a new unit
|
|
4336
|
+
* of its item (one the shop has switched off too), or a roll of one of its boxes (a mystery lot: the box must be
|
|
4337
|
+
* written and make an order with the auction's shares within one call's growth, `LotRefused`; the auction's shares
|
|
4338
|
+
* split the winning bid, so a share of the box that burns burns nothing here). Its address names the seller, so an id
|
|
4339
|
+
* is the seller's own. An NFT lot of another collection can be stopped or taken back by whoever controls that
|
|
4340
|
+
* collection while the project holds it: its seller is paid only when it reaches the winner (`endAuctionIx`).
|
|
3976
4341
|
*/
|
|
3977
4342
|
export function openAuctionIx({ seller, project, collection, tokenMetadata: tm, compressed, id, mint, args }) {
|
|
3978
4343
|
stored(args.shares);
|
|
@@ -4065,18 +4430,89 @@ export function bidIx({
|
|
|
4065
4430
|
);
|
|
4066
4431
|
}
|
|
4067
4432
|
|
|
4433
|
+
/** How an auction's end goes, as `end_auction`'s `ending` takes it. With no bid each is the same: the lot goes home. */
|
|
4434
|
+
export const ENDINGS = Object.freeze(['Deliver', 'LotFirst', 'CallOff']);
|
|
4435
|
+
|
|
4068
4436
|
/**
|
|
4069
|
-
*
|
|
4070
|
-
*
|
|
4071
|
-
*
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4437
|
+
* A Token Metadata NFT's move names two different token accounts, the one it leaves and the one it goes into: Token
|
|
4438
|
+
* Metadata takes a move from an account to itself as done and moves nothing, so the program refuses it (`SameAccount`).
|
|
4439
|
+
* Throws, in words, before anything is signed.
|
|
4440
|
+
*/
|
|
4441
|
+
function twoAccounts(p) {
|
|
4442
|
+
const { token, project_token: other } = p.accounts;
|
|
4443
|
+
if (token && other && token === other)
|
|
4444
|
+
throw new Error(
|
|
4445
|
+
`The NFT ${p.accounts.asset} would move from the token account ${token} to itself: a move names two different accounts (SameAccount).`,
|
|
4446
|
+
);
|
|
4447
|
+
return p;
|
|
4448
|
+
}
|
|
4449
|
+
|
|
4450
|
+
/**
|
|
4451
|
+
* What shows whether the project still holds an NFT, for a route that reads it and moves nothing (`piece::held`): a Core
|
|
4452
|
+
* asset by its own address; a Token Metadata NFT by the project's token account of it (`project_token`), and, once it
|
|
4453
|
+
* left, the account that holds it now or its mint when it was burned (`evidence`, in `token`'s place); a compressed NFT
|
|
4454
|
+
* by its record and tree (`proveLeafIx` first, in the same transaction), its proof first after the instruction's own
|
|
4455
|
+
* accounts, as the program reads it whenever the record and tree are passed.
|
|
4456
|
+
*/
|
|
4457
|
+
function heldPiece({ project, asset, tokenMetadata: tm, compressed: c, evidence = null }) {
|
|
4458
|
+
if (c)
|
|
4459
|
+
return {
|
|
4460
|
+
kind: 'compressed',
|
|
4461
|
+
accounts: { ...NO_PIECE, asset: c.id, leaf: pda.leaf(c.id), tree: c.tree },
|
|
4462
|
+
proof: c.proof.map(node => meta(typeof node === 'string' ? node : encodeBase58(node), false, false)),
|
|
4463
|
+
writable: [],
|
|
4464
|
+
};
|
|
4465
|
+
if (tm)
|
|
4466
|
+
return twoAccounts({
|
|
4467
|
+
kind: 'tokenMetadata',
|
|
4468
|
+
accounts: { ...NO_PIECE, asset, project_token: ata(project, asset, TOKEN_PROGRAM), token: evidence },
|
|
4469
|
+
proof: [],
|
|
4470
|
+
writable: [],
|
|
4471
|
+
});
|
|
4472
|
+
return { kind: 'core', ...standIn(asset) };
|
|
4473
|
+
}
|
|
4474
|
+
|
|
4475
|
+
/**
|
|
4476
|
+
* An NFT lot's or bid's piece on its way to `wallet`, with `evidence` (a Token Metadata NFT's account that holds it now,
|
|
4477
|
+
* or its mint when burned) in the receiver's account's place: the end reads the project's account first, and only when
|
|
4478
|
+
* that shows nothing does it read this one, which then shows the NFT gone.
|
|
4479
|
+
*/
|
|
4480
|
+
function evidencedPiece({ project, asset, wallet, collection, tokenMetadata: tm, compressed, evidence = null }) {
|
|
4481
|
+
const p = piece({ project, asset, wallet, collection, tokenMetadata: tm, compressed, ...ONLY_MOVES });
|
|
4482
|
+
if (tm && evidence) p.accounts.token = evidence;
|
|
4483
|
+
return twoAccounts(p);
|
|
4484
|
+
}
|
|
4485
|
+
|
|
4486
|
+
/**
|
|
4487
|
+
* Anyone ends auction `id` once it is over (its seller calls off one with no bid). `auction` is the decoded Auction,
|
|
4488
|
+
* `ending` one of `ENDINGS` (with no bid each is the same: the lot goes home to its seller, or stays where it is when it
|
|
4489
|
+
* is proven gone). With a bid in:
|
|
4490
|
+
* 'Deliver' (the default): the lot goes to the high bidder and the winning bid through the shares to whoever owned the
|
|
4491
|
+
* lot (the seller of an NFT, the treasury for the project's item or roll), in one instruction, however long after the
|
|
4492
|
+
* end it is sent. An item that is a collectible is minted to the winner at `pda.itemAsset(item, serial)`; a roll of a
|
|
4493
|
+
* box is made for them in an order at `prizeRollNonce(nonce)`, `nonce` being `prizeNonce(auction, last_bid_at)` by
|
|
4494
|
+
* default, which `signer` pays for. An NFT lot that can't move now fails the end, which waits: send it again later.
|
|
4495
|
+
* Once the lot went first (`auction.proceeds_owed` above 0), it pays what the bid still owes and closes the auction:
|
|
4496
|
+
* the auction's address stands in for the lot, then the shares' accounts, then a bid's NFTs to the seller.
|
|
4497
|
+
* 'LotFirst': an NFT lot only, for an end too wide for one transaction (a compressed lot's long proof with many shares
|
|
4498
|
+
* and a hooked coin): the lot goes to the winner now and the whole bid is written as owed; a later 'Deliver' pays it.
|
|
4499
|
+
* Its accounts are the lot's piece and proof, then the tail: no shares, no winning NFT.
|
|
4500
|
+
* 'CallOff': an NFT lot's sale, before the lot moved (`CantCallOff` after): the winning bid goes back to its bidder
|
|
4501
|
+
* (tokens into their account, or its NFTs, each moved or made a hold for them) and the lot home to its seller (moved,
|
|
4502
|
+
* or made a hold for them). Anyone may when the lot or an NFT of the bid is proven gone (its collection took it back
|
|
4503
|
+
* or burned it): that one stays gone. Otherwise only the project's owner or authority, for the project fee.
|
|
4504
|
+
* `signer` opens any account the end pays into that is not there. `item` is the decoded Item of an item lot; for an NFT
|
|
4505
|
+
* lot, `collection` its Core collection now, `tokenMetadata` for a Token Metadata NFT, or `compressed` (after a
|
|
4506
|
+
* `proveLeafIx`: a compressed lot's record must be current whenever it is read, with no bid too). The lot's accounts
|
|
4507
|
+
* also show whether the project still holds it: for a Token Metadata lot that left, `evidence` names the token account
|
|
4508
|
+
* that holds it now, or its mint when it was burned. `held`: the kinds of a winning bid's NFTs (`auction.held`, in order:
|
|
4509
|
+
* { collection, tokenMetadata, evidence }).
|
|
4510
|
+
* `hold` (true for every NFT it may apply to, or a list of assets): a lot going home (no bid, or the sale called off) is
|
|
4511
|
+
* made a hold for its seller (its hold passed as `order`, the lot's accounts only showing it is held); an NFT of a bid
|
|
4512
|
+
* going back on a call-off, or paid to the seller after the lot went first, or to the treasury for the project's own
|
|
4513
|
+
* lot, is made a hold for whoever it goes to (its hold's address in its piece's place). Anyone then delivers each with
|
|
4514
|
+
* `releaseIx`, and the end's sender gets the rent back then. A sold lot is never made a hold (`LotRefused`), nor an NFT
|
|
4515
|
+
* of a bid that buys a player's lot: the two move together, or the end waits.
|
|
4080
4516
|
*/
|
|
4081
4517
|
export function endAuctionIx({
|
|
4082
4518
|
signer,
|
|
@@ -4088,57 +4524,88 @@ export function endAuctionIx({
|
|
|
4088
4524
|
collection,
|
|
4089
4525
|
tokenMetadata: tm,
|
|
4090
4526
|
compressed,
|
|
4527
|
+
evidence = null,
|
|
4091
4528
|
held = [],
|
|
4092
4529
|
hold = false,
|
|
4530
|
+
ending = 'Deliver',
|
|
4093
4531
|
reference: ref,
|
|
4094
4532
|
nonce,
|
|
4095
4533
|
holders,
|
|
4096
4534
|
programs,
|
|
4097
4535
|
extra = [],
|
|
4098
4536
|
}) {
|
|
4099
|
-
|
|
4100
|
-
//
|
|
4537
|
+
if (!ENDINGS.includes(ending)) throw new Error(`An auction ends as one of ${ENDINGS.join(', ')}, not “${ending}”.`);
|
|
4538
|
+
// The lot went at an earlier end (`LotFirst`, or a seller's account that couldn't take the money): this end pays what
|
|
4539
|
+
// the bid still owes and closes the auction.
|
|
4101
4540
|
const owed = BigInt(auction.proceeds_owed ?? 0) > 0n;
|
|
4102
|
-
const lotHeld = !owed && Boolean(auction.lot.Asset) && holds(hold, auction.lot.Asset[0]);
|
|
4103
|
-
const tokenProgram = tokenProgramOf(programs, auction.mint);
|
|
4104
4541
|
const sold = BigInt(auction.high) > 0n;
|
|
4542
|
+
const lot = auction.lot.Asset?.[0] ?? null;
|
|
4543
|
+
if (sold && owed && ending === 'CallOff')
|
|
4544
|
+
throw new Error(
|
|
4545
|
+
'The lot went to its winner already: the sale stands, and only Deliver pays what it owes (CantCallOff).',
|
|
4546
|
+
);
|
|
4547
|
+
if (sold && !owed && ending !== 'Deliver' && !lot)
|
|
4548
|
+
throw new Error(
|
|
4549
|
+
`Only an NFT lot is sent first or called off: an item's or a roll's sale ends with Deliver (${ending === 'CallOff' ? 'CantCallOff' : 'LotRefused'}).`,
|
|
4550
|
+
);
|
|
4551
|
+
const calledOff = sold && !owed && ending === 'CallOff';
|
|
4552
|
+
const first = sold && !owed && ending === 'LotFirst';
|
|
4553
|
+
// The lot goes home: no bid, or the sale called off.
|
|
4554
|
+
const home = !sold || calledOff;
|
|
4555
|
+
const nfts = sold && auction.nft_bids && !first ? (auction.held ?? []) : [];
|
|
4556
|
+
// Where a hold may stand in: the lot going home; a bid's NFTs going back, paid after the lot went first, or to the
|
|
4557
|
+
// treasury for the project's own lot.
|
|
4558
|
+
const mayHold = new Set([
|
|
4559
|
+
...(lot && home && !owed ? [lot] : []),
|
|
4560
|
+
...(calledOff || owed || (sold && !lot) ? nfts : []),
|
|
4561
|
+
]);
|
|
4562
|
+
if (hold && (Array.isArray(hold) ? hold.some(a => !mayHold.has(a)) : !mayHold.size))
|
|
4563
|
+
throw new Error(
|
|
4564
|
+
'A sold lot is never made a hold, nor an NFT of the bid that buys a player’s lot: the two move together, or the end waits (LotRefused). Only a lot going home, or a bid’s NFTs going back or paid after the lot went first, may be held.',
|
|
4565
|
+
);
|
|
4566
|
+
const holding = asset => Boolean(hold) && mayHold.has(asset) && holds(hold, asset);
|
|
4567
|
+
const tokenProgram = tokenProgramOf(programs, auction.mint);
|
|
4105
4568
|
const itemId = owed ? undefined : auction.lot.Item?.[0];
|
|
4106
4569
|
const made =
|
|
4107
4570
|
!owed && sold && item?.kind === 'Collectible' ? pda.itemAsset(pda.item(project, itemId), auction.pin) : null;
|
|
4108
|
-
|
|
4109
|
-
const
|
|
4571
|
+
// What the shares leave, to whoever owned the lot; called off, the whole bid back to its bidder.
|
|
4572
|
+
const owner = lot ? auction.seller : projectState.treasury;
|
|
4573
|
+
const proceeds = calledOff ? auction.high_bidder : owner;
|
|
4110
4574
|
const address = pda.auction(project, auction.seller, id);
|
|
4111
|
-
const receiver =
|
|
4575
|
+
const receiver = home ? auction.seller : auction.high_bidder;
|
|
4112
4576
|
// A roll lot sold: its box, and the order the winner's roll is made in. The winning bid stays in the vault as that
|
|
4113
4577
|
// order's payment, split by the auction's shares when it settles (no shares' accounts here).
|
|
4114
4578
|
const roll = sold && !owed ? auction.lot.Roll?.[0] : undefined;
|
|
4115
4579
|
nonce = toBigInt(nonce ?? prizeNonce(address, BigInt(auction.last_bid_at)));
|
|
4116
|
-
|
|
4117
|
-
//
|
|
4580
|
+
const nft = { project, asset: lot, collection, tokenMetadata: tm, compressed, evidence };
|
|
4581
|
+
// An NFT lot is sent on (and shows whether the project still holds it); made a hold, it only shows that. A
|
|
4582
|
+
// collectible sold is minted into its item's collection, which Core writes. After the lot went first, the auction's
|
|
4583
|
+
// own address stands in for it.
|
|
4118
4584
|
const p = owed
|
|
4119
4585
|
? standIn(address)
|
|
4120
|
-
:
|
|
4121
|
-
?
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
// A
|
|
4131
|
-
const
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4586
|
+
: lot
|
|
4587
|
+
? holding(lot)
|
|
4588
|
+
? heldPiece(nft)
|
|
4589
|
+
: evidencedPiece({ ...nft, wallet: receiver })
|
|
4590
|
+
: made
|
|
4591
|
+
? {
|
|
4592
|
+
...piece({ project, asset: made, collection: item.collection, moves: false, locks: false, reads: false }),
|
|
4593
|
+
writable: ['collection'],
|
|
4594
|
+
}
|
|
4595
|
+
: standIn(address);
|
|
4596
|
+
// A bid's NFTs: each to where the rest of a bid in tokens would go, or back to its bidder on a call-off.
|
|
4597
|
+
const to = calledOff ? auction.high_bidder : owner;
|
|
4598
|
+
const bid = nfts.flatMap((asset, i) => {
|
|
4599
|
+
if (holding(asset)) return [meta(pda.hold(asset, 0), false, true)];
|
|
4600
|
+
const q = evidencedPiece({ project, asset, wallet: to, ...held[i] });
|
|
4601
|
+
return [...pieceMetas(q), ...q.proof];
|
|
4602
|
+
});
|
|
4603
|
+
// Money moves on a Deliver of a bid in tokens (the shares' accounts, but for a roll lot's, whose order splits it
|
|
4604
|
+
// later; after the lot went first, every end is the one that pays) and on a call-off (back to the bidder, no shares).
|
|
4605
|
+
const pays = sold && !auction.nft_bids && (ending === 'Deliver' || owed);
|
|
4139
4606
|
return lootIx(
|
|
4140
4607
|
'end_auction',
|
|
4141
|
-
{ reference: reference(ref), nonce },
|
|
4608
|
+
{ reference: reference(ref), nonce, ending },
|
|
4142
4609
|
{
|
|
4143
4610
|
signer,
|
|
4144
4611
|
project,
|
|
@@ -4151,26 +4618,26 @@ export function endAuctionIx({
|
|
|
4151
4618
|
proceeds_token: ata(proceeds, auction.mint, tokenProgram),
|
|
4152
4619
|
item: itemId !== undefined ? pda.item(project, itemId) : null,
|
|
4153
4620
|
...p.accounts,
|
|
4154
|
-
loot_box: roll !== undefined ? pda.box(project, roll) : null,
|
|
4621
|
+
loot_box: roll !== undefined && ending === 'Deliver' ? pda.box(project, roll) : null,
|
|
4155
4622
|
order:
|
|
4156
|
-
roll !== undefined
|
|
4623
|
+
roll !== undefined && ending === 'Deliver'
|
|
4157
4624
|
? pda.order(project, auction.high_bidder, prizeRollNonce(nonce))
|
|
4158
|
-
:
|
|
4159
|
-
? pda.hold(
|
|
4625
|
+
: lot && holding(lot)
|
|
4626
|
+
? pda.hold(lot, 0)
|
|
4160
4627
|
: null,
|
|
4161
4628
|
},
|
|
4162
4629
|
{
|
|
4163
4630
|
remaining: [
|
|
4164
4631
|
...p.proof,
|
|
4165
|
-
...(
|
|
4632
|
+
...(pays && roll === undefined
|
|
4166
4633
|
? shareAccounts(project, auction.shares, auction.mint, { holders, programs, treasury: projectState.treasury })
|
|
4167
4634
|
: []),
|
|
4168
|
-
...
|
|
4635
|
+
...bid,
|
|
4169
4636
|
...tailOf(extra),
|
|
4170
4637
|
],
|
|
4171
4638
|
writable: [
|
|
4172
|
-
...(
|
|
4173
|
-
...(
|
|
4639
|
+
...(pays && sharesBurn(auction.shares) ? ['mint'] : []),
|
|
4640
|
+
...(pays || (calledOff && !auction.nft_bids) ? ['vault'] : []),
|
|
4174
4641
|
...p.writable,
|
|
4175
4642
|
],
|
|
4176
4643
|
},
|
|
@@ -4253,18 +4720,21 @@ export function sellToOfferIx({
|
|
|
4253
4720
|
extra = [],
|
|
4254
4721
|
}) {
|
|
4255
4722
|
const tokenProgram = tokenProgramOf(programs, offer.mint);
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4723
|
+
// A Token Metadata NFT goes straight from the seller's account into the maker's: never one account in both places.
|
|
4724
|
+
const p = twoAccounts(
|
|
4725
|
+
passedOn({
|
|
4726
|
+
project,
|
|
4727
|
+
owner: seller,
|
|
4728
|
+
receiver: offer.maker,
|
|
4729
|
+
burns: false,
|
|
4730
|
+
asset,
|
|
4731
|
+
collection,
|
|
4732
|
+
hasAuthority,
|
|
4733
|
+
tokenMetadata: tm,
|
|
4734
|
+
compressed,
|
|
4735
|
+
reads: true,
|
|
4736
|
+
}),
|
|
4737
|
+
);
|
|
4268
4738
|
return lootIx(
|
|
4269
4739
|
'sell_to_offer',
|
|
4270
4740
|
{ terms: offerTerms(offer) },
|
|
@@ -4341,29 +4811,63 @@ export function borrowIx({
|
|
|
4341
4811
|
|
|
4342
4812
|
/**
|
|
4343
4813
|
* Anyone (`payer`: the borrower, a friend) repays a loan before it is due (`hold` the decoded Hold, with its debt): the
|
|
4344
|
-
* debt's `repay` goes to the lender
|
|
4345
|
-
*
|
|
4346
|
-
*
|
|
4347
|
-
*
|
|
4814
|
+
* debt's `repay` goes to the lender. With `home` (the default), the NFT goes home to its borrower in the same
|
|
4815
|
+
* instruction and the hold closes, its rent (and a Token Metadata NFT's emptied account's) to the borrower; an NFT that
|
|
4816
|
+
* can't move now (frozen, its collection's rule refuses, a proof out of date) fails the whole call, and nobody pays.
|
|
4817
|
+
* Without `home`, only the money moves: the hold becomes a plain one that is over, and anyone sends the NFT home with
|
|
4818
|
+
* `releaseIx` whenever it can move (for an NFT frozen for now, so the loan does not fall due meanwhile). Free.
|
|
4819
|
+
* The NFT is any kind: `collection` for a Core asset, `tokenMetadata` for a Token Metadata NFT (`into`: the borrower's
|
|
4820
|
+
* account it goes into, when their associated one can't take it), `compressed` for a compressed one. Either way its
|
|
4821
|
+
* accounts must show the project still holds it: the program takes no repayment for an NFT its collection took back
|
|
4822
|
+
* or burned (`NotTheNft`), nor one nothing shows (`NotProven`). Going home, a compressed NFT is proven
|
|
4823
|
+
* (`proveLeafIx`, in the same transaction) and its proof goes first after the repayment's accounts; the money alone
|
|
4824
|
+
* passes its record and tree, and goes through when the record shows nothing, so prove it in the same transaction when
|
|
4825
|
+
* that fits and the repayment of an NFT that is gone is refused. `repay`: the debt the payer was shown
|
|
4826
|
+
* (`hold.debt.repay` as read): any other amount is refused (`TermsChanged`). `address`: the hold's, when its `free_at`
|
|
4827
|
+
* changed since its loan named it. `extra`: a transfer hook's accounts.
|
|
4348
4828
|
*/
|
|
4349
|
-
export function repayIx({
|
|
4829
|
+
export function repayIx({
|
|
4830
|
+
payer,
|
|
4831
|
+
project,
|
|
4832
|
+
hold,
|
|
4833
|
+
repay,
|
|
4834
|
+
home = true,
|
|
4835
|
+
address = null,
|
|
4836
|
+
collection,
|
|
4837
|
+
tokenMetadata: tm,
|
|
4838
|
+
compressed,
|
|
4839
|
+
payerToken,
|
|
4840
|
+
programs,
|
|
4841
|
+
extra = [],
|
|
4842
|
+
}) {
|
|
4350
4843
|
if (repay === undefined || repay === null)
|
|
4351
4844
|
throw new Error('A repay names the debt the payer was shown (repay: hold.debt.repay as read).');
|
|
4352
4845
|
const { lender, mint } = hold.debt;
|
|
4353
4846
|
const tokenProgram = tokenProgramOf(programs, mint);
|
|
4847
|
+
const asset = hold.asset;
|
|
4848
|
+
// Going home, all its move takes (a Token Metadata NFT's `token` the borrower's); the money alone, only what shows
|
|
4849
|
+
// the project holds it: the Core asset, the mint and the project's token account of it, a compressed NFT's record and
|
|
4850
|
+
// tree (no proof: nothing moves, and the program reads the proof only for a move).
|
|
4851
|
+
const p = home
|
|
4852
|
+
? twoAccounts(
|
|
4853
|
+
piece({ project, asset, wallet: hold.player, collection, tokenMetadata: tm, compressed, ...ONLY_MOVES }),
|
|
4854
|
+
)
|
|
4855
|
+
: { ...heldPiece({ project, asset, tokenMetadata: tm, compressed }), proof: [] };
|
|
4354
4856
|
return lootIx(
|
|
4355
4857
|
'repay',
|
|
4356
|
-
{ repay: toBigInt(repay) },
|
|
4858
|
+
{ home, repay: toBigInt(repay) },
|
|
4357
4859
|
{
|
|
4358
4860
|
payer,
|
|
4359
4861
|
project,
|
|
4360
|
-
hold: pda.hold(
|
|
4862
|
+
hold: address ?? pda.hold(asset, hold.free_at),
|
|
4361
4863
|
lender,
|
|
4362
4864
|
lender_token: ata(lender, mint, tokenProgram),
|
|
4363
4865
|
payer_token: payerToken ?? ata(payer, mint, tokenProgram),
|
|
4364
4866
|
mint,
|
|
4867
|
+
borrower: hold.player,
|
|
4868
|
+
...p.accounts,
|
|
4365
4869
|
},
|
|
4366
|
-
{ remaining: tailOf(extra) },
|
|
4870
|
+
{ remaining: [...p.proof, ...tailOf(extra)], writable: p.writable },
|
|
4367
4871
|
);
|
|
4368
4872
|
}
|
|
4369
4873
|
|