@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.
Files changed (41) hide show
  1. package/README.md +145 -228
  2. package/js/approve/details.mjs +52 -2
  3. package/js/approve/live-check.mjs +0 -15
  4. package/js/approve/server.mjs +5 -4
  5. package/js/chain/token2022.js +3 -2
  6. package/js/client/accounts.js +67 -25
  7. package/js/client/grant.js +7 -2
  8. package/js/client/idl/loot.js +1 -1
  9. package/js/client/idl/loot.json +714 -182
  10. package/js/client/ix.js +617 -113
  11. package/js/client/sizes.js +35 -3
  12. package/js/economy/check.mjs +655 -242
  13. package/js/economy/cost.mjs +2 -4
  14. package/js/economy/explain.mjs +16 -2
  15. package/js/economy/guide.mjs +12 -12
  16. package/js/economy/model.mjs +133 -14
  17. package/js/economy/plan.mjs +36 -11
  18. package/js/economy/review.mjs +50 -8
  19. package/js/economy/schema.json +73 -29
  20. package/js/economy/simulate.mjs +116 -13
  21. package/js/mcp/tools.mjs +2 -2
  22. package/js/play/index.js +847 -296
  23. package/package.json +1 -1
  24. package/release.json +5 -5
  25. package/skills/xsolana-economy/SKILL.md +12 -4
  26. package/skills/xsolana-economy/references/after-launch.md +1 -1
  27. package/skills/xsolana-economy/references/apps.md +11 -2
  28. package/skills/xsolana-economy/references/sentence-to-spec.md +3 -3
  29. package/skills/xsolana-economy/references/telling-the-person.md +21 -4
  30. package/skills/xsolana-economy/references/trust-and-cheating.md +15 -5
  31. package/types/chain/token2022.d.ts +3 -2
  32. package/types/client/accounts.d.ts +34 -11
  33. package/types/client/idl/loot.d.ts +25 -4
  34. package/types/client/ix.d.ts +217 -45
  35. package/types/economy/check.d.mts +5 -0
  36. package/types/economy/model.d.mts +75 -12
  37. package/types/economy/plan.d.mts +22 -1
  38. package/types/economy/simulate.d.mts +12 -1
  39. package/types/mcp/server.d.mts +12 -1
  40. package/types/mcp/tools.d.mts +12 -1
  41. 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
- const terms = { nft_ticket: null, returns: false, min_sold: 0, ticket_mint: NO_ADDRESS, ...flat, ...given };
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 nothing else.
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' ? { prize: null, times: 1, ...p } : { bps: p, prize: null, times: 1 },
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 0's once
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: no account it pays is named.
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 0 gave out every NFT of its own stash, `idleRound`), and once the round its `owing` names has
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; `listProof`: for a list gate, the proof's nodes. `chosenRecipient`: the wallet (or, for a share with an
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, then the NFT shown for the gate and the odds (for a box that rolls on an NFT, a compressed one's
1062
- // proof instead: that NFT is the piece).
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.subject ? on.proof : shownAccounts(project, shown)),
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
- ...gateAccounts(project, { gate: box.gate, wallet: player, shown, listProof }),
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
- return order.won.slice(from, upto).flatMap(won => {
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(vault(project, mint, program), false, true),
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). `handBack`: the wallet the box's fate gives the NFT rolled on to
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
- // A part before the last gives only its prizes (and the first, the split): no NFT rolled on (its piece a stand-in).
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 = last
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({ payer, orderAddress, order, box, settler = NOBODY_KEY, poolMints, items = {}, programs }) {
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 (no attributes: they go in its Sheet). A switched-off item still makes; one sold out
1719
- * refuses the whole call (`PrizeNotCovered`).
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({ authority, project, item, itemState, owners, attributes = [], tree = null, reference: ref }) {
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, { gate: lottery.gate, wallet: player, shown, listProof }),
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; `lotteryClosed` when the
2237
- * lottery is gone, so rent it lent goes to the owner). A round that owes nothing and waits only for its close is closed
2238
- * by any pull.
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
- finished || (!partial && (r.status === 'Drawn' || r.status === 'Cancelled') && Number(r.owed) <= settled);
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. Ask each in the pull's transaction. */
2385
- export function pullRolls({ project, round: r, entry, part, nonce = 0n }) {
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
- return prizePlacesOwed(r, entry, run).map(place => ({
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. Leaving a tier
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; a `RollOn`'s order is where `namedRollNonce`
3463
- * says, which reads the rolled box (`boxes`: a box's id to its decoded LootBox). Its player or the project's keys then ask for the roll
3464
- * (`askRollIx`, in the same transaction when it fits).
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 the craft's `nonce`).
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
- nonce,
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, { gate: contest.gate, wallet: player, shown, listProof }),
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: a player for an NFT of the project's own collection they hold; the project's owner or authority for an
3972
- * NFT of any kind it holds (`piece`: `collection` a Core asset's own, `tokenMetadata`, or `compressed` after a
3973
- * `proveLeafIx`), a new unit of its item, or a roll of one of its boxes (a mystery lot: the box must be written, burn
3974
- * no share of its price, and make an order with the auction's shares within one call's growth: `LotRefused`). Its
3975
- * address names the seller, so an id is the seller's own.
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
- * Anyone ends auction `id` once it is over (its seller calls off one with no bid). Sold: the lot goes to the high
4070
- * bidder (an item that is a collectible is minted to them at `pda.itemAsset(item, serial)`; a roll of a box is made
4071
- * for them in an order at `prizeRollNonce(nonce)`, `nonce` being `prizeNonce(auction, last_bid_at)` by default, which `signer` pays for), the price
4072
- * through the shares, the rest to whoever owned the lot (the seller of an NFT, the treasury for the project's item or
4073
- * 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
4074
- * not there. `item` is the decoded Item of an item lot; for an NFT lot, `collection` its Core collection now (the
4075
- * project's when left out), `tokenMetadata` for a Token Metadata NFT, or `compressed` (after a `proveLeafIx`).
4076
- * `hold` (anyone may; true for all, or a list of assets): an end that wouldn't fit otherwise (a compressed lot's long
4077
- * proof, a hooked mint, many shares) makes the NFT lot a hold for whoever it goes to (its hold passed as `order`, no
4078
- * piece nor proof), and a winning NFT of a bid a hold for `proceeds` (its hold's address in its piece's place); anyone
4079
- * then delivers each with `releaseIx`, and the end's sender gets the rent back then.
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
- // The lot went at an earlier end, and its seller's account couldn't take the money then (`proceeds_owed`): this end pays
4100
- // it and closes the auction, naming no lot, share or winning NFT.
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
- const asset = owed ? null : (auction.lot.Asset?.[0] ?? made);
4109
- const proceeds = auction.lot.Asset ? auction.seller : projectState.treasury;
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 = sold ? auction.high_bidder : auction.seller;
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
- // An NFT lot is sent on; a collectible sold is minted into its item's collection, which Core writes.
4117
- // With `hold`, an NFT lot is its address alone (no proof), and becomes a hold for whoever it goes to.
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
- : lotHeld
4121
- ? standIn(asset)
4122
- : auction.lot.Asset
4123
- ? piece({ project, asset, wallet: receiver, collection, tokenMetadata: tm, compressed, ...ONLY_MOVES })
4124
- : asset
4125
- ? {
4126
- ...piece({ project, asset, collection: item.collection, moves: false, locks: false, reads: false }),
4127
- writable: ['collection'],
4128
- }
4129
- : standIn(address);
4130
- // A winning bid of NFTs: each to where the rest of a bid in tokens would go (`held`, their kinds in order).
4131
- const winning =
4132
- !owed && sold && auction.nft_bids
4133
- ? (auction.held ?? []).flatMap((nft, i) => {
4134
- if (holds(hold, nft)) return [meta(pda.hold(nft, 0), false, true)];
4135
- const q = piece({ project, asset: nft, wallet: proceeds, ...held[i], ...ONLY_MOVES });
4136
- return [...pieceMetas(q), ...q.proof];
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
- : lotHeld
4159
- ? pda.hold(asset, 0)
4625
+ : lot && holding(lot)
4626
+ ? pda.hold(lot, 0)
4160
4627
  : null,
4161
4628
  },
4162
4629
  {
4163
4630
  remaining: [
4164
4631
  ...p.proof,
4165
- ...(!owed && sold && roll === undefined
4632
+ ...(pays && roll === undefined
4166
4633
  ? shareAccounts(project, auction.shares, auction.mint, { holders, programs, treasury: projectState.treasury })
4167
4634
  : []),
4168
- ...winning,
4635
+ ...bid,
4169
4636
  ...tailOf(extra),
4170
4637
  ],
4171
4638
  writable: [
4172
- ...(!owed && sold && sharesBurn(auction.shares) ? ['mint'] : []),
4173
- ...(sold && !auction.nft_bids ? ['vault'] : []),
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
- const p = passedOn({
4257
- project,
4258
- owner: seller,
4259
- receiver: offer.maker,
4260
- burns: false,
4261
- asset,
4262
- collection,
4263
- hasAuthority,
4264
- tokenMetadata: tm,
4265
- compressed,
4266
- reads: true,
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, and the NFT is the borrower's again. Only money moves: anyone then sends the NFT
4345
- * home with `releaseIx` (in the same transaction, or later: a repaid loan's release has no deadline). Free. `repay`: the
4346
- * debt the payer was shown (`hold.debt.repay` as read): any other amount is refused (`TermsChanged`). `extra`: a
4347
- * transfer hook's accounts.
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({ payer, project, hold, repay, payerToken, programs, extra = [] }) {
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(hold.asset, hold.free_at),
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