@xgamesfun/solana 0.27.0 → 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/mcp/tools.mjs +1 -1
- package/js/play/index.js +445 -111
- package/package.json +1 -1
- package/skills/xsolana-economy/SKILL.md +4 -4
- package/skills/xsolana-economy/references/apps.md +6 -2
- package/types/play/index.d.ts +178 -29
package/js/play/index.js
CHANGED
|
@@ -421,10 +421,10 @@ export function solanaApp({
|
|
|
421
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
422
|
* Core asset shows it by itself (`kind`: its collection now); a Token Metadata NFT by the token account that holds it
|
|
423
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.
|
|
424
|
+
* leaf is now (`before`, by `signer`). `holder`: as `heldNow` takes it. `now`: what `heldNow` read of it already.
|
|
425
425
|
*/
|
|
426
|
-
async function goneProof(asset, signer = wallet.address, { holder = null } = {}) {
|
|
427
|
-
|
|
426
|
+
async function goneProof(asset, signer = wallet.address, { holder = null, now = null } = {}) {
|
|
427
|
+
now ??= await heldNow(asset, { holder });
|
|
428
428
|
if (now.held !== false) return null;
|
|
429
429
|
if (now.das) {
|
|
430
430
|
const proof = await client.call('getAssetProof', { id: asset });
|
|
@@ -439,14 +439,33 @@ export function solanaApp({
|
|
|
439
439
|
/**
|
|
440
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
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.
|
|
442
443
|
*/
|
|
443
|
-
async function leaving(asset, signer = wallet.address, { to, payer = signer } = {}) {
|
|
444
|
-
const proof = await goneProof(asset, signer);
|
|
444
|
+
async function leaving(asset, signer = wallet.address, { to, payer = signer, now = null } = {}) {
|
|
445
|
+
const proof = await goneProof(asset, signer, { now });
|
|
445
446
|
if (!proof) return { ...(await nftOf(asset, signer, { to, payer })), gone: false };
|
|
446
447
|
// A Core asset shows it by itself, with the accounts it always took.
|
|
447
448
|
if (!proof.kind.tokenMetadata && !proof.kind.compressed) return { ...(await nftOf(asset, signer)), gone: true };
|
|
448
449
|
return { before: proof.before, hasAuthority: false, ...proof.kind, gone: true };
|
|
449
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
|
+
});
|
|
450
469
|
/**
|
|
451
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
|
|
452
471
|
* money in: accounts that start frozen or can be switched to it (a payee closes their own account, and the one
|
|
@@ -1958,16 +1977,18 @@ export function solanaApp({
|
|
|
1958
1977
|
const lotNow = lot ? await heldNow(lot) : { held: null };
|
|
1959
1978
|
const bidNow = [];
|
|
1960
1979
|
if (sold && auction.nft_bids) for (const nft of auction.held) bidNow.push(await heldNow(nft));
|
|
1961
|
-
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;
|
|
1962
1982
|
const ending = sold && lot && (callOff || gone) ? 'CallOff' : 'Deliver';
|
|
1963
1983
|
const home = !sold || ending === 'CallOff';
|
|
1964
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.
|
|
1965
1986
|
const before = [];
|
|
1966
|
-
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) : {};
|
|
1967
1988
|
const bidTo = ending === 'CallOff' ? auction.high_bidder : owner;
|
|
1968
1989
|
const held = [];
|
|
1969
1990
|
if (sold && auction.nft_bids)
|
|
1970
|
-
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));
|
|
1971
1992
|
if (sold && !home) holders ??= await holdersOf(auction.shares);
|
|
1972
1993
|
const end = (ending, hold = false) =>
|
|
1973
1994
|
loot.endAuctionIx({ ...base, auction, item, ...lotKind, held, holders, ending, hold });
|
|
@@ -1982,6 +2003,24 @@ export function solanaApp({
|
|
|
1982
2003
|
...(made ? { asset: made } : {}),
|
|
1983
2004
|
};
|
|
1984
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
|
+
}
|
|
1985
2024
|
|
|
1986
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.
|
|
1987
2026
|
const outs = [
|
|
@@ -2049,15 +2088,15 @@ export function solanaApp({
|
|
|
2049
2088
|
return holding();
|
|
2050
2089
|
}
|
|
2051
2090
|
|
|
2052
|
-
/** An NFT's kind on an auction's end, going to `to`: what `nftOf` reads, its proof put in `before
|
|
2053
|
-
*
|
|
2054
|
-
|
|
2055
|
-
|
|
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 });
|
|
2056
2096
|
before.push(...proof);
|
|
2057
2097
|
return {
|
|
2058
2098
|
...kind,
|
|
2059
2099
|
collection: kind.compressed || kind.tokenMetadata ? undefined : ((await collectionOf(asset)) ?? MPL_CORE),
|
|
2060
|
-
...(evidence && kind.tokenMetadata ? { evidence } : {}),
|
|
2061
2100
|
};
|
|
2062
2101
|
}
|
|
2063
2102
|
|
|
@@ -2106,16 +2145,30 @@ export function solanaApp({
|
|
|
2106
2145
|
if (opens.length) await send([...new Map(opens.map(x => [x.keys[1].pubkey, x])).values()]);
|
|
2107
2146
|
const before = [];
|
|
2108
2147
|
const held = [];
|
|
2109
|
-
|
|
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
|
+
}
|
|
2110
2157
|
const end = (hold = false) => loot.endAuctionIx({ ...base, auction, held, holders, hold });
|
|
2111
2158
|
const steps = ix => [
|
|
2112
2159
|
computeUnitLimit(1_000_000),
|
|
2113
2160
|
...before.filter(x => x.programId !== COMPUTE_BUDGET_PROGRAM),
|
|
2114
2161
|
ix,
|
|
2115
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
|
+
: {};
|
|
2116
2169
|
if (sendPlan({ payer: wallet.address, instructions: steps(end()), alone: isProveLeaf }))
|
|
2117
2170
|
try {
|
|
2118
|
-
return { paid: await send(steps(end())) };
|
|
2171
|
+
return { paid: await send(steps(end())), ...passed };
|
|
2119
2172
|
} catch (e) {
|
|
2120
2173
|
// The lot is the winner's already: what is owed waits until the account it goes into can take it.
|
|
2121
2174
|
return {
|
|
@@ -2123,9 +2176,9 @@ export function solanaApp({
|
|
|
2123
2176
|
says: `The lot is the winner’s; the money waits (${e.message}): end the auction again to pay it.`,
|
|
2124
2177
|
};
|
|
2125
2178
|
}
|
|
2126
|
-
const outs = auction.held.map(x => [x, auction.seller]);
|
|
2127
|
-
const { signature, releases } = await heldThenReleased(at, steps(end(
|
|
2128
|
-
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 };
|
|
2129
2182
|
}
|
|
2130
2183
|
|
|
2131
2184
|
/**
|
|
@@ -2141,9 +2194,12 @@ export function solanaApp({
|
|
|
2141
2194
|
const signature = await send([...instructions, fund]);
|
|
2142
2195
|
const releases = [];
|
|
2143
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;
|
|
2144
2199
|
const release = async signer => {
|
|
2145
2200
|
const hold = await read('Hold', loot.pda.hold(asset, 0));
|
|
2146
|
-
const { before: proof, hasAuthority: _h, ...kind } = await
|
|
2201
|
+
const { before: proof, hasAuthority: _h, gone, ...kind } = await leaving(asset, signer, { to });
|
|
2202
|
+
lost = gone;
|
|
2147
2203
|
return [
|
|
2148
2204
|
...proof,
|
|
2149
2205
|
loot.releaseIx({
|
|
@@ -2160,7 +2216,8 @@ export function solanaApp({
|
|
|
2160
2216
|
const steps = await release(key.address);
|
|
2161
2217
|
const byKey = (await dryRun(key.address, steps)).ok ? await sendAs(key, steps).catch(() => null) : null;
|
|
2162
2218
|
// One that still can't move (frozen for now) stays a hold: claims() lists it for whoever it is owed to.
|
|
2163
|
-
|
|
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);
|
|
2164
2221
|
}
|
|
2165
2222
|
await emptyKey(key);
|
|
2166
2223
|
return { signature, releases };
|
|
@@ -2173,6 +2230,50 @@ export function solanaApp({
|
|
|
2173
2230
|
const GONE_LISTING =
|
|
2174
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.';
|
|
2175
2232
|
|
|
2233
|
+
/**
|
|
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.
|
|
2237
|
+
*/
|
|
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.`;
|
|
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(' ');
|
|
2276
|
+
|
|
2176
2277
|
/**
|
|
2177
2278
|
* This project's records of `asset`: its listing at `["listing", project, asset]` (one read) and its holds (one
|
|
2178
2279
|
* search, by the asset's address). Each is { kind: 'listing' | 'hold', address, record }.
|
|
@@ -2193,6 +2294,30 @@ export function solanaApp({
|
|
|
2193
2294
|
return out;
|
|
2194
2295
|
}
|
|
2195
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
|
+
|
|
2196
2321
|
/**
|
|
2197
2322
|
* The records of this project an earlier owner of `asset` left behind (`recordsOf`), each that names a wallet other
|
|
2198
2323
|
* than the player's: { kind, address, record, says }. Such a record is left when whoever controls the NFT's
|
|
@@ -2470,6 +2595,77 @@ export function solanaApp({
|
|
|
2470
2595
|
];
|
|
2471
2596
|
}
|
|
2472
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
|
+
|
|
2473
2669
|
/**
|
|
2474
2670
|
* When a draw of round `r` (at `key`) would cancel it (an open round short of its least tickets; a waiting one past
|
|
2475
2671
|
* the timeout), the pulls of `who`'s own parts as the cancelled round owes them (a few entries' worth), to go right
|
|
@@ -2529,6 +2725,11 @@ export function solanaApp({
|
|
|
2529
2725
|
* The pulls of `parts` of entry `i` of a round (`partsOwed`; `i` null for parts owed in common, `commonOwed`), signed by
|
|
2530
2726
|
* `signer`, and the rolls they make ([{ order, box }], `loot.pullRolls`). With `closes`, the last pull carries the
|
|
2531
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.
|
|
2532
2733
|
*/
|
|
2533
2734
|
async function pulls(key, r, i, parts, signer, { closes = false, lotteryClosed = false } = {}) {
|
|
2534
2735
|
const e = i == null ? null : r.entries[i];
|
|
@@ -2544,20 +2745,38 @@ export function solanaApp({
|
|
|
2544
2745
|
const extra = await hookTail([...roundMints(r), ...r.prizes.map(p => p.mint)]);
|
|
2545
2746
|
for (const [k, part] of parts.entries()) {
|
|
2546
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();
|
|
2547
2752
|
if (part === 'Own' && e.nft !== NOBODY) {
|
|
2548
2753
|
const then = r.status === 'Cancelled' || r.terms.returns ? null : r.terms.nft_ticket?.then;
|
|
2549
2754
|
more.handBack = Boolean(then?.Give) && loot.handsBack(await client.account(then.Give[0]));
|
|
2550
2755
|
const to = !then || more.handBack ? e.buyer : then.Give ? then.Give[0] : undefined;
|
|
2551
|
-
const { before = [], hasAuthority: _, ...kind } = await
|
|
2756
|
+
const { before = [], hasAuthority: _, gone, ...kind } = await leaving(e.nft, signer, { to });
|
|
2552
2757
|
out.push(...before);
|
|
2553
|
-
|
|
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;
|
|
2554
2764
|
} else if (part.Prize && !loot.tokenRun(r, Number(part.Prize[0]))) {
|
|
2555
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
|
|
2556
2766
|
// out), a project stash's NFT, an item, or a roll (asked in the same transaction).
|
|
2557
2767
|
const run = Number(part.Prize[0]);
|
|
2558
2768
|
const given = r.terms.places[run].prize;
|
|
2559
|
-
|
|
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);
|
|
2560
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
|
+
};
|
|
2561
2780
|
const stash = loot.ownStash(r.terms.places[run])
|
|
2562
2781
|
? loot.decodeStash((await client.account(loot.pda.lotteryStash(r.lottery)))?.data ?? null)
|
|
2563
2782
|
: null;
|
|
@@ -2569,10 +2788,7 @@ export function solanaApp({
|
|
|
2569
2788
|
const predicted = loot.stashNfts(r, stash);
|
|
2570
2789
|
for (const place of loot.prizePlacesOwed(r, i, run)) {
|
|
2571
2790
|
const nft = predicted[loot.nftSlot(r, place)];
|
|
2572
|
-
if (
|
|
2573
|
-
const { before, hasAuthority: _, ...kind } = await nftOf(nft, signer, { to: e.buyer });
|
|
2574
|
-
inputs.nfts[nft] = kind;
|
|
2575
|
-
out.push(...before);
|
|
2791
|
+
if (nft) await hand(nft);
|
|
2576
2792
|
}
|
|
2577
2793
|
}
|
|
2578
2794
|
const stashes = given.Nft
|
|
@@ -2582,10 +2798,17 @@ export function solanaApp({
|
|
|
2582
2798
|
) ?? { nfts: [] },
|
|
2583
2799
|
}
|
|
2584
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);
|
|
2585
2807
|
more.prize = {
|
|
2586
2808
|
stash,
|
|
2587
2809
|
stashes,
|
|
2588
2810
|
nfts: inputs.nfts,
|
|
2811
|
+
hold: holds,
|
|
2589
2812
|
collection: inputs.collection,
|
|
2590
2813
|
poolMints: inputs.poolMints,
|
|
2591
2814
|
items: inputs.items,
|
|
@@ -2620,7 +2843,8 @@ export function solanaApp({
|
|
|
2620
2843
|
) && (await dryRun(signer, tx)).ok
|
|
2621
2844
|
);
|
|
2622
2845
|
};
|
|
2623
|
-
|
|
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;
|
|
2624
2848
|
while (most > 1 && !(await sized(most))) most = Math.max(1, Math.floor(most * 0.75));
|
|
2625
2849
|
if (most < owed) [more.prize.most, partial] = [most, true];
|
|
2626
2850
|
heap = true;
|
|
@@ -2647,21 +2871,26 @@ export function solanaApp({
|
|
|
2647
2871
|
}
|
|
2648
2872
|
const last = k === parts.length - 1;
|
|
2649
2873
|
const round = closes && last ? { ...r, owed: pullSettles(r, i, part) } : { ...r, owed: Number.MAX_SAFE_INTEGER };
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
)
|
|
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));
|
|
2665
2894
|
}
|
|
2666
2895
|
return { instructions: out, rolls, heap, partial };
|
|
2667
2896
|
}
|
|
@@ -4033,7 +4262,9 @@ export function solanaApp({
|
|
|
4033
4262
|
* Bids `amount` (whole units) on an auction. The bid it tops goes back to its bidder in the same transaction
|
|
4034
4263
|
* (the program opens their account again if they closed it, paid by this bidder). In SOL, only what the player's
|
|
4035
4264
|
* wrapped-SOL account does not hold already is wrapped, and the account stays open: if this bid is topped, it comes
|
|
4036
|
-
* 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.
|
|
4037
4268
|
*/
|
|
4038
4269
|
async bid(auctionKey, amount, { shown, listProof, nfts, acceptLeftover, ...others } = NO_OPTIONS) {
|
|
4039
4270
|
only(others, ['shown', 'listProof', 'nfts', 'acceptLeftover'], 'bid');
|
|
@@ -4059,9 +4290,17 @@ export function solanaApp({
|
|
|
4059
4290
|
};
|
|
4060
4291
|
const bid = [];
|
|
4061
4292
|
for (const asset of bundle) bid.push(await kindOf(asset));
|
|
4062
|
-
// 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.
|
|
4063
4296
|
const held = [];
|
|
4064
|
-
|
|
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
|
+
}
|
|
4065
4304
|
const ix = loot.bidIx({
|
|
4066
4305
|
bidder: wallet.address,
|
|
4067
4306
|
project,
|
|
@@ -4071,13 +4310,19 @@ export function solanaApp({
|
|
|
4071
4310
|
listProof,
|
|
4072
4311
|
nfts: bid,
|
|
4073
4312
|
held,
|
|
4313
|
+
hold: [...lost.keys()],
|
|
4074
4314
|
programs,
|
|
4075
4315
|
});
|
|
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
|
+
);
|
|
4076
4320
|
return send(
|
|
4077
4321
|
await closesFirst(clears, [
|
|
4078
4322
|
computeUnitLimit(800_000),
|
|
4079
4323
|
...before.filter(x => x.programId !== COMPUTE_BUDGET_PROGRAM),
|
|
4080
4324
|
ix,
|
|
4325
|
+
...closes,
|
|
4081
4326
|
]),
|
|
4082
4327
|
);
|
|
4083
4328
|
}
|
|
@@ -4125,6 +4370,12 @@ export function solanaApp({
|
|
|
4125
4370
|
* the sale is called off on that evidence, which anyone may: the bid goes back to its bidder (`{ calledOff: true }`).
|
|
4126
4371
|
* With no bid, the lot goes home to its seller. An end too big for one transaction otherwise makes each NFT that may
|
|
4127
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.
|
|
4128
4379
|
*/
|
|
4129
4380
|
async endAuction(auctionKey, { holders, ...others } = NO_OPTIONS) {
|
|
4130
4381
|
only(others, ['holders'], 'endAuction');
|
|
@@ -4375,27 +4626,47 @@ export function solanaApp({
|
|
|
4375
4626
|
|
|
4376
4627
|
/**
|
|
4377
4628
|
* 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`)
|
|
4379
|
-
*
|
|
4380
|
-
*
|
|
4381
|
-
*
|
|
4382
|
-
*
|
|
4383
|
-
*
|
|
4384
|
-
*
|
|
4385
|
-
*
|
|
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.
|
|
4386
4645
|
*/
|
|
4387
4646
|
async closeGone(asset) {
|
|
4388
|
-
const records = await recordsOf(asset);
|
|
4389
|
-
if (!records.length) return { signature: null, closed: [], waits: [] };
|
|
4647
|
+
const [records, stashes] = [await recordsOf(asset), await stashesOf(asset)];
|
|
4648
|
+
if (!records.length && !stashes.length) return { signature: null, closed: [], unstashed: [], waits: [] };
|
|
4390
4649
|
const proof = await goneProof(asset);
|
|
4391
4650
|
if (!proof)
|
|
4392
4651
|
throw new Error(
|
|
4393
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.`,
|
|
4394
4653
|
);
|
|
4395
4654
|
const { closes, closed, waits } = await closesOf(asset, records, proof);
|
|
4396
|
-
const
|
|
4397
|
-
|
|
4398
|
-
|
|
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 };
|
|
4399
4670
|
},
|
|
4400
4671
|
|
|
4401
4672
|
// ---------------------------------------------------------------- raffles
|
|
@@ -4446,9 +4717,20 @@ export function solanaApp({
|
|
|
4446
4717
|
return send([loot.upsertIx({ owner: wallet.address, project, def: { Lottery: { id, args } }, lotteryState })]);
|
|
4447
4718
|
},
|
|
4448
4719
|
|
|
4449
|
-
/**
|
|
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
|
+
*/
|
|
4450
4727
|
async unraffle(id, nft) {
|
|
4451
|
-
const {
|
|
4728
|
+
const {
|
|
4729
|
+
before,
|
|
4730
|
+
hasAuthority: _,
|
|
4731
|
+
gone: _g,
|
|
4732
|
+
...piece
|
|
4733
|
+
} = await leaving(nft, wallet.address, { to: wallet.address });
|
|
4452
4734
|
const lotteryState = await readMaybe('Lottery', loot.pda.lottery(project, id));
|
|
4453
4735
|
return send([
|
|
4454
4736
|
...before,
|
|
@@ -4741,6 +5023,10 @@ export function solanaApp({
|
|
|
4741
5023
|
* SOL), and a drawn lottery's unpaid places, which anyone may pay so the next round can open. [{ kind, says, … }];
|
|
4742
5024
|
* a winner's first claim of a contest of tokens carries `potUnits` and `prizeUnits`, its part as the decision wrote
|
|
4743
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.
|
|
4744
5030
|
*/
|
|
4745
5031
|
async claims() {
|
|
4746
5032
|
const me = wallet.address;
|
|
@@ -4776,8 +5062,8 @@ export function solanaApp({
|
|
|
4776
5062
|
const off = loot.contestOff(contest, time);
|
|
4777
5063
|
if (!(off || contest.verdict === 'Won')) continue;
|
|
4778
5064
|
const list = await client.account(loot.contestEntries(pubkey, contest));
|
|
4779
|
-
const
|
|
4780
|
-
|
|
5065
|
+
const entries = list?.owner === loot.LOOT_PROGRAM ? loot.decodeEntries(list.data) : [];
|
|
5066
|
+
const entry = entries.find(e => e.player === me);
|
|
4781
5067
|
if (!entry) continue;
|
|
4782
5068
|
const won = !off && Number(contest.winner) === Number(entry.side);
|
|
4783
5069
|
const first = won && !(entry.bits & loot.CLAIMED);
|
|
@@ -4800,11 +5086,32 @@ export function solanaApp({
|
|
|
4800
5086
|
: 'Your share of the pot.';
|
|
4801
5087
|
// A winner's part of a contest of tokens is what the decision wrote, whoever claims first (`contestWinnings`).
|
|
4802
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(' ');
|
|
4803
5110
|
out.push({
|
|
4804
5111
|
kind: 'contest',
|
|
4805
5112
|
id: contest.id,
|
|
4806
5113
|
side: entry.side,
|
|
4807
|
-
says,
|
|
5114
|
+
...(lost.length ? { gone: true, assets: lost.map(m => m.nft), says: goneSays } : { says }),
|
|
4808
5115
|
...(winnings ? { potUnits: String(winnings.pot), prizeUnits: String(winnings.prize) } : {}),
|
|
4809
5116
|
});
|
|
4810
5117
|
}
|
|
@@ -4854,32 +5161,47 @@ export function solanaApp({
|
|
|
4854
5161
|
}
|
|
4855
5162
|
const auctions = spec.auctions ?? [];
|
|
4856
5163
|
const found = await client.accounts(auctions.map(a => auctionAt(a)));
|
|
4857
|
-
auctions.
|
|
5164
|
+
for (const [i, a] of auctions.entries()) {
|
|
4858
5165
|
const x = found[i] && loot.decode('Auction', found[i].data);
|
|
4859
|
-
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
|
+
}
|
|
4860
5186
|
// The lot went first (an end too wide for one transaction, or a seller's account that couldn't take the money
|
|
4861
5187
|
// then): only the money is left to pay.
|
|
4862
|
-
if (
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
return;
|
|
4870
|
-
}
|
|
4871
|
-
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.` });
|
|
4872
5195
|
// The seller's own auction, over: ending it pays them (or brings an unsold lot home).
|
|
4873
|
-
else
|
|
5196
|
+
else
|
|
4874
5197
|
out.push({
|
|
4875
5198
|
kind: 'auction',
|
|
4876
5199
|
key: a.key,
|
|
4877
|
-
says:
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
: `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.`,
|
|
4881
5203
|
});
|
|
4882
|
-
}
|
|
5204
|
+
}
|
|
4883
5205
|
for (const l of spec.lotteries ?? []) {
|
|
4884
5206
|
// A lottery the page names but nobody has written yet (a raffle still to open) has nothing to finish.
|
|
4885
5207
|
if (!(await client.account(loot.pda.lottery(project, l.id)))) continue;
|
|
@@ -4918,17 +5240,32 @@ export function solanaApp({
|
|
|
4918
5240
|
const parts = e.buyer === me ? loot.partsOwed(r, i) : [];
|
|
4919
5241
|
return parts.length ? [{ entry: i, parts }] : [];
|
|
4920
5242
|
});
|
|
4921
|
-
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}”.`;
|
|
4922
5255
|
out.push({
|
|
4923
5256
|
kind: 'pull',
|
|
4924
5257
|
key: l.key,
|
|
4925
5258
|
round,
|
|
4926
5259
|
pulls: mine,
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
5260
|
+
...(lostTickets.length + lostPrizes.length
|
|
5261
|
+
? {
|
|
5262
|
+
gone: true,
|
|
5263
|
+
assets: [...lostTickets, ...lostPrizes],
|
|
5264
|
+
says: `${owed} ${goneRoundSays(lostTickets, lostPrizes, back)}`,
|
|
5265
|
+
}
|
|
5266
|
+
: { says: owed }),
|
|
4931
5267
|
});
|
|
5268
|
+
}
|
|
4932
5269
|
// Anyone may pay everyone else what they are owed, and the parts owed in common (about 0.002 SOL of account
|
|
4933
5270
|
// rent for each payee who has no account for it yet, which stays with them): the last pull closes the round,
|
|
4934
5271
|
// and its rent goes back to whoever fronted it (the lottery, or its first buyer).
|
|
@@ -5033,8 +5370,15 @@ export function solanaApp({
|
|
|
5033
5370
|
* Finishes one of `claims()`, signed by this player. Returns the signature. A `release` flagged `gone` closes its
|
|
5034
5371
|
* hold on the proof that the project no longer holds the NFT: nothing is sent, the hold's rent goes back to whoever
|
|
5035
5372
|
* paid it, and a loan's debt ends unpaid. A winner of a contest of NFTs picks
|
|
5036
|
-
* which loser's NFT they take (`{ take: asset }`, the NFT of a losing entry that still holds one;
|
|
5037
|
-
* only the winner can, while any are left.
|
|
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`.
|
|
5038
5382
|
*/
|
|
5039
5383
|
async claim(c, { take, build = false, ...others } = NO_OPTIONS) {
|
|
5040
5384
|
only(others, ['take', 'build'], 'claim');
|
|
@@ -5065,37 +5409,25 @@ export function solanaApp({
|
|
|
5065
5409
|
// lets it pick (`take`: one asset, or several; the first left by default), as many as fit.
|
|
5066
5410
|
const nfts = [];
|
|
5067
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();
|
|
5068
5416
|
if (contest.nft) {
|
|
5069
5417
|
const proofs = {};
|
|
5070
|
-
const
|
|
5071
|
-
const { before, hasAuthority: _, ...kind } = await
|
|
5072
|
-
proofs[
|
|
5073
|
-
kinds[
|
|
5074
|
-
|
|
5075
|
-
if (Number(own.amount) === 1) {
|
|
5076
|
-
nfts.push(i);
|
|
5077
|
-
await kindOf(own.nft, me);
|
|
5078
|
-
}
|
|
5079
|
-
const share = won && !(own.bits & loot.ENDED) ? loot.shareOf(contest, own) - Number(own.taken) : 0;
|
|
5080
|
-
const left = entries
|
|
5081
|
-
.map((e, j) => ({ e, j }))
|
|
5082
|
-
.filter(({ e }) => Number(e.side) !== Number(contest.winner) && Number(e.amount) === 1);
|
|
5083
|
-
const wanted = take ? [take].flat() : [];
|
|
5084
|
-
const picked = (wanted.length ? left.filter(({ e }) => wanted.includes(e.nft)) : left).slice(
|
|
5085
|
-
0,
|
|
5086
|
-
Math.min(share, 4),
|
|
5087
|
-
);
|
|
5088
|
-
if (wanted.length && picked.length < Math.min(wanted.length, share))
|
|
5089
|
-
throw new Error(`${wanted.join(', ')} is not a loser’s NFT left in this contest for you to pick.`);
|
|
5090
|
-
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 });
|
|
5091
5423
|
nfts.push(j);
|
|
5092
|
-
await kindOf(e.nft, me);
|
|
5093
5424
|
}
|
|
5094
5425
|
// A compressed NFT's proof is long (up to 17 nodes): such a claim moves one NFT, as a v0 transaction with a
|
|
5095
5426
|
// lookup table (`send`), and the next claim takes the next.
|
|
5096
5427
|
if (nfts.length > 1 && nfts.some(j => kinds[entries[j].nft]?.compressed)) nfts.splice(1);
|
|
5097
5428
|
for (const j of nfts) given.before.push(...proofs[entries[j].nft]);
|
|
5098
5429
|
}
|
|
5430
|
+
const closes = nfts.map(j => entries[j].nft).filter(nft => lost.has(nft));
|
|
5099
5431
|
const claimed = [
|
|
5100
5432
|
computeUnitLimit(800_000),
|
|
5101
5433
|
...given.before.filter(x => x.programId !== COMPUTE_BUDGET_PROGRAM),
|
|
@@ -5109,12 +5441,14 @@ export function solanaApp({
|
|
|
5109
5441
|
player: me,
|
|
5110
5442
|
nfts: contest.nft ? nfts : undefined,
|
|
5111
5443
|
kinds,
|
|
5444
|
+
hold: closes,
|
|
5112
5445
|
collection: given.collection,
|
|
5113
5446
|
poolMints: given.poolMints,
|
|
5114
5447
|
items: given.items,
|
|
5115
5448
|
nft: given.nft,
|
|
5116
5449
|
programs,
|
|
5117
5450
|
}),
|
|
5451
|
+
...closes.map(nft => releaseOwed(nft, lost.get(nft).kind, lost.get(nft), me, g)),
|
|
5118
5452
|
];
|
|
5119
5453
|
// A prize that rolls: asked for in the claim's own transaction and settled by a key after (one prompt).
|
|
5120
5454
|
return contest.prize?.Roll && !build ? (await sendRolling(claimed)).signature : send(claimed);
|