@xgamesfun/solana 0.26.4 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +519 -193
- 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 +6 -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 +214 -129
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,7 +402,50 @@ 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 };
|
|
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.
|
|
425
|
+
*/
|
|
426
|
+
async function goneProof(asset, signer = wallet.address, { holder = null } = {}) {
|
|
427
|
+
const 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
|
+
*/
|
|
443
|
+
async function leaving(asset, signer = wallet.address, { to, payer = signer } = {}) {
|
|
444
|
+
const proof = await goneProof(asset, signer);
|
|
445
|
+
if (!proof) return { ...(await nftOf(asset, signer, { to, payer })), gone: false };
|
|
446
|
+
// A Core asset shows it by itself, with the accounts it always took.
|
|
447
|
+
if (!proof.kind.tokenMetadata && !proof.kind.compressed) return { ...(await nftOf(asset, signer)), gone: true };
|
|
448
|
+
return { before: proof.before, hasAuthority: false, ...proof.kind, gone: true };
|
|
402
449
|
}
|
|
403
450
|
/**
|
|
404
451
|
* 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
|
|
@@ -1066,7 +1113,10 @@ export function solanaApp({
|
|
|
1066
1113
|
/**
|
|
1067
1114
|
* Waits for an order's roll (the oracle answers a moment after the open), then settles it. With `burner` (the
|
|
1068
1115
|
* 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.
|
|
1116
|
+
* The order names it (`settler`), so it may settle; when the player or the project did first, the burner only gives its lamports back.
|
|
1117
|
+
* An NFT of the order the project no longer holds (its collection took it back or burned it) is passed over: a
|
|
1118
|
+
* stash's NFT won is not given (its place in `assets` is null) and the NFT rolled on is not sent back; `gone` lists
|
|
1119
|
+
* them, with `says`. `putBack`: NFTs
|
|
1070
1120
|
* won that the player puts back into their stash in place of taking them (one whose collection now refuses to let it
|
|
1071
1121
|
* move); `giveUp`: the places in the order's `won` of tokens or collectibles the player gives up because they can't
|
|
1072
1122
|
* receive them (an account the token's issuer froze, a collection that refuses the mint): tokens given up stay in the
|
|
@@ -1191,6 +1241,9 @@ export function solanaApp({
|
|
|
1191
1241
|
};
|
|
1192
1242
|
};
|
|
1193
1243
|
const holders = await holdersOf(order.shares);
|
|
1244
|
+
// NFTs of this order the project no longer holds (their collection took them back or burned them): the settle
|
|
1245
|
+
// passes each over on that proof. A stash's NFT won is then not given; the NFT rolled on is not sent back.
|
|
1246
|
+
const lost = new Set();
|
|
1194
1247
|
// Given by its fate to a wallet that is an account of the loot program, the NFT goes back to the player.
|
|
1195
1248
|
const gives = order.held ? order.fate?.then?.Give?.[0] : undefined;
|
|
1196
1249
|
const handBack = gives !== undefined && loot.handsBack(await client.account(gives));
|
|
@@ -1207,15 +1260,25 @@ export function solanaApp({
|
|
|
1207
1260
|
const {
|
|
1208
1261
|
before,
|
|
1209
1262
|
hasAuthority: _,
|
|
1263
|
+
gone,
|
|
1210
1264
|
...kind
|
|
1211
|
-
} = await
|
|
1265
|
+
} = await leaving(won.Nft.nft, signer, { to: order.player, payer: wallet.address });
|
|
1266
|
+
if (gone) lost.add(won.Nft.nft);
|
|
1212
1267
|
nfts[won.Nft.nft] = kind;
|
|
1213
1268
|
proving.push(...before);
|
|
1214
1269
|
}
|
|
1215
1270
|
}
|
|
1216
|
-
// The NFT the roll was on: its kind, and for a compressed one a proof first.
|
|
1217
|
-
const {
|
|
1218
|
-
|
|
1271
|
+
// The NFT the roll was on: its kind, and for a compressed one a proof first. One the order holds may be gone.
|
|
1272
|
+
const {
|
|
1273
|
+
before: subjectProof,
|
|
1274
|
+
gone: subjectGone,
|
|
1275
|
+
...subject
|
|
1276
|
+
} = order.subject && order.subject !== NOBODY
|
|
1277
|
+
? order.held
|
|
1278
|
+
? await leaving(order.subject, signer)
|
|
1279
|
+
: await nftOf(order.subject, signer)
|
|
1280
|
+
: { before: [] };
|
|
1281
|
+
if (subjectGone) lost.add(order.subject);
|
|
1219
1282
|
const ix = loot.settleIx({
|
|
1220
1283
|
signer,
|
|
1221
1284
|
orderAddress,
|
|
@@ -1296,7 +1359,7 @@ export function solanaApp({
|
|
|
1296
1359
|
if (burner && !keepBurner && !emptied) await emptyKey(burner);
|
|
1297
1360
|
else if (burner && emptied) keys?.forget(burner.name);
|
|
1298
1361
|
const given = new Set(giveUp.map(Number));
|
|
1299
|
-
const assets = wonAssets(order).map((a, k) => (putBack.includes(a) || given.has(k) ? null : a));
|
|
1362
|
+
const assets = wonAssets(order).map((a, k) => (putBack.includes(a) || given.has(k) || lost.has(a) ? null : a));
|
|
1300
1363
|
return {
|
|
1301
1364
|
order: orderAddress,
|
|
1302
1365
|
won: order.won,
|
|
@@ -1304,6 +1367,13 @@ export function solanaApp({
|
|
|
1304
1367
|
settled: signature,
|
|
1305
1368
|
...(signatures.length ? { parts: [...signatures, signature] } : {}),
|
|
1306
1369
|
...(assets.find(Boolean) ? { asset: assets.find(Boolean) } : {}),
|
|
1370
|
+
// What the settle passed over: not given (a stash's NFT won), not sent back (the NFT rolled on).
|
|
1371
|
+
...(lost.size
|
|
1372
|
+
? {
|
|
1373
|
+
gone: [...lost],
|
|
1374
|
+
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.',
|
|
1375
|
+
}
|
|
1376
|
+
: {}),
|
|
1307
1377
|
};
|
|
1308
1378
|
}
|
|
1309
1379
|
|
|
@@ -1595,7 +1665,8 @@ export function solanaApp({
|
|
|
1595
1665
|
// A box that rolls on an NFT takes the one the player brings (`on`), whatever its kind.
|
|
1596
1666
|
if (box.subject && !on) throw new Error('This box rolls on an NFT: say which (on).');
|
|
1597
1667
|
// A box that burns it, gives it away or moves its numbers takes it into the project's hands until the roll settles.
|
|
1598
|
-
|
|
1668
|
+
// What an earlier owner of it left in the project is cleared first (`clearLeftovers`).
|
|
1669
|
+
const clears = box.subject && loot.boxHolds(box) ? await clearLeftovers(on, acceptLeftover) : [];
|
|
1599
1670
|
const brought = box.subject ? await nftOf(on) : { before: [] };
|
|
1600
1671
|
const { before: proving, hasAuthority, ...kind } = brought;
|
|
1601
1672
|
const subject = box.subject
|
|
@@ -1713,7 +1784,7 @@ export function solanaApp({
|
|
|
1713
1784
|
x.keys[0]?.pubkey === associatedTokenAddress(wallet.address, NATIVE_MINT)
|
|
1714
1785
|
),
|
|
1715
1786
|
);
|
|
1716
|
-
const signature = await send(instructions, [], { keepWrapped: solPrize });
|
|
1787
|
+
const signature = await send(await closesFirst(clears, instructions), [], { keepWrapped: solPrize });
|
|
1717
1788
|
// The slot it landed in: the order is read from a node that has seen it (a load-balanced RPC's node lagging
|
|
1718
1789
|
// behind answers nothing for an order just opened).
|
|
1719
1790
|
const landed = (await client.signatureStatus?.(signature).catch(() => null))?.slot ?? null;
|
|
@@ -1751,6 +1822,17 @@ export function solanaApp({
|
|
|
1751
1822
|
);
|
|
1752
1823
|
}
|
|
1753
1824
|
|
|
1825
|
+
/**
|
|
1826
|
+
* Refuses, in words, to buy or rent the listing of an NFT the project no longer holds (whoever controls its
|
|
1827
|
+
* collection took it back or burned it): nothing is there. Sent all the same, the take would only close the listing.
|
|
1828
|
+
*/
|
|
1829
|
+
async function listedStill(asset) {
|
|
1830
|
+
if ((await heldNow(asset)).held === false)
|
|
1831
|
+
throw new Error(
|
|
1832
|
+
`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.`,
|
|
1833
|
+
);
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1754
1836
|
/** A box bought at once when it needs no roll (`buyBox`: a shop item, a bundle, a fixed pack of any count), or
|
|
1755
1837
|
* opened and collected: one that rolls, gives a fresh item (its traits are rolled), or rolls on an NFT. A fixed pack
|
|
1756
1838
|
* too big for one transaction is opened instead (the oracle answers it like any box). */
|
|
@@ -2084,52 +2166,68 @@ export function solanaApp({
|
|
|
2084
2166
|
return { signature, releases };
|
|
2085
2167
|
}
|
|
2086
2168
|
|
|
2087
|
-
/**
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
if ((await heldNow(asset)).held !== false) return claim;
|
|
2094
|
-
return {
|
|
2095
|
-
...claim,
|
|
2096
|
-
gone: true,
|
|
2097
|
-
says: 'The project no longer holds this NFT: whoever controls its collection took it back or burned it while it was held. Its hold says otherwise, but nothing can be sent, and nothing was paid against it.',
|
|
2098
|
-
};
|
|
2099
|
-
}
|
|
2169
|
+
/** What closing a hold whose NFT is gone does, in words, for `claims()`. */
|
|
2170
|
+
const goneHoldSays = hold =>
|
|
2171
|
+
`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.' : ''}`;
|
|
2172
|
+
/** The same for a listing. */
|
|
2173
|
+
const GONE_LISTING =
|
|
2174
|
+
'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.';
|
|
2100
2175
|
|
|
2101
2176
|
/**
|
|
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.
|
|
2177
|
+
* This project's records of `asset`: its listing at `["listing", project, asset]` (one read) and its holds (one
|
|
2178
|
+
* search, by the asset's address). Each is { kind: 'listing' | 'hold', address, record }.
|
|
2109
2179
|
*/
|
|
2110
|
-
async function
|
|
2111
|
-
if (ours(await collectionOf(asset))) return [];
|
|
2112
|
-
const me = wallet.address;
|
|
2180
|
+
async function recordsOf(asset) {
|
|
2113
2181
|
const out = [];
|
|
2114
2182
|
const listingAt = loot.pda.listing(project, asset);
|
|
2115
2183
|
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
|
-
}
|
|
2184
|
+
if (listing) out.push({ kind: 'listing', address: listingAt, record: listing });
|
|
2124
2185
|
const tag = { memcmp: { offset: 0, bytes: encodeBase58(Uint8Array.of(loot.TAGS.Hold)) } };
|
|
2125
2186
|
const holds = await client.programAccounts(loot.LOOT_PROGRAM, [
|
|
2126
2187
|
tag,
|
|
2127
2188
|
{ memcmp: { offset: 1, bytes: project } },
|
|
2128
2189
|
{ memcmp: { offset: 33, bytes: asset } },
|
|
2129
2190
|
]);
|
|
2191
|
+
for (const { pubkey, account } of holds)
|
|
2192
|
+
out.push({ kind: 'hold', address: pubkey, record: loot.decode('Hold', account.data) });
|
|
2193
|
+
return out;
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
/**
|
|
2197
|
+
* The records of this project an earlier owner of `asset` left behind (`recordsOf`), each that names a wallet other
|
|
2198
|
+
* than the player's: { kind, address, record, says }. Such a record is left when whoever controls the NFT's
|
|
2199
|
+
* collection moved it out of the project's hands without the program. Until it is closed, the program hands out
|
|
2200
|
+
* whatever the project holds at that address: once this player's deposit puts the NFT back in the project's hands, the
|
|
2201
|
+
* record can send it to the wallet it names. `own`: the player's own listing of it counts too (a route that lists:
|
|
2202
|
+
* the program takes no second listing of an NFT while one is open). NFTs of the project's own collections are not
|
|
2203
|
+
* read: those collections refuse the permanent delegates that take an NFT out.
|
|
2204
|
+
*/
|
|
2205
|
+
async function leftovers(asset, { own = false } = {}) {
|
|
2206
|
+
if (ours(await collectionOf(asset))) return [];
|
|
2207
|
+
const me = wallet.address;
|
|
2208
|
+
const out = [];
|
|
2130
2209
|
const time = BigInt(await chainNow());
|
|
2131
|
-
|
|
2132
|
-
|
|
2210
|
+
const iso = at => new Date(Number(at) * 1000).toISOString();
|
|
2211
|
+
for (const r of await recordsOf(asset)) {
|
|
2212
|
+
if (r.kind === 'listing') {
|
|
2213
|
+
const listing = r.record;
|
|
2214
|
+
const mine = listing.maker === me;
|
|
2215
|
+
if (mine && !own) continue;
|
|
2216
|
+
const sale = Boolean(listing.deal.Sell);
|
|
2217
|
+
// A running rental is kept: nobody but its maker can even stop the listing until it has run.
|
|
2218
|
+
const rented =
|
|
2219
|
+
time < BigInt(listing.until)
|
|
2220
|
+
? ` 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).`
|
|
2221
|
+
: '';
|
|
2222
|
+
out.push({
|
|
2223
|
+
...r,
|
|
2224
|
+
says: mine
|
|
2225
|
+
? `Your own earlier listing of this NFT (${r.address}) is still open in this project.${rented}`
|
|
2226
|
+
: `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}`,
|
|
2227
|
+
});
|
|
2228
|
+
continue;
|
|
2229
|
+
}
|
|
2230
|
+
const hold = r.record;
|
|
2133
2231
|
const to = hold.player === project ? 'the project’s treasury' : hold.player;
|
|
2134
2232
|
const names = [hold.player, hold.debt?.lender].filter(Boolean);
|
|
2135
2233
|
if (names.every(x => x === me)) continue;
|
|
@@ -2140,31 +2238,137 @@ export function solanaApp({
|
|
|
2140
2238
|
: `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
2239
|
: due
|
|
2142
2240
|
? `a hold that is over: anyone can send the NFT to ${to} at once`
|
|
2143
|
-
: `a hold that runs until ${
|
|
2241
|
+
: `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
2242
|
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.`,
|
|
2243
|
+
...r,
|
|
2244
|
+
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
2245
|
});
|
|
2149
2246
|
}
|
|
2150
2247
|
return out;
|
|
2151
2248
|
}
|
|
2152
2249
|
|
|
2153
2250
|
/**
|
|
2154
|
-
*
|
|
2155
|
-
*
|
|
2156
|
-
*
|
|
2251
|
+
* The instructions that close `records` of `asset` (`recordsOf`) on the proof that the project no longer holds it
|
|
2252
|
+
* (`proof`, from `goneProof`), as `signer` sends them: a hold by `release`, a listing by `take_listing`. Anyone may
|
|
2253
|
+
* send either, at any time; nothing is sent or paid, and no project fee is taken. A listing whose rental still runs
|
|
2254
|
+
* can't be closed (`Rented`): the program keeps a running rental, and the listing closes once it has run.
|
|
2255
|
+
* Returns { closes, closed: [{ kind, address }], waits: the records left }.
|
|
2157
2256
|
*/
|
|
2158
|
-
async function
|
|
2159
|
-
const
|
|
2160
|
-
|
|
2257
|
+
async function closesOf(asset, records, proof, signer = wallet.address) {
|
|
2258
|
+
const g = await projectNow();
|
|
2259
|
+
const time = BigInt(await chainNow());
|
|
2260
|
+
const closes = [];
|
|
2261
|
+
const closed = [];
|
|
2262
|
+
const waits = [];
|
|
2263
|
+
for (const r of records) {
|
|
2264
|
+
if (r.kind === 'listing') {
|
|
2265
|
+
if (time < BigInt(r.record.until)) {
|
|
2266
|
+
waits.push(r);
|
|
2267
|
+
continue;
|
|
2268
|
+
}
|
|
2269
|
+
closes.push(
|
|
2270
|
+
loot.takeListingIx({
|
|
2271
|
+
taker: signer,
|
|
2272
|
+
project,
|
|
2273
|
+
projectState: g,
|
|
2274
|
+
listing: r.record,
|
|
2275
|
+
holders: await holdersOf(r.record.shares ?? []),
|
|
2276
|
+
programs,
|
|
2277
|
+
...proof.kind,
|
|
2278
|
+
}),
|
|
2279
|
+
);
|
|
2280
|
+
} else {
|
|
2281
|
+
const hold = r.record;
|
|
2282
|
+
closes.push(
|
|
2283
|
+
loot.releaseIx({
|
|
2284
|
+
signer,
|
|
2285
|
+
project,
|
|
2286
|
+
asset,
|
|
2287
|
+
...proof.kind,
|
|
2288
|
+
hold,
|
|
2289
|
+
address: r.address,
|
|
2290
|
+
treasury: g.treasury,
|
|
2291
|
+
...(hold.debt ? { to: hold.debt.lender } : {}),
|
|
2292
|
+
}),
|
|
2293
|
+
);
|
|
2294
|
+
}
|
|
2295
|
+
closed.push({ kind: r.kind, address: r.address });
|
|
2296
|
+
}
|
|
2297
|
+
return { closes: closes.length ? [...proof.before, ...closes] : [], closed, waits };
|
|
2298
|
+
}
|
|
2299
|
+
|
|
2300
|
+
/**
|
|
2301
|
+
* The deposit guard. Before the player hands `asset` to the project, what an earlier owner left of it there
|
|
2302
|
+
* (`leftovers`) is cleared: the player holds the NFT, so it is provably gone from the project, and each leftover hold
|
|
2303
|
+
* closes by `release` on that proof and a leftover listing by `take_listing` on it (anyone may, at any time, and
|
|
2304
|
+
* nothing is paid). Returns those instructions, tried in a dry run, to go in front of the deposit (`closesFirst`);
|
|
2305
|
+
* none when nothing was left. The closes cost the player their transaction fee only: each record's rent goes to
|
|
2306
|
+
* whoever that record names (a hold's payer, a listing's maker), never to the player.
|
|
2307
|
+
* What can't be closed still refuses the deposit, in words, with nothing sent: a listing whose rental still runs
|
|
2308
|
+
* (the program keeps it until the rental has run), or a record of an NFT nothing shows gone. `accept` true goes
|
|
2309
|
+
* ahead all the same (the closes that can be made still go first). `listing`: the route is a listing, which the
|
|
2310
|
+
* program refuses outright while a listing of the NFT is open (its address is taken), whatever the option says.
|
|
2311
|
+
*/
|
|
2312
|
+
async function clearLeftovers(asset, accept, { listing = false } = {}) {
|
|
2313
|
+
const found = await leftovers(asset, { own: listing });
|
|
2314
|
+
if (!found.length) return [];
|
|
2315
|
+
const proof = await goneProof(asset, wallet.address, { holder: wallet.address });
|
|
2316
|
+
let { closes, waits } = proof ? await closesOf(asset, found, proof) : { closes: [], waits: found };
|
|
2317
|
+
let why = '';
|
|
2318
|
+
if (closes.length) {
|
|
2319
|
+
const trial = [computeUnitLimit(1_400_000), ...closes.filter(x => x.programId !== COMPUTE_BUDGET_PROGRAM)];
|
|
2320
|
+
// Tried in a dry run when they are one transaction as they stand; a longer list is made to fit as it is sent.
|
|
2321
|
+
const one = compileFitting({
|
|
2322
|
+
payer: wallet.address,
|
|
2323
|
+
blockhash: NOBODY,
|
|
2324
|
+
instructions: trial,
|
|
2325
|
+
tables: await standing(),
|
|
2326
|
+
});
|
|
2327
|
+
const tried = one ? await dryRun(wallet.address, trial) : { ok: true };
|
|
2328
|
+
if (!tried.ok) {
|
|
2329
|
+
why = tried.error ? ` Closing it was refused in a dry run: ${tried.error.msg} (${tried.error.name}).` : '';
|
|
2330
|
+
[closes, waits] = [[], found];
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
const open = waits.find(x => x.kind === 'listing');
|
|
2334
|
+
if (listing && open)
|
|
2161
2335
|
throw new Error(
|
|
2162
|
-
`${
|
|
2336
|
+
`${open.says}${why} The program refuses another listing of this NFT while that one is open (its address is taken): nothing was sent.`,
|
|
2163
2337
|
);
|
|
2164
|
-
if (
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2338
|
+
if (waits.length && accept !== true)
|
|
2339
|
+
throw new Error(
|
|
2340
|
+
`${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.`,
|
|
2341
|
+
);
|
|
2342
|
+
return closes;
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
/**
|
|
2346
|
+
* A deposit's `instructions` with `closes` (`clearLeftovers`) in front of them: in the same transaction when both
|
|
2347
|
+
* fit one as the wallet sends it (a legacy one, or one with the project's standing tables); when they do not, the
|
|
2348
|
+
* closes are sent first in a transaction of their own, and the deposit's instructions come back alone.
|
|
2349
|
+
*/
|
|
2350
|
+
async function closesFirst(closes, instructions) {
|
|
2351
|
+
if (!closes.length) return instructions;
|
|
2352
|
+
const isLimit = ix => ix.programId === COMPUTE_BUDGET_PROGRAM && ix.data[0] === 2;
|
|
2353
|
+
// One proof of a compressed NFT serves both: the closes read its record first, then the deposit moves it.
|
|
2354
|
+
const proven = new Set(closes.filter(isProveLeaf).map(ix => ix.keys[1].pubkey));
|
|
2355
|
+
const together = [
|
|
2356
|
+
computeUnitLimit(1_400_000),
|
|
2357
|
+
...closes.filter(ix => !isLimit(ix)),
|
|
2358
|
+
...instructions.filter(ix => !isLimit(ix) && !(isProveLeaf(ix) && proven.has(ix.keys[1].pubkey))),
|
|
2359
|
+
];
|
|
2360
|
+
const fits =
|
|
2361
|
+
Boolean(
|
|
2362
|
+
compileFitting({
|
|
2363
|
+
payer: wallet.address,
|
|
2364
|
+
blockhash: NOBODY,
|
|
2365
|
+
instructions: loot.withLeafCloses(together),
|
|
2366
|
+
tables: await standing(),
|
|
2367
|
+
}),
|
|
2368
|
+
) && transactionLocks(wallet.address, together) <= 64;
|
|
2369
|
+
if (fits) return together;
|
|
2370
|
+
await send(closes);
|
|
2371
|
+
return instructions;
|
|
2168
2372
|
}
|
|
2169
2373
|
|
|
2170
2374
|
/** Who holds each collectible a split's holder shares name now: { asset: holder }. */
|
|
@@ -2596,15 +2800,18 @@ export function solanaApp({
|
|
|
2596
2800
|
return out;
|
|
2597
2801
|
},
|
|
2598
2802
|
|
|
2599
|
-
/** Gives an order's payment back once its time has come (a roll that failed or never came).
|
|
2803
|
+
/** Gives an order's payment back once its time has come (a roll that failed or never came). An NFT the project
|
|
2804
|
+
* held for the roll goes back with it; one it no longer holds (its collection took it back or burned it) is not
|
|
2805
|
+
* looked for, and the whole payment goes back all the same. */
|
|
2600
2806
|
async refund(orderAddress) {
|
|
2601
2807
|
const order = await read('Order', orderAddress);
|
|
2602
2808
|
// An NFT the project held for the roll goes back.
|
|
2603
2809
|
const {
|
|
2604
2810
|
before = [],
|
|
2605
2811
|
hasAuthority: _,
|
|
2812
|
+
gone: _gone,
|
|
2606
2813
|
...subject
|
|
2607
|
-
} = order.held ? await
|
|
2814
|
+
} = order.held ? await leaving(order.subject, wallet.address, { to: order.player }) : {};
|
|
2608
2815
|
return send([...before, loot.refundIx({ signer: wallet.address, orderAddress, order, subject, programs })]);
|
|
2609
2816
|
},
|
|
2610
2817
|
|
|
@@ -2839,12 +3046,13 @@ export function solanaApp({
|
|
|
2839
3046
|
shown ??= await passing(lottery.gate, 'buy a ticket');
|
|
2840
3047
|
listProof = listProofOf(lottery.gate, l.gate?.list, listProof);
|
|
2841
3048
|
let nft;
|
|
3049
|
+
let clears = [];
|
|
2842
3050
|
const before = [];
|
|
2843
3051
|
if (terms.nft_ticket) {
|
|
2844
3052
|
if (!paidWith) throw new Error(`A ticket of “${l.name}” costs an NFT: say which (nft).`);
|
|
2845
3053
|
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
|
|
3054
|
+
// The project holds an NFT ticket until the round is drawn: what an earlier owner left of it goes first.
|
|
3055
|
+
clears = await clearLeftovers(paidWith, acceptLeftover);
|
|
2848
3056
|
const { before: proof, ...kind } = await nftOf(paidWith);
|
|
2849
3057
|
nft = { asset: paidWith, ...kind };
|
|
2850
3058
|
before.push(...proof);
|
|
@@ -2914,9 +3122,12 @@ export function solanaApp({
|
|
|
2914
3122
|
}
|
|
2915
3123
|
const buy = [...before, ...withSolWrap(wallet.address, mint, BigInt(terms.price) * BigInt(count), [ix])];
|
|
2916
3124
|
return send(
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
3125
|
+
await closesFirst(
|
|
3126
|
+
clears,
|
|
3127
|
+
draw.length
|
|
3128
|
+
? [computeUnitLimit(800_000), ...draw, ...buy.filter(x => x.programId !== COMPUTE_BUDGET_PROGRAM)]
|
|
3129
|
+
: buy,
|
|
3130
|
+
),
|
|
2920
3131
|
);
|
|
2921
3132
|
},
|
|
2922
3133
|
|
|
@@ -3012,9 +3223,9 @@ export function solanaApp({
|
|
|
3012
3223
|
only(others, ['extra', 'transfers', 'acceptLeftover'], 'stakeNft');
|
|
3013
3224
|
const f = part('farms', farmKey);
|
|
3014
3225
|
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
|
-
|
|
3226
|
+
// A farm that keeps its NFTs in escrow hands this one to the project (what an earlier owner left of it there is
|
|
3227
|
+
// cleared first); one that freezes or watches leaves it in the wallet, where no record of the project's can move it.
|
|
3228
|
+
const clears = farm.custody === 'Escrow' ? await clearLeftovers(asset, acceptLeftover) : [];
|
|
3018
3229
|
await openFor(f, farm, tier);
|
|
3019
3230
|
const token = await farmTokens([farm.rewards_mint], [], { extra, transfers });
|
|
3020
3231
|
const { before, ...nft } = await nftOf(asset);
|
|
@@ -3036,11 +3247,24 @@ export function solanaApp({
|
|
|
3036
3247
|
// prize and as a forfeit, is sized before it goes in, and an NFT that could never come back out isn't staked.
|
|
3037
3248
|
if (!farm.farmed.Tokens) await exitFits(f, farm, asset, nft, before, tier, token);
|
|
3038
3249
|
return send(
|
|
3039
|
-
await
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3250
|
+
await closesFirst(
|
|
3251
|
+
clears,
|
|
3252
|
+
await walking(f, farm, [
|
|
3253
|
+
...refresh,
|
|
3254
|
+
...before,
|
|
3255
|
+
loot.stakeNftIx({
|
|
3256
|
+
player: wallet.address,
|
|
3257
|
+
project,
|
|
3258
|
+
id: f.id,
|
|
3259
|
+
farm,
|
|
3260
|
+
asset,
|
|
3261
|
+
...nft,
|
|
3262
|
+
previous,
|
|
3263
|
+
tier,
|
|
3264
|
+
...token,
|
|
3265
|
+
}),
|
|
3266
|
+
]),
|
|
3267
|
+
),
|
|
3044
3268
|
);
|
|
3045
3269
|
},
|
|
3046
3270
|
|
|
@@ -3081,8 +3305,10 @@ export function solanaApp({
|
|
|
3081
3305
|
const boxes = roll != null ? { [roll]: await read('LootBox', loot.pda.box(project, roll)) } : {};
|
|
3082
3306
|
// 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
3307
|
// left it in the wallet.
|
|
3084
|
-
|
|
3085
|
-
|
|
3308
|
+
const clears =
|
|
3309
|
+
roll != null && farm.custody !== 'Escrow' && loot.boxHolds(boxes[roll])
|
|
3310
|
+
? await clearLeftovers(asset, acceptLeftover)
|
|
3311
|
+
: [];
|
|
3086
3312
|
// A roll still at the NFT's order address blocks the leave, unless it was asked and came back with nothing to give
|
|
3087
3313
|
// (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
3314
|
if (roll != null) {
|
|
@@ -3125,7 +3351,7 @@ export function solanaApp({
|
|
|
3125
3351
|
{ code: tried.error.code, name: 'PrizeNotCovered' },
|
|
3126
3352
|
);
|
|
3127
3353
|
}
|
|
3128
|
-
const { signature, rolls } = await sendRolling(steps);
|
|
3354
|
+
const { signature, rolls } = await sendRolling(await closesFirst(clears, steps));
|
|
3129
3355
|
const rolled = rolls.length ? { rolled: rolls[0] } : {};
|
|
3130
3356
|
if (roll != null)
|
|
3131
3357
|
return {
|
|
@@ -3466,23 +3692,37 @@ export function solanaApp({
|
|
|
3466
3692
|
/**
|
|
3467
3693
|
* Hands an NFT of any kind to the project until a time (a hold): `seconds` from now, or `{ until }` (Unix seconds).
|
|
3468
3694
|
* The hold's address names its end. Returns { signature, freeAt }.
|
|
3695
|
+
*
|
|
3696
|
+
* Like every route that hands an NFT in (`stakeNft` in escrow, `borrow`, `enter`, `bid`, `listForSale`,
|
|
3697
|
+
* `listForRent`, a ticket paid with an NFT, a box that rolls on one), it first clears what an earlier owner of the
|
|
3698
|
+
* NFT left in this project: a hold or a listing that stayed open when whoever controls the NFT's collection took it
|
|
3699
|
+
* out of the project's hands. Left open, such a record could take the NFT once the project holds it again. The
|
|
3700
|
+
* player holds the NFT now, so the program closes each on that proof (a hold by `release`, a listing by
|
|
3701
|
+
* `take_listing`, nothing sent or paid), in front of the deposit in the same transaction, or in a transaction of
|
|
3702
|
+
* their own before it when the two do not fit one. The closes cost the player their transaction fee only: each
|
|
3703
|
+
* record's rent goes to whoever that record names (a hold's payer, a listing's maker), never to the player. Only
|
|
3704
|
+
* what can't be closed still refuses the deposit, in words (a listing whose rental still runs: the program keeps
|
|
3705
|
+
* it until the rental has run); `acceptLeftover: true` goes ahead all the same. NFTs of the project's own
|
|
3706
|
+
* collections are not read.
|
|
3469
3707
|
*/
|
|
3470
3708
|
async hold(asset, seconds, { acceptLeftover, ...others } = NO_OPTIONS) {
|
|
3471
3709
|
only(others, ['acceptLeftover'], 'hold');
|
|
3472
|
-
await
|
|
3710
|
+
const clears = await clearLeftovers(asset, acceptLeftover);
|
|
3473
3711
|
const freeAt = BigInt(typeof seconds === 'object' ? seconds.until : now() + Number(seconds));
|
|
3474
3712
|
const { before, hasAuthority: _, ...nft } = await nftOf(asset);
|
|
3475
|
-
const signature = await send(
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3713
|
+
const signature = await send(
|
|
3714
|
+
await closesFirst(clears, [
|
|
3715
|
+
...before,
|
|
3716
|
+
loot.holdIx({
|
|
3717
|
+
player: wallet.address,
|
|
3718
|
+
project,
|
|
3719
|
+
asset,
|
|
3720
|
+
...nft,
|
|
3721
|
+
collection: nft.collection ?? (await collectionOf(asset)) ?? MPL_CORE,
|
|
3722
|
+
freeAt,
|
|
3723
|
+
}),
|
|
3724
|
+
]),
|
|
3725
|
+
);
|
|
3486
3726
|
return { signature, freeAt };
|
|
3487
3727
|
},
|
|
3488
3728
|
|
|
@@ -3540,7 +3780,7 @@ export function solanaApp({
|
|
|
3540
3780
|
*/
|
|
3541
3781
|
async borrow(maker, id, asset, { due, acceptRisks = false, acceptLeftover, extra, ...others } = NO_OPTIONS) {
|
|
3542
3782
|
only(others, ['due', 'acceptRisks', 'acceptLeftover', 'extra'], 'borrow');
|
|
3543
|
-
await
|
|
3783
|
+
const clears = await clearLeftovers(asset, acceptLeftover);
|
|
3544
3784
|
const loan = await this.loanOffer(maker, id);
|
|
3545
3785
|
extra ??= await hookTail([loan.mint]);
|
|
3546
3786
|
if (loan.risks.length && acceptRisks !== true)
|
|
@@ -3564,11 +3804,13 @@ export function solanaApp({
|
|
|
3564
3804
|
programs: loanPrograms,
|
|
3565
3805
|
extra,
|
|
3566
3806
|
});
|
|
3567
|
-
const signature = await send(
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3807
|
+
const signature = await send(
|
|
3808
|
+
await closesFirst(clears, [
|
|
3809
|
+
...before,
|
|
3810
|
+
createAssociatedTokenAccountIdempotent(wallet.address, wallet.address, loan.mint, loan.program),
|
|
3811
|
+
ix,
|
|
3812
|
+
]),
|
|
3813
|
+
);
|
|
3572
3814
|
return { signature, due, hold: loot.pda.hold(asset, due) };
|
|
3573
3815
|
},
|
|
3574
3816
|
|
|
@@ -3593,7 +3835,9 @@ export function solanaApp({
|
|
|
3593
3835
|
* borrower in the same instruction: one prompt. `repay`: the debt the payer was shown (`loan()`'s, in base units);
|
|
3594
3836
|
* the program refuses any other (`TermsChanged`). An NFT its collection took back or burned while the project held
|
|
3595
3837
|
* it is refused here, before anything is signed: the program takes no repayment for it, and the borrower keeps what
|
|
3596
|
-
* they borrowed.
|
|
3838
|
+
* they borrowed. Such a loan is closed for nothing instead: `claims()` lists it for its borrower and its lender, and
|
|
3839
|
+
* `claim` of it closes the hold (nothing is repaid, and the hold's rent goes back to whoever paid it). When the NFT
|
|
3840
|
+
* can't move now (frozen for now, or its collection refuses the move), or `home` is
|
|
3597
3841
|
* false, the money goes alone, so the loan does not fall due: the hold is then over, and anyone sends the NFT home
|
|
3598
3842
|
* once it can move (`claims()` lists it). A compressed NFT is proven in the same transaction; the money alone for one
|
|
3599
3843
|
* whose proof keeps going out of date goes without it. Returns { signature, home }, and `says` when the NFT waits.
|
|
@@ -3614,7 +3858,7 @@ export function solanaApp({
|
|
|
3614
3858
|
);
|
|
3615
3859
|
if ((await heldNow(asset)).held === false)
|
|
3616
3860
|
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.`,
|
|
3861
|
+
`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
3862
|
);
|
|
3619
3863
|
const { before, hasAuthority: _, ...nft } = await nftOf(asset, wallet.address, { to: hold.player });
|
|
3620
3864
|
const kind = nft.compressed || nft.tokenMetadata ? nft : { collection: (await collectionOf(asset)) ?? MPL_CORE };
|
|
@@ -3672,23 +3916,25 @@ export function solanaApp({
|
|
|
3672
3916
|
listProof = listProofOf(contest.gate, null, listProof);
|
|
3673
3917
|
if (contest.nft) {
|
|
3674
3918
|
if (!staked) throw new Error('This contest takes an NFT: say which (nft).');
|
|
3675
|
-
await
|
|
3919
|
+
const clears = await clearLeftovers(staked, acceptLeftover);
|
|
3676
3920
|
const { before, ...kind } = await nftOf(staked);
|
|
3677
|
-
return send(
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3921
|
+
return send(
|
|
3922
|
+
await closesFirst(clears, [
|
|
3923
|
+
...before,
|
|
3924
|
+
loot.enterIx({
|
|
3925
|
+
player: wallet.address,
|
|
3926
|
+
project,
|
|
3927
|
+
id,
|
|
3928
|
+
contest,
|
|
3929
|
+
side,
|
|
3930
|
+
amount: 1n,
|
|
3931
|
+
shown,
|
|
3932
|
+
listProof,
|
|
3933
|
+
nft: { asset: staked, ...kind },
|
|
3934
|
+
programs,
|
|
3935
|
+
}),
|
|
3936
|
+
]),
|
|
3937
|
+
);
|
|
3692
3938
|
}
|
|
3693
3939
|
const t = [...tokens.values()].find(x => mintOf(x.key) === contest.mint);
|
|
3694
3940
|
const value = units(amount, t?.source === 'sol' ? 9 : (t?.decimals ?? 0));
|
|
@@ -3803,7 +4049,8 @@ export function solanaApp({
|
|
|
3803
4049
|
if (bundle.length > loot.MAX_BID_NFTS) throw new Error(`A bid holds at most ${loot.MAX_BID_NFTS} NFTs.`);
|
|
3804
4050
|
if (auction.held.length >= loot.MAX_BID_NFTS)
|
|
3805
4051
|
throw new Error(`The high bid holds ${loot.MAX_BID_NFTS} NFTs, the most a bid holds: it can't be topped.`);
|
|
3806
|
-
|
|
4052
|
+
const clears = [];
|
|
4053
|
+
for (const asset of bundle) clears.push(...(await clearLeftovers(asset, acceptLeftover)));
|
|
3807
4054
|
const before = [];
|
|
3808
4055
|
const kindOf = async (asset, to) => {
|
|
3809
4056
|
const { before: proof, ...kind } = await nftOf(asset, wallet.address, { to });
|
|
@@ -3826,7 +4073,13 @@ export function solanaApp({
|
|
|
3826
4073
|
held,
|
|
3827
4074
|
programs,
|
|
3828
4075
|
});
|
|
3829
|
-
return send(
|
|
4076
|
+
return send(
|
|
4077
|
+
await closesFirst(clears, [
|
|
4078
|
+
computeUnitLimit(800_000),
|
|
4079
|
+
...before.filter(x => x.programId !== COMPUTE_BUDGET_PROGRAM),
|
|
4080
|
+
ix,
|
|
4081
|
+
]),
|
|
4082
|
+
);
|
|
3830
4083
|
}
|
|
3831
4084
|
const mint = auction.mint;
|
|
3832
4085
|
const value = units(amount, decimalsOf(a.currency));
|
|
@@ -3938,21 +4191,23 @@ export function solanaApp({
|
|
|
3938
4191
|
*/
|
|
3939
4192
|
async listForSale(asset, { price, currency, shares = [], acceptLeftover, ...others } = NO_OPTIONS) {
|
|
3940
4193
|
only(others, ['price', 'currency', 'shares', 'acceptLeftover'], 'listForSale');
|
|
3941
|
-
await
|
|
4194
|
+
const clears = await clearLeftovers(asset, acceptLeftover, { listing: true });
|
|
3942
4195
|
const mint = mintOf(currency);
|
|
3943
4196
|
const { before, ...nft } = await nftOf(asset);
|
|
3944
|
-
return send(
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
4197
|
+
return send(
|
|
4198
|
+
await closesFirst(clears, [
|
|
4199
|
+
...before,
|
|
4200
|
+
loot.openListingIx({
|
|
4201
|
+
maker: wallet.address,
|
|
4202
|
+
project,
|
|
4203
|
+
asset,
|
|
4204
|
+
mint,
|
|
4205
|
+
deal: { Sell: { price: units(price, decimalsOf(currency)) } },
|
|
4206
|
+
shares,
|
|
4207
|
+
...nft,
|
|
4208
|
+
}),
|
|
4209
|
+
]),
|
|
4210
|
+
);
|
|
3956
4211
|
},
|
|
3957
4212
|
|
|
3958
4213
|
/**
|
|
@@ -3964,31 +4219,40 @@ export function solanaApp({
|
|
|
3964
4219
|
{ price, lasts, maxPeriods = 1, currency, shares = [], acceptLeftover, ...others } = NO_OPTIONS,
|
|
3965
4220
|
) {
|
|
3966
4221
|
only(others, ['price', 'lasts', 'maxPeriods', 'currency', 'shares', 'acceptLeftover'], 'listForRent');
|
|
3967
|
-
await
|
|
4222
|
+
const clears = await clearLeftovers(asset, acceptLeftover, { listing: true });
|
|
3968
4223
|
const mint = mintOf(currency);
|
|
3969
4224
|
const { before, ...nft } = await nftOf(asset);
|
|
3970
|
-
return send(
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
4225
|
+
return send(
|
|
4226
|
+
await closesFirst(clears, [
|
|
4227
|
+
...before,
|
|
4228
|
+
loot.openListingIx({
|
|
4229
|
+
maker: wallet.address,
|
|
4230
|
+
project,
|
|
4231
|
+
asset,
|
|
4232
|
+
mint,
|
|
4233
|
+
deal: { Rent: { price: units(price, decimalsOf(currency)), lasts, max_periods: maxPeriods } },
|
|
4234
|
+
shares,
|
|
4235
|
+
...nft,
|
|
4236
|
+
}),
|
|
4237
|
+
]),
|
|
4238
|
+
);
|
|
3982
4239
|
},
|
|
3983
4240
|
|
|
3984
|
-
/**
|
|
4241
|
+
/**
|
|
4242
|
+
* The listing of `asset` (one account read), or null: { listing, asset, maker, mint, deal, renter, until }. One more
|
|
4243
|
+
* read says whether the project still holds the NFT: when whoever controls its collection took it back or burned
|
|
4244
|
+
* it, the listing is flagged `gone` with `says`, what that means in words. There is then nothing to buy or rent,
|
|
4245
|
+
* and the listing can be closed for nothing (`unlist` by its maker, `closeGone` by anyone).
|
|
4246
|
+
*/
|
|
3985
4247
|
async listing(asset) {
|
|
3986
4248
|
const listing = await readMaybe('Listing', loot.pda.listing(project, asset));
|
|
3987
4249
|
if (!listing) return null;
|
|
4250
|
+
const gone = (await heldNow(asset)).held === false;
|
|
3988
4251
|
return {
|
|
3989
4252
|
listing: loot.pda.listing(project, asset),
|
|
3990
4253
|
...listing,
|
|
3991
4254
|
renter: loot.renterOf(listing, await chainNow()),
|
|
4255
|
+
...(gone ? { gone: true, says: GONE_LISTING } : {}),
|
|
3992
4256
|
};
|
|
3993
4257
|
},
|
|
3994
4258
|
|
|
@@ -4002,7 +4266,12 @@ export function solanaApp({
|
|
|
4002
4266
|
return holds ? renter : null;
|
|
4003
4267
|
},
|
|
4004
4268
|
|
|
4005
|
-
/**
|
|
4269
|
+
/**
|
|
4270
|
+
* The project's listings, a maker's (`maker`) or all: one search of the program's accounts, then one read of the
|
|
4271
|
+
* listed NFTs together, which flags a listing `gone` (with `says`) when the project no longer holds its NFT: a
|
|
4272
|
+
* Core asset held by another or burned, a Token Metadata NFT whose token left the project's account. A compressed
|
|
4273
|
+
* NFT is not read here (an indexer's answer for each): `listing(asset)` says it for one.
|
|
4274
|
+
*/
|
|
4006
4275
|
async listings({ maker, ...others } = NO_OPTIONS) {
|
|
4007
4276
|
only(others, ['maker'], 'listings');
|
|
4008
4277
|
const filters = [
|
|
@@ -4011,19 +4280,36 @@ export function solanaApp({
|
|
|
4011
4280
|
...(maker ? [{ memcmp: { offset: 33, bytes: maker } }] : []),
|
|
4012
4281
|
];
|
|
4013
4282
|
const time = await chainNow();
|
|
4014
|
-
|
|
4283
|
+
const found = (await client.programAccounts(loot.LOOT_PROGRAM, filters)).map(({ pubkey, account }) => {
|
|
4015
4284
|
const listing = loot.decode('Listing', account.data);
|
|
4016
4285
|
return { listing: pubkey, ...listing, renter: loot.renterOf(listing, time) };
|
|
4017
4286
|
});
|
|
4287
|
+
// Each NFT and the project's token account of it, all in one read.
|
|
4288
|
+
const read = found.length ? await client.accounts(found.flatMap(l => [l.asset, loot.ata(project, l.asset)])) : [];
|
|
4289
|
+
return found.map((l, i) => {
|
|
4290
|
+
const [nft, held] = [read[2 * i], read[2 * i + 1]];
|
|
4291
|
+
const gone =
|
|
4292
|
+
nft?.owner === MPL_CORE
|
|
4293
|
+
? !coreHeld(nft)
|
|
4294
|
+
: nft?.owner === TOKEN_PROGRAM &&
|
|
4295
|
+
!(
|
|
4296
|
+
held?.data?.length >= 72 &&
|
|
4297
|
+
new DataView(held.data.buffer, held.data.byteOffset).getBigUint64(64, true) === 1n
|
|
4298
|
+
);
|
|
4299
|
+
return gone ? { ...l, gone: true, says: GONE_LISTING } : l;
|
|
4300
|
+
});
|
|
4018
4301
|
},
|
|
4019
4302
|
|
|
4020
4303
|
/**
|
|
4021
4304
|
* Buys a listed NFT at the terms the player saw: `listing`, as `listing()` or `listings()` showed it. It is
|
|
4022
4305
|
* required: the terms signed are those, never the listing read now, so a price or a share changed since is refused.
|
|
4306
|
+
* A listing whose NFT the project no longer holds (its collection took it back or burned it) is refused in words
|
|
4307
|
+
* before anything is signed: nothing is there to buy, and anyone can close that listing for nothing (`closeGone`).
|
|
4023
4308
|
*/
|
|
4024
4309
|
async buyListing(asset, { listing, ...others } = NO_OPTIONS) {
|
|
4025
4310
|
only(others, ['listing'], 'buyListing');
|
|
4026
4311
|
await shownListing(asset, listing, 'Sell');
|
|
4312
|
+
await listedStill(asset);
|
|
4027
4313
|
const { before, ...nft } = await nftOf(asset, wallet.address, { to: wallet.address });
|
|
4028
4314
|
const holders = await holdersOf(listing.shares ?? []);
|
|
4029
4315
|
return send([
|
|
@@ -4044,11 +4330,13 @@ export function solanaApp({
|
|
|
4044
4330
|
|
|
4045
4331
|
/**
|
|
4046
4332
|
* 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`).
|
|
4333
|
+
* showed it (required, as for `buyListing`). One whose NFT the project no longer holds is refused in words, as
|
|
4334
|
+
* `buyListing` refuses it.
|
|
4048
4335
|
*/
|
|
4049
4336
|
async rentListing(asset, { periods = 1, listing, ...others } = NO_OPTIONS) {
|
|
4050
4337
|
only(others, ['periods', 'listing'], 'rentListing');
|
|
4051
4338
|
await shownListing(asset, listing, 'Rent');
|
|
4339
|
+
await listedStill(asset);
|
|
4052
4340
|
const holders = await holdersOf(listing.shares ?? []);
|
|
4053
4341
|
const total = BigInt(listing.deal.Rent.price) * BigInt(periods);
|
|
4054
4342
|
// A compressed NFT is read by its record, proven first in the same transaction.
|
|
@@ -4070,13 +4358,46 @@ export function solanaApp({
|
|
|
4070
4358
|
]);
|
|
4071
4359
|
},
|
|
4072
4360
|
|
|
4073
|
-
/**
|
|
4361
|
+
/**
|
|
4362
|
+
* Takes a listed NFT home (its maker). While a rental runs it only stops the listing; call again once it is over.
|
|
4363
|
+
* A listing whose NFT the project no longer holds (its collection took it back or burned it) closes without it, on
|
|
4364
|
+
* that proof: nothing is sent, and the listing's rent comes back to its maker. While a rental of it still runs,
|
|
4365
|
+
* that close too only stops the listing, and closes it once the rental has run.
|
|
4366
|
+
*/
|
|
4074
4367
|
async unlist(asset) {
|
|
4075
4368
|
const listing = await read('Listing', loot.pda.listing(project, asset));
|
|
4369
|
+
const proof = await goneProof(asset);
|
|
4370
|
+
if (proof)
|
|
4371
|
+
return send([...proof.before, loot.closeListingIx({ maker: wallet.address, project, listing, ...proof.kind })]);
|
|
4076
4372
|
const { before, ...nft } = await nftOf(asset, wallet.address, { to: wallet.address });
|
|
4077
4373
|
return send([...before, loot.closeListingIx({ maker: wallet.address, project, listing, ...nft })]);
|
|
4078
4374
|
},
|
|
4079
4375
|
|
|
4376
|
+
/**
|
|
4377
|
+
* Closes what this project still records of an NFT it no longer holds (`asset`: whoever controls its collection
|
|
4378
|
+
* took it out of the project's hands or burned it): each hold of it (`release`) and its listing (`take_listing`),
|
|
4379
|
+
* on the proof that it is gone. Anyone may, at any time, also while a hold runs and for a loan before it is due:
|
|
4380
|
+
* nothing is sent or paid, and a loan's debt ends unpaid. The sender pays the transaction's fee only; each
|
|
4381
|
+
* record's rent goes to whoever that record names (a hold's payer, a listing's maker), never to the sender. A
|
|
4382
|
+
* listing whose rental still runs is left (`waits`): the program keeps a running rental, and the listing closes
|
|
4383
|
+
* once it has run. Refused in words, with nothing sent, when the project still holds the NFT or nothing shows it
|
|
4384
|
+
* gone. Returns { signature, closed: [{ kind, address }], waits: [{ kind, address }] }; `signature` is null when
|
|
4385
|
+
* there was nothing to close.
|
|
4386
|
+
*/
|
|
4387
|
+
async closeGone(asset) {
|
|
4388
|
+
const records = await recordsOf(asset);
|
|
4389
|
+
if (!records.length) return { signature: null, closed: [], waits: [] };
|
|
4390
|
+
const proof = await goneProof(asset);
|
|
4391
|
+
if (!proof)
|
|
4392
|
+
throw new Error(
|
|
4393
|
+
`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.`,
|
|
4394
|
+
);
|
|
4395
|
+
const { closes, closed, waits } = await closesOf(asset, records, proof);
|
|
4396
|
+
const left = waits.map(({ kind, address }) => ({ kind, address }));
|
|
4397
|
+
if (!closes.length) return { signature: null, closed, waits: left };
|
|
4398
|
+
return { signature: await send(closes), closed, waits: left };
|
|
4399
|
+
},
|
|
4400
|
+
|
|
4080
4401
|
// ---------------------------------------------------------------- raffles
|
|
4081
4402
|
|
|
4082
4403
|
/**
|
|
@@ -4413,7 +4734,9 @@ export function solanaApp({
|
|
|
4413
4734
|
|
|
4414
4735
|
/**
|
|
4415
4736
|
* 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,
|
|
4737
|
+
* collectible to take home, a hold of theirs or a loan they made or took whose NFT the project no longer holds (its
|
|
4738
|
+
* collection took it back or burned it: listed at any time, also while the hold runs, as a `release` flagged
|
|
4739
|
+
* `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
4740
|
* won, a draw that is due (anyone may reveal it, or ask again when the oracle never answered; it takes 0.0008
|
|
4418
4741
|
* SOL), and a drawn lottery's unpaid places, which anyone may pay so the next round can open. [{ kind, says, … }];
|
|
4419
4742
|
* a winner's first claim of a contest of tokens carries `potUnits` and `prizeUnits`, its part as the decision wrote
|
|
@@ -4486,46 +4809,31 @@ export function solanaApp({
|
|
|
4486
4809
|
});
|
|
4487
4810
|
}
|
|
4488
4811
|
// 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).
|
|
4812
|
+
// project's own address: a dropped stash NFT, a lottery's or an auction's NFT owed to it). And the loans this
|
|
4813
|
+
// wallet made: one search of the project's holds finds them all.
|
|
4490
4814
|
const g = await projectNow();
|
|
4491
4815
|
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
|
-
);
|
|
4816
|
+
const holds = (await client.programAccounts(loot.LOOT_PROGRAM, [tag('Hold'), at(1, project)]))
|
|
4817
|
+
.map(({ pubkey, account }) => ({ pubkey, hold: loot.decode('Hold', account.data) }))
|
|
4818
|
+
.filter(({ hold }) => holdsOf.includes(hold.player) || hold.debt?.lender === me);
|
|
4819
|
+
// Whether the project still holds each one's NFT: whoever controls its collection can take it back or burn it.
|
|
4820
|
+
const heldOf = await heldEach(holds.map(({ hold }) => hold.asset));
|
|
4821
|
+
for (const { pubkey, hold } of holds) {
|
|
4822
|
+
const claim = { kind: 'release', asset: hold.asset, hold: pubkey };
|
|
4823
|
+
// An NFT proven gone: its hold closes at any time, also while it runs and for a loan before it is due.
|
|
4824
|
+
if (heldOf.get(hold.asset)?.held === false) out.push({ ...claim, gone: true, says: goneHoldSays(hold) });
|
|
4825
|
+
// A repaid loan is a plain hold that is over (no debt, `free_at` 0): its NFT goes home whenever.
|
|
4826
|
+
else if (!hold.debt && holdsOf.includes(hold.player) && time >= BigInt(hold.free_at))
|
|
4827
|
+
out.push({
|
|
4828
|
+
...claim,
|
|
4829
|
+
says:
|
|
4830
|
+
hold.player === project
|
|
4831
|
+
? 'An NFT owed to the treasury: send it there.'
|
|
4832
|
+
: 'Your collectible back: its hold is over.',
|
|
4833
|
+
});
|
|
4834
|
+
// A loan this wallet made that is overdue and unpaid: its NFT is the lender's to take.
|
|
4835
|
+
else if (hold.debt?.lender === me && time >= BigInt(hold.free_at))
|
|
4836
|
+
out.push({ ...claim, says: 'A loan you made is overdue and unpaid: take its NFT.' });
|
|
4529
4837
|
}
|
|
4530
4838
|
// A compressed NFT's proof record this wallet paid for and left open: closing it brings its rent back.
|
|
4531
4839
|
for (const { account } of await client.programAccounts(loot.LOOT_PROGRAM, [tag('Proven')])) {
|
|
@@ -4722,7 +5030,9 @@ export function solanaApp({
|
|
|
4722
5030
|
},
|
|
4723
5031
|
|
|
4724
5032
|
/**
|
|
4725
|
-
* Finishes one of `claims()`, signed by this player. Returns the signature. A
|
|
5033
|
+
* Finishes one of `claims()`, signed by this player. Returns the signature. A `release` flagged `gone` closes its
|
|
5034
|
+
* hold on the proof that the project no longer holds the NFT: nothing is sent, the hold's rent goes back to whoever
|
|
5035
|
+
* paid it, and a loan's debt ends unpaid. A winner of a contest of NFTs picks
|
|
4726
5036
|
* which loser's NFT they take (`{ take: asset }`, the NFT of a losing entry that still holds one; the first found by default):
|
|
4727
5037
|
* only the winner can, while any are left.
|
|
4728
5038
|
*/
|
|
@@ -4810,8 +5120,24 @@ export function solanaApp({
|
|
|
4810
5120
|
return contest.prize?.Roll && !build ? (await sendRolling(claimed)).signature : send(claimed);
|
|
4811
5121
|
}
|
|
4812
5122
|
if (c.kind === 'release') {
|
|
4813
|
-
if (c.gone) throw new Error(c.says);
|
|
4814
5123
|
const hold = await read('Hold', c.hold);
|
|
5124
|
+
// Its NFT proven gone from the project: the hold closes on that proof, at any time, and nothing is sent. Its
|
|
5125
|
+
// rent goes back to whoever paid it; a loan's debt ends unpaid.
|
|
5126
|
+
const proof = await goneProof(c.asset, me);
|
|
5127
|
+
if (proof)
|
|
5128
|
+
return send([
|
|
5129
|
+
...proof.before,
|
|
5130
|
+
loot.releaseIx({
|
|
5131
|
+
signer: me,
|
|
5132
|
+
project,
|
|
5133
|
+
asset: c.asset,
|
|
5134
|
+
...proof.kind,
|
|
5135
|
+
hold,
|
|
5136
|
+
address: c.hold,
|
|
5137
|
+
treasury: g.treasury,
|
|
5138
|
+
...(hold.debt ? { to: hold.debt.lender } : {}),
|
|
5139
|
+
}),
|
|
5140
|
+
]);
|
|
4815
5141
|
const { before, hasAuthority: _, ...nft } = await nftOf(c.asset, me, { to: me });
|
|
4816
5142
|
// An overdue loan's NFT goes to its lender.
|
|
4817
5143
|
return send([
|