@xgamesfun/solana 0.26.4 → 0.27.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -9
- package/js/client/compressed.js +12 -5
- package/js/client/idl/loot.js +1 -1
- package/js/client/idl/loot.json +53 -7
- package/js/client/ix.js +44 -9
- package/js/economy/check.mjs +2 -2
- package/js/economy/guide.mjs +1 -1
- package/js/mcp/tools.mjs +1 -0
- package/js/play/index.js +943 -283
- package/package.json +1 -1
- package/release.json +5 -5
- package/skills/xsolana-economy/SKILL.md +4 -4
- package/skills/xsolana-economy/references/apps.md +10 -0
- package/skills/xsolana-economy/references/trust-and-cheating.md +4 -3
- package/types/client/compressed.d.ts +5 -2
- package/types/client/ix.d.ts +38 -7
- package/types/play/index.d.ts +375 -141
package/js/play/index.js
CHANGED
|
@@ -371,23 +371,27 @@ export function solanaApp({
|
|
|
371
371
|
before: tokenMetadata ? [computeUnitLimit(400_000), ...before] : [],
|
|
372
372
|
};
|
|
373
373
|
}
|
|
374
|
+
/** Whether a Core asset's account shows the project holds it: held by another, or burned (one byte left), it is gone. */
|
|
375
|
+
const coreHeld = account => account.data[0] === 1 && parseCoreAsset(account.data).owner === project;
|
|
374
376
|
/**
|
|
375
|
-
* Whether the project still holds `asset`, as the program reads it (`piece::held`): { held, evidence }. A Core
|
|
376
|
-
* its owner (one burned is gone); a Token Metadata NFT by the project's token account of it, and when that
|
|
377
|
-
* by the account that holds it now or by its mint when it was burned (`evidence`, what
|
|
378
|
-
*
|
|
379
|
-
* either way.
|
|
377
|
+
* Whether the project still holds `asset`, as the program reads it (`piece::held`): { held, evidence, das }. A Core
|
|
378
|
+
* asset by its owner (one burned is gone); a Token Metadata NFT by the project's token account of it, and when that
|
|
379
|
+
* holds none, by the account that holds it now or by its mint when it was burned (`evidence`, what shows it gone); a
|
|
380
|
+
* compressed NFT by the RPC's DAS record of it (`das`, proven again on chain before anything relies on it). `held` is
|
|
381
|
+
* null when nothing shows either way. `holder`: a wallet thought to hold it now (a player handing it in), whose own
|
|
382
|
+
* token account is read before any search.
|
|
380
383
|
*/
|
|
381
|
-
async function heldNow(asset) {
|
|
384
|
+
async function heldNow(asset, { holder = null } = {}) {
|
|
382
385
|
const account = await client.account(asset);
|
|
383
|
-
if (account?.owner === MPL_CORE)
|
|
384
|
-
return { held: account.data[0] === 1 && parseCoreAsset(account.data).owner === project };
|
|
386
|
+
if (account?.owner === MPL_CORE) return { held: coreHeld(account) };
|
|
385
387
|
if (account?.owner === TOKEN_PROGRAM) {
|
|
386
388
|
const u64 = (data, at) => new DataView(data.buffer, data.byteOffset).getBigUint64(at, true);
|
|
387
389
|
const amount = data => (data?.length >= 72 ? u64(data, 64) : 0n);
|
|
388
390
|
if (amount((await client.account(loot.ata(project, asset)))?.data) === 1n) return { held: true };
|
|
389
391
|
// Burned: its mint holds none.
|
|
390
392
|
if (account.data.length >= 44 && u64(account.data, 36) === 0n) return { held: false, evidence: asset };
|
|
393
|
+
const own = holder ? loot.ata(holder, asset) : null;
|
|
394
|
+
if (own && amount((await client.account(own))?.data) === 1n) return { held: false, evidence: own };
|
|
391
395
|
const found = await client.programAccounts(TOKEN_PROGRAM, [
|
|
392
396
|
{ dataSize: 165 },
|
|
393
397
|
{ memcmp: { offset: 0, bytes: asset } },
|
|
@@ -398,8 +402,70 @@ export function solanaApp({
|
|
|
398
402
|
if (account) return { held: null };
|
|
399
403
|
const das = await client.call('getAsset', { id: asset }).catch(() => null);
|
|
400
404
|
if (!das?.compression?.compressed) return { held: null };
|
|
401
|
-
|
|
405
|
+
// DAS says a burned NFT at the asset's top level (`burnt`).
|
|
406
|
+
const burnt = das.burnt === true || das.compression.burnt === true;
|
|
407
|
+
return { held: !burnt && das.ownership?.owner === project, das };
|
|
402
408
|
}
|
|
409
|
+
/** `heldNow` of several NFTs: the Core assets from one read of them all, any other kind one by one. By asset. */
|
|
410
|
+
async function heldEach(assets) {
|
|
411
|
+
const unique = [...new Set(assets)];
|
|
412
|
+
const found = unique.length ? await client.accounts(unique) : [];
|
|
413
|
+
const out = new Map();
|
|
414
|
+
for (const [i, asset] of unique.entries())
|
|
415
|
+
out.set(asset, found[i]?.owner === MPL_CORE ? { held: coreHeld(found[i]) } : await heldNow(asset));
|
|
416
|
+
return out;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* What proves `asset` gone from the project, as the instruction that lets go of it takes it (loot 0.27: a record
|
|
420
|
+
* whose NFT is proven gone closes): { before, kind }, or null when the project still holds it or nothing shows either
|
|
421
|
+
* way. Whoever controls a collection the project does not can take an NFT out of the project's hands or burn it. A
|
|
422
|
+
* Core asset shows it by itself (`kind`: its collection now); a Token Metadata NFT by the token account that holds it
|
|
423
|
+
* now, or its mint once burned (`kind.tokenMetadata.evidence`); a compressed NFT by its record, proven again as its
|
|
424
|
+
* leaf is now (`before`, by `signer`). `holder`: as `heldNow` takes it. `now`: what `heldNow` read of it already.
|
|
425
|
+
*/
|
|
426
|
+
async function goneProof(asset, signer = wallet.address, { holder = null, now = null } = {}) {
|
|
427
|
+
now ??= await heldNow(asset, { holder });
|
|
428
|
+
if (now.held !== false) return null;
|
|
429
|
+
if (now.das) {
|
|
430
|
+
const proof = await client.call('getAssetProof', { id: asset });
|
|
431
|
+
const tree = await client.account(now.das.compression.tree);
|
|
432
|
+
const compressed = loot.compressedFromDas({ asset: now.das, proof, treeData: tree.data });
|
|
433
|
+
return { before: [computeUnitLimit(1_000_000), loot.proveLeafIx({ signer, compressed })], kind: { compressed } };
|
|
434
|
+
}
|
|
435
|
+
if (now.evidence)
|
|
436
|
+
return { before: [], kind: { tokenMetadata: { programmable: false, ruleSet: null, evidence: now.evidence } } };
|
|
437
|
+
return { before: [], kind: { collection: (await collectionOf(asset)) ?? MPL_CORE } };
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* An NFT a record of the project's is to send on (`nftOf`, to `to`), or, when it is proven gone from the project
|
|
441
|
+
* (`goneProof`), what shows that, with `gone` true: the instruction then finishes without it and nothing is sent.
|
|
442
|
+
* `now`: what `heldNow` read of it already.
|
|
443
|
+
*/
|
|
444
|
+
async function leaving(asset, signer = wallet.address, { to, payer = signer, now = null } = {}) {
|
|
445
|
+
const proof = await goneProof(asset, signer, { now });
|
|
446
|
+
if (!proof) return { ...(await nftOf(asset, signer, { to, payer })), gone: false };
|
|
447
|
+
// A Core asset shows it by itself, with the accounts it always took.
|
|
448
|
+
if (!proof.kind.tokenMetadata && !proof.kind.compressed) return { ...(await nftOf(asset, signer)), gone: true };
|
|
449
|
+
return { before: proof.before, hasAuthority: false, ...proof.kind, gone: true };
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* The release of a hold a route just made of `asset`, an NFT it owed and could only hand out (`kind`: from
|
|
453
|
+
* `leaving`). The program makes such a hold at `["hold", asset, 0]` for `player`, the route's `signer` paying its
|
|
454
|
+
* rent and `rentTo` named for its NFT's account's (`hold::owe`). The release right after it, by the same signer,
|
|
455
|
+
* ends it: for an NFT proven gone from the project it closes the hold on the proof and nothing is sent (`NftGone`);
|
|
456
|
+
* for any other it sends the NFT on to `player` (`Released`). Either way the hold's rent comes back to the signer.
|
|
457
|
+
* `g`: the project, for a hold owed to the treasury.
|
|
458
|
+
*/
|
|
459
|
+
const releaseOwed = (asset, kind, { player, rentTo }, signer, g) =>
|
|
460
|
+
loot.releaseIx({
|
|
461
|
+
signer,
|
|
462
|
+
project,
|
|
463
|
+
asset,
|
|
464
|
+
...kind,
|
|
465
|
+
hold: { player, payer: signer, rent_to: rentTo, free_at: 0n },
|
|
466
|
+
address: loot.pda.hold(asset, 0),
|
|
467
|
+
treasury: g.treasury,
|
|
468
|
+
});
|
|
403
469
|
/**
|
|
404
470
|
* 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
471
|
* money in: accounts that start frozen or can be switched to it (a payee closes their own account, and the one
|
|
@@ -1066,7 +1132,10 @@ export function solanaApp({
|
|
|
1066
1132
|
/**
|
|
1067
1133
|
* Waits for an order's roll (the oracle answers a moment after the open), then settles it. With `burner` (the
|
|
1068
1134
|
* throwaway key the open funded), the burner signs and pays the settle, and gives the rest back: no wallet prompt.
|
|
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.
|
|
1135
|
+
* The order names it (`settler`), so it may settle; when the player or the project did first, the burner only gives its lamports back.
|
|
1136
|
+
* An NFT of the order the project no longer holds (its collection took it back or burned it) is passed over: a
|
|
1137
|
+
* stash's NFT won is not given (its place in `assets` is null) and the NFT rolled on is not sent back; `gone` lists
|
|
1138
|
+
* them, with `says`. `putBack`: NFTs
|
|
1070
1139
|
* won that the player puts back into their stash in place of taking them (one whose collection now refuses to let it
|
|
1071
1140
|
* move); `giveUp`: the places in the order's `won` of tokens or collectibles the player gives up because they can't
|
|
1072
1141
|
* receive them (an account the token's issuer froze, a collection that refuses the mint): tokens given up stay in the
|
|
@@ -1191,6 +1260,9 @@ export function solanaApp({
|
|
|
1191
1260
|
};
|
|
1192
1261
|
};
|
|
1193
1262
|
const holders = await holdersOf(order.shares);
|
|
1263
|
+
// NFTs of this order the project no longer holds (their collection took them back or burned them): the settle
|
|
1264
|
+
// passes each over on that proof. A stash's NFT won is then not given; the NFT rolled on is not sent back.
|
|
1265
|
+
const lost = new Set();
|
|
1194
1266
|
// Given by its fate to a wallet that is an account of the loot program, the NFT goes back to the player.
|
|
1195
1267
|
const gives = order.held ? order.fate?.then?.Give?.[0] : undefined;
|
|
1196
1268
|
const handBack = gives !== undefined && loot.handsBack(await client.account(gives));
|
|
@@ -1207,15 +1279,25 @@ export function solanaApp({
|
|
|
1207
1279
|
const {
|
|
1208
1280
|
before,
|
|
1209
1281
|
hasAuthority: _,
|
|
1282
|
+
gone,
|
|
1210
1283
|
...kind
|
|
1211
|
-
} = await
|
|
1284
|
+
} = await leaving(won.Nft.nft, signer, { to: order.player, payer: wallet.address });
|
|
1285
|
+
if (gone) lost.add(won.Nft.nft);
|
|
1212
1286
|
nfts[won.Nft.nft] = kind;
|
|
1213
1287
|
proving.push(...before);
|
|
1214
1288
|
}
|
|
1215
1289
|
}
|
|
1216
|
-
// The NFT the roll was on: its kind, and for a compressed one a proof first.
|
|
1217
|
-
const {
|
|
1218
|
-
|
|
1290
|
+
// The NFT the roll was on: its kind, and for a compressed one a proof first. One the order holds may be gone.
|
|
1291
|
+
const {
|
|
1292
|
+
before: subjectProof,
|
|
1293
|
+
gone: subjectGone,
|
|
1294
|
+
...subject
|
|
1295
|
+
} = order.subject && order.subject !== NOBODY
|
|
1296
|
+
? order.held
|
|
1297
|
+
? await leaving(order.subject, signer)
|
|
1298
|
+
: await nftOf(order.subject, signer)
|
|
1299
|
+
: { before: [] };
|
|
1300
|
+
if (subjectGone) lost.add(order.subject);
|
|
1219
1301
|
const ix = loot.settleIx({
|
|
1220
1302
|
signer,
|
|
1221
1303
|
orderAddress,
|
|
@@ -1296,7 +1378,7 @@ export function solanaApp({
|
|
|
1296
1378
|
if (burner && !keepBurner && !emptied) await emptyKey(burner);
|
|
1297
1379
|
else if (burner && emptied) keys?.forget(burner.name);
|
|
1298
1380
|
const given = new Set(giveUp.map(Number));
|
|
1299
|
-
const assets = wonAssets(order).map((a, k) => (putBack.includes(a) || given.has(k) ? null : a));
|
|
1381
|
+
const assets = wonAssets(order).map((a, k) => (putBack.includes(a) || given.has(k) || lost.has(a) ? null : a));
|
|
1300
1382
|
return {
|
|
1301
1383
|
order: orderAddress,
|
|
1302
1384
|
won: order.won,
|
|
@@ -1304,6 +1386,13 @@ export function solanaApp({
|
|
|
1304
1386
|
settled: signature,
|
|
1305
1387
|
...(signatures.length ? { parts: [...signatures, signature] } : {}),
|
|
1306
1388
|
...(assets.find(Boolean) ? { asset: assets.find(Boolean) } : {}),
|
|
1389
|
+
// What the settle passed over: not given (a stash's NFT won), not sent back (the NFT rolled on).
|
|
1390
|
+
...(lost.size
|
|
1391
|
+
? {
|
|
1392
|
+
gone: [...lost],
|
|
1393
|
+
says: 'The project no longer holds an NFT of this order (whoever controls its collection took it back or burned it): it was not given or sent back. Everything else settled as rolled.',
|
|
1394
|
+
}
|
|
1395
|
+
: {}),
|
|
1307
1396
|
};
|
|
1308
1397
|
}
|
|
1309
1398
|
|
|
@@ -1595,7 +1684,8 @@ export function solanaApp({
|
|
|
1595
1684
|
// A box that rolls on an NFT takes the one the player brings (`on`), whatever its kind.
|
|
1596
1685
|
if (box.subject && !on) throw new Error('This box rolls on an NFT: say which (on).');
|
|
1597
1686
|
// A box that burns it, gives it away or moves its numbers takes it into the project's hands until the roll settles.
|
|
1598
|
-
|
|
1687
|
+
// What an earlier owner of it left in the project is cleared first (`clearLeftovers`).
|
|
1688
|
+
const clears = box.subject && loot.boxHolds(box) ? await clearLeftovers(on, acceptLeftover) : [];
|
|
1599
1689
|
const brought = box.subject ? await nftOf(on) : { before: [] };
|
|
1600
1690
|
const { before: proving, hasAuthority, ...kind } = brought;
|
|
1601
1691
|
const subject = box.subject
|
|
@@ -1713,7 +1803,7 @@ export function solanaApp({
|
|
|
1713
1803
|
x.keys[0]?.pubkey === associatedTokenAddress(wallet.address, NATIVE_MINT)
|
|
1714
1804
|
),
|
|
1715
1805
|
);
|
|
1716
|
-
const signature = await send(instructions, [], { keepWrapped: solPrize });
|
|
1806
|
+
const signature = await send(await closesFirst(clears, instructions), [], { keepWrapped: solPrize });
|
|
1717
1807
|
// The slot it landed in: the order is read from a node that has seen it (a load-balanced RPC's node lagging
|
|
1718
1808
|
// behind answers nothing for an order just opened).
|
|
1719
1809
|
const landed = (await client.signatureStatus?.(signature).catch(() => null))?.slot ?? null;
|
|
@@ -1751,6 +1841,17 @@ export function solanaApp({
|
|
|
1751
1841
|
);
|
|
1752
1842
|
}
|
|
1753
1843
|
|
|
1844
|
+
/**
|
|
1845
|
+
* Refuses, in words, to buy or rent the listing of an NFT the project no longer holds (whoever controls its
|
|
1846
|
+
* collection took it back or burned it): nothing is there. Sent all the same, the take would only close the listing.
|
|
1847
|
+
*/
|
|
1848
|
+
async function listedStill(asset) {
|
|
1849
|
+
if ((await heldNow(asset)).held === false)
|
|
1850
|
+
throw new Error(
|
|
1851
|
+
`The project no longer holds the NFT ${asset}: whoever controls its collection took it back or burned it while it was listed. There is nothing to buy or rent, and nothing was sent. Anyone can close this listing for nothing (closeGone): its rent goes back to its maker.`,
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1754
1855
|
/** A box bought at once when it needs no roll (`buyBox`: a shop item, a bundle, a fixed pack of any count), or
|
|
1755
1856
|
* opened and collected: one that rolls, gives a fresh item (its traits are rolled), or rolls on an NFT. A fixed pack
|
|
1756
1857
|
* too big for one transaction is opened instead (the oracle answers it like any box). */
|
|
@@ -1876,16 +1977,18 @@ export function solanaApp({
|
|
|
1876
1977
|
const lotNow = lot ? await heldNow(lot) : { held: null };
|
|
1877
1978
|
const bidNow = [];
|
|
1878
1979
|
if (sold && auction.nft_bids) for (const nft of auction.held) bidNow.push(await heldNow(nft));
|
|
1879
|
-
const
|
|
1980
|
+
const lostBids = sold && auction.nft_bids ? auction.held.filter((_, i) => bidNow[i].held === false) : [];
|
|
1981
|
+
const gone = lotNow.held === false || lostBids.length > 0;
|
|
1880
1982
|
const ending = sold && lot && (callOff || gone) ? 'CallOff' : 'Deliver';
|
|
1881
1983
|
const home = !sold || ending === 'CallOff';
|
|
1882
1984
|
// The lot to its winner, or home to its seller; a bid's NFTs to whoever owned the lot, or back to their bidder.
|
|
1985
|
+
// One that is gone is not sent: its accounts only show the end that the project no longer holds it.
|
|
1883
1986
|
const before = [];
|
|
1884
|
-
const lotKind = lot ? await kindFor(lot, home ? auction.seller : auction.high_bidder, lotNow
|
|
1987
|
+
const lotKind = lot ? await kindFor(lot, home ? auction.seller : auction.high_bidder, lotNow, before) : {};
|
|
1885
1988
|
const bidTo = ending === 'CallOff' ? auction.high_bidder : owner;
|
|
1886
1989
|
const held = [];
|
|
1887
1990
|
if (sold && auction.nft_bids)
|
|
1888
|
-
for (const [i, nft] of auction.held.entries()) held.push(await kindFor(nft, bidTo, bidNow[i]
|
|
1991
|
+
for (const [i, nft] of auction.held.entries()) held.push(await kindFor(nft, bidTo, bidNow[i], before));
|
|
1889
1992
|
if (sold && !home) holders ??= await holdersOf(auction.shares);
|
|
1890
1993
|
const end = (ending, hold = false) =>
|
|
1891
1994
|
loot.endAuctionIx({ ...base, auction, item, ...lotKind, held, holders, ending, hold });
|
|
@@ -1900,6 +2003,24 @@ export function solanaApp({
|
|
|
1900
2003
|
...(made ? { asset: made } : {}),
|
|
1901
2004
|
};
|
|
1902
2005
|
if (ending === 'CallOff') Object.assign(result, { calledOff: true, gone });
|
|
2006
|
+
// What the end did about an NFT that is gone, in words: nothing here says it moved.
|
|
2007
|
+
if (gone) {
|
|
2008
|
+
const which =
|
|
2009
|
+
lotNow.held === false
|
|
2010
|
+
? `the lot (${lot})`
|
|
2011
|
+
: `${lostBids.length === 1 ? 'an NFT' : 'NFTs'} of the winning bid (${lostBids.join(', ')})`;
|
|
2012
|
+
const did = !sold
|
|
2013
|
+
? 'Nothing went home: the auction is closed, and its rent went back to its seller.'
|
|
2014
|
+
: ending === 'Deliver'
|
|
2015
|
+
? 'Nothing was paid for it: the lot went to its winner, and the rest of the bid was paid.'
|
|
2016
|
+
: lotNow.held === false
|
|
2017
|
+
? `The sale was called off: ${auction.nft_bids ? 'the NFTs of the winning bid the project still held' : 'the winning bid'} went back to its bidder, and nothing went home to the seller.`
|
|
2018
|
+
: 'The sale was called off: the lot went home to its seller, and the rest of the bid went back to its bidder.';
|
|
2019
|
+
Object.assign(result, {
|
|
2020
|
+
gone: true,
|
|
2021
|
+
says: `The project no longer holds ${which}: whoever controls its collection took it back or burned it while the auction held it. ${did}`,
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
1903
2024
|
|
|
1904
2025
|
// 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.
|
|
1905
2026
|
const outs = [
|
|
@@ -1967,15 +2088,15 @@ export function solanaApp({
|
|
|
1967
2088
|
return holding();
|
|
1968
2089
|
}
|
|
1969
2090
|
|
|
1970
|
-
/** An NFT's kind on an auction's end, going to `to`: what `nftOf` reads, its proof put in `before
|
|
1971
|
-
*
|
|
1972
|
-
|
|
1973
|
-
|
|
2091
|
+
/** An NFT's kind on an auction's end, going to `to`: what `nftOf` reads, its proof put in `before`. `now`: what
|
|
2092
|
+
* `heldNow` read of it; one that is gone carries what shows it instead (a Token Metadata NFT's account that holds it
|
|
2093
|
+
* now, or its mint; a compressed NFT's leaf as it is now), and is sent nowhere. */
|
|
2094
|
+
async function kindFor(asset, to, now, before) {
|
|
2095
|
+
const { before: proof, hasAuthority: _h, gone: _g, ...kind } = await leaving(asset, wallet.address, { to, now });
|
|
1974
2096
|
before.push(...proof);
|
|
1975
2097
|
return {
|
|
1976
2098
|
...kind,
|
|
1977
2099
|
collection: kind.compressed || kind.tokenMetadata ? undefined : ((await collectionOf(asset)) ?? MPL_CORE),
|
|
1978
|
-
...(evidence && kind.tokenMetadata ? { evidence } : {}),
|
|
1979
2100
|
};
|
|
1980
2101
|
}
|
|
1981
2102
|
|
|
@@ -2024,16 +2145,30 @@ export function solanaApp({
|
|
|
2024
2145
|
if (opens.length) await send([...new Map(opens.map(x => [x.keys[1].pubkey, x])).values()]);
|
|
2025
2146
|
const before = [];
|
|
2026
2147
|
const held = [];
|
|
2027
|
-
|
|
2148
|
+
// An NFT of the bid that is gone (its collection took it back or burned it) is not paid: its accounts show the
|
|
2149
|
+
// end that the project no longer holds it, and the end passes it over.
|
|
2150
|
+
const lost = [];
|
|
2151
|
+
if (auction.nft_bids)
|
|
2152
|
+
for (const nft of auction.held) {
|
|
2153
|
+
const now = await heldNow(nft);
|
|
2154
|
+
if (now.held === false) lost.push(nft);
|
|
2155
|
+
held.push(await kindFor(nft, auction.seller, now, before));
|
|
2156
|
+
}
|
|
2028
2157
|
const end = (hold = false) => loot.endAuctionIx({ ...base, auction, held, holders, hold });
|
|
2029
2158
|
const steps = ix => [
|
|
2030
2159
|
computeUnitLimit(1_000_000),
|
|
2031
2160
|
...before.filter(x => x.programId !== COMPUTE_BUDGET_PROGRAM),
|
|
2032
2161
|
ix,
|
|
2033
2162
|
];
|
|
2163
|
+
const passed = lost.length
|
|
2164
|
+
? {
|
|
2165
|
+
gone: true,
|
|
2166
|
+
says: `The project no longer holds ${lost.length === 1 ? 'an NFT' : 'NFTs'} of the winning bid (${lost.join(', ')}): whoever controls its collection took it back or burned it while the auction held it. Nothing was paid for it: the rest of the bid was paid to the seller.`,
|
|
2167
|
+
}
|
|
2168
|
+
: {};
|
|
2034
2169
|
if (sendPlan({ payer: wallet.address, instructions: steps(end()), alone: isProveLeaf }))
|
|
2035
2170
|
try {
|
|
2036
|
-
return { paid: await send(steps(end())) };
|
|
2171
|
+
return { paid: await send(steps(end())), ...passed };
|
|
2037
2172
|
} catch (e) {
|
|
2038
2173
|
// The lot is the winner's already: what is owed waits until the account it goes into can take it.
|
|
2039
2174
|
return {
|
|
@@ -2041,9 +2176,9 @@ export function solanaApp({
|
|
|
2041
2176
|
says: `The lot is the winner’s; the money waits (${e.message}): end the auction again to pay it.`,
|
|
2042
2177
|
};
|
|
2043
2178
|
}
|
|
2044
|
-
const outs = auction.held.map(x => [x, auction.seller]);
|
|
2045
|
-
const { signature, releases } = await heldThenReleased(at, steps(end(
|
|
2046
|
-
return { paid: signature, releases };
|
|
2179
|
+
const outs = auction.held.filter(x => !lost.includes(x)).map(x => [x, auction.seller]);
|
|
2180
|
+
const { signature, releases } = await heldThenReleased(at, steps(end(outs.map(([x]) => x))), outs);
|
|
2181
|
+
return { paid: signature, releases, ...passed };
|
|
2047
2182
|
}
|
|
2048
2183
|
|
|
2049
2184
|
/**
|
|
@@ -2059,9 +2194,12 @@ export function solanaApp({
|
|
|
2059
2194
|
const signature = await send([...instructions, fund]);
|
|
2060
2195
|
const releases = [];
|
|
2061
2196
|
for (const [asset, to] of outs) {
|
|
2197
|
+
// Taken back or burned since the end made it a hold: its release closes the hold on that proof, and sends nothing.
|
|
2198
|
+
let lost = false;
|
|
2062
2199
|
const release = async signer => {
|
|
2063
2200
|
const hold = await read('Hold', loot.pda.hold(asset, 0));
|
|
2064
|
-
const { before: proof, hasAuthority: _h, ...kind } = await
|
|
2201
|
+
const { before: proof, hasAuthority: _h, gone, ...kind } = await leaving(asset, signer, { to });
|
|
2202
|
+
lost = gone;
|
|
2065
2203
|
return [
|
|
2066
2204
|
...proof,
|
|
2067
2205
|
loot.releaseIx({
|
|
@@ -2078,58 +2216,143 @@ export function solanaApp({
|
|
|
2078
2216
|
const steps = await release(key.address);
|
|
2079
2217
|
const byKey = (await dryRun(key.address, steps)).ok ? await sendAs(key, steps).catch(() => null) : null;
|
|
2080
2218
|
// One that still can't move (frozen for now) stays a hold: claims() lists it for whoever it is owed to.
|
|
2081
|
-
|
|
2219
|
+
const sent = byKey ?? (await send(await release(wallet.address)).catch(e => ({ asset, waits: e.message })));
|
|
2220
|
+
releases.push(lost && typeof sent === 'string' ? { asset, gone: true, closed: sent } : sent);
|
|
2082
2221
|
}
|
|
2083
2222
|
await emptyKey(key);
|
|
2084
2223
|
return { signature, releases };
|
|
2085
2224
|
}
|
|
2086
2225
|
|
|
2226
|
+
/** What closing a hold whose NFT is gone does, in words, for `claims()`. */
|
|
2227
|
+
const goneHoldSays = hold =>
|
|
2228
|
+
`The project no longer holds this NFT: whoever controls its collection took it back or burned it while it was held. Its hold can be closed now, by anyone: closing it sends nothing, and the hold's rent goes back to whoever paid it (${hold.payer}).${hold.debt ? ' It was a loan: nothing is repaid, the borrower keeps what they borrowed, and the lender gets neither the NFT nor the repayment.' : ''}`;
|
|
2229
|
+
/** The same for a listing. */
|
|
2230
|
+
const GONE_LISTING =
|
|
2231
|
+
'The project no longer holds this NFT: whoever controls its collection took it back or burned it while it was listed. There is nothing to buy or rent. The listing can be closed for nothing, by its maker (unlist) or by anyone (closeGone), once no rental runs: its rent goes back to its maker.';
|
|
2232
|
+
|
|
2087
2233
|
/**
|
|
2088
|
-
*
|
|
2089
|
-
*
|
|
2090
|
-
*
|
|
2234
|
+
* What ending an auction does when its lot or an NFT of its winning bid is gone, in words, for `claims()`: `x` the
|
|
2235
|
+
* auction, `me` the wallet told (its seller, or its high bidder), `lot` the lot when it is gone, `bids` the NFTs of
|
|
2236
|
+
* the winning bid that are gone.
|
|
2091
2237
|
*/
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2238
|
+
function goneSaleSays(name, x, me, lot, bids) {
|
|
2239
|
+
const [sold, owed] = [BigInt(x.high) > 0n, BigInt(x.proceeds_owed) > 0n];
|
|
2240
|
+
const mine = sold && x.high_bidder === me;
|
|
2241
|
+
const why = 'whoever controls its collection took it back or burned it while the auction held it';
|
|
2242
|
+
const lostBids = `${bids.length === 1 ? 'an NFT' : 'NFTs'} of ${mine ? 'your' : 'the winning'} bid (${bids.join(', ')})`;
|
|
2243
|
+
if (lot && !sold)
|
|
2244
|
+
return `The project no longer holds its lot (${lot}): ${why}. “${name}” ended with no bid, and nothing comes back to you: ending it closes the auction, and its rent comes back to you.`;
|
|
2245
|
+
if (lot) {
|
|
2246
|
+
const back = x.nft_bids
|
|
2247
|
+
? `the NFTs of ${mine ? 'your' : 'the winning'} bid that the project still holds go back to ${mine ? 'you' : 'their bidder'}`
|
|
2248
|
+
: mine
|
|
2249
|
+
? 'your bid comes back to you'
|
|
2250
|
+
: 'the winning bid goes back to its bidder';
|
|
2251
|
+
return `The project no longer holds its lot (${lot}): ${why}. The sale of “${name}” is called off by its end, which anyone may send: ${back}, and ${mine ? 'the lot is not yours' : 'you are paid nothing'}.${bids.length ? ` It no longer holds ${lostBids} either: nothing comes back for it.` : ''}`;
|
|
2252
|
+
}
|
|
2253
|
+
const first = `The project no longer holds ${lostBids}: ${why}.`;
|
|
2254
|
+
// A player's lot that has not moved: the two sides move together or not at all, so the sale is called off.
|
|
2255
|
+
if (x.lot.Asset && !owed)
|
|
2256
|
+
return `${first} The sale of “${name}” is called off by its end, which anyone may send: the lot goes home to ${mine ? 'its seller' : 'you'}, and the rest of the bid back to ${mine ? 'you' : 'its bidder'}. Nothing comes back for the one gone.`;
|
|
2257
|
+
// The lot went first, or it is the project's own: the end passes that NFT over and pays the rest.
|
|
2258
|
+
return owed
|
|
2259
|
+
? `${first} The winner of “${name}” has the lot: ending it again pays you the rest of the bid, and nothing for the one gone.`
|
|
2260
|
+
: `${first} Ending “${name}” gives the lot to ${mine ? 'you' : 'its winner'} and pays the rest of the bid: nothing is paid for the one gone.`;
|
|
2099
2261
|
}
|
|
2262
|
+
/** What claiming does about the NFTs of a round's parts that are gone, in words, for `claims()`: `tickets` the NFTs
|
|
2263
|
+
* that paid for the wallet's tickets, `prizes` the NFTs its places won; `back` when its tickets go back to their
|
|
2264
|
+
* buyers (a round called off, a lottery that returns its tickets). */
|
|
2265
|
+
const goneRoundSays = (tickets, prizes, back) =>
|
|
2266
|
+
[
|
|
2267
|
+
tickets.length
|
|
2268
|
+
? `The project no longer holds the NFT that paid for your ticket (${tickets.join(', ')}): whoever controls its collection took it back or burned it while the lottery held it. ${back ? 'It can’t come back to you' : 'It goes nowhere'}: claiming settles that part and sends nothing.`
|
|
2269
|
+
: '',
|
|
2270
|
+
prizes.length
|
|
2271
|
+
? `The project no longer holds an NFT you won (${prizes.join(', ')}): whoever controls its collection took it back or burned it. It can’t be given: claiming closes that prize and sends nothing.`
|
|
2272
|
+
: '',
|
|
2273
|
+
]
|
|
2274
|
+
.filter(Boolean)
|
|
2275
|
+
.join(' ');
|
|
2100
2276
|
|
|
2101
2277
|
/**
|
|
2102
|
-
*
|
|
2103
|
-
*
|
|
2104
|
-
* other than the player's. Each is { kind, address, says }. Such a record stays when whoever controls the NFT's
|
|
2105
|
-
* collection moved it out of the project's hands without the program, and the program hands out whatever the project
|
|
2106
|
-
* holds at that address: once this player's deposit puts the NFT back in the project's hands, the record can send it
|
|
2107
|
-
* to the wallet it names. NFTs of the project's own collections are not read: those collections refuse the permanent
|
|
2108
|
-
* delegates that take an NFT out.
|
|
2278
|
+
* This project's records of `asset`: its listing at `["listing", project, asset]` (one read) and its holds (one
|
|
2279
|
+
* search, by the asset's address). Each is { kind: 'listing' | 'hold', address, record }.
|
|
2109
2280
|
*/
|
|
2110
|
-
async function
|
|
2111
|
-
if (ours(await collectionOf(asset))) return [];
|
|
2112
|
-
const me = wallet.address;
|
|
2281
|
+
async function recordsOf(asset) {
|
|
2113
2282
|
const out = [];
|
|
2114
2283
|
const listingAt = loot.pda.listing(project, asset);
|
|
2115
2284
|
const listing = await readMaybe('Listing', listingAt);
|
|
2116
|
-
if (listing
|
|
2117
|
-
const sale = Boolean(listing.deal.Sell);
|
|
2118
|
-
out.push({
|
|
2119
|
-
kind: 'listing',
|
|
2120
|
-
address: listingAt,
|
|
2121
|
-
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}` : ''}.`,
|
|
2122
|
-
});
|
|
2123
|
-
}
|
|
2285
|
+
if (listing) out.push({ kind: 'listing', address: listingAt, record: listing });
|
|
2124
2286
|
const tag = { memcmp: { offset: 0, bytes: encodeBase58(Uint8Array.of(loot.TAGS.Hold)) } };
|
|
2125
2287
|
const holds = await client.programAccounts(loot.LOOT_PROGRAM, [
|
|
2126
2288
|
tag,
|
|
2127
2289
|
{ memcmp: { offset: 1, bytes: project } },
|
|
2128
2290
|
{ memcmp: { offset: 33, bytes: asset } },
|
|
2129
2291
|
]);
|
|
2292
|
+
for (const { pubkey, account } of holds)
|
|
2293
|
+
out.push({ kind: 'hold', address: pubkey, record: loot.decode('Hold', account.data) });
|
|
2294
|
+
return out;
|
|
2295
|
+
}
|
|
2296
|
+
|
|
2297
|
+
/**
|
|
2298
|
+
* The stashes of this project that list `asset`: a project's stash (`kind` 'stash') or a lottery's own
|
|
2299
|
+
* ('lottery-stash'), each { kind, address, id } (`id`: the stash's, or the lottery's). A stash keeps its NFTs in
|
|
2300
|
+
* rows of no fixed place, so no filter finds one NFT's row: this is one search of the project's stashes
|
|
2301
|
+
* (`getProgramAccounts` by tag, project and payload), which returns every stash of the project whole, 36 bytes for
|
|
2302
|
+
* each NFT a stash lists, and reads them here.
|
|
2303
|
+
*/
|
|
2304
|
+
async function stashesOf(asset) {
|
|
2305
|
+
const at = (offset, bytes) => ({ memcmp: { offset, bytes } });
|
|
2306
|
+
const found = await client.programAccounts(loot.LOOT_PROGRAM, [
|
|
2307
|
+
at(0, encodeBase58(Uint8Array.of(loot.TAGS.Table))),
|
|
2308
|
+
at(1, project),
|
|
2309
|
+
at(37, encodeBase58(Uint8Array.of(loot.PAYLOAD.Nft))),
|
|
2310
|
+
]);
|
|
2311
|
+
const out = [];
|
|
2312
|
+
for (const { pubkey, account } of found) {
|
|
2313
|
+
const stash = loot.decodeStash(account.data);
|
|
2314
|
+
if (!stash?.nfts.includes(asset)) continue;
|
|
2315
|
+
const own = pubkey === loot.pda.stash(project, stash.id);
|
|
2316
|
+
out.push({ kind: own ? 'stash' : 'lottery-stash', address: pubkey, id: stash.id });
|
|
2317
|
+
}
|
|
2318
|
+
return out;
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
/**
|
|
2322
|
+
* The records of this project an earlier owner of `asset` left behind (`recordsOf`), each that names a wallet other
|
|
2323
|
+
* than the player's: { kind, address, record, says }. Such a record is left when whoever controls the NFT's
|
|
2324
|
+
* collection moved it out of the project's hands without the program. Until it is closed, the program hands out
|
|
2325
|
+
* whatever the project holds at that address: once this player's deposit puts the NFT back in the project's hands, the
|
|
2326
|
+
* record can send it to the wallet it names. `own`: the player's own listing of it counts too (a route that lists:
|
|
2327
|
+
* the program takes no second listing of an NFT while one is open). NFTs of the project's own collections are not
|
|
2328
|
+
* read: those collections refuse the permanent delegates that take an NFT out.
|
|
2329
|
+
*/
|
|
2330
|
+
async function leftovers(asset, { own = false } = {}) {
|
|
2331
|
+
if (ours(await collectionOf(asset))) return [];
|
|
2332
|
+
const me = wallet.address;
|
|
2333
|
+
const out = [];
|
|
2130
2334
|
const time = BigInt(await chainNow());
|
|
2131
|
-
|
|
2132
|
-
|
|
2335
|
+
const iso = at => new Date(Number(at) * 1000).toISOString();
|
|
2336
|
+
for (const r of await recordsOf(asset)) {
|
|
2337
|
+
if (r.kind === 'listing') {
|
|
2338
|
+
const listing = r.record;
|
|
2339
|
+
const mine = listing.maker === me;
|
|
2340
|
+
if (mine && !own) continue;
|
|
2341
|
+
const sale = Boolean(listing.deal.Sell);
|
|
2342
|
+
// A running rental is kept: nobody but its maker can even stop the listing until it has run.
|
|
2343
|
+
const rented =
|
|
2344
|
+
time < BigInt(listing.until)
|
|
2345
|
+
? ` It is rented until ${iso(listing.until)}: the program keeps a running rental, so nobody can close this listing before then (anyone can after, for nothing).`
|
|
2346
|
+
: '';
|
|
2347
|
+
out.push({
|
|
2348
|
+
...r,
|
|
2349
|
+
says: mine
|
|
2350
|
+
? `Your own earlier listing of this NFT (${r.address}) is still open in this project.${rented}`
|
|
2351
|
+
: `A listing of this NFT (${r.address}) 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}` : ''}.${rented}`,
|
|
2352
|
+
});
|
|
2353
|
+
continue;
|
|
2354
|
+
}
|
|
2355
|
+
const hold = r.record;
|
|
2133
2356
|
const to = hold.player === project ? 'the project’s treasury' : hold.player;
|
|
2134
2357
|
const names = [hold.player, hold.debt?.lender].filter(Boolean);
|
|
2135
2358
|
if (names.every(x => x === me)) continue;
|
|
@@ -2140,31 +2363,137 @@ export function solanaApp({
|
|
|
2140
2363
|
: `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`
|
|
2141
2364
|
: due
|
|
2142
2365
|
? `a hold that is over: anyone can send the NFT to ${to} at once`
|
|
2143
|
-
: `a hold that runs until ${
|
|
2366
|
+
: `a hold that runs until ${iso(hold.free_at)}: the project's authority decides where the NFT goes until then, and anyone sends it to ${to} after`;
|
|
2144
2367
|
out.push({
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
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.`,
|
|
2368
|
+
...r,
|
|
2369
|
+
says: `This project still has ${what} (${r.address}), left from an earlier owner of this NFT. Once the project holds the NFT again, it can be sent there.`,
|
|
2148
2370
|
});
|
|
2149
2371
|
}
|
|
2150
2372
|
return out;
|
|
2151
2373
|
}
|
|
2152
2374
|
|
|
2153
2375
|
/**
|
|
2154
|
-
*
|
|
2155
|
-
*
|
|
2156
|
-
*
|
|
2376
|
+
* The instructions that close `records` of `asset` (`recordsOf`) on the proof that the project no longer holds it
|
|
2377
|
+
* (`proof`, from `goneProof`), as `signer` sends them: a hold by `release`, a listing by `take_listing`. Anyone may
|
|
2378
|
+
* send either, at any time; nothing is sent or paid, and no project fee is taken. A listing whose rental still runs
|
|
2379
|
+
* can't be closed (`Rented`): the program keeps a running rental, and the listing closes once it has run.
|
|
2380
|
+
* Returns { closes, closed: [{ kind, address }], waits: the records left }.
|
|
2381
|
+
*/
|
|
2382
|
+
async function closesOf(asset, records, proof, signer = wallet.address) {
|
|
2383
|
+
const g = await projectNow();
|
|
2384
|
+
const time = BigInt(await chainNow());
|
|
2385
|
+
const closes = [];
|
|
2386
|
+
const closed = [];
|
|
2387
|
+
const waits = [];
|
|
2388
|
+
for (const r of records) {
|
|
2389
|
+
if (r.kind === 'listing') {
|
|
2390
|
+
if (time < BigInt(r.record.until)) {
|
|
2391
|
+
waits.push(r);
|
|
2392
|
+
continue;
|
|
2393
|
+
}
|
|
2394
|
+
closes.push(
|
|
2395
|
+
loot.takeListingIx({
|
|
2396
|
+
taker: signer,
|
|
2397
|
+
project,
|
|
2398
|
+
projectState: g,
|
|
2399
|
+
listing: r.record,
|
|
2400
|
+
holders: await holdersOf(r.record.shares ?? []),
|
|
2401
|
+
programs,
|
|
2402
|
+
...proof.kind,
|
|
2403
|
+
}),
|
|
2404
|
+
);
|
|
2405
|
+
} else {
|
|
2406
|
+
const hold = r.record;
|
|
2407
|
+
closes.push(
|
|
2408
|
+
loot.releaseIx({
|
|
2409
|
+
signer,
|
|
2410
|
+
project,
|
|
2411
|
+
asset,
|
|
2412
|
+
...proof.kind,
|
|
2413
|
+
hold,
|
|
2414
|
+
address: r.address,
|
|
2415
|
+
treasury: g.treasury,
|
|
2416
|
+
...(hold.debt ? { to: hold.debt.lender } : {}),
|
|
2417
|
+
}),
|
|
2418
|
+
);
|
|
2419
|
+
}
|
|
2420
|
+
closed.push({ kind: r.kind, address: r.address });
|
|
2421
|
+
}
|
|
2422
|
+
return { closes: closes.length ? [...proof.before, ...closes] : [], closed, waits };
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
/**
|
|
2426
|
+
* The deposit guard. Before the player hands `asset` to the project, what an earlier owner left of it there
|
|
2427
|
+
* (`leftovers`) is cleared: the player holds the NFT, so it is provably gone from the project, and each leftover hold
|
|
2428
|
+
* closes by `release` on that proof and a leftover listing by `take_listing` on it (anyone may, at any time, and
|
|
2429
|
+
* nothing is paid). Returns those instructions, tried in a dry run, to go in front of the deposit (`closesFirst`);
|
|
2430
|
+
* none when nothing was left. The closes cost the player their transaction fee only: each record's rent goes to
|
|
2431
|
+
* whoever that record names (a hold's payer, a listing's maker), never to the player.
|
|
2432
|
+
* What can't be closed still refuses the deposit, in words, with nothing sent: a listing whose rental still runs
|
|
2433
|
+
* (the program keeps it until the rental has run), or a record of an NFT nothing shows gone. `accept` true goes
|
|
2434
|
+
* ahead all the same (the closes that can be made still go first). `listing`: the route is a listing, which the
|
|
2435
|
+
* program refuses outright while a listing of the NFT is open (its address is taken), whatever the option says.
|
|
2157
2436
|
*/
|
|
2158
|
-
async function
|
|
2159
|
-
const found = await leftovers(asset);
|
|
2160
|
-
if (
|
|
2437
|
+
async function clearLeftovers(asset, accept, { listing = false } = {}) {
|
|
2438
|
+
const found = await leftovers(asset, { own: listing });
|
|
2439
|
+
if (!found.length) return [];
|
|
2440
|
+
const proof = await goneProof(asset, wallet.address, { holder: wallet.address });
|
|
2441
|
+
let { closes, waits } = proof ? await closesOf(asset, found, proof) : { closes: [], waits: found };
|
|
2442
|
+
let why = '';
|
|
2443
|
+
if (closes.length) {
|
|
2444
|
+
const trial = [computeUnitLimit(1_400_000), ...closes.filter(x => x.programId !== COMPUTE_BUDGET_PROGRAM)];
|
|
2445
|
+
// Tried in a dry run when they are one transaction as they stand; a longer list is made to fit as it is sent.
|
|
2446
|
+
const one = compileFitting({
|
|
2447
|
+
payer: wallet.address,
|
|
2448
|
+
blockhash: NOBODY,
|
|
2449
|
+
instructions: trial,
|
|
2450
|
+
tables: await standing(),
|
|
2451
|
+
});
|
|
2452
|
+
const tried = one ? await dryRun(wallet.address, trial) : { ok: true };
|
|
2453
|
+
if (!tried.ok) {
|
|
2454
|
+
why = tried.error ? ` Closing it was refused in a dry run: ${tried.error.msg} (${tried.error.name}).` : '';
|
|
2455
|
+
[closes, waits] = [[], found];
|
|
2456
|
+
}
|
|
2457
|
+
}
|
|
2458
|
+
const open = waits.find(x => x.kind === 'listing');
|
|
2459
|
+
if (listing && open)
|
|
2161
2460
|
throw new Error(
|
|
2162
|
-
`${
|
|
2461
|
+
`${open.says}${why} The program refuses another listing of this NFT while that one is open (its address is taken): nothing was sent.`,
|
|
2163
2462
|
);
|
|
2164
|
-
if (
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2463
|
+
if (waits.length && accept !== true)
|
|
2464
|
+
throw new Error(
|
|
2465
|
+
`${waits.map(x => x.says).join(' ')}${why} 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.`,
|
|
2466
|
+
);
|
|
2467
|
+
return closes;
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
/**
|
|
2471
|
+
* A deposit's `instructions` with `closes` (`clearLeftovers`) in front of them: in the same transaction when both
|
|
2472
|
+
* fit one as the wallet sends it (a legacy one, or one with the project's standing tables); when they do not, the
|
|
2473
|
+
* closes are sent first in a transaction of their own, and the deposit's instructions come back alone.
|
|
2474
|
+
*/
|
|
2475
|
+
async function closesFirst(closes, instructions) {
|
|
2476
|
+
if (!closes.length) return instructions;
|
|
2477
|
+
const isLimit = ix => ix.programId === COMPUTE_BUDGET_PROGRAM && ix.data[0] === 2;
|
|
2478
|
+
// One proof of a compressed NFT serves both: the closes read its record first, then the deposit moves it.
|
|
2479
|
+
const proven = new Set(closes.filter(isProveLeaf).map(ix => ix.keys[1].pubkey));
|
|
2480
|
+
const together = [
|
|
2481
|
+
computeUnitLimit(1_400_000),
|
|
2482
|
+
...closes.filter(ix => !isLimit(ix)),
|
|
2483
|
+
...instructions.filter(ix => !isLimit(ix) && !(isProveLeaf(ix) && proven.has(ix.keys[1].pubkey))),
|
|
2484
|
+
];
|
|
2485
|
+
const fits =
|
|
2486
|
+
Boolean(
|
|
2487
|
+
compileFitting({
|
|
2488
|
+
payer: wallet.address,
|
|
2489
|
+
blockhash: NOBODY,
|
|
2490
|
+
instructions: loot.withLeafCloses(together),
|
|
2491
|
+
tables: await standing(),
|
|
2492
|
+
}),
|
|
2493
|
+
) && transactionLocks(wallet.address, together) <= 64;
|
|
2494
|
+
if (fits) return together;
|
|
2495
|
+
await send(closes);
|
|
2496
|
+
return instructions;
|
|
2168
2497
|
}
|
|
2169
2498
|
|
|
2170
2499
|
/** Who holds each collectible a split's holder shares name now: { asset: holder }. */
|
|
@@ -2266,6 +2595,77 @@ export function solanaApp({
|
|
|
2266
2595
|
];
|
|
2267
2596
|
}
|
|
2268
2597
|
|
|
2598
|
+
/** How many of the losers' NFTs left a winner's claim reads to choose its picks among. */
|
|
2599
|
+
const PICKS_READ = 8;
|
|
2600
|
+
/**
|
|
2601
|
+
* The NFTs a claim of entry `i` of a contest of NFTs moves, as `claim` sends it: the entry's own while the contest
|
|
2602
|
+
* holds it, and for a winner the losers' NFTs its share still lets it pick, at most 4 a claim. `take` names the
|
|
2603
|
+
* picks (one asset, or several). With none named, the first of the losers' NFTs left that the project still holds
|
|
2604
|
+
* are picked (the first `PICKS_READ` are read), and one that is gone only when no other is left there: a pick of an
|
|
2605
|
+
* NFT gone takes nothing. Each is { j, nft, gone }: its entry's place in the list, and whether the project no longer
|
|
2606
|
+
* holds it (its collection took it back or burned it).
|
|
2607
|
+
*/
|
|
2608
|
+
async function contestNfts(contest, entries, i, take) {
|
|
2609
|
+
const own = entries[i];
|
|
2610
|
+
const won = contest.verdict === 'Won' && Number(contest.winner) === Number(own.side);
|
|
2611
|
+
const out = Number(own.amount) === 1 ? [{ j: i, nft: own.nft }] : [];
|
|
2612
|
+
const share = won && !(own.bits & loot.ENDED) ? loot.shareOf(contest, own) - Number(own.taken) : 0;
|
|
2613
|
+
const left = entries
|
|
2614
|
+
.map((e, j) => ({ j, nft: e.nft, e }))
|
|
2615
|
+
.filter(({ e }) => Number(e.side) !== Number(contest.winner) && Number(e.amount) === 1);
|
|
2616
|
+
const wanted = take ? [take].flat() : [];
|
|
2617
|
+
const among =
|
|
2618
|
+
share <= 0 ? [] : wanted.length ? left.filter(x => wanted.includes(x.nft)) : left.slice(0, PICKS_READ);
|
|
2619
|
+
if (wanted.length && among.length < Math.min(wanted.length, share))
|
|
2620
|
+
throw new Error(`${wanted.join(', ')} is not a loser’s NFT left in this contest for you to pick.`);
|
|
2621
|
+
const heldOf = await heldEach([...out, ...among].map(x => x.nft));
|
|
2622
|
+
const isGone = x => heldOf.get(x.nft)?.held === false;
|
|
2623
|
+
const picks = wanted.length ? among : [...among.filter(x => !isGone(x)), ...among.filter(isGone)];
|
|
2624
|
+
out.push(...picks.slice(0, Math.min(share, 4)));
|
|
2625
|
+
return out.map(x => ({ j: x.j, nft: x.nft, gone: isGone(x) }));
|
|
2626
|
+
}
|
|
2627
|
+
/** What a contest's claim does about its NFTs that are gone, in words, for `claims()`: `own` the wallet's own NFT
|
|
2628
|
+
* when it is gone, `picks` the losers' NFTs gone that its picks land on. */
|
|
2629
|
+
const goneContestSays = (own, picks) =>
|
|
2630
|
+
[
|
|
2631
|
+
own
|
|
2632
|
+
? `The project no longer holds the NFT you entered (${own}): whoever controls its collection took it back or burned it while the contest held it. It can’t come back to you: claiming closes your entry and sends nothing.`
|
|
2633
|
+
: '',
|
|
2634
|
+
picks.length
|
|
2635
|
+
? `The project no longer holds ${picks.length === 1 ? 'the loser’s NFT' : 'the losers’ NFTs'} left for you to pick (${picks.join(', ')}): whoever controls its collection took it back or burned it. A pick of it takes nothing: claiming closes it and sends nothing.`
|
|
2636
|
+
: '',
|
|
2637
|
+
]
|
|
2638
|
+
.filter(Boolean)
|
|
2639
|
+
.join(' ');
|
|
2640
|
+
|
|
2641
|
+
/**
|
|
2642
|
+
* The NFTs the pulls of a wallet's parts of round `r` (`mine`: [{ entry, parts }]) are to hand out: each entry's NFT
|
|
2643
|
+
* ticket ('Own'), and each NFT its places are still owed (`Prize`: the lottery's own stash's, as the pulls take them
|
|
2644
|
+
* out; a project stash's named one). Returns { tickets, prizes }, their addresses.
|
|
2645
|
+
*/
|
|
2646
|
+
async function roundNfts(r, mine) {
|
|
2647
|
+
const [tickets, prizes] = [[], []];
|
|
2648
|
+
let own;
|
|
2649
|
+
for (const { entry, parts } of mine) {
|
|
2650
|
+
const e = r.entries[entry];
|
|
2651
|
+
for (const part of parts) {
|
|
2652
|
+
if (part === 'Own' && e.nft !== NOBODY) tickets.push(e.nft);
|
|
2653
|
+
if (!part.Prize || loot.tokenRun(r, Number(part.Prize[0]))) continue;
|
|
2654
|
+
const run = Number(part.Prize[0]);
|
|
2655
|
+
const place = r.terms.places[run];
|
|
2656
|
+
if (place.prize?.Nft) prizes.push(place.prize.Nft.nft);
|
|
2657
|
+
if (!loot.ownStash(place)) continue;
|
|
2658
|
+
own ??= { stash: loot.decodeStash((await client.account(loot.pda.lotteryStash(r.lottery)))?.data ?? null) };
|
|
2659
|
+
const predicted = loot.stashNfts(r, own.stash);
|
|
2660
|
+
for (const p of loot.prizePlacesOwed(r, entry, run)) {
|
|
2661
|
+
const nft = predicted[loot.nftSlot(r, p)];
|
|
2662
|
+
if (nft) prizes.push(nft);
|
|
2663
|
+
}
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
return { tickets, prizes };
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2269
2669
|
/**
|
|
2270
2670
|
* When a draw of round `r` (at `key`) would cancel it (an open round short of its least tickets; a waiting one past
|
|
2271
2671
|
* the timeout), the pulls of `who`'s own parts as the cancelled round owes them (a few entries' worth), to go right
|
|
@@ -2325,6 +2725,11 @@ export function solanaApp({
|
|
|
2325
2725
|
* The pulls of `parts` of entry `i` of a round (`partsOwed`; `i` null for parts owed in common, `commonOwed`), signed by
|
|
2326
2726
|
* `signer`, and the rolls they make ([{ order, box }], `loot.pullRolls`). With `closes`, the last pull carries the
|
|
2327
2727
|
* round's close (when the parts it pays are the last owed).
|
|
2728
|
+
* An NFT of a part that is proven gone from the project is not sent anywhere. A ticket whose price sends it on
|
|
2729
|
+
* passes what proves it gone, and its part settles (`NftGone`, the round its record). A ticket going back to its
|
|
2730
|
+
* buyer and a place's NFT are only handed out by the program: the pull passes each as its hold, and a release on
|
|
2731
|
+
* the proof follows it here, which closes the hold (`NftGone`, the hold its record). A pull makes holds of all its
|
|
2732
|
+
* prize NFTs or of none, so with one of them gone the others are made holds too and released on to their winner.
|
|
2328
2733
|
*/
|
|
2329
2734
|
async function pulls(key, r, i, parts, signer, { closes = false, lotteryClosed = false } = {}) {
|
|
2330
2735
|
const e = i == null ? null : r.entries[i];
|
|
@@ -2340,20 +2745,38 @@ export function solanaApp({
|
|
|
2340
2745
|
const extra = await hookTail([...roundMints(r), ...r.prizes.map(p => p.mint)]);
|
|
2341
2746
|
for (const [k, part] of parts.entries()) {
|
|
2342
2747
|
const more = {};
|
|
2748
|
+
// The NFTs this pull makes holds in place of handing them out (`hold::owe`: for whom, and whose its NFT's
|
|
2749
|
+
// account's rent is), each released right after it. The round only hands an NFT out and reads nothing, so one
|
|
2750
|
+
// proven gone from the project is made a hold, and that hold closes on the proof.
|
|
2751
|
+
const holdsMade = new Map();
|
|
2343
2752
|
if (part === 'Own' && e.nft !== NOBODY) {
|
|
2344
2753
|
const then = r.status === 'Cancelled' || r.terms.returns ? null : r.terms.nft_ticket?.then;
|
|
2345
2754
|
more.handBack = Boolean(then?.Give) && loot.handsBack(await client.account(then.Give[0]));
|
|
2346
2755
|
const to = !then || more.handBack ? e.buyer : then.Give ? then.Give[0] : undefined;
|
|
2347
|
-
const { before = [], hasAuthority: _, ...kind } = await
|
|
2756
|
+
const { before = [], hasAuthority: _, gone, ...kind } = await leaving(e.nft, signer, { to });
|
|
2348
2757
|
out.push(...before);
|
|
2349
|
-
|
|
2758
|
+
// Where its price sends it on (burned, given, stashed) the round reads the ticket itself: proven gone, it goes
|
|
2759
|
+
// nowhere and the part settles. Going back to its buyer it is only handed out: made a hold, closed below.
|
|
2760
|
+
if (gone && !then) {
|
|
2761
|
+
more.hold = true;
|
|
2762
|
+
holdsMade.set(e.nft, { kind, player: e.buyer, rentTo: e.buyer });
|
|
2763
|
+
} else more.ticket = kind;
|
|
2350
2764
|
} else if (part.Prize && !loot.tokenRun(r, Number(part.Prize[0]))) {
|
|
2351
2765
|
// Each of the entry's places in the run: an NFT of the lottery's own stash (which one, as the pull takes them
|
|
2352
2766
|
// out), a project stash's NFT, an item, or a roll (asked in the same transaction).
|
|
2353
2767
|
const run = Number(part.Prize[0]);
|
|
2354
2768
|
const given = r.terms.places[run].prize;
|
|
2355
|
-
|
|
2769
|
+
// An NFT's kind is read below, with whether the project still holds it.
|
|
2770
|
+
const inputs = await prizeInputs([given.Nft || given.Held ? null : given], signer, e.buyer);
|
|
2356
2771
|
out.push(...inputs.before);
|
|
2772
|
+
const handed = new Map();
|
|
2773
|
+
const hand = async nft => {
|
|
2774
|
+
if (inputs.nfts[nft]) return;
|
|
2775
|
+
const { before, hasAuthority: _, gone, ...kind } = await leaving(nft, signer, { to: e.buyer });
|
|
2776
|
+
inputs.nfts[nft] = kind;
|
|
2777
|
+
out.push(...before);
|
|
2778
|
+
handed.set(nft, { kind, gone, player: e.buyer, rentTo: signer });
|
|
2779
|
+
};
|
|
2357
2780
|
const stash = loot.ownStash(r.terms.places[run])
|
|
2358
2781
|
? loot.decodeStash((await client.account(loot.pda.lotteryStash(r.lottery)))?.data ?? null)
|
|
2359
2782
|
: null;
|
|
@@ -2365,10 +2788,7 @@ export function solanaApp({
|
|
|
2365
2788
|
const predicted = loot.stashNfts(r, stash);
|
|
2366
2789
|
for (const place of loot.prizePlacesOwed(r, i, run)) {
|
|
2367
2790
|
const nft = predicted[loot.nftSlot(r, place)];
|
|
2368
|
-
if (
|
|
2369
|
-
const { before, hasAuthority: _, ...kind } = await nftOf(nft, signer, { to: e.buyer });
|
|
2370
|
-
inputs.nfts[nft] = kind;
|
|
2371
|
-
out.push(...before);
|
|
2791
|
+
if (nft) await hand(nft);
|
|
2372
2792
|
}
|
|
2373
2793
|
}
|
|
2374
2794
|
const stashes = given.Nft
|
|
@@ -2378,10 +2798,17 @@ export function solanaApp({
|
|
|
2378
2798
|
) ?? { nfts: [] },
|
|
2379
2799
|
}
|
|
2380
2800
|
: {};
|
|
2801
|
+
// A project stash's named NFT is handed out only while that stash lists it.
|
|
2802
|
+
if (given.Nft && stashes[given.Nft.stash].nfts.includes(given.Nft.nft)) await hand(given.Nft.nft);
|
|
2803
|
+
// One of them gone: a pull makes holds of all its NFTs or of none, so each becomes a hold, and its release
|
|
2804
|
+
// below closes the one that is gone and sends any other on to its winner.
|
|
2805
|
+
const holds = [...handed.values()].some(x => x.gone);
|
|
2806
|
+
if (holds) for (const [nft, x] of handed) holdsMade.set(nft, x);
|
|
2381
2807
|
more.prize = {
|
|
2382
2808
|
stash,
|
|
2383
2809
|
stashes,
|
|
2384
2810
|
nfts: inputs.nfts,
|
|
2811
|
+
hold: holds,
|
|
2385
2812
|
collection: inputs.collection,
|
|
2386
2813
|
poolMints: inputs.poolMints,
|
|
2387
2814
|
items: inputs.items,
|
|
@@ -2416,7 +2843,8 @@ export function solanaApp({
|
|
|
2416
2843
|
) && (await dryRun(signer, tx)).ok
|
|
2417
2844
|
);
|
|
2418
2845
|
};
|
|
2419
|
-
|
|
2846
|
+
// (Made holds, each NFT's release follows the pull in its transaction: a few places a pull then.)
|
|
2847
|
+
let most = holds ? Math.min(owed, 4) : owed;
|
|
2420
2848
|
while (most > 1 && !(await sized(most))) most = Math.max(1, Math.floor(most * 0.75));
|
|
2421
2849
|
if (most < owed) [more.prize.most, partial] = [most, true];
|
|
2422
2850
|
heap = true;
|
|
@@ -2443,21 +2871,26 @@ export function solanaApp({
|
|
|
2443
2871
|
}
|
|
2444
2872
|
const last = k === parts.length - 1;
|
|
2445
2873
|
const round = closes && last ? { ...r, owed: pullSettles(r, i, part) } : { ...r, owed: Number.MAX_SAFE_INTEGER };
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
)
|
|
2874
|
+
const pull = loot.pullIx({
|
|
2875
|
+
payer: signer,
|
|
2876
|
+
project,
|
|
2877
|
+
roundAddress: key,
|
|
2878
|
+
round,
|
|
2879
|
+
entry: i ?? 0,
|
|
2880
|
+
part,
|
|
2881
|
+
projectState: g,
|
|
2882
|
+
lotteryClosed,
|
|
2883
|
+
programs,
|
|
2884
|
+
extra,
|
|
2885
|
+
...more,
|
|
2886
|
+
});
|
|
2887
|
+
out.push(pull);
|
|
2888
|
+
// Each hold this pull makes (a pull that stops short carries only some) is released at once: one whose NFT is
|
|
2889
|
+
// gone closes on the proof and nothing is sent, any other goes on to its winner, and the hold's rent comes back
|
|
2890
|
+
// to the signer, who paid it.
|
|
2891
|
+
for (const [nft, { kind, player, rentTo }] of holdsMade)
|
|
2892
|
+
if (pull.keys.some(x => x.pubkey === loot.pda.hold(nft, 0)))
|
|
2893
|
+
out.push(releaseOwed(nft, kind, { player, rentTo }, signer, g));
|
|
2461
2894
|
}
|
|
2462
2895
|
return { instructions: out, rolls, heap, partial };
|
|
2463
2896
|
}
|
|
@@ -2596,15 +3029,18 @@ export function solanaApp({
|
|
|
2596
3029
|
return out;
|
|
2597
3030
|
},
|
|
2598
3031
|
|
|
2599
|
-
/** Gives an order's payment back once its time has come (a roll that failed or never came).
|
|
3032
|
+
/** Gives an order's payment back once its time has come (a roll that failed or never came). An NFT the project
|
|
3033
|
+
* held for the roll goes back with it; one it no longer holds (its collection took it back or burned it) is not
|
|
3034
|
+
* looked for, and the whole payment goes back all the same. */
|
|
2600
3035
|
async refund(orderAddress) {
|
|
2601
3036
|
const order = await read('Order', orderAddress);
|
|
2602
3037
|
// An NFT the project held for the roll goes back.
|
|
2603
3038
|
const {
|
|
2604
3039
|
before = [],
|
|
2605
3040
|
hasAuthority: _,
|
|
3041
|
+
gone: _gone,
|
|
2606
3042
|
...subject
|
|
2607
|
-
} = order.held ? await
|
|
3043
|
+
} = order.held ? await leaving(order.subject, wallet.address, { to: order.player }) : {};
|
|
2608
3044
|
return send([...before, loot.refundIx({ signer: wallet.address, orderAddress, order, subject, programs })]);
|
|
2609
3045
|
},
|
|
2610
3046
|
|
|
@@ -2839,12 +3275,13 @@ export function solanaApp({
|
|
|
2839
3275
|
shown ??= await passing(lottery.gate, 'buy a ticket');
|
|
2840
3276
|
listProof = listProofOf(lottery.gate, l.gate?.list, listProof);
|
|
2841
3277
|
let nft;
|
|
3278
|
+
let clears = [];
|
|
2842
3279
|
const before = [];
|
|
2843
3280
|
if (terms.nft_ticket) {
|
|
2844
3281
|
if (!paidWith) throw new Error(`A ticket of “${l.name}” costs an NFT: say which (nft).`);
|
|
2845
3282
|
if (BigInt(count) !== 1n) throw new Error('An NFT pays for one ticket at a time.');
|
|
2846
|
-
// The project holds an NFT ticket until the round is drawn.
|
|
2847
|
-
await
|
|
3283
|
+
// The project holds an NFT ticket until the round is drawn: what an earlier owner left of it goes first.
|
|
3284
|
+
clears = await clearLeftovers(paidWith, acceptLeftover);
|
|
2848
3285
|
const { before: proof, ...kind } = await nftOf(paidWith);
|
|
2849
3286
|
nft = { asset: paidWith, ...kind };
|
|
2850
3287
|
before.push(...proof);
|
|
@@ -2914,9 +3351,12 @@ export function solanaApp({
|
|
|
2914
3351
|
}
|
|
2915
3352
|
const buy = [...before, ...withSolWrap(wallet.address, mint, BigInt(terms.price) * BigInt(count), [ix])];
|
|
2916
3353
|
return send(
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
3354
|
+
await closesFirst(
|
|
3355
|
+
clears,
|
|
3356
|
+
draw.length
|
|
3357
|
+
? [computeUnitLimit(800_000), ...draw, ...buy.filter(x => x.programId !== COMPUTE_BUDGET_PROGRAM)]
|
|
3358
|
+
: buy,
|
|
3359
|
+
),
|
|
2920
3360
|
);
|
|
2921
3361
|
},
|
|
2922
3362
|
|
|
@@ -3012,9 +3452,9 @@ export function solanaApp({
|
|
|
3012
3452
|
only(others, ['extra', 'transfers', 'acceptLeftover'], 'stakeNft');
|
|
3013
3453
|
const f = part('farms', farmKey);
|
|
3014
3454
|
const farm = await read('Farm', loot.pda.farm(project, f.id));
|
|
3015
|
-
// A farm that keeps its NFTs in escrow hands this one to the project
|
|
3016
|
-
// wallet, where no record of the project's can move it.
|
|
3017
|
-
|
|
3455
|
+
// A farm that keeps its NFTs in escrow hands this one to the project (what an earlier owner left of it there is
|
|
3456
|
+
// cleared first); one that freezes or watches leaves it in the wallet, where no record of the project's can move it.
|
|
3457
|
+
const clears = farm.custody === 'Escrow' ? await clearLeftovers(asset, acceptLeftover) : [];
|
|
3018
3458
|
await openFor(f, farm, tier);
|
|
3019
3459
|
const token = await farmTokens([farm.rewards_mint], [], { extra, transfers });
|
|
3020
3460
|
const { before, ...nft } = await nftOf(asset);
|
|
@@ -3036,11 +3476,24 @@ export function solanaApp({
|
|
|
3036
3476
|
// prize and as a forfeit, is sized before it goes in, and an NFT that could never come back out isn't staked.
|
|
3037
3477
|
if (!farm.farmed.Tokens) await exitFits(f, farm, asset, nft, before, tier, token);
|
|
3038
3478
|
return send(
|
|
3039
|
-
await
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3479
|
+
await closesFirst(
|
|
3480
|
+
clears,
|
|
3481
|
+
await walking(f, farm, [
|
|
3482
|
+
...refresh,
|
|
3483
|
+
...before,
|
|
3484
|
+
loot.stakeNftIx({
|
|
3485
|
+
player: wallet.address,
|
|
3486
|
+
project,
|
|
3487
|
+
id: f.id,
|
|
3488
|
+
farm,
|
|
3489
|
+
asset,
|
|
3490
|
+
...nft,
|
|
3491
|
+
previous,
|
|
3492
|
+
tier,
|
|
3493
|
+
...token,
|
|
3494
|
+
}),
|
|
3495
|
+
]),
|
|
3496
|
+
),
|
|
3044
3497
|
);
|
|
3045
3498
|
},
|
|
3046
3499
|
|
|
@@ -3081,8 +3534,10 @@ export function solanaApp({
|
|
|
3081
3534
|
const boxes = roll != null ? { [roll]: await read('LootBox', loot.pda.box(project, roll)) } : {};
|
|
3082
3535
|
// 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
|
|
3083
3536
|
// left it in the wallet.
|
|
3084
|
-
|
|
3085
|
-
|
|
3537
|
+
const clears =
|
|
3538
|
+
roll != null && farm.custody !== 'Escrow' && loot.boxHolds(boxes[roll])
|
|
3539
|
+
? await clearLeftovers(asset, acceptLeftover)
|
|
3540
|
+
: [];
|
|
3086
3541
|
// A roll still at the NFT's order address blocks the leave, unless it was asked and came back with nothing to give
|
|
3087
3542
|
// (Waiting, asked once): that one stands in for this. Any other is asked (its player or the project's keys), answered and settled first.
|
|
3088
3543
|
if (roll != null) {
|
|
@@ -3125,7 +3580,7 @@ export function solanaApp({
|
|
|
3125
3580
|
{ code: tried.error.code, name: 'PrizeNotCovered' },
|
|
3126
3581
|
);
|
|
3127
3582
|
}
|
|
3128
|
-
const { signature, rolls } = await sendRolling(steps);
|
|
3583
|
+
const { signature, rolls } = await sendRolling(await closesFirst(clears, steps));
|
|
3129
3584
|
const rolled = rolls.length ? { rolled: rolls[0] } : {};
|
|
3130
3585
|
if (roll != null)
|
|
3131
3586
|
return {
|
|
@@ -3466,23 +3921,37 @@ export function solanaApp({
|
|
|
3466
3921
|
/**
|
|
3467
3922
|
* Hands an NFT of any kind to the project until a time (a hold): `seconds` from now, or `{ until }` (Unix seconds).
|
|
3468
3923
|
* The hold's address names its end. Returns { signature, freeAt }.
|
|
3924
|
+
*
|
|
3925
|
+
* Like every route that hands an NFT in (`stakeNft` in escrow, `borrow`, `enter`, `bid`, `listForSale`,
|
|
3926
|
+
* `listForRent`, a ticket paid with an NFT, a box that rolls on one), it first clears what an earlier owner of the
|
|
3927
|
+
* NFT left in this project: a hold or a listing that stayed open when whoever controls the NFT's collection took it
|
|
3928
|
+
* out of the project's hands. Left open, such a record could take the NFT once the project holds it again. The
|
|
3929
|
+
* player holds the NFT now, so the program closes each on that proof (a hold by `release`, a listing by
|
|
3930
|
+
* `take_listing`, nothing sent or paid), in front of the deposit in the same transaction, or in a transaction of
|
|
3931
|
+
* their own before it when the two do not fit one. The closes cost the player their transaction fee only: each
|
|
3932
|
+
* record's rent goes to whoever that record names (a hold's payer, a listing's maker), never to the player. Only
|
|
3933
|
+
* what can't be closed still refuses the deposit, in words (a listing whose rental still runs: the program keeps
|
|
3934
|
+
* it until the rental has run); `acceptLeftover: true` goes ahead all the same. NFTs of the project's own
|
|
3935
|
+
* collections are not read.
|
|
3469
3936
|
*/
|
|
3470
3937
|
async hold(asset, seconds, { acceptLeftover, ...others } = NO_OPTIONS) {
|
|
3471
3938
|
only(others, ['acceptLeftover'], 'hold');
|
|
3472
|
-
await
|
|
3939
|
+
const clears = await clearLeftovers(asset, acceptLeftover);
|
|
3473
3940
|
const freeAt = BigInt(typeof seconds === 'object' ? seconds.until : now() + Number(seconds));
|
|
3474
3941
|
const { before, hasAuthority: _, ...nft } = await nftOf(asset);
|
|
3475
|
-
const signature = await send(
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3942
|
+
const signature = await send(
|
|
3943
|
+
await closesFirst(clears, [
|
|
3944
|
+
...before,
|
|
3945
|
+
loot.holdIx({
|
|
3946
|
+
player: wallet.address,
|
|
3947
|
+
project,
|
|
3948
|
+
asset,
|
|
3949
|
+
...nft,
|
|
3950
|
+
collection: nft.collection ?? (await collectionOf(asset)) ?? MPL_CORE,
|
|
3951
|
+
freeAt,
|
|
3952
|
+
}),
|
|
3953
|
+
]),
|
|
3954
|
+
);
|
|
3486
3955
|
return { signature, freeAt };
|
|
3487
3956
|
},
|
|
3488
3957
|
|
|
@@ -3540,7 +4009,7 @@ export function solanaApp({
|
|
|
3540
4009
|
*/
|
|
3541
4010
|
async borrow(maker, id, asset, { due, acceptRisks = false, acceptLeftover, extra, ...others } = NO_OPTIONS) {
|
|
3542
4011
|
only(others, ['due', 'acceptRisks', 'acceptLeftover', 'extra'], 'borrow');
|
|
3543
|
-
await
|
|
4012
|
+
const clears = await clearLeftovers(asset, acceptLeftover);
|
|
3544
4013
|
const loan = await this.loanOffer(maker, id);
|
|
3545
4014
|
extra ??= await hookTail([loan.mint]);
|
|
3546
4015
|
if (loan.risks.length && acceptRisks !== true)
|
|
@@ -3564,11 +4033,13 @@ export function solanaApp({
|
|
|
3564
4033
|
programs: loanPrograms,
|
|
3565
4034
|
extra,
|
|
3566
4035
|
});
|
|
3567
|
-
const signature = await send(
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
4036
|
+
const signature = await send(
|
|
4037
|
+
await closesFirst(clears, [
|
|
4038
|
+
...before,
|
|
4039
|
+
createAssociatedTokenAccountIdempotent(wallet.address, wallet.address, loan.mint, loan.program),
|
|
4040
|
+
ix,
|
|
4041
|
+
]),
|
|
4042
|
+
);
|
|
3572
4043
|
return { signature, due, hold: loot.pda.hold(asset, due) };
|
|
3573
4044
|
},
|
|
3574
4045
|
|
|
@@ -3593,7 +4064,9 @@ export function solanaApp({
|
|
|
3593
4064
|
* borrower in the same instruction: one prompt. `repay`: the debt the payer was shown (`loan()`'s, in base units);
|
|
3594
4065
|
* the program refuses any other (`TermsChanged`). An NFT its collection took back or burned while the project held
|
|
3595
4066
|
* it is refused here, before anything is signed: the program takes no repayment for it, and the borrower keeps what
|
|
3596
|
-
* they borrowed.
|
|
4067
|
+
* they borrowed. Such a loan is closed for nothing instead: `claims()` lists it for its borrower and its lender, and
|
|
4068
|
+
* `claim` of it closes the hold (nothing is repaid, and the hold's rent goes back to whoever paid it). When the NFT
|
|
4069
|
+
* can't move now (frozen for now, or its collection refuses the move), or `home` is
|
|
3597
4070
|
* false, the money goes alone, so the loan does not fall due: the hold is then over, and anyone sends the NFT home
|
|
3598
4071
|
* once it can move (`claims()` lists it). A compressed NFT is proven in the same transaction; the money alone for one
|
|
3599
4072
|
* whose proof keeps going out of date goes without it. Returns { signature, home }, and `says` when the NFT waits.
|
|
@@ -3614,7 +4087,7 @@ export function solanaApp({
|
|
|
3614
4087
|
);
|
|
3615
4088
|
if ((await heldNow(asset)).held === false)
|
|
3616
4089
|
throw new Error(
|
|
3617
|
-
`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.`,
|
|
4090
|
+
`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), so nothing was sent: the borrower keeps what they borrowed. This loan can be closed for nothing: claims() lists it for its borrower and for its lender, and claim() of it closes the hold (nothing is repaid, and the hold's rent goes back to whoever paid it).`,
|
|
3618
4091
|
);
|
|
3619
4092
|
const { before, hasAuthority: _, ...nft } = await nftOf(asset, wallet.address, { to: hold.player });
|
|
3620
4093
|
const kind = nft.compressed || nft.tokenMetadata ? nft : { collection: (await collectionOf(asset)) ?? MPL_CORE };
|
|
@@ -3672,23 +4145,25 @@ export function solanaApp({
|
|
|
3672
4145
|
listProof = listProofOf(contest.gate, null, listProof);
|
|
3673
4146
|
if (contest.nft) {
|
|
3674
4147
|
if (!staked) throw new Error('This contest takes an NFT: say which (nft).');
|
|
3675
|
-
await
|
|
4148
|
+
const clears = await clearLeftovers(staked, acceptLeftover);
|
|
3676
4149
|
const { before, ...kind } = await nftOf(staked);
|
|
3677
|
-
return send(
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
4150
|
+
return send(
|
|
4151
|
+
await closesFirst(clears, [
|
|
4152
|
+
...before,
|
|
4153
|
+
loot.enterIx({
|
|
4154
|
+
player: wallet.address,
|
|
4155
|
+
project,
|
|
4156
|
+
id,
|
|
4157
|
+
contest,
|
|
4158
|
+
side,
|
|
4159
|
+
amount: 1n,
|
|
4160
|
+
shown,
|
|
4161
|
+
listProof,
|
|
4162
|
+
nft: { asset: staked, ...kind },
|
|
4163
|
+
programs,
|
|
4164
|
+
}),
|
|
4165
|
+
]),
|
|
4166
|
+
);
|
|
3692
4167
|
}
|
|
3693
4168
|
const t = [...tokens.values()].find(x => mintOf(x.key) === contest.mint);
|
|
3694
4169
|
const value = units(amount, t?.source === 'sol' ? 9 : (t?.decimals ?? 0));
|
|
@@ -3787,7 +4262,9 @@ export function solanaApp({
|
|
|
3787
4262
|
* Bids `amount` (whole units) on an auction. The bid it tops goes back to its bidder in the same transaction
|
|
3788
4263
|
* (the program opens their account again if they closed it, paid by this bidder). In SOL, only what the player's
|
|
3789
4264
|
* wrapped-SOL account does not hold already is wrapped, and the account stays open: if this bid is topped, it comes
|
|
3790
|
-
* back there.
|
|
4265
|
+
* back there. In an auction of NFT bids, an NFT of the topped bid that the project no longer holds (its collection
|
|
4266
|
+
* took it back or burned it) can't go back: the bid makes it a hold for its bidder and closes that hold on the
|
|
4267
|
+
* proof in the same transaction, so a high bid with an NFT gone can still be topped.
|
|
3791
4268
|
*/
|
|
3792
4269
|
async bid(auctionKey, amount, { shown, listProof, nfts, acceptLeftover, ...others } = NO_OPTIONS) {
|
|
3793
4270
|
only(others, ['shown', 'listProof', 'nfts', 'acceptLeftover'], 'bid');
|
|
@@ -3803,7 +4280,8 @@ export function solanaApp({
|
|
|
3803
4280
|
if (bundle.length > loot.MAX_BID_NFTS) throw new Error(`A bid holds at most ${loot.MAX_BID_NFTS} NFTs.`);
|
|
3804
4281
|
if (auction.held.length >= loot.MAX_BID_NFTS)
|
|
3805
4282
|
throw new Error(`The high bid holds ${loot.MAX_BID_NFTS} NFTs, the most a bid holds: it can't be topped.`);
|
|
3806
|
-
|
|
4283
|
+
const clears = [];
|
|
4284
|
+
for (const asset of bundle) clears.push(...(await clearLeftovers(asset, acceptLeftover)));
|
|
3807
4285
|
const before = [];
|
|
3808
4286
|
const kindOf = async (asset, to) => {
|
|
3809
4287
|
const { before: proof, ...kind } = await nftOf(asset, wallet.address, { to });
|
|
@@ -3812,9 +4290,17 @@ export function solanaApp({
|
|
|
3812
4290
|
};
|
|
3813
4291
|
const bid = [];
|
|
3814
4292
|
for (const asset of bundle) bid.push(await kindOf(asset));
|
|
3815
|
-
// The bid it tops goes back to its bidder: into a fresh account of theirs when their own can't take it.
|
|
4293
|
+
// The bid it tops goes back to its bidder: into a fresh account of theirs when their own can't take it. An
|
|
4294
|
+
// NFT of it that is gone (its collection took it back or burned it) can't go back: the auction only hands it
|
|
4295
|
+
// out, so it is made a hold for its bidder, and that hold closes on the proof right after the bid.
|
|
3816
4296
|
const held = [];
|
|
3817
|
-
|
|
4297
|
+
const lost = new Map();
|
|
4298
|
+
for (const asset of auction.held) {
|
|
4299
|
+
const { before: proof, gone, ...kind } = await leaving(asset, wallet.address, { to: auction.high_bidder });
|
|
4300
|
+
before.push(...proof);
|
|
4301
|
+
held.push({ asset, ...kind });
|
|
4302
|
+
if (gone) lost.set(asset, kind);
|
|
4303
|
+
}
|
|
3818
4304
|
const ix = loot.bidIx({
|
|
3819
4305
|
bidder: wallet.address,
|
|
3820
4306
|
project,
|
|
@@ -3824,9 +4310,21 @@ export function solanaApp({
|
|
|
3824
4310
|
listProof,
|
|
3825
4311
|
nfts: bid,
|
|
3826
4312
|
held,
|
|
4313
|
+
hold: [...lost.keys()],
|
|
3827
4314
|
programs,
|
|
3828
4315
|
});
|
|
3829
|
-
|
|
4316
|
+
const g = await projectNow();
|
|
4317
|
+
const closes = [...lost].map(([asset, { hasAuthority: _, ...kind }]) =>
|
|
4318
|
+
releaseOwed(asset, kind, { player: auction.high_bidder, rentTo: auction.high_bidder }, wallet.address, g),
|
|
4319
|
+
);
|
|
4320
|
+
return send(
|
|
4321
|
+
await closesFirst(clears, [
|
|
4322
|
+
computeUnitLimit(800_000),
|
|
4323
|
+
...before.filter(x => x.programId !== COMPUTE_BUDGET_PROGRAM),
|
|
4324
|
+
ix,
|
|
4325
|
+
...closes,
|
|
4326
|
+
]),
|
|
4327
|
+
);
|
|
3830
4328
|
}
|
|
3831
4329
|
const mint = auction.mint;
|
|
3832
4330
|
const value = units(amount, decimalsOf(a.currency));
|
|
@@ -3872,6 +4370,12 @@ export function solanaApp({
|
|
|
3872
4370
|
* the sale is called off on that evidence, which anyone may: the bid goes back to its bidder (`{ calledOff: true }`).
|
|
3873
4371
|
* With no bid, the lot goes home to its seller. An end too big for one transaction otherwise makes each NFT that may
|
|
3874
4372
|
* be a hold one, and sends each on with a release after it (`releases`).
|
|
4373
|
+
* An NFT that is gone is never sent, whatever its kind, and the result says so (`gone: true`, and `says`, what the
|
|
4374
|
+
* end did in words): a lot gone with no bid stays where it is and the auction closes, its rent to its seller; a
|
|
4375
|
+
* sale with the lot or an NFT of the bid gone is called off, and what the project still holds goes back; for the
|
|
4376
|
+
* project's own lot, or a lot that went first, the end passes the bid's NFT that is gone over and pays the rest.
|
|
4377
|
+
* A hold made at the end whose NFT goes before its release is closed on the proof: its entry in `releases` is
|
|
4378
|
+
* then `{ asset, gone: true, closed }`, `closed` the signature.
|
|
3875
4379
|
*/
|
|
3876
4380
|
async endAuction(auctionKey, { holders, ...others } = NO_OPTIONS) {
|
|
3877
4381
|
only(others, ['holders'], 'endAuction');
|
|
@@ -3938,21 +4442,23 @@ export function solanaApp({
|
|
|
3938
4442
|
*/
|
|
3939
4443
|
async listForSale(asset, { price, currency, shares = [], acceptLeftover, ...others } = NO_OPTIONS) {
|
|
3940
4444
|
only(others, ['price', 'currency', 'shares', 'acceptLeftover'], 'listForSale');
|
|
3941
|
-
await
|
|
4445
|
+
const clears = await clearLeftovers(asset, acceptLeftover, { listing: true });
|
|
3942
4446
|
const mint = mintOf(currency);
|
|
3943
4447
|
const { before, ...nft } = await nftOf(asset);
|
|
3944
|
-
return send(
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
4448
|
+
return send(
|
|
4449
|
+
await closesFirst(clears, [
|
|
4450
|
+
...before,
|
|
4451
|
+
loot.openListingIx({
|
|
4452
|
+
maker: wallet.address,
|
|
4453
|
+
project,
|
|
4454
|
+
asset,
|
|
4455
|
+
mint,
|
|
4456
|
+
deal: { Sell: { price: units(price, decimalsOf(currency)) } },
|
|
4457
|
+
shares,
|
|
4458
|
+
...nft,
|
|
4459
|
+
}),
|
|
4460
|
+
]),
|
|
4461
|
+
);
|
|
3956
4462
|
},
|
|
3957
4463
|
|
|
3958
4464
|
/**
|
|
@@ -3964,31 +4470,40 @@ export function solanaApp({
|
|
|
3964
4470
|
{ price, lasts, maxPeriods = 1, currency, shares = [], acceptLeftover, ...others } = NO_OPTIONS,
|
|
3965
4471
|
) {
|
|
3966
4472
|
only(others, ['price', 'lasts', 'maxPeriods', 'currency', 'shares', 'acceptLeftover'], 'listForRent');
|
|
3967
|
-
await
|
|
4473
|
+
const clears = await clearLeftovers(asset, acceptLeftover, { listing: true });
|
|
3968
4474
|
const mint = mintOf(currency);
|
|
3969
4475
|
const { before, ...nft } = await nftOf(asset);
|
|
3970
|
-
return send(
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
4476
|
+
return send(
|
|
4477
|
+
await closesFirst(clears, [
|
|
4478
|
+
...before,
|
|
4479
|
+
loot.openListingIx({
|
|
4480
|
+
maker: wallet.address,
|
|
4481
|
+
project,
|
|
4482
|
+
asset,
|
|
4483
|
+
mint,
|
|
4484
|
+
deal: { Rent: { price: units(price, decimalsOf(currency)), lasts, max_periods: maxPeriods } },
|
|
4485
|
+
shares,
|
|
4486
|
+
...nft,
|
|
4487
|
+
}),
|
|
4488
|
+
]),
|
|
4489
|
+
);
|
|
3982
4490
|
},
|
|
3983
4491
|
|
|
3984
|
-
/**
|
|
4492
|
+
/**
|
|
4493
|
+
* The listing of `asset` (one account read), or null: { listing, asset, maker, mint, deal, renter, until }. One more
|
|
4494
|
+
* read says whether the project still holds the NFT: when whoever controls its collection took it back or burned
|
|
4495
|
+
* it, the listing is flagged `gone` with `says`, what that means in words. There is then nothing to buy or rent,
|
|
4496
|
+
* and the listing can be closed for nothing (`unlist` by its maker, `closeGone` by anyone).
|
|
4497
|
+
*/
|
|
3985
4498
|
async listing(asset) {
|
|
3986
4499
|
const listing = await readMaybe('Listing', loot.pda.listing(project, asset));
|
|
3987
4500
|
if (!listing) return null;
|
|
4501
|
+
const gone = (await heldNow(asset)).held === false;
|
|
3988
4502
|
return {
|
|
3989
4503
|
listing: loot.pda.listing(project, asset),
|
|
3990
4504
|
...listing,
|
|
3991
4505
|
renter: loot.renterOf(listing, await chainNow()),
|
|
4506
|
+
...(gone ? { gone: true, says: GONE_LISTING } : {}),
|
|
3992
4507
|
};
|
|
3993
4508
|
},
|
|
3994
4509
|
|
|
@@ -4002,7 +4517,12 @@ export function solanaApp({
|
|
|
4002
4517
|
return holds ? renter : null;
|
|
4003
4518
|
},
|
|
4004
4519
|
|
|
4005
|
-
/**
|
|
4520
|
+
/**
|
|
4521
|
+
* The project's listings, a maker's (`maker`) or all: one search of the program's accounts, then one read of the
|
|
4522
|
+
* listed NFTs together, which flags a listing `gone` (with `says`) when the project no longer holds its NFT: a
|
|
4523
|
+
* Core asset held by another or burned, a Token Metadata NFT whose token left the project's account. A compressed
|
|
4524
|
+
* NFT is not read here (an indexer's answer for each): `listing(asset)` says it for one.
|
|
4525
|
+
*/
|
|
4006
4526
|
async listings({ maker, ...others } = NO_OPTIONS) {
|
|
4007
4527
|
only(others, ['maker'], 'listings');
|
|
4008
4528
|
const filters = [
|
|
@@ -4011,19 +4531,36 @@ export function solanaApp({
|
|
|
4011
4531
|
...(maker ? [{ memcmp: { offset: 33, bytes: maker } }] : []),
|
|
4012
4532
|
];
|
|
4013
4533
|
const time = await chainNow();
|
|
4014
|
-
|
|
4534
|
+
const found = (await client.programAccounts(loot.LOOT_PROGRAM, filters)).map(({ pubkey, account }) => {
|
|
4015
4535
|
const listing = loot.decode('Listing', account.data);
|
|
4016
4536
|
return { listing: pubkey, ...listing, renter: loot.renterOf(listing, time) };
|
|
4017
4537
|
});
|
|
4538
|
+
// Each NFT and the project's token account of it, all in one read.
|
|
4539
|
+
const read = found.length ? await client.accounts(found.flatMap(l => [l.asset, loot.ata(project, l.asset)])) : [];
|
|
4540
|
+
return found.map((l, i) => {
|
|
4541
|
+
const [nft, held] = [read[2 * i], read[2 * i + 1]];
|
|
4542
|
+
const gone =
|
|
4543
|
+
nft?.owner === MPL_CORE
|
|
4544
|
+
? !coreHeld(nft)
|
|
4545
|
+
: nft?.owner === TOKEN_PROGRAM &&
|
|
4546
|
+
!(
|
|
4547
|
+
held?.data?.length >= 72 &&
|
|
4548
|
+
new DataView(held.data.buffer, held.data.byteOffset).getBigUint64(64, true) === 1n
|
|
4549
|
+
);
|
|
4550
|
+
return gone ? { ...l, gone: true, says: GONE_LISTING } : l;
|
|
4551
|
+
});
|
|
4018
4552
|
},
|
|
4019
4553
|
|
|
4020
4554
|
/**
|
|
4021
4555
|
* Buys a listed NFT at the terms the player saw: `listing`, as `listing()` or `listings()` showed it. It is
|
|
4022
4556
|
* required: the terms signed are those, never the listing read now, so a price or a share changed since is refused.
|
|
4557
|
+
* A listing whose NFT the project no longer holds (its collection took it back or burned it) is refused in words
|
|
4558
|
+
* before anything is signed: nothing is there to buy, and anyone can close that listing for nothing (`closeGone`).
|
|
4023
4559
|
*/
|
|
4024
4560
|
async buyListing(asset, { listing, ...others } = NO_OPTIONS) {
|
|
4025
4561
|
only(others, ['listing'], 'buyListing');
|
|
4026
4562
|
await shownListing(asset, listing, 'Sell');
|
|
4563
|
+
await listedStill(asset);
|
|
4027
4564
|
const { before, ...nft } = await nftOf(asset, wallet.address, { to: wallet.address });
|
|
4028
4565
|
const holders = await holdersOf(listing.shares ?? []);
|
|
4029
4566
|
return send([
|
|
@@ -4044,11 +4581,13 @@ export function solanaApp({
|
|
|
4044
4581
|
|
|
4045
4582
|
/**
|
|
4046
4583
|
* Rents a listed NFT for `periods` periods, at the terms the player saw: `listing`, as `listing()` or `listings()`
|
|
4047
|
-
* showed it (required, as for `buyListing`).
|
|
4584
|
+
* showed it (required, as for `buyListing`). One whose NFT the project no longer holds is refused in words, as
|
|
4585
|
+
* `buyListing` refuses it.
|
|
4048
4586
|
*/
|
|
4049
4587
|
async rentListing(asset, { periods = 1, listing, ...others } = NO_OPTIONS) {
|
|
4050
4588
|
only(others, ['periods', 'listing'], 'rentListing');
|
|
4051
4589
|
await shownListing(asset, listing, 'Rent');
|
|
4590
|
+
await listedStill(asset);
|
|
4052
4591
|
const holders = await holdersOf(listing.shares ?? []);
|
|
4053
4592
|
const total = BigInt(listing.deal.Rent.price) * BigInt(periods);
|
|
4054
4593
|
// A compressed NFT is read by its record, proven first in the same transaction.
|
|
@@ -4070,13 +4609,66 @@ export function solanaApp({
|
|
|
4070
4609
|
]);
|
|
4071
4610
|
},
|
|
4072
4611
|
|
|
4073
|
-
/**
|
|
4612
|
+
/**
|
|
4613
|
+
* Takes a listed NFT home (its maker). While a rental runs it only stops the listing; call again once it is over.
|
|
4614
|
+
* A listing whose NFT the project no longer holds (its collection took it back or burned it) closes without it, on
|
|
4615
|
+
* that proof: nothing is sent, and the listing's rent comes back to its maker. While a rental of it still runs,
|
|
4616
|
+
* that close too only stops the listing, and closes it once the rental has run.
|
|
4617
|
+
*/
|
|
4074
4618
|
async unlist(asset) {
|
|
4075
4619
|
const listing = await read('Listing', loot.pda.listing(project, asset));
|
|
4620
|
+
const proof = await goneProof(asset);
|
|
4621
|
+
if (proof)
|
|
4622
|
+
return send([...proof.before, loot.closeListingIx({ maker: wallet.address, project, listing, ...proof.kind })]);
|
|
4076
4623
|
const { before, ...nft } = await nftOf(asset, wallet.address, { to: wallet.address });
|
|
4077
4624
|
return send([...before, loot.closeListingIx({ maker: wallet.address, project, listing, ...nft })]);
|
|
4078
4625
|
},
|
|
4079
4626
|
|
|
4627
|
+
/**
|
|
4628
|
+
* Closes what this project still records of an NFT it no longer holds (`asset`: whoever controls its collection
|
|
4629
|
+
* took it out of the project's hands or burned it): each hold of it (`release`), its listing (`take_listing`),
|
|
4630
|
+
* and its row on each of the project's stashes that lists it (`stash`, out), on the proof that it is gone. Anyone
|
|
4631
|
+
* may, at any time, also while a hold runs and for a loan before it is due: nothing is sent or paid, and a loan's
|
|
4632
|
+
* debt ends unpaid. The sender pays the transaction's fee only; each record's rent goes to whoever that record
|
|
4633
|
+
* names (a hold's payer, a listing's maker), never to the sender, and a stash keeps the rent its row no longer
|
|
4634
|
+
* needs (it goes to the sender only when that is one of the project's keys). With its row off a stash, no roll
|
|
4635
|
+
* draws the NFT, and a prize that names it is passed over in place of failing on an NFT that can't move.
|
|
4636
|
+
* What is left is in `waits`: a listing whose rental still runs (the program keeps a running rental, and the
|
|
4637
|
+
* listing closes once it has run), and a lottery's own stash that lists the NFT (`kind` 'lottery-stash', with its
|
|
4638
|
+
* `lottery`, the lottery's id): only the project's keys take a row out of that one (`unraffle`), gone or not.
|
|
4639
|
+
* Refused in words, with nothing sent, when the project still holds the NFT or nothing shows it gone.
|
|
4640
|
+
* Besides one search of the NFT's holds, this makes one search of the project's stashes, which returns every
|
|
4641
|
+
* stash of the project whole (36 bytes for each NFT a stash lists): a stash's rows have no fixed place to look in.
|
|
4642
|
+
* Returns { signature, closed: [{ kind, address }], unstashed: [{ address, id }], waits: [{ kind, address }] }:
|
|
4643
|
+
* `closed` the holds and the listing it closed, `unstashed` the project's stashes it took the row off;
|
|
4644
|
+
* `signature` is null when there was nothing to close.
|
|
4645
|
+
*/
|
|
4646
|
+
async closeGone(asset) {
|
|
4647
|
+
const [records, stashes] = [await recordsOf(asset), await stashesOf(asset)];
|
|
4648
|
+
if (!records.length && !stashes.length) return { signature: null, closed: [], unstashed: [], waits: [] };
|
|
4649
|
+
const proof = await goneProof(asset);
|
|
4650
|
+
if (!proof)
|
|
4651
|
+
throw new Error(
|
|
4652
|
+
`Nothing shows that the project no longer holds the NFT ${asset}: its records close this way only once it is proven gone (its collection took it back or burned it). Nothing was sent.`,
|
|
4653
|
+
);
|
|
4654
|
+
const { closes, closed, waits } = await closesOf(asset, records, proof);
|
|
4655
|
+
const listed = stashes.filter(x => x.kind === 'stash');
|
|
4656
|
+
// The row comes off each of the project's stashes: free, nothing moves, and where it would go is not read.
|
|
4657
|
+
const offs = listed.map(({ id }) =>
|
|
4658
|
+
loot.stashIx({ signer: wallet.address, project, id, asset, out: wallet.address, ...proof.kind }),
|
|
4659
|
+
);
|
|
4660
|
+
const left = [
|
|
4661
|
+
...waits.map(({ kind, address }) => ({ kind, address })),
|
|
4662
|
+
...stashes
|
|
4663
|
+
.filter(x => x.kind === 'lottery-stash')
|
|
4664
|
+
.map(({ kind, address, id }) => ({ kind, address, lottery: id })),
|
|
4665
|
+
];
|
|
4666
|
+
const unstashed = listed.map(({ address, id }) => ({ address, id }));
|
|
4667
|
+
const instructions = [...(closes.length ? closes : offs.length ? proof.before : []), ...offs];
|
|
4668
|
+
if (!instructions.length) return { signature: null, closed, unstashed, waits: left };
|
|
4669
|
+
return { signature: await send(instructions), closed, unstashed, waits: left };
|
|
4670
|
+
},
|
|
4671
|
+
|
|
4080
4672
|
// ---------------------------------------------------------------- raffles
|
|
4081
4673
|
|
|
4082
4674
|
/**
|
|
@@ -4125,9 +4717,20 @@ export function solanaApp({
|
|
|
4125
4717
|
return send([loot.upsertIx({ owner: wallet.address, project, def: { Lottery: { id, args } }, lotteryState })]);
|
|
4126
4718
|
},
|
|
4127
4719
|
|
|
4128
|
-
/**
|
|
4720
|
+
/**
|
|
4721
|
+
* The raffle's owner takes the NFT back out of lottery `id`'s own stash once nothing promises it (the lottery
|
|
4722
|
+
* ended, or no place names it). When the project no longer holds the NFT (its collection took it back or burned
|
|
4723
|
+
* it), only its row comes off the stash, on that proof: nothing is sent, and the owner gets no NFT back. Only the
|
|
4724
|
+
* project's owner or authority may, gone or not: a lottery's own stash is theirs alone (`NotAuthority` for anyone
|
|
4725
|
+
* else; `closeGone` leaves it and says so). Returns the signature.
|
|
4726
|
+
*/
|
|
4129
4727
|
async unraffle(id, nft) {
|
|
4130
|
-
const {
|
|
4728
|
+
const {
|
|
4729
|
+
before,
|
|
4730
|
+
hasAuthority: _,
|
|
4731
|
+
gone: _g,
|
|
4732
|
+
...piece
|
|
4733
|
+
} = await leaving(nft, wallet.address, { to: wallet.address });
|
|
4131
4734
|
const lotteryState = await readMaybe('Lottery', loot.pda.lottery(project, id));
|
|
4132
4735
|
return send([
|
|
4133
4736
|
...before,
|
|
@@ -4413,11 +5016,17 @@ export function solanaApp({
|
|
|
4413
5016
|
|
|
4414
5017
|
/**
|
|
4415
5018
|
* What this player can finish now, themselves: a box to settle or refund, a contest entry to claim, a held
|
|
4416
|
-
* collectible to take home,
|
|
5019
|
+
* collectible to take home, a hold of theirs or a loan they made or took whose NFT the project no longer holds (its
|
|
5020
|
+
* collection took it back or burned it: listed at any time, also while the hold runs, as a `release` flagged
|
|
5021
|
+
* `gone`, whose claim closes the hold and sends nothing), an auction they won (or one off by the clock: their bid back), a lottery place they
|
|
4417
5022
|
* won, a draw that is due (anyone may reveal it, or ask again when the oracle never answered; it takes 0.0008
|
|
4418
5023
|
* SOL), and a drawn lottery's unpaid places, which anyone may pay so the next round can open. [{ kind, says, … }];
|
|
4419
5024
|
* a winner's first claim of a contest of tokens carries `potUnits` and `prizeUnits`, its part as the decision wrote
|
|
4420
5025
|
* it (base units, as text). Pass one to `claim`.
|
|
5026
|
+
* A `pull`, a `contest` or an `auction` whose claim would hand out an NFT the project no longer holds is flagged
|
|
5027
|
+
* `gone` too, with `assets` (those NFTs) and `says`, what the claim does about each in words: a ticket's NFT, a
|
|
5028
|
+
* place's NFT, a contest's entry or a loser's NFT picked is not sent anywhere and its part closes; an auction with
|
|
5029
|
+
* its lot or an NFT of its winning bid gone is called off, or ended without it. Show `says` before the player signs.
|
|
4421
5030
|
*/
|
|
4422
5031
|
async claims() {
|
|
4423
5032
|
const me = wallet.address;
|
|
@@ -4453,8 +5062,8 @@ export function solanaApp({
|
|
|
4453
5062
|
const off = loot.contestOff(contest, time);
|
|
4454
5063
|
if (!(off || contest.verdict === 'Won')) continue;
|
|
4455
5064
|
const list = await client.account(loot.contestEntries(pubkey, contest));
|
|
4456
|
-
const
|
|
4457
|
-
|
|
5065
|
+
const entries = list?.owner === loot.LOOT_PROGRAM ? loot.decodeEntries(list.data) : [];
|
|
5066
|
+
const entry = entries.find(e => e.player === me);
|
|
4458
5067
|
if (!entry) continue;
|
|
4459
5068
|
const won = !off && Number(contest.winner) === Number(entry.side);
|
|
4460
5069
|
const first = won && !(entry.bits & loot.CLAIMED);
|
|
@@ -4477,55 +5086,61 @@ export function solanaApp({
|
|
|
4477
5086
|
: 'Your share of the pot.';
|
|
4478
5087
|
// A winner's part of a contest of tokens is what the decision wrote, whoever claims first (`contestWinnings`).
|
|
4479
5088
|
const winnings = first && !contest.nft ? loot.contestWinnings(contest, entry.amount) : null;
|
|
5089
|
+
// A contest of NFTs: what its claim moves, and which of those the project no longer holds (its collection
|
|
5090
|
+
// took it back or burned it). Such an NFT is not sent anywhere: the claim closes it, and says so here first.
|
|
5091
|
+
const moves = contest.nft ? await contestNfts(contest, entries, entries.indexOf(entry), undefined) : [];
|
|
5092
|
+
const lost = moves.filter(m => m.gone);
|
|
5093
|
+
const mine = lost.find(m => m.nft === entry.nft && entries[m.j] === entry);
|
|
5094
|
+
const there = [
|
|
5095
|
+
moves.some(m => entries[m.j] === entry && !m.gone) ? 'your NFT back' : '',
|
|
5096
|
+
moves.some(m => entries[m.j] !== entry && !m.gone) ? 'your pick of the losers’' : '',
|
|
5097
|
+
]
|
|
5098
|
+
.filter(Boolean)
|
|
5099
|
+
.join(', and ');
|
|
5100
|
+
const goneSays = [
|
|
5101
|
+
off ? 'The contest is off.' : '',
|
|
5102
|
+
there ? `${there[0].toUpperCase()}${there.slice(1)}.` : '',
|
|
5103
|
+
goneContestSays(
|
|
5104
|
+
mine?.nft,
|
|
5105
|
+
lost.filter(m => m !== mine).map(m => m.nft),
|
|
5106
|
+
),
|
|
5107
|
+
]
|
|
5108
|
+
.filter(Boolean)
|
|
5109
|
+
.join(' ');
|
|
4480
5110
|
out.push({
|
|
4481
5111
|
kind: 'contest',
|
|
4482
5112
|
id: contest.id,
|
|
4483
5113
|
side: entry.side,
|
|
4484
|
-
says,
|
|
5114
|
+
...(lost.length ? { gone: true, assets: lost.map(m => m.nft), says: goneSays } : { says }),
|
|
4485
5115
|
...(winnings ? { potUnits: String(winnings.pot), prizeUnits: String(winnings.prize) } : {}),
|
|
4486
5116
|
});
|
|
4487
5117
|
}
|
|
4488
5118
|
// The player's holds; the project's treasury wallet's also those owed to the treasury (a hold whose player is the
|
|
4489
|
-
// project's own address: a dropped stash NFT, a lottery's or an auction's NFT owed to it).
|
|
5119
|
+
// project's own address: a dropped stash NFT, a lottery's or an auction's NFT owed to it). And the loans this
|
|
5120
|
+
// wallet made: one search of the project's holds finds them all.
|
|
4490
5121
|
const g = await projectNow();
|
|
4491
5122
|
const holdsOf = [me, ...(g.treasury === me ? [project] : [])];
|
|
4492
|
-
const holds = (
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
for (const { pubkey,
|
|
4498
|
-
const
|
|
4499
|
-
//
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
hold
|
|
4507
|
-
|
|
4508
|
-
hold
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
);
|
|
4513
|
-
}
|
|
4514
|
-
// A loan this wallet made that is overdue and unpaid: its NFT is the lender's to take.
|
|
4515
|
-
for (const { pubkey, account } of await client.programAccounts(loot.LOOT_PROGRAM, [
|
|
4516
|
-
tag('Hold'),
|
|
4517
|
-
at(1, project),
|
|
4518
|
-
])) {
|
|
4519
|
-
const hold = loot.decode('Hold', account.data);
|
|
4520
|
-
if (hold.debt?.lender === me && time >= BigInt(hold.free_at))
|
|
4521
|
-
out.push(
|
|
4522
|
-
await goneOr(hold.asset, {
|
|
4523
|
-
kind: 'release',
|
|
4524
|
-
asset: hold.asset,
|
|
4525
|
-
hold: pubkey,
|
|
4526
|
-
says: 'A loan you made is overdue and unpaid: take its NFT.',
|
|
4527
|
-
}),
|
|
4528
|
-
);
|
|
5123
|
+
const holds = (await client.programAccounts(loot.LOOT_PROGRAM, [tag('Hold'), at(1, project)]))
|
|
5124
|
+
.map(({ pubkey, account }) => ({ pubkey, hold: loot.decode('Hold', account.data) }))
|
|
5125
|
+
.filter(({ hold }) => holdsOf.includes(hold.player) || hold.debt?.lender === me);
|
|
5126
|
+
// Whether the project still holds each one's NFT: whoever controls its collection can take it back or burn it.
|
|
5127
|
+
const heldOf = await heldEach(holds.map(({ hold }) => hold.asset));
|
|
5128
|
+
for (const { pubkey, hold } of holds) {
|
|
5129
|
+
const claim = { kind: 'release', asset: hold.asset, hold: pubkey };
|
|
5130
|
+
// An NFT proven gone: its hold closes at any time, also while it runs and for a loan before it is due.
|
|
5131
|
+
if (heldOf.get(hold.asset)?.held === false) out.push({ ...claim, gone: true, says: goneHoldSays(hold) });
|
|
5132
|
+
// A repaid loan is a plain hold that is over (no debt, `free_at` 0): its NFT goes home whenever.
|
|
5133
|
+
else if (!hold.debt && holdsOf.includes(hold.player) && time >= BigInt(hold.free_at))
|
|
5134
|
+
out.push({
|
|
5135
|
+
...claim,
|
|
5136
|
+
says:
|
|
5137
|
+
hold.player === project
|
|
5138
|
+
? 'An NFT owed to the treasury: send it there.'
|
|
5139
|
+
: 'Your collectible back: its hold is over.',
|
|
5140
|
+
});
|
|
5141
|
+
// A loan this wallet made that is overdue and unpaid: its NFT is the lender's to take.
|
|
5142
|
+
else if (hold.debt?.lender === me && time >= BigInt(hold.free_at))
|
|
5143
|
+
out.push({ ...claim, says: 'A loan you made is overdue and unpaid: take its NFT.' });
|
|
4529
5144
|
}
|
|
4530
5145
|
// A compressed NFT's proof record this wallet paid for and left open: closing it brings its rent back.
|
|
4531
5146
|
for (const { account } of await client.programAccounts(loot.LOOT_PROGRAM, [tag('Proven')])) {
|
|
@@ -4546,32 +5161,47 @@ export function solanaApp({
|
|
|
4546
5161
|
}
|
|
4547
5162
|
const auctions = spec.auctions ?? [];
|
|
4548
5163
|
const found = await client.accounts(auctions.map(a => auctionAt(a)));
|
|
4549
|
-
auctions.
|
|
5164
|
+
for (const [i, a] of auctions.entries()) {
|
|
4550
5165
|
const x = found[i] && loot.decode('Auction', found[i].data);
|
|
4551
|
-
if (!x || time < loot.auctionEnds(x))
|
|
5166
|
+
if (!x || time < loot.auctionEnds(x)) continue;
|
|
5167
|
+
const [sold, owed] = [BigInt(x.high) > 0n, BigInt(x.proceeds_owed) > 0n];
|
|
5168
|
+
if (!(x.seller === me || (sold && !owed && x.high_bidder === me))) continue;
|
|
5169
|
+
// Its lot, or an NFT of the winning bid, that the project no longer holds (its collection took it back or
|
|
5170
|
+
// burned it): the end does not send it anywhere, and this says what the end does instead.
|
|
5171
|
+
const lot = owed ? null : (x.lot.Asset?.[0] ?? null);
|
|
5172
|
+
const bids = sold && x.nft_bids ? x.held : [];
|
|
5173
|
+
const heldOf = lot || bids.length ? await heldEach([...(lot ? [lot] : []), ...bids]) : new Map();
|
|
5174
|
+
const lostBids = bids.filter(nft => heldOf.get(nft)?.held === false);
|
|
5175
|
+
if ((lot && heldOf.get(lot)?.held === false) || lostBids.length) {
|
|
5176
|
+
const lotGone = lot && heldOf.get(lot)?.held === false;
|
|
5177
|
+
out.push({
|
|
5178
|
+
kind: 'auction',
|
|
5179
|
+
key: a.key,
|
|
5180
|
+
gone: true,
|
|
5181
|
+
assets: [...(lotGone ? [lot] : []), ...lostBids],
|
|
5182
|
+
says: goneSaleSays(a.name, x, me, lotGone ? lot : null, lostBids),
|
|
5183
|
+
});
|
|
5184
|
+
continue;
|
|
5185
|
+
}
|
|
4552
5186
|
// The lot went first (an end too wide for one transaction, or a seller's account that couldn't take the money
|
|
4553
5187
|
// then): only the money is left to pay.
|
|
4554
|
-
if (
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
return;
|
|
4562
|
-
}
|
|
4563
|
-
if (x.high_bidder === me) out.push({ kind: 'auction', key: a.key, says: `You won “${a.name}”: take it.` });
|
|
5188
|
+
if (owed)
|
|
5189
|
+
out.push({
|
|
5190
|
+
kind: 'auction',
|
|
5191
|
+
key: a.key,
|
|
5192
|
+
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).`,
|
|
5193
|
+
});
|
|
5194
|
+
else if (x.high_bidder === me) out.push({ kind: 'auction', key: a.key, says: `You won “${a.name}”: take it.` });
|
|
4564
5195
|
// The seller's own auction, over: ending it pays them (or brings an unsold lot home).
|
|
4565
|
-
else
|
|
5196
|
+
else
|
|
4566
5197
|
out.push({
|
|
4567
5198
|
kind: 'auction',
|
|
4568
5199
|
key: a.key,
|
|
4569
|
-
says:
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
: `Your auction “${a.name}” ended with no bid: take your lot back.`,
|
|
5200
|
+
says: sold
|
|
5201
|
+
? `Your auction “${a.name}” is over: end it, and the winning bid is paid to you.`
|
|
5202
|
+
: `Your auction “${a.name}” ended with no bid: take your lot back.`,
|
|
4573
5203
|
});
|
|
4574
|
-
}
|
|
5204
|
+
}
|
|
4575
5205
|
for (const l of spec.lotteries ?? []) {
|
|
4576
5206
|
// A lottery the page names but nobody has written yet (a raffle still to open) has nothing to finish.
|
|
4577
5207
|
if (!(await client.account(loot.pda.lottery(project, l.id)))) continue;
|
|
@@ -4610,17 +5240,32 @@ export function solanaApp({
|
|
|
4610
5240
|
const parts = e.buyer === me ? loot.partsOwed(r, i) : [];
|
|
4611
5241
|
return parts.length ? [{ entry: i, parts }] : [];
|
|
4612
5242
|
});
|
|
4613
|
-
if (mine.length)
|
|
5243
|
+
if (mine.length) {
|
|
5244
|
+
// An NFT of these parts that the project no longer holds (its collection took it back or burned it) is
|
|
5245
|
+
// not sent anywhere: the claim settles its part without it, and says so here first.
|
|
5246
|
+
const { tickets, prizes } = await roundNfts(r, mine);
|
|
5247
|
+
const heldOf = tickets.length + prizes.length ? await heldEach([...tickets, ...prizes]) : new Map();
|
|
5248
|
+
const lost = list => [...new Set(list)].filter(nft => heldOf.get(nft)?.held === false);
|
|
5249
|
+
const [lostTickets, lostPrizes] = [lost(tickets), lost(prizes)];
|
|
5250
|
+
const back = r.status === 'Cancelled' || Boolean(r.terms.returns);
|
|
5251
|
+
const owed =
|
|
5252
|
+
r.status === 'Cancelled'
|
|
5253
|
+
? `Recover your ticket payment for “${l.name}”.`
|
|
5254
|
+
: `Take what you are owed in “${l.name}”.`;
|
|
4614
5255
|
out.push({
|
|
4615
5256
|
kind: 'pull',
|
|
4616
5257
|
key: l.key,
|
|
4617
5258
|
round,
|
|
4618
5259
|
pulls: mine,
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
5260
|
+
...(lostTickets.length + lostPrizes.length
|
|
5261
|
+
? {
|
|
5262
|
+
gone: true,
|
|
5263
|
+
assets: [...lostTickets, ...lostPrizes],
|
|
5264
|
+
says: `${owed} ${goneRoundSays(lostTickets, lostPrizes, back)}`,
|
|
5265
|
+
}
|
|
5266
|
+
: { says: owed }),
|
|
4623
5267
|
});
|
|
5268
|
+
}
|
|
4624
5269
|
// Anyone may pay everyone else what they are owed, and the parts owed in common (about 0.002 SOL of account
|
|
4625
5270
|
// rent for each payee who has no account for it yet, which stays with them): the last pull closes the round,
|
|
4626
5271
|
// and its rent goes back to whoever fronted it (the lottery, or its first buyer).
|
|
@@ -4722,9 +5367,18 @@ export function solanaApp({
|
|
|
4722
5367
|
},
|
|
4723
5368
|
|
|
4724
5369
|
/**
|
|
4725
|
-
* Finishes one of `claims()`, signed by this player. Returns the signature. A
|
|
4726
|
-
*
|
|
4727
|
-
*
|
|
5370
|
+
* Finishes one of `claims()`, signed by this player. Returns the signature. A `release` flagged `gone` closes its
|
|
5371
|
+
* hold on the proof that the project no longer holds the NFT: nothing is sent, the hold's rent goes back to whoever
|
|
5372
|
+
* paid it, and a loan's debt ends unpaid. A winner of a contest of NFTs picks
|
|
5373
|
+
* which loser's NFT they take (`{ take: asset }`, the NFT of a losing entry that still holds one; by default the
|
|
5374
|
+
* first found that the project still holds): only the winner can, while any are left.
|
|
5375
|
+
* An NFT a lottery's round or a contest owes that the project no longer holds (its collection took it back or
|
|
5376
|
+
* burned it) is not sent anywhere, and the claim still finishes. A ticket whose price sends it on (burned, given,
|
|
5377
|
+
* stashed) settles on the proof with nothing moved. A ticket going back to its buyer, a place's NFT and a
|
|
5378
|
+
* contest's NFT are only handed out by the program, which reads nothing first: the claim has it make a hold of
|
|
5379
|
+
* that NFT (`["hold", nft, 0]`) and closes that hold on the proof right after, in the same transaction when it
|
|
5380
|
+
* fits. The signer pays that hold's rent and gets it back at once. The program's receipts for it are those of a
|
|
5381
|
+
* hold made (`Held`, and `PrizeGiven` for a place's prize) and then `NftGone`: no `Released`.
|
|
4728
5382
|
*/
|
|
4729
5383
|
async claim(c, { take, build = false, ...others } = NO_OPTIONS) {
|
|
4730
5384
|
only(others, ['take', 'build'], 'claim');
|
|
@@ -4755,37 +5409,25 @@ export function solanaApp({
|
|
|
4755
5409
|
// lets it pick (`take`: one asset, or several; the first left by default), as many as fit.
|
|
4756
5410
|
const nfts = [];
|
|
4757
5411
|
const kinds = {};
|
|
5412
|
+
// The NFTs of this claim that are proven gone from the project, each with what shows it and who staked it.
|
|
5413
|
+
// The contest only hands an NFT out and reads nothing, so each is passed as its hold (`["hold", nft, 0]`, made
|
|
5414
|
+
// for this player, who gets it), and that hold closes on the proof right after the claim: nothing is sent.
|
|
5415
|
+
const lost = new Map();
|
|
4758
5416
|
if (contest.nft) {
|
|
4759
5417
|
const proofs = {};
|
|
4760
|
-
const
|
|
4761
|
-
const { before, hasAuthority: _, ...kind } = await
|
|
4762
|
-
proofs[
|
|
4763
|
-
kinds[
|
|
4764
|
-
|
|
4765
|
-
if (Number(own.amount) === 1) {
|
|
4766
|
-
nfts.push(i);
|
|
4767
|
-
await kindOf(own.nft, me);
|
|
4768
|
-
}
|
|
4769
|
-
const share = won && !(own.bits & loot.ENDED) ? loot.shareOf(contest, own) - Number(own.taken) : 0;
|
|
4770
|
-
const left = entries
|
|
4771
|
-
.map((e, j) => ({ e, j }))
|
|
4772
|
-
.filter(({ e }) => Number(e.side) !== Number(contest.winner) && Number(e.amount) === 1);
|
|
4773
|
-
const wanted = take ? [take].flat() : [];
|
|
4774
|
-
const picked = (wanted.length ? left.filter(({ e }) => wanted.includes(e.nft)) : left).slice(
|
|
4775
|
-
0,
|
|
4776
|
-
Math.min(share, 4),
|
|
4777
|
-
);
|
|
4778
|
-
if (wanted.length && picked.length < Math.min(wanted.length, share))
|
|
4779
|
-
throw new Error(`${wanted.join(', ')} is not a loser’s NFT left in this contest for you to pick.`);
|
|
4780
|
-
for (const { e, j } of picked) {
|
|
5418
|
+
for (const { j, nft } of await contestNfts(contest, entries, i, take)) {
|
|
5419
|
+
const { before, hasAuthority: _, gone, ...kind } = await leaving(nft, me, { to: me });
|
|
5420
|
+
proofs[nft] = before;
|
|
5421
|
+
kinds[nft] = kind;
|
|
5422
|
+
if (gone) lost.set(nft, { kind, player: me, rentTo: entries[j].player });
|
|
4781
5423
|
nfts.push(j);
|
|
4782
|
-
await kindOf(e.nft, me);
|
|
4783
5424
|
}
|
|
4784
5425
|
// A compressed NFT's proof is long (up to 17 nodes): such a claim moves one NFT, as a v0 transaction with a
|
|
4785
5426
|
// lookup table (`send`), and the next claim takes the next.
|
|
4786
5427
|
if (nfts.length > 1 && nfts.some(j => kinds[entries[j].nft]?.compressed)) nfts.splice(1);
|
|
4787
5428
|
for (const j of nfts) given.before.push(...proofs[entries[j].nft]);
|
|
4788
5429
|
}
|
|
5430
|
+
const closes = nfts.map(j => entries[j].nft).filter(nft => lost.has(nft));
|
|
4789
5431
|
const claimed = [
|
|
4790
5432
|
computeUnitLimit(800_000),
|
|
4791
5433
|
...given.before.filter(x => x.programId !== COMPUTE_BUDGET_PROGRAM),
|
|
@@ -4799,19 +5441,37 @@ export function solanaApp({
|
|
|
4799
5441
|
player: me,
|
|
4800
5442
|
nfts: contest.nft ? nfts : undefined,
|
|
4801
5443
|
kinds,
|
|
5444
|
+
hold: closes,
|
|
4802
5445
|
collection: given.collection,
|
|
4803
5446
|
poolMints: given.poolMints,
|
|
4804
5447
|
items: given.items,
|
|
4805
5448
|
nft: given.nft,
|
|
4806
5449
|
programs,
|
|
4807
5450
|
}),
|
|
5451
|
+
...closes.map(nft => releaseOwed(nft, lost.get(nft).kind, lost.get(nft), me, g)),
|
|
4808
5452
|
];
|
|
4809
5453
|
// A prize that rolls: asked for in the claim's own transaction and settled by a key after (one prompt).
|
|
4810
5454
|
return contest.prize?.Roll && !build ? (await sendRolling(claimed)).signature : send(claimed);
|
|
4811
5455
|
}
|
|
4812
5456
|
if (c.kind === 'release') {
|
|
4813
|
-
if (c.gone) throw new Error(c.says);
|
|
4814
5457
|
const hold = await read('Hold', c.hold);
|
|
5458
|
+
// Its NFT proven gone from the project: the hold closes on that proof, at any time, and nothing is sent. Its
|
|
5459
|
+
// rent goes back to whoever paid it; a loan's debt ends unpaid.
|
|
5460
|
+
const proof = await goneProof(c.asset, me);
|
|
5461
|
+
if (proof)
|
|
5462
|
+
return send([
|
|
5463
|
+
...proof.before,
|
|
5464
|
+
loot.releaseIx({
|
|
5465
|
+
signer: me,
|
|
5466
|
+
project,
|
|
5467
|
+
asset: c.asset,
|
|
5468
|
+
...proof.kind,
|
|
5469
|
+
hold,
|
|
5470
|
+
address: c.hold,
|
|
5471
|
+
treasury: g.treasury,
|
|
5472
|
+
...(hold.debt ? { to: hold.debt.lender } : {}),
|
|
5473
|
+
}),
|
|
5474
|
+
]);
|
|
4815
5475
|
const { before, hasAuthority: _, ...nft } = await nftOf(c.asset, me, { to: me });
|
|
4816
5476
|
// An overdue loan's NFT goes to its lender.
|
|
4817
5477
|
return send([
|