@xgamesfun/solana 0.25.4 → 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 +8 -8
- 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/play/index.js
CHANGED
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
withSolWrap,
|
|
41
41
|
} from '../chain/programs.js';
|
|
42
42
|
import { checkCluster, notMainnet, rpcClient } from '../chain/rpc.js';
|
|
43
|
-
import {
|
|
43
|
+
import { extension, mayStartFrozen, mintPowers, resolveTransfers } from '../chain/token2022.js';
|
|
44
44
|
import {
|
|
45
45
|
compileFitting,
|
|
46
46
|
closeTableIx,
|
|
@@ -371,6 +371,62 @@ export function solanaApp({
|
|
|
371
371
|
before: tokenMetadata ? [computeUnitLimit(400_000), ...before] : [],
|
|
372
372
|
};
|
|
373
373
|
}
|
|
374
|
+
/**
|
|
375
|
+
* Whether the project still holds `asset`, as the program reads it (`piece::held`): { held, evidence }. A Core asset by
|
|
376
|
+
* its owner (one burned is gone); a Token Metadata NFT by the project's token account of it, and when that holds none,
|
|
377
|
+
* by the account that holds it now or by its mint when it was burned (`evidence`, what a call-off passes to show it
|
|
378
|
+
* gone); a compressed NFT by the RPC's DAS record of it (proven again by `nftOf`). `held` is null when nothing shows
|
|
379
|
+
* either way.
|
|
380
|
+
*/
|
|
381
|
+
async function heldNow(asset) {
|
|
382
|
+
const account = await client.account(asset);
|
|
383
|
+
if (account?.owner === MPL_CORE)
|
|
384
|
+
return { held: account.data[0] === 1 && parseCoreAsset(account.data).owner === project };
|
|
385
|
+
if (account?.owner === TOKEN_PROGRAM) {
|
|
386
|
+
const u64 = (data, at) => new DataView(data.buffer, data.byteOffset).getBigUint64(at, true);
|
|
387
|
+
const amount = data => (data?.length >= 72 ? u64(data, 64) : 0n);
|
|
388
|
+
if (amount((await client.account(loot.ata(project, asset)))?.data) === 1n) return { held: true };
|
|
389
|
+
// Burned: its mint holds none.
|
|
390
|
+
if (account.data.length >= 44 && u64(account.data, 36) === 0n) return { held: false, evidence: asset };
|
|
391
|
+
const found = await client.programAccounts(TOKEN_PROGRAM, [
|
|
392
|
+
{ dataSize: 165 },
|
|
393
|
+
{ memcmp: { offset: 0, bytes: asset } },
|
|
394
|
+
]);
|
|
395
|
+
const now = found.find(x => amount(x.account.data) === 1n);
|
|
396
|
+
return now ? { held: false, evidence: now.pubkey } : { held: null };
|
|
397
|
+
}
|
|
398
|
+
if (account) return { held: null };
|
|
399
|
+
const das = await client.call('getAsset', { id: asset }).catch(() => null);
|
|
400
|
+
if (!das?.compression?.compressed) return { held: null };
|
|
401
|
+
return { held: !das.compression.burnt && das.ownership?.owner === project };
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* A token's own settings that let whoever is to be paid turn a payment away, for a page to say before a player puts
|
|
405
|
+
* money in: accounts that start frozen or can be switched to it (a payee closes their own account, and the one
|
|
406
|
+
* opened again in its place is frozen), confidential transfers (a payee's account can refuse plain ones), and a token
|
|
407
|
+
* that can't be transferred at all. `what`: the payment ("repaying", "a higher bid"); `who` and `then`: who could and
|
|
408
|
+
* what they gain. Each is { power, says }.
|
|
409
|
+
*/
|
|
410
|
+
function tokenSettings(account, what, who, then) {
|
|
411
|
+
if (account?.owner !== TOKEN_2022_PROGRAM || account.data.length <= 166 || account.data[165] !== 1) return [];
|
|
412
|
+
const out = [];
|
|
413
|
+
if (mayStartFrozen(account))
|
|
414
|
+
out.push({
|
|
415
|
+
power: 'startsFrozen',
|
|
416
|
+
says: `New accounts of this token start frozen, or can be switched to start frozen: ${who} can close their own account so that ${what} cannot arrive, ${then}.`,
|
|
417
|
+
});
|
|
418
|
+
if (extension(account.data, 4))
|
|
419
|
+
out.push({
|
|
420
|
+
power: 'confidential',
|
|
421
|
+
says: `This token has confidential transfers: ${who} can turn plain transfers into their account off so that ${what} cannot arrive, ${then}.`,
|
|
422
|
+
});
|
|
423
|
+
if (extension(account.data, 9))
|
|
424
|
+
out.push({
|
|
425
|
+
power: 'nonTransferable',
|
|
426
|
+
says: `This token can't be transferred: nothing can be paid in it, ${what} included.`,
|
|
427
|
+
});
|
|
428
|
+
return out;
|
|
429
|
+
}
|
|
374
430
|
/**
|
|
375
431
|
* What `loot.prizeMetas` needs to give these prizes (`null`s are skipped): each pool's mint, each item (its kind, its
|
|
376
432
|
* collection and its next serial; null for one never written), each NFT's kind (`nfts`, and `nft` for one), the
|
|
@@ -521,6 +577,21 @@ export function solanaApp({
|
|
|
521
577
|
return true;
|
|
522
578
|
};
|
|
523
579
|
const pass = c => rules.every(r => passes(r, c));
|
|
580
|
+
// A rule on the player's profile: the client passes the profile itself (`loot.shownFor`); here it is only read, to
|
|
581
|
+
// say plainly when it falls short.
|
|
582
|
+
if (loot.readsProfile(rule)) {
|
|
583
|
+
const found = await client.account(loot.pda.profile(project, wallet.address));
|
|
584
|
+
const attributes = Object.fromEntries(
|
|
585
|
+
(loot.decodeProfile(found?.owner === loot.LOOT_PROGRAM ? found.data : null)?.attributes ?? []).map(a => [
|
|
586
|
+
a.key,
|
|
587
|
+
a.value,
|
|
588
|
+
]),
|
|
589
|
+
);
|
|
590
|
+
if (passes(rule, { attributes })) return null;
|
|
591
|
+
throw new Error(
|
|
592
|
+
`You need ${rule.key} ${atLeast !== undefined ? `${atLeast} or more` : equals !== undefined ? equals : 'set'} on your profile to ${what}.`,
|
|
593
|
+
);
|
|
594
|
+
}
|
|
524
595
|
if (other) {
|
|
525
596
|
const found = await client.programAccounts(MPL_CORE, ownedAssetFilters(wallet.address, other));
|
|
526
597
|
for (const { pubkey, account } of found) {
|
|
@@ -916,16 +987,6 @@ export function solanaApp({
|
|
|
916
987
|
if (key.name && !(BigInt(await client.balance(key.address).catch(() => 1n)) > 0n)) keys?.forget(key.name);
|
|
917
988
|
}
|
|
918
989
|
|
|
919
|
-
/** A key's job: its step (`build(signer)`), simulated as the key sends it, then sent by the key, or by the wallet when
|
|
920
|
-
* the simulation fails (one more prompt); then the key empties itself back to the wallet. Returns the signature. */
|
|
921
|
-
async function keyRun(key, build) {
|
|
922
|
-
const steps = await build(key.address);
|
|
923
|
-
const signature = (await dryRun(key.address, steps)).ok ? await sendAs(key, steps).catch(() => null) : null;
|
|
924
|
-
const sent = signature ?? (await send(await build(wallet.address)));
|
|
925
|
-
await emptyKey(key);
|
|
926
|
-
return sent;
|
|
927
|
-
}
|
|
928
|
-
|
|
929
990
|
/**
|
|
930
991
|
* Rolls in one prompt: a step that makes orders waiting for their roll (a craft, a buy, a mission left, a grant or a
|
|
931
992
|
* contest prize, a lot won) asks for each in the same transaction, and a throwaway key it funds then settles each (the
|
|
@@ -1007,10 +1068,13 @@ export function solanaApp({
|
|
|
1007
1068
|
* throwaway key the open funded), the burner signs and pays the settle, and gives the rest back: no wallet prompt.
|
|
1008
1069
|
* The order names it (`settler`), so it may settle; when the player or the project did first, the burner only gives its lamports back. `putBack`: NFTs
|
|
1009
1070
|
* won that the player puts back into their stash in place of taking them (one whose collection now refuses to let it
|
|
1010
|
-
* move); the player
|
|
1071
|
+
* move); `giveUp`: the places in the order's `won` of tokens or collectibles the player gives up because they can't
|
|
1072
|
+
* receive them (an account the token's issuer froze, a collection that refuses the mint): tokens given up stay in the
|
|
1073
|
+
* project's vault, a collectible is never made. The player signs either settle. A settle that fails because the player
|
|
1074
|
+
* can't receive a prize says which (`error.giveUp`), so a page can offer to give them up and take the rest.
|
|
1011
1075
|
*/
|
|
1012
1076
|
async function collect(orderAddress, options = {}) {
|
|
1013
|
-
only(options, ['timeoutMs', 'burner', 'putBack', 'opened', 'landed', 'keepBurner'], 'collect');
|
|
1077
|
+
only(options, ['timeoutMs', 'burner', 'putBack', 'giveUp', 'opened', 'landed', 'keepBurner'], 'collect');
|
|
1014
1078
|
try {
|
|
1015
1079
|
return await collectOnce(orderAddress, options);
|
|
1016
1080
|
} catch (e) {
|
|
@@ -1028,8 +1092,18 @@ export function solanaApp({
|
|
|
1028
1092
|
|
|
1029
1093
|
async function collectOnce(
|
|
1030
1094
|
orderAddress,
|
|
1031
|
-
{
|
|
1095
|
+
{
|
|
1096
|
+
timeoutMs = 60_000,
|
|
1097
|
+
burner = null,
|
|
1098
|
+
putBack = [],
|
|
1099
|
+
giveUp = [],
|
|
1100
|
+
opened = false,
|
|
1101
|
+
landed = null,
|
|
1102
|
+
keepBurner = false,
|
|
1103
|
+
} = {},
|
|
1032
1104
|
) {
|
|
1105
|
+
// The player's own choices (an NFT put back, a prize given up) are theirs to sign: no burner settles them.
|
|
1106
|
+
const chooses = putBack.length > 0 || giveUp.length > 0;
|
|
1033
1107
|
const started = Date.now();
|
|
1034
1108
|
// `landed`: the slot one's own open landed in; the order is read from a node that has seen it.
|
|
1035
1109
|
const current = async () => {
|
|
@@ -1150,6 +1224,7 @@ export function solanaApp({
|
|
|
1150
1224
|
items,
|
|
1151
1225
|
nfts,
|
|
1152
1226
|
putBack,
|
|
1227
|
+
giveUp,
|
|
1153
1228
|
subject,
|
|
1154
1229
|
handBack,
|
|
1155
1230
|
holders,
|
|
@@ -1167,7 +1242,7 @@ export function solanaApp({
|
|
|
1167
1242
|
// with; the player's wallet, a part it can't.
|
|
1168
1243
|
const signatures = [];
|
|
1169
1244
|
// A key the order doesn't name can't settle it (`Order::settler`): then the player signs.
|
|
1170
|
-
const by = burner && !
|
|
1245
|
+
const by = burner && !chooses && order.settler === burner.address ? burner.address : wallet.address;
|
|
1171
1246
|
const fits = async instructions =>
|
|
1172
1247
|
Boolean(compileFitting({ payer: by, blockhash: NOBODY, instructions, tables: await standing() })) &&
|
|
1173
1248
|
transactionLocks(by, instructions) <= 64;
|
|
@@ -1196,8 +1271,7 @@ export function solanaApp({
|
|
|
1196
1271
|
// player's own choice: they sign.
|
|
1197
1272
|
let signature = null;
|
|
1198
1273
|
let emptied = false;
|
|
1199
|
-
const byBurner =
|
|
1200
|
-
burner && !putBack.length && order.settler === burner.address ? await settleBy(burner.address) : null;
|
|
1274
|
+
const byBurner = burner && !chooses && order.settler === burner.address ? await settleBy(burner.address) : null;
|
|
1201
1275
|
const alone = byBurner && !byBurner.before.length ? [...budget, byBurner.ix] : null;
|
|
1202
1276
|
const legacy = instructions => Boolean(compileFitting({ payer: burner.address, blockhash: NOBODY, instructions }));
|
|
1203
1277
|
const tried = alone && legacy(alone) ? await dryRun(burner.address, alone, [burner.address]) : null;
|
|
@@ -1214,12 +1288,15 @@ export function solanaApp({
|
|
|
1214
1288
|
if (!signature) {
|
|
1215
1289
|
// The wallet's settle: as a v0 transaction with a lookup table when it doesn't fit a legacy one (`send`).
|
|
1216
1290
|
const { ix, before } = await settleBy(wallet.address);
|
|
1217
|
-
signature = await send([...budget, ...before, ix])
|
|
1291
|
+
signature = await send([...budget, ...before, ix]).catch(async e => {
|
|
1292
|
+
throw await cannotReceive(order, e);
|
|
1293
|
+
});
|
|
1218
1294
|
}
|
|
1219
1295
|
// The burner gives back what it holds after its last settle (a purchase of several orders: after the last order's).
|
|
1220
1296
|
if (burner && !keepBurner && !emptied) await emptyKey(burner);
|
|
1221
1297
|
else if (burner && emptied) keys?.forget(burner.name);
|
|
1222
|
-
const
|
|
1298
|
+
const given = new Set(giveUp.map(Number));
|
|
1299
|
+
const assets = wonAssets(order).map((a, k) => (putBack.includes(a) || given.has(k) ? null : a));
|
|
1223
1300
|
return {
|
|
1224
1301
|
order: orderAddress,
|
|
1225
1302
|
won: order.won,
|
|
@@ -1230,6 +1307,32 @@ export function solanaApp({
|
|
|
1230
1307
|
};
|
|
1231
1308
|
}
|
|
1232
1309
|
|
|
1310
|
+
/**
|
|
1311
|
+
* A settle of `order` that failed (`e`): when a prize can't reach the player (their token account of its mint frozen by
|
|
1312
|
+
* the token's issuer; a collectible whose collection refuses the mint), an error that says so in words and names the
|
|
1313
|
+
* places in `won` they may give up (`giveUp`, for `collect`); otherwise `e` itself.
|
|
1314
|
+
*/
|
|
1315
|
+
async function cannotReceive(order, e) {
|
|
1316
|
+
const from = Number(order.settled ?? 0);
|
|
1317
|
+
const stuck = [];
|
|
1318
|
+
for (const [k, won] of order.won.entries()) {
|
|
1319
|
+
if (k < from || !won.Tokens) continue;
|
|
1320
|
+
const account = await client.account(loot.ata(order.player, won.Tokens.mint, programOf(won.Tokens.mint)));
|
|
1321
|
+
// A token account's state is its byte 108: 2 is frozen.
|
|
1322
|
+
if (account && account.data?.[108] === 2) stuck.push(k);
|
|
1323
|
+
}
|
|
1324
|
+
// Core refusing the mint (a collection's plugin): the collectibles of the order are what it would not take.
|
|
1325
|
+
const core = (e?.logs ?? []).some(line => line.startsWith(`Program ${MPL_CORE} failed`));
|
|
1326
|
+
if (!stuck.length && core)
|
|
1327
|
+
for (const [k, won] of order.won.entries()) if (k >= from && won.Item?.serial != null) stuck.push(k);
|
|
1328
|
+
if (!stuck.length) return e;
|
|
1329
|
+
const err = new Error(
|
|
1330
|
+
`This order can't be settled as it is: ${stuck.length === 1 ? 'one prize' : `${stuck.length} prizes`} can't reach your wallet (a token account its issuer froze, or a collection that refuses the mint). Give ${stuck.length === 1 ? 'it' : 'them'} up to take the rest (collect again with giveUp: [${stuck.join(', ')}]): tokens given up stay in the project's vault, and a collectible given up is never made.`,
|
|
1331
|
+
);
|
|
1332
|
+
Object.assign(err, { giveUp: stuck, cause: e, logs: e?.logs });
|
|
1333
|
+
return err;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1233
1336
|
/**
|
|
1234
1337
|
* For each prize an order won, what it is now: the collectible minted (`["asset", item, serial]`), the NFT handed
|
|
1235
1338
|
* over; null for tokens. A normal item is the project's backend's to deliver.
|
|
@@ -1282,9 +1385,14 @@ export function solanaApp({
|
|
|
1282
1385
|
|
|
1283
1386
|
/** The instructions that ask the oracle for the roll of `order` (at `orderAddress`), `payer` paying it. Each prize
|
|
1284
1387
|
* mint's token program is read from the mint when the app wasn't given it (`programs`): the ask opens the project's
|
|
1285
|
-
* vault of it.
|
|
1286
|
-
|
|
1388
|
+
* vault of it. Its first ask fixes its odds (`loot.askReads`): it passes the order's player's profile when the box's
|
|
1389
|
+
* odds read one, and for a roll on no NFT, when they read an NFT, `shown` (an NFT the player holds, as `openBox` takes
|
|
1390
|
+
* it), or else the player's collectible of this project that raises them the most (`loot.bestShown`). */
|
|
1391
|
+
async function asksFor(payer, orderAddress, order, settler, shown = null) {
|
|
1287
1392
|
const box = await read('LootBox', order.loot_box);
|
|
1393
|
+
const player = order.player ?? payer;
|
|
1394
|
+
if (!shown && loot.firstAsk(order, box) && loot.askReads(box, order).nft && player === wallet?.address)
|
|
1395
|
+
shown = loot.bestShown(box, await owned(player)).best;
|
|
1288
1396
|
const poolMints = await poolMintsOf(box);
|
|
1289
1397
|
const mints = [
|
|
1290
1398
|
...Object.values(poolMints),
|
|
@@ -1295,20 +1403,22 @@ export function solanaApp({
|
|
|
1295
1403
|
return loot.askRollIx({
|
|
1296
1404
|
payer,
|
|
1297
1405
|
orderAddress,
|
|
1298
|
-
order,
|
|
1406
|
+
order: { ...order, player },
|
|
1299
1407
|
box,
|
|
1300
1408
|
settler,
|
|
1409
|
+
shown,
|
|
1301
1410
|
poolMints,
|
|
1302
1411
|
items: await itemsOf(box),
|
|
1303
1412
|
programs: known,
|
|
1304
1413
|
});
|
|
1305
1414
|
}
|
|
1306
1415
|
|
|
1307
|
-
/** Asks the oracle for a roll a mission, a recipe or a prize made (`Waiting`): the player pays the oracle.
|
|
1308
|
-
|
|
1416
|
+
/** Asks the oracle for a roll a mission, a recipe or a prize made (`Waiting`): the player pays the oracle. `shown`: an
|
|
1417
|
+
* NFT the player shows for the box's odds at its first ask (by default the one of theirs that raises them most). */
|
|
1418
|
+
async function askRoll(orderAddress, { shown = null } = {}) {
|
|
1309
1419
|
return send([
|
|
1310
1420
|
computeUnitLimit(400_000),
|
|
1311
|
-
...(await asksFor(wallet.address, orderAddress, await read('Order', orderAddress))),
|
|
1421
|
+
...(await asksFor(wallet.address, orderAddress, await read('Order', orderAddress), undefined, shown)),
|
|
1312
1422
|
]);
|
|
1313
1423
|
}
|
|
1314
1424
|
|
|
@@ -1349,7 +1459,17 @@ export function solanaApp({
|
|
|
1349
1459
|
);
|
|
1350
1460
|
}
|
|
1351
1461
|
/** What `buy` and `openBox` take: a box bought at once (`buyBox`) or opened (`open`). */
|
|
1352
|
-
const BOX_OPTIONS = [
|
|
1462
|
+
const BOX_OPTIONS = [
|
|
1463
|
+
'shown',
|
|
1464
|
+
'listProof',
|
|
1465
|
+
'maxPrice',
|
|
1466
|
+
'listing',
|
|
1467
|
+
'chosenRecipient',
|
|
1468
|
+
'on',
|
|
1469
|
+
'oneSignature',
|
|
1470
|
+
'count',
|
|
1471
|
+
'acceptLeftover',
|
|
1472
|
+
];
|
|
1353
1473
|
|
|
1354
1474
|
/**
|
|
1355
1475
|
* Buys `count` of a box that needs no roll (a shop item, a bundle, a fixed pack: one entry, nothing random, no NFT
|
|
@@ -1455,6 +1575,7 @@ export function solanaApp({
|
|
|
1455
1575
|
chosenRecipient = null,
|
|
1456
1576
|
listProof,
|
|
1457
1577
|
count = 1,
|
|
1578
|
+
acceptLeftover,
|
|
1458
1579
|
} = {},
|
|
1459
1580
|
) {
|
|
1460
1581
|
const g = await projectNow();
|
|
@@ -1468,6 +1589,8 @@ export function solanaApp({
|
|
|
1468
1589
|
const items = await itemsOf(box);
|
|
1469
1590
|
// A box that rolls on an NFT takes the one the player brings (`on`), whatever its kind.
|
|
1470
1591
|
if (box.subject && !on) throw new Error('This box rolls on an NFT: say which (on).');
|
|
1592
|
+
// A box that burns it, gives it away or moves its numbers takes it into the project's hands until the roll settles.
|
|
1593
|
+
if (box.subject && loot.boxHolds(box)) await noLeftovers(on, acceptLeftover);
|
|
1471
1594
|
const brought = box.subject ? await nftOf(on) : { before: [] };
|
|
1472
1595
|
const { before: proving, hasAuthority, ...kind } = brought;
|
|
1473
1596
|
const subject = box.subject
|
|
@@ -1706,6 +1829,339 @@ export function solanaApp({
|
|
|
1706
1829
|
);
|
|
1707
1830
|
}
|
|
1708
1831
|
|
|
1832
|
+
/**
|
|
1833
|
+
* An auction's end, as `endAuction` and `callOffSale` send it (`a`: the auction's part of the spec). Each step is
|
|
1834
|
+
* checked before the wallet signs: a dry run when it is one legacy transaction, the size otherwise.
|
|
1835
|
+
*/
|
|
1836
|
+
async function endSale(a, { holders, callOff = false } = {}) {
|
|
1837
|
+
const at = auctionAt(a);
|
|
1838
|
+
let auction = await read('Auction', at);
|
|
1839
|
+
const g = await projectNow();
|
|
1840
|
+
// What a transfer of the bids' coin carries: the Memo program for Token-2022, and a transfer hook's accounts.
|
|
1841
|
+
const memo = await hookTail([auction.mint]);
|
|
1842
|
+
const sold = auction.high > 0n;
|
|
1843
|
+
const lot = auction.lot.Asset?.[0] ?? null;
|
|
1844
|
+
const owner = lot ? auction.seller : g.treasury;
|
|
1845
|
+
// The bids' coin's token program, read from the mint when the app wasn't told it.
|
|
1846
|
+
const coin = programs[auction.mint] ?? (await client.account(auction.mint))?.owner ?? TOKEN_PROGRAM;
|
|
1847
|
+
const base = {
|
|
1848
|
+
signer: wallet.address,
|
|
1849
|
+
project,
|
|
1850
|
+
projectState: g,
|
|
1851
|
+
id: a.id,
|
|
1852
|
+
programs: { ...programs, [auction.mint]: coin },
|
|
1853
|
+
extra: memo,
|
|
1854
|
+
};
|
|
1855
|
+
if (callOff && (!sold || !lot))
|
|
1856
|
+
throw new Error('Only the sale of an NFT lot that has a bid is called off: end this auction with endAuction.');
|
|
1857
|
+
// The lot went at an earlier end: this one pays what the bid still owes and closes the auction.
|
|
1858
|
+
if (BigInt(auction.proceeds_owed) > 0n) {
|
|
1859
|
+
if (callOff)
|
|
1860
|
+
throw new Error(
|
|
1861
|
+
'The lot went to its winner already: the sale stands (CantCallOff). End it to pay what it owes.',
|
|
1862
|
+
);
|
|
1863
|
+
return { ...(await payOwed(a, auction, base, holders)), owedPaid: auction.proceeds_owed };
|
|
1864
|
+
}
|
|
1865
|
+
const item = auction.lot.Item ? await read('Item', loot.pda.item(project, auction.lot.Item[0])) : null;
|
|
1866
|
+
const made =
|
|
1867
|
+
sold && item?.kind === 'Collectible'
|
|
1868
|
+
? loot.pda.itemAsset(loot.pda.item(project, auction.lot.Item[0]), auction.pin)
|
|
1869
|
+
: null;
|
|
1870
|
+
// Gone (taken back or burned by whoever controls its collection): the lot, or an NFT of the winning bid.
|
|
1871
|
+
const lotNow = lot ? await heldNow(lot) : { held: null };
|
|
1872
|
+
const bidNow = [];
|
|
1873
|
+
if (sold && auction.nft_bids) for (const nft of auction.held) bidNow.push(await heldNow(nft));
|
|
1874
|
+
const gone = lotNow.held === false || bidNow.some(n => n.held === false);
|
|
1875
|
+
const ending = sold && lot && (callOff || gone) ? 'CallOff' : 'Deliver';
|
|
1876
|
+
const home = !sold || ending === 'CallOff';
|
|
1877
|
+
// The lot to its winner, or home to its seller; a bid's NFTs to whoever owned the lot, or back to their bidder.
|
|
1878
|
+
const before = [];
|
|
1879
|
+
const lotKind = lot ? await kindFor(lot, home ? auction.seller : auction.high_bidder, lotNow.evidence, before) : {};
|
|
1880
|
+
const bidTo = ending === 'CallOff' ? auction.high_bidder : owner;
|
|
1881
|
+
const held = [];
|
|
1882
|
+
if (sold && auction.nft_bids)
|
|
1883
|
+
for (const [i, nft] of auction.held.entries()) held.push(await kindFor(nft, bidTo, bidNow[i].evidence, before));
|
|
1884
|
+
if (sold && !home) holders ??= await holdersOf(auction.shares);
|
|
1885
|
+
const end = (ending, hold = false) =>
|
|
1886
|
+
loot.endAuctionIx({ ...base, auction, item, ...lotKind, held, holders, ending, hold });
|
|
1887
|
+
const steps = ix => [
|
|
1888
|
+
computeUnitLimit(1_000_000),
|
|
1889
|
+
...before.filter(x => x.programId !== COMPUTE_BUDGET_PROGRAM),
|
|
1890
|
+
ix,
|
|
1891
|
+
];
|
|
1892
|
+
const fits = ix => Boolean(sendPlan({ payer: wallet.address, instructions: steps(ix), alone: isProveLeaf }));
|
|
1893
|
+
const result = {
|
|
1894
|
+
...(sold && ending === 'Deliver' ? { winner: auction.high_bidder } : {}),
|
|
1895
|
+
...(made ? { asset: made } : {}),
|
|
1896
|
+
};
|
|
1897
|
+
if (ending === 'CallOff') Object.assign(result, { calledOff: true, gone });
|
|
1898
|
+
|
|
1899
|
+
// An end that may make holds: a lot going home, a bid's NFTs going back or to the treasury for the project's lot.
|
|
1900
|
+
const outs = [
|
|
1901
|
+
...(lot && home && lotNow.held !== false ? [[lot, auction.seller]] : []),
|
|
1902
|
+
...(sold && auction.nft_bids && (home || !lot)
|
|
1903
|
+
? auction.held.filter((_, i) => bidNow[i].held !== false).map(x => [x, bidTo])
|
|
1904
|
+
: []),
|
|
1905
|
+
];
|
|
1906
|
+
const holding = async () => ({
|
|
1907
|
+
...(await heldThenReleased(
|
|
1908
|
+
at,
|
|
1909
|
+
steps(
|
|
1910
|
+
end(
|
|
1911
|
+
ending,
|
|
1912
|
+
outs.map(([x]) => x),
|
|
1913
|
+
),
|
|
1914
|
+
),
|
|
1915
|
+
outs,
|
|
1916
|
+
)),
|
|
1917
|
+
...result,
|
|
1918
|
+
});
|
|
1919
|
+
if (fits(end(ending))) {
|
|
1920
|
+
if (await stuck(steps(end(ending)))) {
|
|
1921
|
+
// What can't move now goes home or back as a hold, sent on once it can; a sold lot waits instead.
|
|
1922
|
+
if (outs.length) return holding();
|
|
1923
|
+
return {
|
|
1924
|
+
waiting: true,
|
|
1925
|
+
says: 'The lot can’t move to its winner now (its collection froze it, or refuses the move): the sale waits, the winning bid in the project’s vault. End it again later; the project’s keys can call it off (callOffSale), and if the lot is taken back or burned anyone can.',
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
// A roll lot sold: the winner's roll, made at the default nonce, waits for its winner or the project's keys to ask it
|
|
1929
|
+
// (`collect`); the winner ending it asks in the same transaction and a key settles it.
|
|
1930
|
+
const roll =
|
|
1931
|
+
sold && auction.lot.Roll
|
|
1932
|
+
? loot.pda.order(
|
|
1933
|
+
project,
|
|
1934
|
+
auction.high_bidder,
|
|
1935
|
+
loot.prizeRollNonce(loot.prizeNonce(at, BigInt(auction.last_bid_at))),
|
|
1936
|
+
)
|
|
1937
|
+
: null;
|
|
1938
|
+
const { signature, rolls } =
|
|
1939
|
+
roll && auction.high_bidder === wallet.address
|
|
1940
|
+
? await sendRolling(steps(end(ending)))
|
|
1941
|
+
: { signature: await send(steps(end(ending))), rolls: [] };
|
|
1942
|
+
return {
|
|
1943
|
+
signature,
|
|
1944
|
+
...result,
|
|
1945
|
+
...(roll ? { order: roll } : {}),
|
|
1946
|
+
...(rolls.length ? { rolled: rolls[0] } : {}),
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1949
|
+
// Too wide for one transaction. A sold NFT lot goes first, and a second end pays the bid.
|
|
1950
|
+
if (sold && lot && ending === 'Deliver') {
|
|
1951
|
+
const first = steps(end('LotFirst'));
|
|
1952
|
+
if (await stuck(first))
|
|
1953
|
+
return {
|
|
1954
|
+
waiting: true,
|
|
1955
|
+
says: 'The lot can’t move to its winner now (its collection froze it, or refuses the move): the sale waits, the winning bid in the project’s vault. End it again later; the project’s keys can call it off (callOffSale).',
|
|
1956
|
+
};
|
|
1957
|
+
const signature = await send(first);
|
|
1958
|
+
auction = await read('Auction', at);
|
|
1959
|
+
return { signature, ...result, lotFirst: true, ...(await payOwed(a, auction, base, holders)) };
|
|
1960
|
+
}
|
|
1961
|
+
// Otherwise each NFT that may be a hold becomes one, and is sent on with a release after.
|
|
1962
|
+
return holding();
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
/** An NFT's kind on an auction's end, going to `to`: what `nftOf` reads, its proof put in `before`; `evidence` for a Token
|
|
1966
|
+
* Metadata NFT that left the project (the account that holds it now, or its mint). */
|
|
1967
|
+
async function kindFor(asset, to, evidence, before) {
|
|
1968
|
+
const { before: proof, hasAuthority: _, ...kind } = await nftOf(asset, wallet.address, { to });
|
|
1969
|
+
before.push(...proof);
|
|
1970
|
+
return {
|
|
1971
|
+
...kind,
|
|
1972
|
+
collection: kind.compressed || kind.tokenMetadata ? undefined : ((await collectionOf(asset)) ?? MPL_CORE),
|
|
1973
|
+
...(evidence && kind.tokenMetadata ? { evidence } : {}),
|
|
1974
|
+
};
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
/**
|
|
1978
|
+
* Whether an end can't move an NFT now: dry-run as the wallet would send it (one legacy transaction; a bigger one is
|
|
1979
|
+
* sent as it is, and counts as one that can). A refusal of loot's is said in words; a failure in the NFT's own program
|
|
1980
|
+
* (it is frozen, or its collection refuses the move) is one that is stuck: true, and nothing is sent.
|
|
1981
|
+
*/
|
|
1982
|
+
async function stuck(instructions) {
|
|
1983
|
+
const plan = sendPlan({ payer: wallet.address, instructions, alone: isProveLeaf });
|
|
1984
|
+
if (!plan?.legacy) return false;
|
|
1985
|
+
const tried = await dryRun(wallet.address, instructions);
|
|
1986
|
+
if (tried.ok) return false;
|
|
1987
|
+
if (tried.error) throw Object.assign(new Error(`${tried.error.msg} (${tried.error.name}).`), tried.error);
|
|
1988
|
+
return true;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
/**
|
|
1992
|
+
* After an NFT lot went first: the end that pays what the bid still owes and closes the auction. The accounts its
|
|
1993
|
+
* money goes into (each share's wallet and the seller's) are opened first, in a transaction of their own when any is
|
|
1994
|
+
* missing: the end itself can make up to 64 calls. A bid's NFTs go to the seller, made holds and released after when
|
|
1995
|
+
* they don't all fit. Returns { paid } (its signature), and `releases`.
|
|
1996
|
+
*/
|
|
1997
|
+
async function payOwed(a, auction, base, holders) {
|
|
1998
|
+
const at = auctionAt(a);
|
|
1999
|
+
holders ??= await holdersOf(auction.shares);
|
|
2000
|
+
const opens = [];
|
|
2001
|
+
if (!auction.nft_bids) {
|
|
2002
|
+
const payees = [
|
|
2003
|
+
auction.seller,
|
|
2004
|
+
...auction.shares.flatMap(sh =>
|
|
2005
|
+
sh.to.Wallet
|
|
2006
|
+
? [sh.to.Wallet[0]]
|
|
2007
|
+
: sh.to.Holder
|
|
2008
|
+
? [holders[sh.to.Holder[0]]]
|
|
2009
|
+
: sh.to === 'Treasury'
|
|
2010
|
+
? [base.projectState.treasury]
|
|
2011
|
+
: [],
|
|
2012
|
+
),
|
|
2013
|
+
].filter(Boolean);
|
|
2014
|
+
const program = base.programs[auction.mint];
|
|
2015
|
+
const accounts = await client.accounts(payees.map(w => loot.ata(w, auction.mint, program)));
|
|
2016
|
+
for (const [i, w] of payees.entries())
|
|
2017
|
+
if (!accounts[i]) opens.push(createAssociatedTokenAccountIdempotent(wallet.address, w, auction.mint, program));
|
|
2018
|
+
}
|
|
2019
|
+
if (opens.length) await send([...new Map(opens.map(x => [x.keys[1].pubkey, x])).values()]);
|
|
2020
|
+
const before = [];
|
|
2021
|
+
const held = [];
|
|
2022
|
+
if (auction.nft_bids) for (const nft of auction.held) held.push(await kindFor(nft, auction.seller, null, before));
|
|
2023
|
+
const end = (hold = false) => loot.endAuctionIx({ ...base, auction, held, holders, hold });
|
|
2024
|
+
const steps = ix => [
|
|
2025
|
+
computeUnitLimit(1_000_000),
|
|
2026
|
+
...before.filter(x => x.programId !== COMPUTE_BUDGET_PROGRAM),
|
|
2027
|
+
ix,
|
|
2028
|
+
];
|
|
2029
|
+
if (sendPlan({ payer: wallet.address, instructions: steps(end()), alone: isProveLeaf }))
|
|
2030
|
+
try {
|
|
2031
|
+
return { paid: await send(steps(end())) };
|
|
2032
|
+
} catch (e) {
|
|
2033
|
+
// The lot is the winner's already: what is owed waits until the account it goes into can take it.
|
|
2034
|
+
return {
|
|
2035
|
+
paid: null,
|
|
2036
|
+
says: `The lot is the winner’s; the money waits (${e.message}): end the auction again to pay it.`,
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
2039
|
+
const outs = auction.held.map(x => [x, auction.seller]);
|
|
2040
|
+
const { signature, releases } = await heldThenReleased(at, steps(end(true)), outs);
|
|
2041
|
+
return { paid: signature, releases };
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
/**
|
|
2045
|
+
* An end that makes NFTs holds (`outs`: [asset, to]), sent by the wallet with a transfer that funds a throwaway key; the
|
|
2046
|
+
* key then sends each on with a release (anyone may), funded for what each may open (a pNFT's token account and record,
|
|
2047
|
+
* a compressed NFT's proof record). One the key can't send, the wallet does. Returns { signature, releases }.
|
|
2048
|
+
*/
|
|
2049
|
+
async function heldThenReleased(at, instructions, outs) {
|
|
2050
|
+
const { key, fund } = await keyFor(
|
|
2051
|
+
BigInt(outs.length) * (2n * SIGNATURE_FEE + loot.SETTLE_RENT_UNIT),
|
|
2052
|
+
await jobKey(at),
|
|
2053
|
+
);
|
|
2054
|
+
const signature = await send([...instructions, fund]);
|
|
2055
|
+
const releases = [];
|
|
2056
|
+
for (const [asset, to] of outs) {
|
|
2057
|
+
const release = async signer => {
|
|
2058
|
+
const hold = await read('Hold', loot.pda.hold(asset, 0));
|
|
2059
|
+
const { before: proof, hasAuthority: _h, ...kind } = await nftOf(asset, signer, { to });
|
|
2060
|
+
return [
|
|
2061
|
+
...proof,
|
|
2062
|
+
loot.releaseIx({
|
|
2063
|
+
signer,
|
|
2064
|
+
project,
|
|
2065
|
+
asset,
|
|
2066
|
+
...kind,
|
|
2067
|
+
collection: kind.compressed || kind.tokenMetadata ? undefined : ((await collectionOf(asset)) ?? MPL_CORE),
|
|
2068
|
+
hold,
|
|
2069
|
+
to,
|
|
2070
|
+
}),
|
|
2071
|
+
];
|
|
2072
|
+
};
|
|
2073
|
+
const steps = await release(key.address);
|
|
2074
|
+
const byKey = (await dryRun(key.address, steps)).ok ? await sendAs(key, steps).catch(() => null) : null;
|
|
2075
|
+
// One that still can't move (frozen for now) stays a hold: claims() lists it for whoever it is owed to.
|
|
2076
|
+
releases.push(byKey ?? (await send(await release(wallet.address)).catch(e => ({ asset, waits: e.message }))));
|
|
2077
|
+
}
|
|
2078
|
+
await emptyKey(key);
|
|
2079
|
+
return { signature, releases };
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
/**
|
|
2083
|
+
* A hold's claim, or, when the project no longer holds its NFT (whoever controls its collection took it back or
|
|
2084
|
+
* burned it while it was held), the same claim flagged `gone`: the hold's record still says the NFT is held, and it
|
|
2085
|
+
* is not, so nothing can be sent.
|
|
2086
|
+
*/
|
|
2087
|
+
async function goneOr(asset, claim) {
|
|
2088
|
+
if ((await heldNow(asset)).held !== false) return claim;
|
|
2089
|
+
return {
|
|
2090
|
+
...claim,
|
|
2091
|
+
gone: true,
|
|
2092
|
+
says: 'The project no longer holds this NFT: whoever controls its collection took it back or burned it while it was held. Its hold says otherwise, but nothing can be sent, and nothing was paid against it.',
|
|
2093
|
+
};
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
/**
|
|
2097
|
+
* The records of this project an earlier owner of `asset` left behind: its listing at
|
|
2098
|
+
* `["listing", project, asset]` (one read) and its holds (one search, by the asset's address), each that names a wallet
|
|
2099
|
+
* other than the player's. Each is { kind, address, says }. Such a record stays when whoever controls the NFT's
|
|
2100
|
+
* collection moved it out of the project's hands without the program, and the program hands out whatever the project
|
|
2101
|
+
* holds at that address: once this player's deposit puts the NFT back in the project's hands, the record can send it
|
|
2102
|
+
* to the wallet it names. NFTs of the project's own collections are not read: those collections refuse the permanent
|
|
2103
|
+
* delegates that take an NFT out.
|
|
2104
|
+
*/
|
|
2105
|
+
async function leftovers(asset) {
|
|
2106
|
+
if (ours(await collectionOf(asset))) return [];
|
|
2107
|
+
const me = wallet.address;
|
|
2108
|
+
const out = [];
|
|
2109
|
+
const listingAt = loot.pda.listing(project, asset);
|
|
2110
|
+
const listing = await readMaybe('Listing', listingAt);
|
|
2111
|
+
if (listing && listing.maker !== me) {
|
|
2112
|
+
const sale = Boolean(listing.deal.Sell);
|
|
2113
|
+
out.push({
|
|
2114
|
+
kind: 'listing',
|
|
2115
|
+
address: listingAt,
|
|
2116
|
+
says: `A listing of this NFT (${listingAt}) is still open in this project, left by an earlier owner, ${listing.maker}. Once the project holds the NFT again, ${listing.maker} can take it home by closing that listing${sale ? `, and anyone can buy it at the listing's price, which is paid to ${listing.maker}` : ''}.`,
|
|
2117
|
+
});
|
|
2118
|
+
}
|
|
2119
|
+
const tag = { memcmp: { offset: 0, bytes: encodeBase58(Uint8Array.of(loot.TAGS.Hold)) } };
|
|
2120
|
+
const holds = await client.programAccounts(loot.LOOT_PROGRAM, [
|
|
2121
|
+
tag,
|
|
2122
|
+
{ memcmp: { offset: 1, bytes: project } },
|
|
2123
|
+
{ memcmp: { offset: 33, bytes: asset } },
|
|
2124
|
+
]);
|
|
2125
|
+
const time = BigInt(await chainNow());
|
|
2126
|
+
for (const { pubkey, account } of holds) {
|
|
2127
|
+
const hold = loot.decode('Hold', account.data);
|
|
2128
|
+
const to = hold.player === project ? 'the project’s treasury' : hold.player;
|
|
2129
|
+
const names = [hold.player, hold.debt?.lender].filter(Boolean);
|
|
2130
|
+
if (names.every(x => x === me)) continue;
|
|
2131
|
+
const due = time >= BigInt(hold.free_at);
|
|
2132
|
+
const what = hold.debt
|
|
2133
|
+
? due
|
|
2134
|
+
? `a loan's hold whose loan is overdue: anyone can send the NFT to its lender, ${hold.debt.lender}`
|
|
2135
|
+
: `a loan's hold: anyone who repays it (${hold.debt.repay} to ${hold.debt.lender}) sends the NFT to ${to}, and once it is due anyone sends it to the lender`
|
|
2136
|
+
: due
|
|
2137
|
+
? `a hold that is over: anyone can send the NFT to ${to} at once`
|
|
2138
|
+
: `a hold that runs until ${new Date(Number(hold.free_at) * 1000).toISOString()}: the project's authority decides where the NFT goes until then, and anyone sends it to ${to} after`;
|
|
2139
|
+
out.push({
|
|
2140
|
+
kind: 'hold',
|
|
2141
|
+
address: pubkey,
|
|
2142
|
+
says: `This project still has ${what} (${pubkey}), left from an earlier owner of this NFT. Once the project holds the NFT again, it can be sent there.`,
|
|
2143
|
+
});
|
|
2144
|
+
}
|
|
2145
|
+
return out;
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
/**
|
|
2149
|
+
* Refuses, in words, to hand `asset` to the project while a record an earlier owner left can take it (`leftovers`),
|
|
2150
|
+
* unless the caller says the player accepts that (`accept` true). `listing`: the route is a listing, which the program
|
|
2151
|
+
* refuses outright while a listing of the NFT is open (its address is taken), whatever the option says.
|
|
2152
|
+
*/
|
|
2153
|
+
async function noLeftovers(asset, accept, { listing = false } = {}) {
|
|
2154
|
+
const found = await leftovers(asset);
|
|
2155
|
+
if (listing && found.some(x => x.kind === 'listing'))
|
|
2156
|
+
throw new Error(
|
|
2157
|
+
`${found.find(x => x.kind === 'listing').says} The program refuses another listing of this NFT while that one is open (its address is taken): nothing was sent.`,
|
|
2158
|
+
);
|
|
2159
|
+
if (!found.length || accept === true) return;
|
|
2160
|
+
throw new Error(
|
|
2161
|
+
`${found.map(x => x.says).join(' ')} This deposit would put the NFT where that record can take it: it would be lost to you. Nothing was sent. Go ahead only if the player accepts this: pass acceptLeftover: true.`,
|
|
2162
|
+
);
|
|
2163
|
+
}
|
|
2164
|
+
|
|
1709
2165
|
/** Who holds each collectible a split's holder shares name now: { asset: holder }. */
|
|
1710
2166
|
async function holdersOf(shares) {
|
|
1711
2167
|
const holders = {};
|
|
@@ -1846,7 +2302,9 @@ export function solanaApp({
|
|
|
1846
2302
|
return out;
|
|
1847
2303
|
}
|
|
1848
2304
|
|
|
1849
|
-
/** The nonce a pull of entry `i` of the round at `key` names: a place's roll is made at `prizeRollNonce(nonce + place)`.
|
|
2305
|
+
/** The nonce a pull of entry `i` of the round at `key` names: a place's roll is made at `prizeRollNonce(nonce + place)`.
|
|
2306
|
+
* The one nonce of that entry: every pull of it (the winner's, a payout's, a cancel's, a run of rolls in parts) names
|
|
2307
|
+
* this and no other, so no later pull lands on an order an earlier one made. */
|
|
1850
2308
|
const pullNonce = (key, i) => loot.prizeNonce(key, BigInt(i));
|
|
1851
2309
|
|
|
1852
2310
|
/** How many of a round's owed parts a pull of `part` (of entry `i`) settles. */
|
|
@@ -1863,10 +2321,14 @@ export function solanaApp({
|
|
|
1863
2321
|
* `signer`, and the rolls they make ([{ order, box }], `loot.pullRolls`). With `closes`, the last pull carries the
|
|
1864
2322
|
* round's close (when the parts it pays are the last owed).
|
|
1865
2323
|
*/
|
|
1866
|
-
async function pulls(key, r, i, parts, signer, { closes = false } = {}) {
|
|
2324
|
+
async function pulls(key, r, i, parts, signer, { closes = false, lotteryClosed = false } = {}) {
|
|
1867
2325
|
const e = i == null ? null : r.entries[i];
|
|
1868
2326
|
const out = [];
|
|
1869
2327
|
const rolls = [];
|
|
2328
|
+
// Whether a pull here wants the largest heap (a run of rolls), and whether one stops short of the part (a raffle's
|
|
2329
|
+
// NFTs past what one pull takes out, a run of rolls past what one transaction carries): a next pull carries on.
|
|
2330
|
+
let heap = false;
|
|
2331
|
+
let partial = false;
|
|
1870
2332
|
const g = await projectNow();
|
|
1871
2333
|
// A run's token prize is in its own mint: the prizes the round recorded name each.
|
|
1872
2334
|
const extra = await hookTail([...roundMints(r), ...r.prizes.map(p => p.mint)]);
|
|
@@ -1890,6 +2352,10 @@ export function solanaApp({
|
|
|
1890
2352
|
? loot.decodeStash((await client.account(loot.pda.lotteryStash(r.lottery)))?.data ?? null)
|
|
1891
2353
|
: null;
|
|
1892
2354
|
if (stash) {
|
|
2355
|
+
// One pull takes out at most so many of the stash's NFTs (`loot.pullAssigns`): the places past them wait.
|
|
2356
|
+
const owedPlaces = loot.prizePlacesOwed(r, i, run);
|
|
2357
|
+
const last = loot.placesIn(r, run, e.start, e.end).at(-1) ?? 0;
|
|
2358
|
+
if (owedPlaces.some(p => loot.nftSlot(r, p) >= loot.pullAssigns(r, stash, last))) partial = true;
|
|
1893
2359
|
const predicted = loot.stashNfts(r, stash);
|
|
1894
2360
|
for (const place of loot.prizePlacesOwed(r, i, run)) {
|
|
1895
2361
|
const nft = predicted[loot.nftSlot(r, place)];
|
|
@@ -1916,7 +2382,38 @@ export function solanaApp({
|
|
|
1916
2382
|
projectMint: mintOf(),
|
|
1917
2383
|
};
|
|
1918
2384
|
more.nonce = pullNonce(key, i);
|
|
1919
|
-
|
|
2385
|
+
// A run of places that each give a roll: two accounts a place, as many as one transaction carries, on the
|
|
2386
|
+
// largest heap. The pull is sized from a dry run, largest first; the next pull carries on, and only the last
|
|
2387
|
+
// carries the close.
|
|
2388
|
+
if (given.Roll) {
|
|
2389
|
+
const owed = loot.prizePlacesOwed(r, i, run).length;
|
|
2390
|
+
const sized = async most => {
|
|
2391
|
+
const ix = loot.pullIx({
|
|
2392
|
+
payer: signer,
|
|
2393
|
+
project,
|
|
2394
|
+
roundAddress: key,
|
|
2395
|
+
round: { ...r, owed: Number.MAX_SAFE_INTEGER },
|
|
2396
|
+
entry: i,
|
|
2397
|
+
part,
|
|
2398
|
+
projectState: g,
|
|
2399
|
+
programs,
|
|
2400
|
+
extra,
|
|
2401
|
+
...more,
|
|
2402
|
+
prize: { ...more.prize, most },
|
|
2403
|
+
});
|
|
2404
|
+
const tx = [computeUnitLimit(1_400_000), requestHeapFrame(loot.LARGE_SETTLE_HEAP), ix];
|
|
2405
|
+
return (
|
|
2406
|
+
Boolean(
|
|
2407
|
+
compileFitting({ payer: signer, blockhash: NOBODY, instructions: tx, tables: await standing() }),
|
|
2408
|
+
) && (await dryRun(signer, tx)).ok
|
|
2409
|
+
);
|
|
2410
|
+
};
|
|
2411
|
+
let most = owed;
|
|
2412
|
+
while (most > 1 && !(await sized(most))) most = Math.max(1, Math.floor(most * 0.75));
|
|
2413
|
+
if (most < owed) [more.prize.most, partial] = [most, true];
|
|
2414
|
+
heap = true;
|
|
2415
|
+
}
|
|
2416
|
+
rolls.push(...loot.pullRolls({ project, round: r, entry: i, part, nonce: more.nonce, most: more.prize.most }));
|
|
1920
2417
|
} else if (part.Common) {
|
|
1921
2418
|
// Where a part owed in common goes: a share's payee (a collectible's holder now), what the places leave (the
|
|
1922
2419
|
// pot, or the `rest` a round paid from its own money names), a pool; one that can't take it (a pool never
|
|
@@ -1947,13 +2444,14 @@ export function solanaApp({
|
|
|
1947
2444
|
entry: i ?? 0,
|
|
1948
2445
|
part,
|
|
1949
2446
|
projectState: g,
|
|
2447
|
+
lotteryClosed,
|
|
1950
2448
|
programs,
|
|
1951
2449
|
extra,
|
|
1952
2450
|
...more,
|
|
1953
2451
|
}),
|
|
1954
2452
|
);
|
|
1955
2453
|
}
|
|
1956
|
-
return { instructions: out, rolls };
|
|
2454
|
+
return { instructions: out, rolls, heap, partial };
|
|
1957
2455
|
}
|
|
1958
2456
|
|
|
1959
2457
|
return {
|
|
@@ -2102,15 +2600,20 @@ export function solanaApp({
|
|
|
2102
2600
|
return send([...before, loot.refundIx({ signer: wallet.address, orderAddress, order, subject, programs })]);
|
|
2103
2601
|
},
|
|
2104
2602
|
|
|
2105
|
-
/** Asks the oracle for a roll a mission, a recipe or a prize made (
|
|
2106
|
-
*
|
|
2107
|
-
*
|
|
2108
|
-
|
|
2603
|
+
/** Asks the oracle for a roll a mission, a recipe or a prize made (its player or the project's keys may; the asker
|
|
2604
|
+
* pays the oracle). Its first ask fixes its odds: the player's profile, and an NFT they show (`shown`; by default
|
|
2605
|
+
* their collectible that raises the box's odds the most), count then. On a box with nothing in stock only the order's
|
|
2606
|
+
* player may ask, a slot after it was made or last asked, and that ends the roll there with nothing, no oracle (a lot
|
|
2607
|
+
* its winner paid for fails, for its refund). */
|
|
2608
|
+
askRoll: (orderAddress, options = {}) => {
|
|
2609
|
+
only(options, ['shown'], 'askRoll');
|
|
2610
|
+
return askRoll(orderAddress, options);
|
|
2611
|
+
},
|
|
2109
2612
|
|
|
2110
2613
|
/**
|
|
2111
2614
|
* Pays a payment of the economy (an entry, an attempt, a tip). `amount`: what the player chose, in whole units, for
|
|
2112
2615
|
* a payment with `any` (at least its `amount`). `chosenRecipient`: the wallet the player names for a share the
|
|
2113
|
-
* payment lets them choose (a referrer
|
|
2616
|
+
* payment lets them choose (a referrer; their own wallet too, which then pays them back their share). `holders`: asset → holder, for a holder's share (read
|
|
2114
2617
|
* now when not given).
|
|
2115
2618
|
*/
|
|
2116
2619
|
async pay(paymentKey, { amount: chosen, holders, chosenRecipient = null, ...others } = NO_OPTIONS) {
|
|
@@ -2125,8 +2628,6 @@ export function solanaApp({
|
|
|
2125
2628
|
const pools = new Map((spec.pools ?? []).map(x => [x.key, x.index]));
|
|
2126
2629
|
// `pay` takes its shares from the payer each time: a "chosen" share pays the wallet they name, and with none named
|
|
2127
2630
|
// it is left out, so what it would pay goes to the treasury with the rest (as a purchase's does).
|
|
2128
|
-
if (chosenRecipient && chosenRecipient === wallet.address)
|
|
2129
|
-
throw new Error('A payer can’t name their own wallet for a share they choose.');
|
|
2130
2631
|
const shares = (p.split ?? []).flatMap(s => {
|
|
2131
2632
|
const [kind, key] = s.to.split(':');
|
|
2132
2633
|
const bps = Math.round(s.percent * 100);
|
|
@@ -2203,7 +2704,10 @@ export function solanaApp({
|
|
|
2203
2704
|
const copy = current ?? lottery;
|
|
2204
2705
|
const terms = copy?.terms;
|
|
2205
2706
|
const currency = lottery ? ticketCurrencyOf(l, lottery) : (l.ticketCurrency ?? currencyKey);
|
|
2206
|
-
const ended = lottery &&
|
|
2707
|
+
const ended = lottery && loot.lotteryEnded(lottery);
|
|
2708
|
+
// A lottery that weighs tickets by time: what one ticket bought now weighs, and its share of the round's weight
|
|
2709
|
+
// (the same ticket bought later weighs less: show it on the buy screen).
|
|
2710
|
+
const odds = terms && !ended ? loot.purchaseOdds(current, terms, 1n, await chainNow()) : null;
|
|
2207
2711
|
out.push({
|
|
2208
2712
|
key: l.key,
|
|
2209
2713
|
name: l.name,
|
|
@@ -2214,6 +2718,11 @@ export function solanaApp({
|
|
|
2214
2718
|
round: lottery?.round ?? null,
|
|
2215
2719
|
sold: lottery ? (current?.sold ?? 0n) : null,
|
|
2216
2720
|
drawsAt: terms ? Number(terms.draws_at) : null,
|
|
2721
|
+
salesCloseAt: terms ? Number(loot.salesCloseAt(terms)) : null,
|
|
2722
|
+
timeWeighted: Boolean(terms?.time_weighted),
|
|
2723
|
+
weight: current ? loot.roundTotal(current) : null,
|
|
2724
|
+
ticketWeight: odds?.weight ?? null,
|
|
2725
|
+
ticketShare: odds?.share ?? null,
|
|
2217
2726
|
nftTicket: Boolean(lottery?.terms.nft_ticket),
|
|
2218
2727
|
incarnation: lottery?.incarnation ?? null,
|
|
2219
2728
|
revision: copy?.revision ?? null,
|
|
@@ -2222,6 +2731,25 @@ export function solanaApp({
|
|
|
2222
2731
|
return out;
|
|
2223
2732
|
},
|
|
2224
2733
|
|
|
2734
|
+
/**
|
|
2735
|
+
* What `count` tickets of a lottery bought now would weigh in its current round's draw, before they are bought:
|
|
2736
|
+
* { tickets, weight, total, share, salesCloseAt, timeWeighted }. In a lottery that weighs tickets by time, each
|
|
2737
|
+
* ticket weighs the seconds from its purchase to the close of sales, so the same tickets bought later weigh less;
|
|
2738
|
+
* `share` is the purchase's part of the round's weight with it (0 to 1), its chance at each place as things stand.
|
|
2739
|
+
*/
|
|
2740
|
+
async ticketWeight(lotteryKey, count = 1) {
|
|
2741
|
+
const l = part('lotteries', lotteryKey);
|
|
2742
|
+
const { lottery, current } = await roundOf(l);
|
|
2743
|
+
const terms = (current ?? lottery).terms;
|
|
2744
|
+
const odds = loot.purchaseOdds(current, terms, BigInt(count), await chainNow());
|
|
2745
|
+
return {
|
|
2746
|
+
tickets: BigInt(count),
|
|
2747
|
+
...odds,
|
|
2748
|
+
salesCloseAt: Number(loot.salesCloseAt(terms)),
|
|
2749
|
+
timeWeighted: Boolean(terms.time_weighted),
|
|
2750
|
+
};
|
|
2751
|
+
},
|
|
2752
|
+
|
|
2225
2753
|
/**
|
|
2226
2754
|
* Buys `count` tickets of a lottery, on the terms the player was shown: `listing`, the lottery as `lotteries()`
|
|
2227
2755
|
* showed it (a lottery changed since, its price or anything else, is refused rather than paid), or `maxPrice`, the
|
|
@@ -2234,9 +2762,22 @@ export function solanaApp({
|
|
|
2234
2762
|
async buyTickets(
|
|
2235
2763
|
lotteryKey,
|
|
2236
2764
|
count = 1,
|
|
2237
|
-
{
|
|
2765
|
+
{
|
|
2766
|
+
shown,
|
|
2767
|
+
chosenRecipient = null,
|
|
2768
|
+
listProof,
|
|
2769
|
+
nft: paidWith,
|
|
2770
|
+
listing,
|
|
2771
|
+
maxPrice,
|
|
2772
|
+
acceptLeftover,
|
|
2773
|
+
...others
|
|
2774
|
+
} = NO_OPTIONS,
|
|
2238
2775
|
) {
|
|
2239
|
-
only(
|
|
2776
|
+
only(
|
|
2777
|
+
others,
|
|
2778
|
+
['shown', 'chosenRecipient', 'listProof', 'nft', 'listing', 'maxPrice', 'acceptLeftover'],
|
|
2779
|
+
'buyTickets',
|
|
2780
|
+
);
|
|
2240
2781
|
const l = part('lotteries', lotteryKey);
|
|
2241
2782
|
const name = l.name ?? lotteryKey;
|
|
2242
2783
|
if (listing && listing.key !== undefined && listing.key !== l.key)
|
|
@@ -2279,7 +2820,14 @@ export function solanaApp({
|
|
|
2279
2820
|
throw new Error(
|
|
2280
2821
|
`A ticket of “${name}” now costs ${show(terms.price, decimalsOf(currency))}, more than the ${show(BigInt(given), decimalsOf(currency))} the player was shown: show them the new price first.`,
|
|
2281
2822
|
);
|
|
2282
|
-
if (
|
|
2823
|
+
if (loot.lotteryEnded(lottery)) throw new Error(`“${l.name}” has drawn and ended.`);
|
|
2824
|
+
// Sales close `closes_before` seconds before the draw (SalesClosed after).
|
|
2825
|
+
// (A repeating lottery's current round nobody bought into moves to its next date as this purchase opens it.)
|
|
2826
|
+
const movesOn = !round && Number(lottery.every) > 0 && BigInt(now()) >= BigInt(terms.draws_at);
|
|
2827
|
+
if (!due && !movesOn && BigInt(now()) >= loot.salesCloseAt(terms))
|
|
2828
|
+
throw new Error(
|
|
2829
|
+
`Sales of “${name}” closed at ${new Date(Number(loot.salesCloseAt(terms)) * 1000).toISOString()}, before its draw: wait for the next round.`,
|
|
2830
|
+
);
|
|
2283
2831
|
shown ??= await passing(lottery.gate, 'buy a ticket');
|
|
2284
2832
|
listProof = listProofOf(lottery.gate, l.gate?.list, listProof);
|
|
2285
2833
|
let nft;
|
|
@@ -2287,6 +2835,8 @@ export function solanaApp({
|
|
|
2287
2835
|
if (terms.nft_ticket) {
|
|
2288
2836
|
if (!paidWith) throw new Error(`A ticket of “${l.name}” costs an NFT: say which (nft).`);
|
|
2289
2837
|
if (BigInt(count) !== 1n) throw new Error('An NFT pays for one ticket at a time.');
|
|
2838
|
+
// The project holds an NFT ticket until the round is drawn.
|
|
2839
|
+
await noLeftovers(paidWith, acceptLeftover);
|
|
2290
2840
|
const { before: proof, ...kind } = await nftOf(paidWith);
|
|
2291
2841
|
nft = { asset: paidWith, ...kind };
|
|
2292
2842
|
before.push(...proof);
|
|
@@ -2450,10 +3000,13 @@ export function solanaApp({
|
|
|
2450
3000
|
* Core asset, a Token Metadata NFT's mint (legacy or pNFT), or a compressed NFT's asset id. In a farm that watches,
|
|
2451
3001
|
* an NFT whose last staker sold it takes their seat over. A farm with an end takes none at its end or after it.
|
|
2452
3002
|
*/
|
|
2453
|
-
async stakeNft(farmKey, asset, tier = 0, { extra, transfers, ...others } = NO_OPTIONS) {
|
|
2454
|
-
only(others, ['extra', 'transfers'], 'stakeNft');
|
|
3003
|
+
async stakeNft(farmKey, asset, tier = 0, { extra, transfers, acceptLeftover, ...others } = NO_OPTIONS) {
|
|
3004
|
+
only(others, ['extra', 'transfers', 'acceptLeftover'], 'stakeNft');
|
|
2455
3005
|
const f = part('farms', farmKey);
|
|
2456
3006
|
const farm = await read('Farm', loot.pda.farm(project, f.id));
|
|
3007
|
+
// A farm that keeps its NFTs in escrow hands this one to the project; one that freezes or watches leaves it in the
|
|
3008
|
+
// wallet, where no record of the project's can move it.
|
|
3009
|
+
if (farm.custody === 'Escrow') await noLeftovers(asset, acceptLeftover);
|
|
2457
3010
|
await openFor(f, farm, tier);
|
|
2458
3011
|
const token = await farmTokens([farm.rewards_mint], [], { extra, transfers });
|
|
2459
3012
|
const { before, ...nft } = await nftOf(asset);
|
|
@@ -2485,12 +3038,17 @@ export function solanaApp({
|
|
|
2485
3038
|
|
|
2486
3039
|
/**
|
|
2487
3040
|
* Releases the collectible after its lock: it grows on its way out (a farm with `growth`), and from its own seat
|
|
2488
|
-
* takes its tier's prize (a mission: `prize.rs`). `forfeit` gives up rewards, and the prize, that cannot move.
|
|
2489
|
-
*
|
|
3041
|
+
* takes its tier's prize (a mission: `prize.rs`). `forfeit` gives up rewards, and the prize, that cannot move. A
|
|
3042
|
+
* prize that can't be given now refuses the leave and the NFT stays (`needPrize`, true by default); `needPrize: false`
|
|
3043
|
+
* leaves without it (the receipt says `prize_missed`). Returns the signature, or { signature, roll } when it makes a roll (a mission's result on the NFT, or a chest),
|
|
2490
3044
|
* which `collect` then finishes.
|
|
2491
3045
|
*/
|
|
2492
|
-
async unstakeNft(
|
|
2493
|
-
|
|
3046
|
+
async unstakeNft(
|
|
3047
|
+
farmKey,
|
|
3048
|
+
asset,
|
|
3049
|
+
{ forfeit = false, needPrize = true, extra, transfers, acceptLeftover, ...others } = NO_OPTIONS,
|
|
3050
|
+
) {
|
|
3051
|
+
only(others, ['forfeit', 'needPrize', 'extra', 'transfers', 'acceptLeftover'], 'unstakeNft');
|
|
2494
3052
|
const f = part('farms', farmKey);
|
|
2495
3053
|
const farm = await read('Farm', loot.pda.farm(project, f.id));
|
|
2496
3054
|
const token = await farmTokens(
|
|
@@ -2513,6 +3071,10 @@ export function solanaApp({
|
|
|
2513
3071
|
// `prizeRollNonce` of the one named (`namedRollNonce`).
|
|
2514
3072
|
const nonce = roll != null ? loot.nonceOn(asset) : freshNonce();
|
|
2515
3073
|
const boxes = roll != null ? { [roll]: await read('LootBox', loot.pda.box(project, roll)) } : {};
|
|
3074
|
+
// A mission whose box holds the NFT it rolls on takes it into the project's hands on the way out, from a farm that
|
|
3075
|
+
// left it in the wallet.
|
|
3076
|
+
if (roll != null && farm.custody !== 'Escrow' && loot.boxHolds(boxes[roll]))
|
|
3077
|
+
await noLeftovers(asset, acceptLeftover);
|
|
2516
3078
|
// A roll still at the NFT's order address blocks the leave, unless it was asked and came back with nothing to give
|
|
2517
3079
|
// (Waiting, asked once): that one stands in for this. Any other is asked (its player or the project's keys), answered and settled first.
|
|
2518
3080
|
if (roll != null) {
|
|
@@ -2531,6 +3093,7 @@ export function solanaApp({
|
|
|
2531
3093
|
asset,
|
|
2532
3094
|
...nft,
|
|
2533
3095
|
forfeit,
|
|
3096
|
+
needPrize,
|
|
2534
3097
|
roll,
|
|
2535
3098
|
nonce,
|
|
2536
3099
|
tier: seat.tier,
|
|
@@ -2541,9 +3104,20 @@ export function solanaApp({
|
|
|
2541
3104
|
boxes,
|
|
2542
3105
|
...token,
|
|
2543
3106
|
});
|
|
2544
|
-
const
|
|
2545
|
-
|
|
2546
|
-
)
|
|
3107
|
+
const steps = await walking(f, farm, [...before, ...given.before, ...token.before, ix]);
|
|
3108
|
+
// A mission prize that can't be given now (its pool short, an item sold out) refuses the leave and the NFT stays
|
|
3109
|
+
// (PrizeNotCovered): say so, and offer to leave without it (`needPrize: false`).
|
|
3110
|
+
if (prize != null && !forfeit && needPrize) {
|
|
3111
|
+
const tried = await dryRun(wallet.address, steps);
|
|
3112
|
+
if (!tried.ok && tried.error?.name === 'PrizeNotCovered')
|
|
3113
|
+
throw Object.assign(
|
|
3114
|
+
new Error(
|
|
3115
|
+
`This NFT's mission prize can't be given now, so it stays staked. Leave without the prize (needPrize: false) to take the NFT home now; the prize is then not given.`,
|
|
3116
|
+
),
|
|
3117
|
+
{ code: tried.error.code, name: 'PrizeNotCovered' },
|
|
3118
|
+
);
|
|
3119
|
+
}
|
|
3120
|
+
const { signature, rolls } = await sendRolling(steps);
|
|
2547
3121
|
const rolled = rolls.length ? { rolled: rolls[0] } : {};
|
|
2548
3122
|
if (roll != null)
|
|
2549
3123
|
return {
|
|
@@ -2712,6 +3286,9 @@ export function solanaApp({
|
|
|
2712
3286
|
freeAt: farm.lock_key ? null : Number(BigInt(seat.since) + lock),
|
|
2713
3287
|
...(farm.lock_key ? { lockKey: farm.lock_key } : {}),
|
|
2714
3288
|
boostPercent: Number(seat.boost_bps) / 100,
|
|
3289
|
+
// Its weight in the farm, as the seat holds it: near the farm's total cap a seat weighs only what is left, so
|
|
3290
|
+
// it is read, never worked out from the tier and the boost.
|
|
3291
|
+
weight: BigInt(seat.weight),
|
|
2715
3292
|
rewards: rewards?.key ?? null,
|
|
2716
3293
|
waiting: show(seat.owed, rewards ? decimalsOf(rewards.key) : 0),
|
|
2717
3294
|
owedUnits: BigInt(seat.owed),
|
|
@@ -2882,7 +3459,9 @@ export function solanaApp({
|
|
|
2882
3459
|
* Hands an NFT of any kind to the project until a time (a hold): `seconds` from now, or `{ until }` (Unix seconds).
|
|
2883
3460
|
* The hold's address names its end. Returns { signature, freeAt }.
|
|
2884
3461
|
*/
|
|
2885
|
-
async hold(asset, seconds) {
|
|
3462
|
+
async hold(asset, seconds, { acceptLeftover, ...others } = NO_OPTIONS) {
|
|
3463
|
+
only(others, ['acceptLeftover'], 'hold');
|
|
3464
|
+
await noLeftovers(asset, acceptLeftover);
|
|
2886
3465
|
const freeAt = BigInt(typeof seconds === 'object' ? seconds.until : now() + Number(seconds));
|
|
2887
3466
|
const { before, hasAuthority: _, ...nft } = await nftOf(asset);
|
|
2888
3467
|
const signature = await send([
|
|
@@ -2902,9 +3481,10 @@ export function solanaApp({
|
|
|
2902
3481
|
/**
|
|
2903
3482
|
* A lender's offer to borrow against: `maker`'s offer number `id` in this project. Returns { address, offer, mint,
|
|
2904
3483
|
* amount, repay, lasts, risks }: `risks`, in words, each power over the loan's token that could make repaying fail
|
|
2905
|
-
* (its freeze authority, a Token-2022 pause, a transfer hook, a permanent delegate) and who holds it
|
|
2906
|
-
*
|
|
2907
|
-
*
|
|
3484
|
+
* (its freeze authority, a Token-2022 pause, a transfer hook, a permanent delegate) and who holds it, and each setting
|
|
3485
|
+
* of the token that lets the lender turn the repayment away (accounts that start frozen or can be made to,
|
|
3486
|
+
* confidential transfers) or blocks it (a token that can't be transferred). The program takes such tokens, and a
|
|
3487
|
+
* lender who can stop every repayment takes the NFT once it is due: show the risks before the player borrows.
|
|
2908
3488
|
*/
|
|
2909
3489
|
async loanOffer(maker, id) {
|
|
2910
3490
|
const address = loot.pda.offer(project, maker, id);
|
|
@@ -2937,6 +3517,8 @@ export function solanaApp({
|
|
|
2937
3517
|
says: `${p.holder} can move anyone's tokens of the loan's token, the ones you would repay with too.${lender(p.holder)}`,
|
|
2938
3518
|
};
|
|
2939
3519
|
});
|
|
3520
|
+
// What the token's own settings let a lender do: the program no longer refuses these, so the kit says them.
|
|
3521
|
+
risks.push(...tokenSettings(account, 'repaying', 'the lender', 'and take your NFT when the loan is due'));
|
|
2940
3522
|
const { amount, repay, lasts } = offer.deal.Lend;
|
|
2941
3523
|
return { address, offer, mint: offer.mint, program: account.owner, amount, repay, lasts, risks };
|
|
2942
3524
|
},
|
|
@@ -2944,12 +3526,15 @@ export function solanaApp({
|
|
|
2944
3526
|
/**
|
|
2945
3527
|
* Borrows against the player's NFT `asset` from `maker`'s offer `id`: the NFT is held until the loan is repaid, or
|
|
2946
3528
|
* goes to the lender once it is due (`due`, Unix seconds; by default the longest the offer allows, less a minute).
|
|
2947
|
-
* A loan whose token someone could freeze, pause, hook or move
|
|
2948
|
-
* `acceptRisks` is true: the page shows the player
|
|
3529
|
+
* A loan whose token someone could freeze, pause, hook or move, or whose own settings let the lender turn the
|
|
3530
|
+
* repayment away (`loanOffer`'s `risks`), is refused unless `acceptRisks` is true: the page shows the player each
|
|
3531
|
+
* risk first. `extra`: a transfer hook's accounts.
|
|
2949
3532
|
*/
|
|
2950
|
-
async borrow(maker, id, asset, { due, acceptRisks = false, extra
|
|
2951
|
-
only(others, ['due', 'acceptRisks', 'extra'], 'borrow');
|
|
3533
|
+
async borrow(maker, id, asset, { due, acceptRisks = false, acceptLeftover, extra, ...others } = NO_OPTIONS) {
|
|
3534
|
+
only(others, ['due', 'acceptRisks', 'acceptLeftover', 'extra'], 'borrow');
|
|
3535
|
+
await noLeftovers(asset, acceptLeftover);
|
|
2952
3536
|
const loan = await this.loanOffer(maker, id);
|
|
3537
|
+
extra ??= await hookTail([loan.mint]);
|
|
2953
3538
|
if (loan.risks.length && acceptRisks !== true)
|
|
2954
3539
|
throw new Error(
|
|
2955
3540
|
`Repaying this loan can be blocked. ${loan.risks.map(r => r.says).join(' ')} Borrow only if the player accepts this: pass acceptRisks: true.`,
|
|
@@ -2996,18 +3581,21 @@ export function solanaApp({
|
|
|
2996
3581
|
},
|
|
2997
3582
|
|
|
2998
3583
|
/**
|
|
2999
|
-
* Repays the loan on `asset` (held until `due`), the player's own or a friend's,
|
|
3000
|
-
* borrower. `repay`: the debt the payer was shown (`loan()`'s, in base units);
|
|
3001
|
-
* (`TermsChanged`).
|
|
3002
|
-
*
|
|
3003
|
-
*
|
|
3004
|
-
*
|
|
3005
|
-
*
|
|
3584
|
+
* Repays the loan on `asset` (held until `due`), the player's own or a friend's, and brings the NFT home to its
|
|
3585
|
+
* borrower in the same instruction: one prompt. `repay`: the debt the payer was shown (`loan()`'s, in base units);
|
|
3586
|
+
* the program refuses any other (`TermsChanged`). An NFT its collection took back or burned while the project held
|
|
3587
|
+
* it is refused here, before anything is signed: the program takes no repayment for it, and the borrower keeps what
|
|
3588
|
+
* they borrowed. When the NFT can't move now (frozen for now, or its collection refuses the move), or `home` is
|
|
3589
|
+
* false, the money goes alone, so the loan does not fall due: the hold is then over, and anyone sends the NFT home
|
|
3590
|
+
* once it can move (`claims()` lists it). A compressed NFT is proven in the same transaction; the money alone for one
|
|
3591
|
+
* whose proof keeps going out of date goes without it. Returns { signature, home }, and `says` when the NFT waits.
|
|
3006
3592
|
*/
|
|
3007
|
-
async repay(asset, due, { repay,
|
|
3008
|
-
only(others, ['repay', 'extra'], 'repay');
|
|
3009
|
-
const
|
|
3593
|
+
async repay(asset, due, { repay, home = true, extra, ...others } = NO_OPTIONS) {
|
|
3594
|
+
only(others, ['repay', 'home', 'extra'], 'repay');
|
|
3595
|
+
const at = loot.pda.hold(asset, due);
|
|
3596
|
+
const hold = await read('Hold', at);
|
|
3010
3597
|
if (!hold.debt) throw new Error('This NFT is held for no loan left to repay.');
|
|
3598
|
+
extra ??= await hookTail([hold.debt.mint]);
|
|
3011
3599
|
if (repay === undefined || repay === null)
|
|
3012
3600
|
throw new Error(
|
|
3013
3601
|
'Say the debt the payer was shown (repay, from loan()): the amount read now is never paid in its place.',
|
|
@@ -3016,43 +3604,52 @@ export function solanaApp({
|
|
|
3016
3604
|
throw new Error(
|
|
3017
3605
|
`This loan now asks ${hold.debt.repay}, not the ${repay} shown: show the payer the loan as it is now first.`,
|
|
3018
3606
|
);
|
|
3607
|
+
if ((await heldNow(asset)).held === false)
|
|
3608
|
+
throw new Error(
|
|
3609
|
+
`The project no longer holds the NFT ${asset}: whoever controls its collection took it back or burned it while it was held. The program takes no repayment for it (NotTheNft): the borrower keeps what they borrowed.`,
|
|
3610
|
+
);
|
|
3019
3611
|
const { before, hasAuthority: _, ...nft } = await nftOf(asset, wallet.address, { to: hold.player });
|
|
3020
|
-
const
|
|
3021
|
-
// A
|
|
3022
|
-
|
|
3023
|
-
const
|
|
3024
|
-
const
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
heavy ? loot.SETTLE_RENT_UNIT + SIGNATURE_FEE : SIGNATURE_FEE,
|
|
3028
|
-
await jobKey(at),
|
|
3029
|
-
);
|
|
3030
|
-
const repaid = loot.repayIx({ payer: wallet.address, project, hold, repay, programs, extra });
|
|
3031
|
-
const signature = await send(
|
|
3032
|
-
withSolWrap(wallet.address, hold.debt.mint, BigInt(hold.debt.repay), [...opens, repaid, fund]),
|
|
3033
|
-
);
|
|
3034
|
-
// Repaid, the hold is a plain one that is over (no debt, `free_at` 0) at the address its loan named.
|
|
3035
|
-
const release = async signer => {
|
|
3036
|
-
const {
|
|
3037
|
-
before: first,
|
|
3038
|
-
hasAuthority: _h,
|
|
3039
|
-
...kind
|
|
3040
|
-
} = heavy ? await nftOf(asset, signer, { to: hold.player }) : { before: [], ...nft };
|
|
3041
|
-
return [
|
|
3612
|
+
const kind = nft.compressed || nft.tokenMetadata ? nft : { collection: (await collectionOf(asset)) ?? MPL_CORE };
|
|
3613
|
+
// A Token Metadata NFT's move and a compressed NFT's proof take more than the default compute.
|
|
3614
|
+
const budget = nft.compressed || nft.tokenMetadata ? [computeUnitLimit(1_000_000)] : [];
|
|
3615
|
+
const opens = before.filter(x => x.programId !== COMPUTE_BUDGET_PROGRAM);
|
|
3616
|
+
const repaying = (goesHome, first = []) =>
|
|
3617
|
+
withSolWrap(wallet.address, hold.debt.mint, BigInt(hold.debt.repay), [
|
|
3618
|
+
...budget,
|
|
3042
3619
|
...first,
|
|
3043
|
-
loot.
|
|
3044
|
-
|
|
3620
|
+
loot.repayIx({
|
|
3621
|
+
payer: wallet.address,
|
|
3045
3622
|
project,
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
hold: { ...hold, debt: null, free_at: 0 },
|
|
3623
|
+
hold,
|
|
3624
|
+
repay,
|
|
3625
|
+
home: goesHome,
|
|
3050
3626
|
address: at,
|
|
3627
|
+
...kind,
|
|
3628
|
+
programs,
|
|
3629
|
+
extra,
|
|
3051
3630
|
}),
|
|
3052
|
-
];
|
|
3631
|
+
]);
|
|
3632
|
+
if (home) {
|
|
3633
|
+
const all = repaying(true, opens);
|
|
3634
|
+
// Too big for a dry run as one legacy transaction (a compressed NFT's long proof): sent as it is, the wallet's
|
|
3635
|
+
// one transaction made to fit.
|
|
3636
|
+
const plan = sendPlan({ payer: wallet.address, instructions: all, alone: isProveLeaf });
|
|
3637
|
+
if (plan && !plan.legacy) return { signature: await send(all), home: true };
|
|
3638
|
+
const tried = await dryRun(wallet.address, all);
|
|
3639
|
+
if (tried.ok) return { signature: await send(all), home: true };
|
|
3640
|
+
// What would refuse the money alone too is said now.
|
|
3641
|
+
if (['TermsChanged', 'LoanDue', 'NotALoan', 'NotTheNft'].includes(tried.error?.name))
|
|
3642
|
+
throw Object.assign(new Error(`${tried.error.msg} (${tried.error.name}).`), tried.error);
|
|
3643
|
+
}
|
|
3644
|
+
// The money alone: a compressed NFT proven in the same transaction when that lands, so one that is gone is refused.
|
|
3645
|
+
const proven = nft.compressed ? repaying(false, opens.filter(isProveLeaf)) : null;
|
|
3646
|
+
const money = proven && (await dryRun(wallet.address, proven)).ok ? proven : repaying(false);
|
|
3647
|
+
const signature = await send(money);
|
|
3648
|
+
return {
|
|
3649
|
+
signature,
|
|
3650
|
+
home: false,
|
|
3651
|
+
says: 'The loan is repaid. The NFT stays with the project until it can move (frozen for now, or its collection refuses the move): anyone sends it home to its borrower then, and claims() lists it.',
|
|
3053
3652
|
};
|
|
3054
|
-
const released = await keyRun(key, release);
|
|
3055
|
-
return { signature, released };
|
|
3056
3653
|
},
|
|
3057
3654
|
|
|
3058
3655
|
/**
|
|
@@ -3060,13 +3657,14 @@ export function solanaApp({
|
|
|
3060
3657
|
* of NFTs takes one of the player's NFTs that passes its rule instead (`nft`, its address), held until it is
|
|
3061
3658
|
* claimed. `listProof`: for a list gate.
|
|
3062
3659
|
*/
|
|
3063
|
-
async enter(id, side, amount, { shown, listProof, nft: staked, ...others } = NO_OPTIONS) {
|
|
3064
|
-
only(others, ['shown', 'listProof', 'nft'], 'enter');
|
|
3660
|
+
async enter(id, side, amount, { shown, listProof, nft: staked, acceptLeftover, ...others } = NO_OPTIONS) {
|
|
3661
|
+
only(others, ['shown', 'listProof', 'nft', 'acceptLeftover'], 'enter');
|
|
3065
3662
|
const contest = await read('Contest', loot.pda.contest(project, id));
|
|
3066
3663
|
shown ??= await passing(contest.gate, 'enter');
|
|
3067
3664
|
listProof = listProofOf(contest.gate, null, listProof);
|
|
3068
3665
|
if (contest.nft) {
|
|
3069
3666
|
if (!staked) throw new Error('This contest takes an NFT: say which (nft).');
|
|
3667
|
+
await noLeftovers(staked, acceptLeftover);
|
|
3070
3668
|
const { before, ...kind } = await nftOf(staked);
|
|
3071
3669
|
return send([
|
|
3072
3670
|
...before,
|
|
@@ -3106,7 +3704,8 @@ export function solanaApp({
|
|
|
3106
3704
|
/**
|
|
3107
3705
|
* The auctions: each one's lot, high bid, the least the next bid must be, when it ends and how long is left, and
|
|
3108
3706
|
* whether the player leads. One no longer on chain has ended. `waiting`: SOL that came back to the player's
|
|
3109
|
-
* wrapped-SOL account from a bid that was topped (`unwrap` turns it into plain SOL).
|
|
3707
|
+
* wrapped-SOL account from a bid that was topped (`unwrap` turns it into plain SOL). `warnings`: what to show every
|
|
3708
|
+
* bidder before a bid (a token whose accounts can be frozen, a lot of another collection).
|
|
3110
3709
|
*/
|
|
3111
3710
|
async auctions() {
|
|
3112
3711
|
const list = spec.auctions ?? [];
|
|
@@ -3118,8 +3717,28 @@ export function solanaApp({
|
|
|
3118
3717
|
const waiting = wrapped
|
|
3119
3718
|
? show(new DataView(wrapped.data.buffer, wrapped.data.byteOffset).getBigUint64(64, true), 9)
|
|
3120
3719
|
: '0';
|
|
3720
|
+
const auctions = accounts.map(x => x && loot.decode('Auction', x.data));
|
|
3721
|
+
// What every bidder is told before a bid: what the bids' token lets the high bidder do, and what whoever
|
|
3722
|
+
// controls an NFT lot of another collection can do to it.
|
|
3723
|
+
const mints = [...new Set(auctions.filter(Boolean).map(x => x.mint))];
|
|
3724
|
+
const mintAccounts = Object.fromEntries((await client.accounts(mints)).map((m, k) => [mints[k], m]));
|
|
3725
|
+
const warningsOf = async x => {
|
|
3726
|
+
const out = tokenSettings(
|
|
3727
|
+
mintAccounts[x.mint],
|
|
3728
|
+
'the refund of the bid a higher one tops',
|
|
3729
|
+
'the high bidder',
|
|
3730
|
+
'so every higher bid fails and they win at their price',
|
|
3731
|
+
).map(r => r.says);
|
|
3732
|
+
const lot = x.lot.Asset?.[0];
|
|
3733
|
+
if (lot && !ours(await collectionOf(lot)))
|
|
3734
|
+
out.push(
|
|
3735
|
+
'This lot is an NFT outside the project’s collections: whoever controls its collection can stop it moving or take it back while the project holds it. The seller is paid only when the lot reaches the winner; while it can’t move, the winning bid waits in the project’s vault, until it can or the project calls the sale off, and a lot taken back or burned sends the bid back. A bid can be tied up, not lost.',
|
|
3736
|
+
);
|
|
3737
|
+
return out;
|
|
3738
|
+
};
|
|
3739
|
+
const warned = await Promise.all(auctions.map(x => (x ? warningsOf(x) : [])));
|
|
3121
3740
|
return list.map((a, i) => {
|
|
3122
|
-
const x =
|
|
3741
|
+
const x = auctions[i];
|
|
3123
3742
|
const base = {
|
|
3124
3743
|
key: a.key,
|
|
3125
3744
|
name: a.name,
|
|
@@ -3151,6 +3770,7 @@ export function solanaApp({
|
|
|
3151
3770
|
// A bid of NFTs: a picker of 1 to `maxNfts` (and at least `nextBid`) of the player's NFTs.
|
|
3152
3771
|
...(x.nft_bids ? { nftBids: true, held: x.held, maxNfts: loot.MAX_BID_NFTS } : {}),
|
|
3153
3772
|
...(tokens.get(a.currency ?? currencyKey)?.source === 'sol' ? { waiting } : {}),
|
|
3773
|
+
...(warned[i].length ? { warnings: warned[i] } : {}),
|
|
3154
3774
|
};
|
|
3155
3775
|
});
|
|
3156
3776
|
},
|
|
@@ -3161,8 +3781,8 @@ export function solanaApp({
|
|
|
3161
3781
|
* wrapped-SOL account does not hold already is wrapped, and the account stays open: if this bid is topped, it comes
|
|
3162
3782
|
* back there.
|
|
3163
3783
|
*/
|
|
3164
|
-
async bid(auctionKey, amount, { shown, listProof, nfts, ...others } = NO_OPTIONS) {
|
|
3165
|
-
only(others, ['shown', 'listProof', 'nfts'], 'bid');
|
|
3784
|
+
async bid(auctionKey, amount, { shown, listProof, nfts, acceptLeftover, ...others } = NO_OPTIONS) {
|
|
3785
|
+
only(others, ['shown', 'listProof', 'nfts', 'acceptLeftover'], 'bid');
|
|
3166
3786
|
const a = part('auctions', auctionKey);
|
|
3167
3787
|
const auction = await read('Auction', auctionAt(a));
|
|
3168
3788
|
shown ??= await passing(auction.gate, 'bid');
|
|
@@ -3175,6 +3795,7 @@ export function solanaApp({
|
|
|
3175
3795
|
if (bundle.length > loot.MAX_BID_NFTS) throw new Error(`A bid holds at most ${loot.MAX_BID_NFTS} NFTs.`);
|
|
3176
3796
|
if (auction.held.length >= loot.MAX_BID_NFTS)
|
|
3177
3797
|
throw new Error(`The high bid holds ${loot.MAX_BID_NFTS} NFTs, the most a bid holds: it can't be topped.`);
|
|
3798
|
+
for (const asset of bundle) await noLeftovers(asset, acceptLeftover);
|
|
3178
3799
|
const before = [];
|
|
3179
3800
|
const kindOf = async (asset, to) => {
|
|
3180
3801
|
const { before: proof, ...kind } = await nftOf(asset, wallet.address, { to });
|
|
@@ -3210,9 +3831,11 @@ export function solanaApp({
|
|
|
3210
3831
|
if (value > held)
|
|
3211
3832
|
instructions.push(transferLamports(wallet.address, wrapped, value - held), syncNative(wrapped));
|
|
3212
3833
|
}
|
|
3213
|
-
// A Token-2022 account can ask for a memo on every transfer into it: the Memo program rides along
|
|
3214
|
-
|
|
3215
|
-
|
|
3834
|
+
// A Token-2022 account can ask for a memo on every transfer into it: the Memo program rides along, and a transfer
|
|
3835
|
+
// hook's accounts.
|
|
3836
|
+
const memo = await hookTail([mint]);
|
|
3837
|
+
// The coin's token program, read from the mint when the app wasn't told it.
|
|
3838
|
+
const coin = programs[mint] ?? (await client.account(mint))?.owner ?? TOKEN_PROGRAM;
|
|
3216
3839
|
instructions.push(
|
|
3217
3840
|
loot.bidIx({
|
|
3218
3841
|
bidder: wallet.address,
|
|
@@ -3222,7 +3845,7 @@ export function solanaApp({
|
|
|
3222
3845
|
amount: value,
|
|
3223
3846
|
shown,
|
|
3224
3847
|
listProof,
|
|
3225
|
-
programs,
|
|
3848
|
+
programs: { ...programs, [mint]: coin },
|
|
3226
3849
|
extra: memo,
|
|
3227
3850
|
}),
|
|
3228
3851
|
);
|
|
@@ -3230,167 +3853,32 @@ export function solanaApp({
|
|
|
3230
3853
|
},
|
|
3231
3854
|
|
|
3232
3855
|
/**
|
|
3233
|
-
* Ends an auction that is over (anyone can; the winner usually does): the lot to the highest bidder
|
|
3234
|
-
*
|
|
3235
|
-
* roll lot makes the winner's roll of its box (`order`), which
|
|
3236
|
-
*
|
|
3856
|
+
* Ends an auction that is over (anyone can; the winner usually does): the lot to the highest bidder and the winning
|
|
3857
|
+
* bid to whoever owned it, in one transaction (a new collectible minted for the winner at the address its item and
|
|
3858
|
+
* serial name), however long after the end it is sent. A roll lot makes the winner's roll of its box (`order`), which
|
|
3859
|
+
* `collect` asks for and settles. An NFT lot whose end can't fit one transaction (a compressed lot's long proof, many
|
|
3860
|
+
* shares, a hooked coin) goes first, and the bid is paid by a second end, the accounts it pays into opened before it.
|
|
3861
|
+
* When the lot can't move now (its collection froze it or refuses the move), nothing is sent: the sale waits, the
|
|
3862
|
+
* winning bid in the project's vault (`{ waiting: true, says }`); end it again later, or the project's keys call it
|
|
3863
|
+
* off (`callOffSale`). When the lot or an NFT of the winning bid is gone (its collection took it back or burned it),
|
|
3864
|
+
* the sale is called off on that evidence, which anyone may: the bid goes back to its bidder (`{ calledOff: true }`).
|
|
3865
|
+
* With no bid, the lot goes home to its seller. An end too big for one transaction otherwise makes each NFT that may
|
|
3866
|
+
* be a hold one, and sends each on with a release after it (`releases`).
|
|
3237
3867
|
*/
|
|
3238
3868
|
async endAuction(auctionKey, { holders, ...others } = NO_OPTIONS) {
|
|
3239
3869
|
only(others, ['holders'], 'endAuction');
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
signer: wallet.address,
|
|
3253
|
-
project,
|
|
3254
|
-
projectState: g,
|
|
3255
|
-
id: a.id,
|
|
3256
|
-
auction,
|
|
3257
|
-
programs,
|
|
3258
|
-
extra: memo,
|
|
3259
|
-
}),
|
|
3260
|
-
]),
|
|
3261
|
-
owedPaid: auction.proceeds_owed,
|
|
3262
|
-
};
|
|
3263
|
-
}
|
|
3264
|
-
const sold = auction.high > 0n;
|
|
3265
|
-
const item = auction.lot.Item ? await read('Item', loot.pda.item(project, auction.lot.Item[0])) : null;
|
|
3266
|
-
const made =
|
|
3267
|
-
sold && item?.kind === 'Collectible'
|
|
3268
|
-
? loot.pda.itemAsset(loot.pda.item(project, auction.lot.Item[0]), auction.pin)
|
|
3269
|
-
: null;
|
|
3270
|
-
const memo =
|
|
3271
|
-
programOf(auction.mint) === TOKEN_2022_PROGRAM
|
|
3272
|
-
? [{ pubkey: MEMO_PROGRAM, isSigner: false, isWritable: false }]
|
|
3273
|
-
: [];
|
|
3274
|
-
holders ??= sold ? await holdersOf(auction.shares) : {};
|
|
3275
|
-
const lot = auction.lot.Asset?.[0];
|
|
3276
|
-
// The lot goes to its winner (back to its seller, unsold); a winning bid of NFTs to whoever the rest of a bid in
|
|
3277
|
-
// tokens would go to: the seller of an NFT, the treasury for a new item.
|
|
3278
|
-
const {
|
|
3279
|
-
before,
|
|
3280
|
-
hasAuthority: _,
|
|
3281
|
-
...nft
|
|
3282
|
-
} = lot ? await nftOf(lot, wallet.address, { to: sold ? auction.high_bidder : auction.seller }) : { before: [] };
|
|
3283
|
-
const collection =
|
|
3284
|
-
lot && !nft.compressed && !nft.tokenMetadata ? ((await collectionOf(lot)) ?? MPL_CORE) : undefined;
|
|
3285
|
-
const proceeds = auction.lot.Asset ? auction.seller : g.treasury;
|
|
3286
|
-
const held = [];
|
|
3287
|
-
if (sold && auction.nft_bids) {
|
|
3288
|
-
for (const asset of auction.held) {
|
|
3289
|
-
const { before: proof, ...kind } = await nftOf(asset, wallet.address, { to: proceeds });
|
|
3290
|
-
before.push(...proof);
|
|
3291
|
-
held.push(kind);
|
|
3292
|
-
}
|
|
3293
|
-
}
|
|
3294
|
-
const ix = loot.endAuctionIx({
|
|
3295
|
-
signer: wallet.address,
|
|
3296
|
-
project,
|
|
3297
|
-
projectState: g,
|
|
3298
|
-
id: a.id,
|
|
3299
|
-
auction,
|
|
3300
|
-
item,
|
|
3301
|
-
...nft,
|
|
3302
|
-
collection,
|
|
3303
|
-
held,
|
|
3304
|
-
holders,
|
|
3305
|
-
programs,
|
|
3306
|
-
extra: memo,
|
|
3307
|
-
});
|
|
3308
|
-
let all = [computeUnitLimit(800_000), ...before.filter(x => x.programId !== COMPUTE_BUDGET_PROGRAM), ix];
|
|
3309
|
-
// An end that can't fit one transaction (a compressed lot's long proof, a hooked mint, many shares) makes each NFT
|
|
3310
|
-
// a hold for whoever it goes to, then sends each home with its own release: anyone may, and the rent comes back.
|
|
3311
|
-
const holds =
|
|
3312
|
-
lot || (sold && auction.nft_bids)
|
|
3313
|
-
? !sendPlan({ payer: wallet.address, instructions: all, alone: isProveLeaf })
|
|
3314
|
-
: false;
|
|
3315
|
-
if (holds) {
|
|
3316
|
-
// The end's own transaction funds a throwaway key that sends each release after it (anyone may): each one's
|
|
3317
|
-
// fees, and what it may open (a pNFT's token account and record, a compressed NFT's proof record). One the key
|
|
3318
|
-
// can't send, the wallet does.
|
|
3319
|
-
const outs = [
|
|
3320
|
-
...(lot ? [[lot, sold ? auction.high_bidder : auction.seller]] : []),
|
|
3321
|
-
...(sold && auction.nft_bids ? auction.held.map(x => [x, proceeds]) : []),
|
|
3322
|
-
];
|
|
3323
|
-
const { key, fund } = await keyFor(
|
|
3324
|
-
BigInt(outs.length) * (2n * SIGNATURE_FEE + loot.SETTLE_RENT_UNIT),
|
|
3325
|
-
await jobKey(auctionAt(a)),
|
|
3326
|
-
);
|
|
3327
|
-
all = [
|
|
3328
|
-
computeUnitLimit(800_000),
|
|
3329
|
-
loot.endAuctionIx({
|
|
3330
|
-
signer: wallet.address,
|
|
3331
|
-
project,
|
|
3332
|
-
projectState: g,
|
|
3333
|
-
id: a.id,
|
|
3334
|
-
auction,
|
|
3335
|
-
item,
|
|
3336
|
-
...nft,
|
|
3337
|
-
collection,
|
|
3338
|
-
held,
|
|
3339
|
-
hold: true,
|
|
3340
|
-
holders,
|
|
3341
|
-
programs,
|
|
3342
|
-
extra: memo,
|
|
3343
|
-
}),
|
|
3344
|
-
fund,
|
|
3345
|
-
];
|
|
3346
|
-
const signature = await send(all);
|
|
3347
|
-
const releases = [];
|
|
3348
|
-
for (const [asset, to] of outs) {
|
|
3349
|
-
const release = async signer => {
|
|
3350
|
-
const hold = await read('Hold', loot.pda.hold(asset, 0));
|
|
3351
|
-
const { before: proof, hasAuthority: _h, ...kind } = await nftOf(asset, signer, { to });
|
|
3352
|
-
return [
|
|
3353
|
-
...proof,
|
|
3354
|
-
loot.releaseIx({
|
|
3355
|
-
signer,
|
|
3356
|
-
project,
|
|
3357
|
-
asset,
|
|
3358
|
-
...kind,
|
|
3359
|
-
collection:
|
|
3360
|
-
kind.compressed || kind.tokenMetadata ? undefined : ((await collectionOf(asset)) ?? MPL_CORE),
|
|
3361
|
-
hold,
|
|
3362
|
-
to,
|
|
3363
|
-
}),
|
|
3364
|
-
];
|
|
3365
|
-
};
|
|
3366
|
-
const steps = await release(key.address);
|
|
3367
|
-
const byKey = (await dryRun(key.address, steps)).ok ? await sendAs(key, steps).catch(() => null) : null;
|
|
3368
|
-
releases.push(byKey ?? (await send(await release(wallet.address))));
|
|
3369
|
-
}
|
|
3370
|
-
await emptyKey(key);
|
|
3371
|
-
return { signature, releases, ...(sold ? { winner: auction.high_bidder } : {}) };
|
|
3372
|
-
}
|
|
3373
|
-
// A roll lot sold: the winner's roll, made at the default nonce, waits for its winner or the project's keys to ask it (`collect`); the winner
|
|
3374
|
-
// ending it asks in the same transaction and a key settles it.
|
|
3375
|
-
const roll =
|
|
3376
|
-
sold && auction.lot.Roll
|
|
3377
|
-
? loot.pda.order(
|
|
3378
|
-
project,
|
|
3379
|
-
auction.high_bidder,
|
|
3380
|
-
loot.prizeRollNonce(loot.prizeNonce(auctionAt(a), BigInt(auction.last_bid_at))),
|
|
3381
|
-
)
|
|
3382
|
-
: null;
|
|
3383
|
-
const { signature, rolls } =
|
|
3384
|
-
roll && auction.high_bidder === wallet.address
|
|
3385
|
-
? await sendRolling(all)
|
|
3386
|
-
: { signature: await send(all), rolls: [] };
|
|
3387
|
-
return {
|
|
3388
|
-
signature,
|
|
3389
|
-
...(sold ? { winner: auction.high_bidder } : {}),
|
|
3390
|
-
...(made ? { asset: made } : {}),
|
|
3391
|
-
...(roll ? { order: roll } : {}),
|
|
3392
|
-
...(rolls.length ? { rolled: rolls[0] } : {}),
|
|
3393
|
-
};
|
|
3870
|
+
return endSale(part('auctions', auctionKey), { holders });
|
|
3871
|
+
},
|
|
3872
|
+
|
|
3873
|
+
/**
|
|
3874
|
+
* The project's owner or authority calls off the sale of an NFT lot whose auction is over and whose lot has not moved
|
|
3875
|
+
* (one that never thaws, say), for the project fee: the winning bid goes back to its bidder and the lot home to its
|
|
3876
|
+
* seller, each moved, or made a hold that anyone sends on once it can move (`releases`). Anyone may when the lot or
|
|
3877
|
+
* an NFT of the winning bid is gone (`endAuction` does it then). Refused once the lot went to its winner (`CantCallOff`).
|
|
3878
|
+
*/
|
|
3879
|
+
async callOffSale(auctionKey, { ...others } = NO_OPTIONS) {
|
|
3880
|
+
only(others, [], 'callOffSale');
|
|
3881
|
+
return endSale(part('auctions', auctionKey), { callOff: true });
|
|
3394
3882
|
},
|
|
3395
3883
|
|
|
3396
3884
|
/**
|
|
@@ -3440,8 +3928,9 @@ export function solanaApp({
|
|
|
3440
3928
|
* Lists one of the player's NFTs (any kind) for sale at `price` (whole units of `currency`, the economy's by
|
|
3441
3929
|
* default), `shares` taken first (a royalty, a platform's cut). The project holds it until it sells or is unlisted.
|
|
3442
3930
|
*/
|
|
3443
|
-
async listForSale(asset, { price, currency, shares = [], ...others } = NO_OPTIONS) {
|
|
3444
|
-
only(others, ['price', 'currency', 'shares'], 'listForSale');
|
|
3931
|
+
async listForSale(asset, { price, currency, shares = [], acceptLeftover, ...others } = NO_OPTIONS) {
|
|
3932
|
+
only(others, ['price', 'currency', 'shares', 'acceptLeftover'], 'listForSale');
|
|
3933
|
+
await noLeftovers(asset, acceptLeftover, { listing: true });
|
|
3445
3934
|
const mint = mintOf(currency);
|
|
3446
3935
|
const { before, ...nft } = await nftOf(asset);
|
|
3447
3936
|
return send([
|
|
@@ -3462,8 +3951,12 @@ export function solanaApp({
|
|
|
3462
3951
|
* Lists one of the player's NFTs for rent: `price` (whole units) a period of `lasts` seconds, a renter taking 1 to
|
|
3463
3952
|
* `maxPeriods` at once. Nobody can end a rental early; the next renter takes it once one is over, until it is unlisted.
|
|
3464
3953
|
*/
|
|
3465
|
-
async listForRent(
|
|
3466
|
-
|
|
3954
|
+
async listForRent(
|
|
3955
|
+
asset,
|
|
3956
|
+
{ price, lasts, maxPeriods = 1, currency, shares = [], acceptLeftover, ...others } = NO_OPTIONS,
|
|
3957
|
+
) {
|
|
3958
|
+
only(others, ['price', 'lasts', 'maxPeriods', 'currency', 'shares', 'acceptLeftover'], 'listForRent');
|
|
3959
|
+
await noLeftovers(asset, acceptLeftover, { listing: true });
|
|
3467
3960
|
const mint = mintOf(currency);
|
|
3468
3961
|
const { before, ...nft } = await nftOf(asset);
|
|
3469
3962
|
return send([
|
|
@@ -3998,15 +4491,17 @@ export function solanaApp({
|
|
|
3998
4491
|
// A repaid loan is a plain hold that is over (no debt, `free_at` 0): its NFT goes home whenever. An unpaid one's
|
|
3999
4492
|
// is the lender's once due.
|
|
4000
4493
|
if (!hold.debt && time >= BigInt(hold.free_at))
|
|
4001
|
-
out.push(
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4494
|
+
out.push(
|
|
4495
|
+
await goneOr(hold.asset, {
|
|
4496
|
+
kind: 'release',
|
|
4497
|
+
asset: hold.asset,
|
|
4498
|
+
hold: pubkey,
|
|
4499
|
+
says:
|
|
4500
|
+
hold.player === project
|
|
4501
|
+
? 'An NFT owed to the treasury: send it there.'
|
|
4502
|
+
: 'Your collectible back: its hold is over.',
|
|
4503
|
+
}),
|
|
4504
|
+
);
|
|
4010
4505
|
}
|
|
4011
4506
|
// A loan this wallet made that is overdue and unpaid: its NFT is the lender's to take.
|
|
4012
4507
|
for (const { pubkey, account } of await client.programAccounts(loot.LOOT_PROGRAM, [
|
|
@@ -4015,12 +4510,14 @@ export function solanaApp({
|
|
|
4015
4510
|
])) {
|
|
4016
4511
|
const hold = loot.decode('Hold', account.data);
|
|
4017
4512
|
if (hold.debt?.lender === me && time >= BigInt(hold.free_at))
|
|
4018
|
-
out.push(
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4513
|
+
out.push(
|
|
4514
|
+
await goneOr(hold.asset, {
|
|
4515
|
+
kind: 'release',
|
|
4516
|
+
asset: hold.asset,
|
|
4517
|
+
hold: pubkey,
|
|
4518
|
+
says: 'A loan you made is overdue and unpaid: take its NFT.',
|
|
4519
|
+
}),
|
|
4520
|
+
);
|
|
4024
4521
|
}
|
|
4025
4522
|
// A compressed NFT's proof record this wallet paid for and left open: closing it brings its rent back.
|
|
4026
4523
|
for (const { account } of await client.programAccounts(loot.LOOT_PROGRAM, [tag('Proven')])) {
|
|
@@ -4044,13 +4541,14 @@ export function solanaApp({
|
|
|
4044
4541
|
auctions.forEach((a, i) => {
|
|
4045
4542
|
const x = found[i] && loot.decode('Auction', found[i].data);
|
|
4046
4543
|
if (!x || time < loot.auctionEnds(x)) return;
|
|
4047
|
-
// The lot went
|
|
4544
|
+
// The lot went first (an end too wide for one transaction, or a seller's account that couldn't take the money
|
|
4545
|
+
// then): only the money is left to pay.
|
|
4048
4546
|
if (BigInt(x.proceeds_owed) > 0n) {
|
|
4049
4547
|
if (x.seller === me)
|
|
4050
4548
|
out.push({
|
|
4051
4549
|
kind: 'auction',
|
|
4052
4550
|
key: a.key,
|
|
4053
|
-
says: `The winner of “${a.name}” has the lot, and
|
|
4551
|
+
says: `The winner of “${a.name}” has the lot, and the winning bid is still to be paid to you: end it again, and it is paid (once your account can take it).`,
|
|
4054
4552
|
});
|
|
4055
4553
|
return;
|
|
4056
4554
|
}
|
|
@@ -4250,9 +4748,10 @@ export function solanaApp({
|
|
|
4250
4748
|
const nfts = [];
|
|
4251
4749
|
const kinds = {};
|
|
4252
4750
|
if (contest.nft) {
|
|
4751
|
+
const proofs = {};
|
|
4253
4752
|
const kindOf = async (asset, to) => {
|
|
4254
4753
|
const { before, hasAuthority: _, ...kind } = await nftOf(asset, me, { to });
|
|
4255
|
-
|
|
4754
|
+
proofs[asset] = before;
|
|
4256
4755
|
kinds[asset] = kind;
|
|
4257
4756
|
};
|
|
4258
4757
|
if (Number(own.amount) === 1) {
|
|
@@ -4274,6 +4773,10 @@ export function solanaApp({
|
|
|
4274
4773
|
nfts.push(j);
|
|
4275
4774
|
await kindOf(e.nft, me);
|
|
4276
4775
|
}
|
|
4776
|
+
// A compressed NFT's proof is long (up to 17 nodes): such a claim moves one NFT, as a v0 transaction with a
|
|
4777
|
+
// lookup table (`send`), and the next claim takes the next.
|
|
4778
|
+
if (nfts.length > 1 && nfts.some(j => kinds[entries[j].nft]?.compressed)) nfts.splice(1);
|
|
4779
|
+
for (const j of nfts) given.before.push(...proofs[entries[j].nft]);
|
|
4277
4780
|
}
|
|
4278
4781
|
const claimed = [
|
|
4279
4782
|
computeUnitLimit(800_000),
|
|
@@ -4299,6 +4802,7 @@ export function solanaApp({
|
|
|
4299
4802
|
return contest.prize?.Roll && !build ? (await sendRolling(claimed)).signature : send(claimed);
|
|
4300
4803
|
}
|
|
4301
4804
|
if (c.kind === 'release') {
|
|
4805
|
+
if (c.gone) throw new Error(c.says);
|
|
4302
4806
|
const hold = await read('Hold', c.hold);
|
|
4303
4807
|
const { before, hasAuthority: _, ...nft } = await nftOf(c.asset, me, { to: me });
|
|
4304
4808
|
// An overdue loan's NFT goes to its lender.
|
|
@@ -4317,7 +4821,12 @@ export function solanaApp({
|
|
|
4317
4821
|
}),
|
|
4318
4822
|
]);
|
|
4319
4823
|
}
|
|
4320
|
-
if (c.kind === 'auction')
|
|
4824
|
+
if (c.kind === 'auction') {
|
|
4825
|
+
const ended = await this.endAuction(c.key);
|
|
4826
|
+
// A lot that can't move now: the sale waits, and nothing was sent.
|
|
4827
|
+
if (ended.waiting) throw new Error(ended.says);
|
|
4828
|
+
return ended.signature;
|
|
4829
|
+
}
|
|
4321
4830
|
if (c.kind === 'close-leaf') return send([loot.closeLeafIx({ asset: c.asset, tree: c.tree, payer: me })]);
|
|
4322
4831
|
if (c.kind === 'walk') return this.walkFarm(c.key);
|
|
4323
4832
|
if (c.kind === 'vest' || c.kind === 'schedule') return this.runSchedule(c.key);
|
|
@@ -4361,13 +4870,24 @@ export function solanaApp({
|
|
|
4361
4870
|
return build ? steps : send(steps);
|
|
4362
4871
|
}
|
|
4363
4872
|
if (!r) return null;
|
|
4873
|
+
// A round whose lottery was closed since: its lent rent is the owner's, on the owner's own close.
|
|
4874
|
+
const lotteryClosed = !lottery;
|
|
4364
4875
|
// Pulls: this wallet's own parts, or (`payout`) everyone's and the parts owed in common, a few to a transaction;
|
|
4365
4876
|
// when they are the last owed, the last pull closes the round. A round that owes nothing is only closed.
|
|
4366
4877
|
// A named stash NFT that is gone refuses a stranger's pull: only its winner or a project key passes it over.
|
|
4367
4878
|
const projectKey = me === g.owner || me === g.authority;
|
|
4368
4879
|
const stranger = async (i, p) => {
|
|
4880
|
+
if (projectKey || r.entries[i].buyer === me) return false;
|
|
4881
|
+
// A minted prize the project can't mint now (not the mint's authority): a stranger's pull fails, so it is left
|
|
4882
|
+
// for later, when the project can; its winner or the project's keys may pass it over for nothing.
|
|
4883
|
+
const prized = p.Prize ? r.prizes.find(x => Number(x.run) === Number(p.Prize[0])) : null;
|
|
4884
|
+
if (prized?.minted) {
|
|
4885
|
+
const data = (await client.account(prized.mint))?.data;
|
|
4886
|
+
const authority = data && data[0] === 1 ? encodeBase58(data.subarray(4, 36)) : null;
|
|
4887
|
+
if (authority !== project) return true;
|
|
4888
|
+
}
|
|
4369
4889
|
const named = p.Prize ? r.terms.places[Number(p.Prize[0])].prize?.Nft : null;
|
|
4370
|
-
if (!named
|
|
4890
|
+
if (!named) return false;
|
|
4371
4891
|
const stash = loot.decodeStash((await client.account(loot.pda.stash(project, named.stash)))?.data ?? null);
|
|
4372
4892
|
return !stash?.nfts.includes(named.nft);
|
|
4373
4893
|
};
|
|
@@ -4382,7 +4902,10 @@ export function solanaApp({
|
|
|
4382
4902
|
c.kind === 'pull' ? c.pulls : [...loot.commonOwed(r).map(p => ({ entry: null, parts: [p] })), ...theirs];
|
|
4383
4903
|
if (!owed.length) {
|
|
4384
4904
|
if (!loot.roundFinished(r)) return null;
|
|
4385
|
-
|
|
4905
|
+
// Rent the lottery lent goes to the owner once it is closed, and only on the owner's own close.
|
|
4906
|
+
const close = [
|
|
4907
|
+
loot.pullIx({ payer: me, project, roundAddress: key, round: r, projectState: g, lotteryClosed, programs }),
|
|
4908
|
+
];
|
|
4386
4909
|
return build ? close : send(close);
|
|
4387
4910
|
}
|
|
4388
4911
|
const batch = owed.slice(0, 4);
|
|
@@ -4393,21 +4916,49 @@ export function solanaApp({
|
|
|
4393
4916
|
const closes = settles >= Number(r.owed);
|
|
4394
4917
|
const instructions = [computeUnitLimit(1_400_000)];
|
|
4395
4918
|
const rolls = [];
|
|
4919
|
+
let heap = false;
|
|
4920
|
+
let partial = false;
|
|
4396
4921
|
for (const [k, { entry, parts }] of batch.entries()) {
|
|
4397
|
-
const built = await pulls(key, r, entry, parts, me, {
|
|
4922
|
+
const built = await pulls(key, r, entry, parts, me, {
|
|
4923
|
+
closes: closes && k === batch.length - 1,
|
|
4924
|
+
lotteryClosed,
|
|
4925
|
+
});
|
|
4398
4926
|
instructions.push(...built.instructions.filter(x => x.programId !== COMPUTE_BUDGET_PROGRAM));
|
|
4399
4927
|
rolls.push(...built.rolls);
|
|
4928
|
+
heap ||= built.heap;
|
|
4929
|
+
partial ||= built.partial;
|
|
4400
4930
|
}
|
|
4931
|
+
// A run of rolls asks for the largest heap.
|
|
4932
|
+
if (heap) instructions.splice(1, 0, requestHeapFrame(loot.LARGE_SETTLE_HEAP));
|
|
4933
|
+
// A pull that stopped short of its part (a raffle's NFTs, 64 a pull; a run of rolls): sent, then the next carries
|
|
4934
|
+
// on while the round moves (its NFTs out, its places paid) and still owes it.
|
|
4935
|
+
const carryOn = async signature => {
|
|
4936
|
+
if (build || !partial) return signature;
|
|
4937
|
+
const after = await client.account(key);
|
|
4938
|
+
const r2 = after?.owner === loot.LOOT_PROGRAM ? loot.decodeRound(after.data) : null;
|
|
4939
|
+
const moved = r2 && (Number(r2.assigned) > Number(r.assigned) || Number(r2.owed) < Number(r.owed));
|
|
4940
|
+
if (!moved) return signature;
|
|
4941
|
+
const left =
|
|
4942
|
+
c.kind === 'pull'
|
|
4943
|
+
? r2.entries.flatMap((e, i) => {
|
|
4944
|
+
const parts = e.buyer === me ? loot.partsOwed(r2, i) : [];
|
|
4945
|
+
return parts.length ? [{ entry: i, parts }] : [];
|
|
4946
|
+
})
|
|
4947
|
+
: null;
|
|
4948
|
+
if (left && !left.length) return signature;
|
|
4949
|
+
await this.claim(left ? { ...c, pulls: left } : c);
|
|
4950
|
+
return signature;
|
|
4951
|
+
};
|
|
4401
4952
|
// A place's roll is asked for in the pull's own transaction (catalog P1): the winner's own, with a key that settles
|
|
4402
4953
|
// it after (one prompt); someone else's, asked for when the box can give, else left for its winner to ask.
|
|
4403
|
-
if (!rolls.length) return build ? instructions : send(instructions);
|
|
4404
|
-
if (c.kind === 'pull' && !build) return (await sendRolling(instructions)).signature;
|
|
4954
|
+
if (!rolls.length) return build ? instructions : carryOn(await send(instructions));
|
|
4955
|
+
if (c.kind === 'pull' && !build) return carryOn((await sendRolling(instructions)).signature);
|
|
4405
4956
|
const asks = [];
|
|
4406
4957
|
for (const { order, box } of rolls)
|
|
4407
4958
|
asks.push(...(await asksFor(me, order, { project, loot_box: loot.pda.box(project, box) })));
|
|
4408
4959
|
const all = [...instructions, ...asks];
|
|
4409
4960
|
const asked = (await dryRun(me, all)).ok ? all : instructions;
|
|
4410
|
-
return build ? asked : send(asked);
|
|
4961
|
+
return build ? asked : carryOn(await send(asked));
|
|
4411
4962
|
},
|
|
4412
4963
|
|
|
4413
4964
|
/**
|