@xgamesfun/solana 0.27.0 → 0.27.2
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 +34 -17
- package/bin/xsolana.mjs +72 -7
- package/js/approve/details.mjs +1 -1
- package/js/approve/keyfile.mjs +242 -0
- package/js/approve/live-check.mjs +5 -2
- package/js/approve/server.mjs +94 -13
- package/js/approve/with-key.mjs +298 -0
- package/js/chain/lookup.js +11 -0
- package/js/chain/programs.js +49 -1
- package/js/chain/rpc.js +53 -6
- package/js/economy/check.mjs +2 -2
- package/js/economy/cost.mjs +20 -4
- package/js/economy/explain.mjs +3 -3
- package/js/economy/guide.mjs +2 -2
- package/js/economy/live.mjs +1 -1
- package/js/economy/plan.mjs +34 -6
- package/js/economy/review.mjs +1 -1
- package/js/economy/schema.json +1 -1
- package/js/economy/txsize.mjs +3 -2
- package/js/mcp/server.mjs +3 -3
- package/js/mcp/tools.mjs +51 -14
- package/js/play/index.js +466 -118
- package/package.json +5 -1
- package/release.json +1 -1
- package/skills/xsolana-economy/SKILL.md +58 -19
- package/skills/xsolana-economy/references/after-launch.md +43 -12
- package/skills/xsolana-economy/references/apps.md +7 -3
- package/skills/xsolana-economy/references/sentence-to-spec.md +3 -3
- package/skills/xsolana-economy/references/telling-the-person.md +18 -11
- package/skills/xsolana-economy/references/trust-and-cheating.md +4 -2
- package/types/approve/keyfile.d.mts +36 -0
- package/types/chain/lookup.d.ts +12 -0
- package/types/chain/programs.d.ts +22 -0
- package/types/chain/rpc.d.ts +15 -1
- package/types/economy/plan.d.mts +21 -4
- package/types/mcp/server.d.mts +4 -3
- package/types/mcp/tools.d.mts +14 -5
- package/types/play/index.d.ts +178 -29
package/js/play/index.js
CHANGED
|
@@ -37,12 +37,14 @@ import {
|
|
|
37
37
|
requestHeapFrame,
|
|
38
38
|
syncNative,
|
|
39
39
|
transferLamports,
|
|
40
|
+
withComputeBudget,
|
|
40
41
|
withSolWrap,
|
|
41
42
|
} from '../chain/programs.js';
|
|
42
43
|
import { checkCluster, notMainnet, rpcClient } from '../chain/rpc.js';
|
|
43
44
|
import { extension, mayStartFrozen, mintPowers, resolveTransfers } from '../chain/token2022.js';
|
|
44
45
|
import {
|
|
45
46
|
compileFitting,
|
|
47
|
+
compileKeepingKind,
|
|
46
48
|
closeTableIx,
|
|
47
49
|
deactivateTableIx,
|
|
48
50
|
readTable,
|
|
@@ -74,6 +76,9 @@ export {
|
|
|
74
76
|
|
|
75
77
|
// The options of a call made without any: read-only, and typed so the generated declarations take any option.
|
|
76
78
|
/** A lottery's steps in `claims()`: each depends on the one before, so `claimAll` takes one of a lottery's a call. */
|
|
79
|
+
/** The compute units a sponsored step asks for: all a transaction can have. It sets a price of 0, so they cost nothing. */
|
|
80
|
+
const SPONSORED_UNITS = 1_400_000;
|
|
81
|
+
|
|
77
82
|
const LOTTERY_STEPS = new Set(['reveal', 'cancel-draw', 'pull', 'payout']);
|
|
78
83
|
/** The claims that are one transaction of the wallet's: `claimAll` signs them together. */
|
|
79
84
|
const BATCHED = new Set(['contest', 'release', 'lookup-table', 'close-leaf', ...LOTTERY_STEPS]);
|
|
@@ -421,10 +426,10 @@ export function solanaApp({
|
|
|
421
426
|
* way. Whoever controls a collection the project does not can take an NFT out of the project's hands or burn it. A
|
|
422
427
|
* Core asset shows it by itself (`kind`: its collection now); a Token Metadata NFT by the token account that holds it
|
|
423
428
|
* 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.
|
|
429
|
+
* leaf is now (`before`, by `signer`). `holder`: as `heldNow` takes it. `now`: what `heldNow` read of it already.
|
|
425
430
|
*/
|
|
426
|
-
async function goneProof(asset, signer = wallet.address, { holder = null } = {}) {
|
|
427
|
-
|
|
431
|
+
async function goneProof(asset, signer = wallet.address, { holder = null, now = null } = {}) {
|
|
432
|
+
now ??= await heldNow(asset, { holder });
|
|
428
433
|
if (now.held !== false) return null;
|
|
429
434
|
if (now.das) {
|
|
430
435
|
const proof = await client.call('getAssetProof', { id: asset });
|
|
@@ -439,14 +444,33 @@ export function solanaApp({
|
|
|
439
444
|
/**
|
|
440
445
|
* 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
446
|
* (`goneProof`), what shows that, with `gone` true: the instruction then finishes without it and nothing is sent.
|
|
447
|
+
* `now`: what `heldNow` read of it already.
|
|
442
448
|
*/
|
|
443
|
-
async function leaving(asset, signer = wallet.address, { to, payer = signer } = {}) {
|
|
444
|
-
const proof = await goneProof(asset, signer);
|
|
449
|
+
async function leaving(asset, signer = wallet.address, { to, payer = signer, now = null } = {}) {
|
|
450
|
+
const proof = await goneProof(asset, signer, { now });
|
|
445
451
|
if (!proof) return { ...(await nftOf(asset, signer, { to, payer })), gone: false };
|
|
446
452
|
// A Core asset shows it by itself, with the accounts it always took.
|
|
447
453
|
if (!proof.kind.tokenMetadata && !proof.kind.compressed) return { ...(await nftOf(asset, signer)), gone: true };
|
|
448
454
|
return { before: proof.before, hasAuthority: false, ...proof.kind, gone: true };
|
|
449
455
|
}
|
|
456
|
+
/**
|
|
457
|
+
* The release of a hold a route just made of `asset`, an NFT it owed and could only hand out (`kind`: from
|
|
458
|
+
* `leaving`). The program makes such a hold at `["hold", asset, 0]` for `player`, the route's `signer` paying its
|
|
459
|
+
* rent and `rentTo` named for its NFT's account's (`hold::owe`). The release right after it, by the same signer,
|
|
460
|
+
* ends it: for an NFT proven gone from the project it closes the hold on the proof and nothing is sent (`NftGone`);
|
|
461
|
+
* for any other it sends the NFT on to `player` (`Released`). Either way the hold's rent comes back to the signer.
|
|
462
|
+
* `g`: the project, for a hold owed to the treasury.
|
|
463
|
+
*/
|
|
464
|
+
const releaseOwed = (asset, kind, { player, rentTo }, signer, g) =>
|
|
465
|
+
loot.releaseIx({
|
|
466
|
+
signer,
|
|
467
|
+
project,
|
|
468
|
+
asset,
|
|
469
|
+
...kind,
|
|
470
|
+
hold: { player, payer: signer, rent_to: rentTo, free_at: 0n },
|
|
471
|
+
address: loot.pda.hold(asset, 0),
|
|
472
|
+
treasury: g.treasury,
|
|
473
|
+
});
|
|
450
474
|
/**
|
|
451
475
|
* 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
476
|
* money in: accounts that start frozen or can be switched to it (a payee closes their own account, and the one
|
|
@@ -780,17 +804,26 @@ export function solanaApp({
|
|
|
780
804
|
const { blockhash, lastValidBlockHeight } = await client.latestBlockhash();
|
|
781
805
|
// Lent the most, the step is tried; then sent lent the same, the player giving back last all but what it costs in
|
|
782
806
|
// fees and rent: a step can need more SOL midway than it costs (a wrapped-SOL account it opens and closes).
|
|
783
|
-
|
|
784
|
-
|
|
807
|
+
// It leads with a compute unit limit (all a transaction can have) and a unit price of 0, the price a sponsor
|
|
808
|
+
// pays unless its server says more: a wallet that adds its own priority fee only where a transaction sets none
|
|
809
|
+
// (Phantom does, to one with no signature on it yet) is then handed one it signs as it stands, and the sponsor's
|
|
810
|
+
// server, which refuses a price it did not agree to, takes it. A step keeps the kind of transaction it had: one
|
|
811
|
+
// that fits a legacy transaction stays one (without the two where they would not fit), and only a step that
|
|
812
|
+
// needed the project's lookup tables before goes as v0.
|
|
813
|
+
const compile = (back = 0n) => {
|
|
814
|
+
const list = [
|
|
815
|
+
sponsorTopUp(sponsor.address, me, sponsorMax),
|
|
816
|
+
...instructions,
|
|
817
|
+
...(back > 0n ? [sponsorGiveBack(sponsor.address, me, back)] : []),
|
|
818
|
+
];
|
|
819
|
+
return compileKeepingKind({
|
|
785
820
|
payer: sponsor.address,
|
|
786
821
|
blockhash,
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
...instructions,
|
|
790
|
-
...(back > 0n ? [sponsorGiveBack(sponsor.address, me, back)] : []),
|
|
791
|
-
],
|
|
822
|
+
led: withComputeBudget(list, { units: SPONSORED_UNITS, microLamports: 0n }),
|
|
823
|
+
plain: list,
|
|
792
824
|
tables,
|
|
793
825
|
});
|
|
826
|
+
};
|
|
794
827
|
const tried = compile();
|
|
795
828
|
if (!tried) return null;
|
|
796
829
|
const addresses = namedIn([sponsorTopUp(sponsor.address, me, 0n), ...instructions]);
|
|
@@ -1958,16 +1991,18 @@ export function solanaApp({
|
|
|
1958
1991
|
const lotNow = lot ? await heldNow(lot) : { held: null };
|
|
1959
1992
|
const bidNow = [];
|
|
1960
1993
|
if (sold && auction.nft_bids) for (const nft of auction.held) bidNow.push(await heldNow(nft));
|
|
1961
|
-
const
|
|
1994
|
+
const lostBids = sold && auction.nft_bids ? auction.held.filter((_, i) => bidNow[i].held === false) : [];
|
|
1995
|
+
const gone = lotNow.held === false || lostBids.length > 0;
|
|
1962
1996
|
const ending = sold && lot && (callOff || gone) ? 'CallOff' : 'Deliver';
|
|
1963
1997
|
const home = !sold || ending === 'CallOff';
|
|
1964
1998
|
// The lot to its winner, or home to its seller; a bid's NFTs to whoever owned the lot, or back to their bidder.
|
|
1999
|
+
// One that is gone is not sent: its accounts only show the end that the project no longer holds it.
|
|
1965
2000
|
const before = [];
|
|
1966
|
-
const lotKind = lot ? await kindFor(lot, home ? auction.seller : auction.high_bidder, lotNow
|
|
2001
|
+
const lotKind = lot ? await kindFor(lot, home ? auction.seller : auction.high_bidder, lotNow, before) : {};
|
|
1967
2002
|
const bidTo = ending === 'CallOff' ? auction.high_bidder : owner;
|
|
1968
2003
|
const held = [];
|
|
1969
2004
|
if (sold && auction.nft_bids)
|
|
1970
|
-
for (const [i, nft] of auction.held.entries()) held.push(await kindFor(nft, bidTo, bidNow[i]
|
|
2005
|
+
for (const [i, nft] of auction.held.entries()) held.push(await kindFor(nft, bidTo, bidNow[i], before));
|
|
1971
2006
|
if (sold && !home) holders ??= await holdersOf(auction.shares);
|
|
1972
2007
|
const end = (ending, hold = false) =>
|
|
1973
2008
|
loot.endAuctionIx({ ...base, auction, item, ...lotKind, held, holders, ending, hold });
|
|
@@ -1982,6 +2017,24 @@ export function solanaApp({
|
|
|
1982
2017
|
...(made ? { asset: made } : {}),
|
|
1983
2018
|
};
|
|
1984
2019
|
if (ending === 'CallOff') Object.assign(result, { calledOff: true, gone });
|
|
2020
|
+
// What the end did about an NFT that is gone, in words: nothing here says it moved.
|
|
2021
|
+
if (gone) {
|
|
2022
|
+
const which =
|
|
2023
|
+
lotNow.held === false
|
|
2024
|
+
? `the lot (${lot})`
|
|
2025
|
+
: `${lostBids.length === 1 ? 'an NFT' : 'NFTs'} of the winning bid (${lostBids.join(', ')})`;
|
|
2026
|
+
const did = !sold
|
|
2027
|
+
? 'Nothing went home: the auction is closed, and its rent went back to its seller.'
|
|
2028
|
+
: ending === 'Deliver'
|
|
2029
|
+
? 'Nothing was paid for it: the lot went to its winner, and the rest of the bid was paid.'
|
|
2030
|
+
: lotNow.held === false
|
|
2031
|
+
? `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.`
|
|
2032
|
+
: 'The sale was called off: the lot went home to its seller, and the rest of the bid went back to its bidder.';
|
|
2033
|
+
Object.assign(result, {
|
|
2034
|
+
gone: true,
|
|
2035
|
+
says: `The project no longer holds ${which}: whoever controls its collection took it back or burned it while the auction held it. ${did}`,
|
|
2036
|
+
});
|
|
2037
|
+
}
|
|
1985
2038
|
|
|
1986
2039
|
// 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
2040
|
const outs = [
|
|
@@ -2049,15 +2102,15 @@ export function solanaApp({
|
|
|
2049
2102
|
return holding();
|
|
2050
2103
|
}
|
|
2051
2104
|
|
|
2052
|
-
/** An NFT's kind on an auction's end, going to `to`: what `nftOf` reads, its proof put in `before
|
|
2053
|
-
*
|
|
2054
|
-
|
|
2055
|
-
|
|
2105
|
+
/** An NFT's kind on an auction's end, going to `to`: what `nftOf` reads, its proof put in `before`. `now`: what
|
|
2106
|
+
* `heldNow` read of it; one that is gone carries what shows it instead (a Token Metadata NFT's account that holds it
|
|
2107
|
+
* now, or its mint; a compressed NFT's leaf as it is now), and is sent nowhere. */
|
|
2108
|
+
async function kindFor(asset, to, now, before) {
|
|
2109
|
+
const { before: proof, hasAuthority: _h, gone: _g, ...kind } = await leaving(asset, wallet.address, { to, now });
|
|
2056
2110
|
before.push(...proof);
|
|
2057
2111
|
return {
|
|
2058
2112
|
...kind,
|
|
2059
2113
|
collection: kind.compressed || kind.tokenMetadata ? undefined : ((await collectionOf(asset)) ?? MPL_CORE),
|
|
2060
|
-
...(evidence && kind.tokenMetadata ? { evidence } : {}),
|
|
2061
2114
|
};
|
|
2062
2115
|
}
|
|
2063
2116
|
|
|
@@ -2106,16 +2159,30 @@ export function solanaApp({
|
|
|
2106
2159
|
if (opens.length) await send([...new Map(opens.map(x => [x.keys[1].pubkey, x])).values()]);
|
|
2107
2160
|
const before = [];
|
|
2108
2161
|
const held = [];
|
|
2109
|
-
|
|
2162
|
+
// An NFT of the bid that is gone (its collection took it back or burned it) is not paid: its accounts show the
|
|
2163
|
+
// end that the project no longer holds it, and the end passes it over.
|
|
2164
|
+
const lost = [];
|
|
2165
|
+
if (auction.nft_bids)
|
|
2166
|
+
for (const nft of auction.held) {
|
|
2167
|
+
const now = await heldNow(nft);
|
|
2168
|
+
if (now.held === false) lost.push(nft);
|
|
2169
|
+
held.push(await kindFor(nft, auction.seller, now, before));
|
|
2170
|
+
}
|
|
2110
2171
|
const end = (hold = false) => loot.endAuctionIx({ ...base, auction, held, holders, hold });
|
|
2111
2172
|
const steps = ix => [
|
|
2112
2173
|
computeUnitLimit(1_000_000),
|
|
2113
2174
|
...before.filter(x => x.programId !== COMPUTE_BUDGET_PROGRAM),
|
|
2114
2175
|
ix,
|
|
2115
2176
|
];
|
|
2177
|
+
const passed = lost.length
|
|
2178
|
+
? {
|
|
2179
|
+
gone: true,
|
|
2180
|
+
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.`,
|
|
2181
|
+
}
|
|
2182
|
+
: {};
|
|
2116
2183
|
if (sendPlan({ payer: wallet.address, instructions: steps(end()), alone: isProveLeaf }))
|
|
2117
2184
|
try {
|
|
2118
|
-
return { paid: await send(steps(end())) };
|
|
2185
|
+
return { paid: await send(steps(end())), ...passed };
|
|
2119
2186
|
} catch (e) {
|
|
2120
2187
|
// The lot is the winner's already: what is owed waits until the account it goes into can take it.
|
|
2121
2188
|
return {
|
|
@@ -2123,9 +2190,9 @@ export function solanaApp({
|
|
|
2123
2190
|
says: `The lot is the winner’s; the money waits (${e.message}): end the auction again to pay it.`,
|
|
2124
2191
|
};
|
|
2125
2192
|
}
|
|
2126
|
-
const outs = auction.held.map(x => [x, auction.seller]);
|
|
2127
|
-
const { signature, releases } = await heldThenReleased(at, steps(end(
|
|
2128
|
-
return { paid: signature, releases };
|
|
2193
|
+
const outs = auction.held.filter(x => !lost.includes(x)).map(x => [x, auction.seller]);
|
|
2194
|
+
const { signature, releases } = await heldThenReleased(at, steps(end(outs.map(([x]) => x))), outs);
|
|
2195
|
+
return { paid: signature, releases, ...passed };
|
|
2129
2196
|
}
|
|
2130
2197
|
|
|
2131
2198
|
/**
|
|
@@ -2141,9 +2208,12 @@ export function solanaApp({
|
|
|
2141
2208
|
const signature = await send([...instructions, fund]);
|
|
2142
2209
|
const releases = [];
|
|
2143
2210
|
for (const [asset, to] of outs) {
|
|
2211
|
+
// Taken back or burned since the end made it a hold: its release closes the hold on that proof, and sends nothing.
|
|
2212
|
+
let lost = false;
|
|
2144
2213
|
const release = async signer => {
|
|
2145
2214
|
const hold = await read('Hold', loot.pda.hold(asset, 0));
|
|
2146
|
-
const { before: proof, hasAuthority: _h, ...kind } = await
|
|
2215
|
+
const { before: proof, hasAuthority: _h, gone, ...kind } = await leaving(asset, signer, { to });
|
|
2216
|
+
lost = gone;
|
|
2147
2217
|
return [
|
|
2148
2218
|
...proof,
|
|
2149
2219
|
loot.releaseIx({
|
|
@@ -2160,7 +2230,8 @@ export function solanaApp({
|
|
|
2160
2230
|
const steps = await release(key.address);
|
|
2161
2231
|
const byKey = (await dryRun(key.address, steps)).ok ? await sendAs(key, steps).catch(() => null) : null;
|
|
2162
2232
|
// One that still can't move (frozen for now) stays a hold: claims() lists it for whoever it is owed to.
|
|
2163
|
-
|
|
2233
|
+
const sent = byKey ?? (await send(await release(wallet.address)).catch(e => ({ asset, waits: e.message })));
|
|
2234
|
+
releases.push(lost && typeof sent === 'string' ? { asset, gone: true, closed: sent } : sent);
|
|
2164
2235
|
}
|
|
2165
2236
|
await emptyKey(key);
|
|
2166
2237
|
return { signature, releases };
|
|
@@ -2173,6 +2244,50 @@ export function solanaApp({
|
|
|
2173
2244
|
const GONE_LISTING =
|
|
2174
2245
|
'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
2246
|
|
|
2247
|
+
/**
|
|
2248
|
+
* What ending an auction does when its lot or an NFT of its winning bid is gone, in words, for `claims()`: `x` the
|
|
2249
|
+
* auction, `me` the wallet told (its seller, or its high bidder), `lot` the lot when it is gone, `bids` the NFTs of
|
|
2250
|
+
* the winning bid that are gone.
|
|
2251
|
+
*/
|
|
2252
|
+
function goneSaleSays(name, x, me, lot, bids) {
|
|
2253
|
+
const [sold, owed] = [BigInt(x.high) > 0n, BigInt(x.proceeds_owed) > 0n];
|
|
2254
|
+
const mine = sold && x.high_bidder === me;
|
|
2255
|
+
const why = 'whoever controls its collection took it back or burned it while the auction held it';
|
|
2256
|
+
const lostBids = `${bids.length === 1 ? 'an NFT' : 'NFTs'} of ${mine ? 'your' : 'the winning'} bid (${bids.join(', ')})`;
|
|
2257
|
+
if (lot && !sold)
|
|
2258
|
+
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.`;
|
|
2259
|
+
if (lot) {
|
|
2260
|
+
const back = x.nft_bids
|
|
2261
|
+
? `the NFTs of ${mine ? 'your' : 'the winning'} bid that the project still holds go back to ${mine ? 'you' : 'their bidder'}`
|
|
2262
|
+
: mine
|
|
2263
|
+
? 'your bid comes back to you'
|
|
2264
|
+
: 'the winning bid goes back to its bidder';
|
|
2265
|
+
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.` : ''}`;
|
|
2266
|
+
}
|
|
2267
|
+
const first = `The project no longer holds ${lostBids}: ${why}.`;
|
|
2268
|
+
// A player's lot that has not moved: the two sides move together or not at all, so the sale is called off.
|
|
2269
|
+
if (x.lot.Asset && !owed)
|
|
2270
|
+
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.`;
|
|
2271
|
+
// The lot went first, or it is the project's own: the end passes that NFT over and pays the rest.
|
|
2272
|
+
return owed
|
|
2273
|
+
? `${first} The winner of “${name}” has the lot: ending it again pays you the rest of the bid, and nothing for the one gone.`
|
|
2274
|
+
: `${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.`;
|
|
2275
|
+
}
|
|
2276
|
+
/** What claiming does about the NFTs of a round's parts that are gone, in words, for `claims()`: `tickets` the NFTs
|
|
2277
|
+
* that paid for the wallet's tickets, `prizes` the NFTs its places won; `back` when its tickets go back to their
|
|
2278
|
+
* buyers (a round called off, a lottery that returns its tickets). */
|
|
2279
|
+
const goneRoundSays = (tickets, prizes, back) =>
|
|
2280
|
+
[
|
|
2281
|
+
tickets.length
|
|
2282
|
+
? `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.`
|
|
2283
|
+
: '',
|
|
2284
|
+
prizes.length
|
|
2285
|
+
? `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.`
|
|
2286
|
+
: '',
|
|
2287
|
+
]
|
|
2288
|
+
.filter(Boolean)
|
|
2289
|
+
.join(' ');
|
|
2290
|
+
|
|
2176
2291
|
/**
|
|
2177
2292
|
* This project's records of `asset`: its listing at `["listing", project, asset]` (one read) and its holds (one
|
|
2178
2293
|
* search, by the asset's address). Each is { kind: 'listing' | 'hold', address, record }.
|
|
@@ -2193,6 +2308,30 @@ export function solanaApp({
|
|
|
2193
2308
|
return out;
|
|
2194
2309
|
}
|
|
2195
2310
|
|
|
2311
|
+
/**
|
|
2312
|
+
* The stashes of this project that list `asset`: a project's stash (`kind` 'stash') or a lottery's own
|
|
2313
|
+
* ('lottery-stash'), each { kind, address, id } (`id`: the stash's, or the lottery's). A stash keeps its NFTs in
|
|
2314
|
+
* rows of no fixed place, so no filter finds one NFT's row: this is one search of the project's stashes
|
|
2315
|
+
* (`getProgramAccounts` by tag, project and payload), which returns every stash of the project whole, 36 bytes for
|
|
2316
|
+
* each NFT a stash lists, and reads them here.
|
|
2317
|
+
*/
|
|
2318
|
+
async function stashesOf(asset) {
|
|
2319
|
+
const at = (offset, bytes) => ({ memcmp: { offset, bytes } });
|
|
2320
|
+
const found = await client.programAccounts(loot.LOOT_PROGRAM, [
|
|
2321
|
+
at(0, encodeBase58(Uint8Array.of(loot.TAGS.Table))),
|
|
2322
|
+
at(1, project),
|
|
2323
|
+
at(37, encodeBase58(Uint8Array.of(loot.PAYLOAD.Nft))),
|
|
2324
|
+
]);
|
|
2325
|
+
const out = [];
|
|
2326
|
+
for (const { pubkey, account } of found) {
|
|
2327
|
+
const stash = loot.decodeStash(account.data);
|
|
2328
|
+
if (!stash?.nfts.includes(asset)) continue;
|
|
2329
|
+
const own = pubkey === loot.pda.stash(project, stash.id);
|
|
2330
|
+
out.push({ kind: own ? 'stash' : 'lottery-stash', address: pubkey, id: stash.id });
|
|
2331
|
+
}
|
|
2332
|
+
return out;
|
|
2333
|
+
}
|
|
2334
|
+
|
|
2196
2335
|
/**
|
|
2197
2336
|
* The records of this project an earlier owner of `asset` left behind (`recordsOf`), each that names a wallet other
|
|
2198
2337
|
* than the player's: { kind, address, record, says }. Such a record is left when whoever controls the NFT's
|
|
@@ -2470,6 +2609,77 @@ export function solanaApp({
|
|
|
2470
2609
|
];
|
|
2471
2610
|
}
|
|
2472
2611
|
|
|
2612
|
+
/** How many of the losers' NFTs left a winner's claim reads to choose its picks among. */
|
|
2613
|
+
const PICKS_READ = 8;
|
|
2614
|
+
/**
|
|
2615
|
+
* The NFTs a claim of entry `i` of a contest of NFTs moves, as `claim` sends it: the entry's own while the contest
|
|
2616
|
+
* holds it, and for a winner the losers' NFTs its share still lets it pick, at most 4 a claim. `take` names the
|
|
2617
|
+
* picks (one asset, or several). With none named, the first of the losers' NFTs left that the project still holds
|
|
2618
|
+
* are picked (the first `PICKS_READ` are read), and one that is gone only when no other is left there: a pick of an
|
|
2619
|
+
* NFT gone takes nothing. Each is { j, nft, gone }: its entry's place in the list, and whether the project no longer
|
|
2620
|
+
* holds it (its collection took it back or burned it).
|
|
2621
|
+
*/
|
|
2622
|
+
async function contestNfts(contest, entries, i, take) {
|
|
2623
|
+
const own = entries[i];
|
|
2624
|
+
const won = contest.verdict === 'Won' && Number(contest.winner) === Number(own.side);
|
|
2625
|
+
const out = Number(own.amount) === 1 ? [{ j: i, nft: own.nft }] : [];
|
|
2626
|
+
const share = won && !(own.bits & loot.ENDED) ? loot.shareOf(contest, own) - Number(own.taken) : 0;
|
|
2627
|
+
const left = entries
|
|
2628
|
+
.map((e, j) => ({ j, nft: e.nft, e }))
|
|
2629
|
+
.filter(({ e }) => Number(e.side) !== Number(contest.winner) && Number(e.amount) === 1);
|
|
2630
|
+
const wanted = take ? [take].flat() : [];
|
|
2631
|
+
const among =
|
|
2632
|
+
share <= 0 ? [] : wanted.length ? left.filter(x => wanted.includes(x.nft)) : left.slice(0, PICKS_READ);
|
|
2633
|
+
if (wanted.length && among.length < Math.min(wanted.length, share))
|
|
2634
|
+
throw new Error(`${wanted.join(', ')} is not a loser’s NFT left in this contest for you to pick.`);
|
|
2635
|
+
const heldOf = await heldEach([...out, ...among].map(x => x.nft));
|
|
2636
|
+
const isGone = x => heldOf.get(x.nft)?.held === false;
|
|
2637
|
+
const picks = wanted.length ? among : [...among.filter(x => !isGone(x)), ...among.filter(isGone)];
|
|
2638
|
+
out.push(...picks.slice(0, Math.min(share, 4)));
|
|
2639
|
+
return out.map(x => ({ j: x.j, nft: x.nft, gone: isGone(x) }));
|
|
2640
|
+
}
|
|
2641
|
+
/** What a contest's claim does about its NFTs that are gone, in words, for `claims()`: `own` the wallet's own NFT
|
|
2642
|
+
* when it is gone, `picks` the losers' NFTs gone that its picks land on. */
|
|
2643
|
+
const goneContestSays = (own, picks) =>
|
|
2644
|
+
[
|
|
2645
|
+
own
|
|
2646
|
+
? `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.`
|
|
2647
|
+
: '',
|
|
2648
|
+
picks.length
|
|
2649
|
+
? `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.`
|
|
2650
|
+
: '',
|
|
2651
|
+
]
|
|
2652
|
+
.filter(Boolean)
|
|
2653
|
+
.join(' ');
|
|
2654
|
+
|
|
2655
|
+
/**
|
|
2656
|
+
* The NFTs the pulls of a wallet's parts of round `r` (`mine`: [{ entry, parts }]) are to hand out: each entry's NFT
|
|
2657
|
+
* ticket ('Own'), and each NFT its places are still owed (`Prize`: the lottery's own stash's, as the pulls take them
|
|
2658
|
+
* out; a project stash's named one). Returns { tickets, prizes }, their addresses.
|
|
2659
|
+
*/
|
|
2660
|
+
async function roundNfts(r, mine) {
|
|
2661
|
+
const [tickets, prizes] = [[], []];
|
|
2662
|
+
let own;
|
|
2663
|
+
for (const { entry, parts } of mine) {
|
|
2664
|
+
const e = r.entries[entry];
|
|
2665
|
+
for (const part of parts) {
|
|
2666
|
+
if (part === 'Own' && e.nft !== NOBODY) tickets.push(e.nft);
|
|
2667
|
+
if (!part.Prize || loot.tokenRun(r, Number(part.Prize[0]))) continue;
|
|
2668
|
+
const run = Number(part.Prize[0]);
|
|
2669
|
+
const place = r.terms.places[run];
|
|
2670
|
+
if (place.prize?.Nft) prizes.push(place.prize.Nft.nft);
|
|
2671
|
+
if (!loot.ownStash(place)) continue;
|
|
2672
|
+
own ??= { stash: loot.decodeStash((await client.account(loot.pda.lotteryStash(r.lottery)))?.data ?? null) };
|
|
2673
|
+
const predicted = loot.stashNfts(r, own.stash);
|
|
2674
|
+
for (const p of loot.prizePlacesOwed(r, entry, run)) {
|
|
2675
|
+
const nft = predicted[loot.nftSlot(r, p)];
|
|
2676
|
+
if (nft) prizes.push(nft);
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
return { tickets, prizes };
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2473
2683
|
/**
|
|
2474
2684
|
* When a draw of round `r` (at `key`) would cancel it (an open round short of its least tickets; a waiting one past
|
|
2475
2685
|
* the timeout), the pulls of `who`'s own parts as the cancelled round owes them (a few entries' worth), to go right
|
|
@@ -2529,6 +2739,11 @@ export function solanaApp({
|
|
|
2529
2739
|
* The pulls of `parts` of entry `i` of a round (`partsOwed`; `i` null for parts owed in common, `commonOwed`), signed by
|
|
2530
2740
|
* `signer`, and the rolls they make ([{ order, box }], `loot.pullRolls`). With `closes`, the last pull carries the
|
|
2531
2741
|
* round's close (when the parts it pays are the last owed).
|
|
2742
|
+
* An NFT of a part that is proven gone from the project is not sent anywhere. A ticket whose price sends it on
|
|
2743
|
+
* passes what proves it gone, and its part settles (`NftGone`, the round its record). A ticket going back to its
|
|
2744
|
+
* buyer and a place's NFT are only handed out by the program: the pull passes each as its hold, and a release on
|
|
2745
|
+
* the proof follows it here, which closes the hold (`NftGone`, the hold its record). A pull makes holds of all its
|
|
2746
|
+
* prize NFTs or of none, so with one of them gone the others are made holds too and released on to their winner.
|
|
2532
2747
|
*/
|
|
2533
2748
|
async function pulls(key, r, i, parts, signer, { closes = false, lotteryClosed = false } = {}) {
|
|
2534
2749
|
const e = i == null ? null : r.entries[i];
|
|
@@ -2544,20 +2759,38 @@ export function solanaApp({
|
|
|
2544
2759
|
const extra = await hookTail([...roundMints(r), ...r.prizes.map(p => p.mint)]);
|
|
2545
2760
|
for (const [k, part] of parts.entries()) {
|
|
2546
2761
|
const more = {};
|
|
2762
|
+
// The NFTs this pull makes holds in place of handing them out (`hold::owe`: for whom, and whose its NFT's
|
|
2763
|
+
// account's rent is), each released right after it. The round only hands an NFT out and reads nothing, so one
|
|
2764
|
+
// proven gone from the project is made a hold, and that hold closes on the proof.
|
|
2765
|
+
const holdsMade = new Map();
|
|
2547
2766
|
if (part === 'Own' && e.nft !== NOBODY) {
|
|
2548
2767
|
const then = r.status === 'Cancelled' || r.terms.returns ? null : r.terms.nft_ticket?.then;
|
|
2549
2768
|
more.handBack = Boolean(then?.Give) && loot.handsBack(await client.account(then.Give[0]));
|
|
2550
2769
|
const to = !then || more.handBack ? e.buyer : then.Give ? then.Give[0] : undefined;
|
|
2551
|
-
const { before = [], hasAuthority: _, ...kind } = await
|
|
2770
|
+
const { before = [], hasAuthority: _, gone, ...kind } = await leaving(e.nft, signer, { to });
|
|
2552
2771
|
out.push(...before);
|
|
2553
|
-
|
|
2772
|
+
// Where its price sends it on (burned, given, stashed) the round reads the ticket itself: proven gone, it goes
|
|
2773
|
+
// nowhere and the part settles. Going back to its buyer it is only handed out: made a hold, closed below.
|
|
2774
|
+
if (gone && !then) {
|
|
2775
|
+
more.hold = true;
|
|
2776
|
+
holdsMade.set(e.nft, { kind, player: e.buyer, rentTo: e.buyer });
|
|
2777
|
+
} else more.ticket = kind;
|
|
2554
2778
|
} else if (part.Prize && !loot.tokenRun(r, Number(part.Prize[0]))) {
|
|
2555
2779
|
// 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
2780
|
// out), a project stash's NFT, an item, or a roll (asked in the same transaction).
|
|
2557
2781
|
const run = Number(part.Prize[0]);
|
|
2558
2782
|
const given = r.terms.places[run].prize;
|
|
2559
|
-
|
|
2783
|
+
// An NFT's kind is read below, with whether the project still holds it.
|
|
2784
|
+
const inputs = await prizeInputs([given.Nft || given.Held ? null : given], signer, e.buyer);
|
|
2560
2785
|
out.push(...inputs.before);
|
|
2786
|
+
const handed = new Map();
|
|
2787
|
+
const hand = async nft => {
|
|
2788
|
+
if (inputs.nfts[nft]) return;
|
|
2789
|
+
const { before, hasAuthority: _, gone, ...kind } = await leaving(nft, signer, { to: e.buyer });
|
|
2790
|
+
inputs.nfts[nft] = kind;
|
|
2791
|
+
out.push(...before);
|
|
2792
|
+
handed.set(nft, { kind, gone, player: e.buyer, rentTo: signer });
|
|
2793
|
+
};
|
|
2561
2794
|
const stash = loot.ownStash(r.terms.places[run])
|
|
2562
2795
|
? loot.decodeStash((await client.account(loot.pda.lotteryStash(r.lottery)))?.data ?? null)
|
|
2563
2796
|
: null;
|
|
@@ -2569,10 +2802,7 @@ export function solanaApp({
|
|
|
2569
2802
|
const predicted = loot.stashNfts(r, stash);
|
|
2570
2803
|
for (const place of loot.prizePlacesOwed(r, i, run)) {
|
|
2571
2804
|
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);
|
|
2805
|
+
if (nft) await hand(nft);
|
|
2576
2806
|
}
|
|
2577
2807
|
}
|
|
2578
2808
|
const stashes = given.Nft
|
|
@@ -2582,10 +2812,17 @@ export function solanaApp({
|
|
|
2582
2812
|
) ?? { nfts: [] },
|
|
2583
2813
|
}
|
|
2584
2814
|
: {};
|
|
2815
|
+
// A project stash's named NFT is handed out only while that stash lists it.
|
|
2816
|
+
if (given.Nft && stashes[given.Nft.stash].nfts.includes(given.Nft.nft)) await hand(given.Nft.nft);
|
|
2817
|
+
// One of them gone: a pull makes holds of all its NFTs or of none, so each becomes a hold, and its release
|
|
2818
|
+
// below closes the one that is gone and sends any other on to its winner.
|
|
2819
|
+
const holds = [...handed.values()].some(x => x.gone);
|
|
2820
|
+
if (holds) for (const [nft, x] of handed) holdsMade.set(nft, x);
|
|
2585
2821
|
more.prize = {
|
|
2586
2822
|
stash,
|
|
2587
2823
|
stashes,
|
|
2588
2824
|
nfts: inputs.nfts,
|
|
2825
|
+
hold: holds,
|
|
2589
2826
|
collection: inputs.collection,
|
|
2590
2827
|
poolMints: inputs.poolMints,
|
|
2591
2828
|
items: inputs.items,
|
|
@@ -2620,7 +2857,8 @@ export function solanaApp({
|
|
|
2620
2857
|
) && (await dryRun(signer, tx)).ok
|
|
2621
2858
|
);
|
|
2622
2859
|
};
|
|
2623
|
-
|
|
2860
|
+
// (Made holds, each NFT's release follows the pull in its transaction: a few places a pull then.)
|
|
2861
|
+
let most = holds ? Math.min(owed, 4) : owed;
|
|
2624
2862
|
while (most > 1 && !(await sized(most))) most = Math.max(1, Math.floor(most * 0.75));
|
|
2625
2863
|
if (most < owed) [more.prize.most, partial] = [most, true];
|
|
2626
2864
|
heap = true;
|
|
@@ -2647,21 +2885,26 @@ export function solanaApp({
|
|
|
2647
2885
|
}
|
|
2648
2886
|
const last = k === parts.length - 1;
|
|
2649
2887
|
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
|
-
)
|
|
2888
|
+
const pull = loot.pullIx({
|
|
2889
|
+
payer: signer,
|
|
2890
|
+
project,
|
|
2891
|
+
roundAddress: key,
|
|
2892
|
+
round,
|
|
2893
|
+
entry: i ?? 0,
|
|
2894
|
+
part,
|
|
2895
|
+
projectState: g,
|
|
2896
|
+
lotteryClosed,
|
|
2897
|
+
programs,
|
|
2898
|
+
extra,
|
|
2899
|
+
...more,
|
|
2900
|
+
});
|
|
2901
|
+
out.push(pull);
|
|
2902
|
+
// Each hold this pull makes (a pull that stops short carries only some) is released at once: one whose NFT is
|
|
2903
|
+
// gone closes on the proof and nothing is sent, any other goes on to its winner, and the hold's rent comes back
|
|
2904
|
+
// to the signer, who paid it.
|
|
2905
|
+
for (const [nft, { kind, player, rentTo }] of holdsMade)
|
|
2906
|
+
if (pull.keys.some(x => x.pubkey === loot.pda.hold(nft, 0)))
|
|
2907
|
+
out.push(releaseOwed(nft, kind, { player, rentTo }, signer, g));
|
|
2665
2908
|
}
|
|
2666
2909
|
return { instructions: out, rolls, heap, partial };
|
|
2667
2910
|
}
|
|
@@ -4033,7 +4276,9 @@ export function solanaApp({
|
|
|
4033
4276
|
* Bids `amount` (whole units) on an auction. The bid it tops goes back to its bidder in the same transaction
|
|
4034
4277
|
* (the program opens their account again if they closed it, paid by this bidder). In SOL, only what the player's
|
|
4035
4278
|
* wrapped-SOL account does not hold already is wrapped, and the account stays open: if this bid is topped, it comes
|
|
4036
|
-
* back there.
|
|
4279
|
+
* back there. In an auction of NFT bids, an NFT of the topped bid that the project no longer holds (its collection
|
|
4280
|
+
* 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
|
|
4281
|
+
* proof in the same transaction, so a high bid with an NFT gone can still be topped.
|
|
4037
4282
|
*/
|
|
4038
4283
|
async bid(auctionKey, amount, { shown, listProof, nfts, acceptLeftover, ...others } = NO_OPTIONS) {
|
|
4039
4284
|
only(others, ['shown', 'listProof', 'nfts', 'acceptLeftover'], 'bid');
|
|
@@ -4059,9 +4304,17 @@ export function solanaApp({
|
|
|
4059
4304
|
};
|
|
4060
4305
|
const bid = [];
|
|
4061
4306
|
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.
|
|
4307
|
+
// The bid it tops goes back to its bidder: into a fresh account of theirs when their own can't take it. An
|
|
4308
|
+
// NFT of it that is gone (its collection took it back or burned it) can't go back: the auction only hands it
|
|
4309
|
+
// out, so it is made a hold for its bidder, and that hold closes on the proof right after the bid.
|
|
4063
4310
|
const held = [];
|
|
4064
|
-
|
|
4311
|
+
const lost = new Map();
|
|
4312
|
+
for (const asset of auction.held) {
|
|
4313
|
+
const { before: proof, gone, ...kind } = await leaving(asset, wallet.address, { to: auction.high_bidder });
|
|
4314
|
+
before.push(...proof);
|
|
4315
|
+
held.push({ asset, ...kind });
|
|
4316
|
+
if (gone) lost.set(asset, kind);
|
|
4317
|
+
}
|
|
4065
4318
|
const ix = loot.bidIx({
|
|
4066
4319
|
bidder: wallet.address,
|
|
4067
4320
|
project,
|
|
@@ -4071,13 +4324,19 @@ export function solanaApp({
|
|
|
4071
4324
|
listProof,
|
|
4072
4325
|
nfts: bid,
|
|
4073
4326
|
held,
|
|
4327
|
+
hold: [...lost.keys()],
|
|
4074
4328
|
programs,
|
|
4075
4329
|
});
|
|
4330
|
+
const g = await projectNow();
|
|
4331
|
+
const closes = [...lost].map(([asset, { hasAuthority: _, ...kind }]) =>
|
|
4332
|
+
releaseOwed(asset, kind, { player: auction.high_bidder, rentTo: auction.high_bidder }, wallet.address, g),
|
|
4333
|
+
);
|
|
4076
4334
|
return send(
|
|
4077
4335
|
await closesFirst(clears, [
|
|
4078
4336
|
computeUnitLimit(800_000),
|
|
4079
4337
|
...before.filter(x => x.programId !== COMPUTE_BUDGET_PROGRAM),
|
|
4080
4338
|
ix,
|
|
4339
|
+
...closes,
|
|
4081
4340
|
]),
|
|
4082
4341
|
);
|
|
4083
4342
|
}
|
|
@@ -4125,6 +4384,12 @@ export function solanaApp({
|
|
|
4125
4384
|
* the sale is called off on that evidence, which anyone may: the bid goes back to its bidder (`{ calledOff: true }`).
|
|
4126
4385
|
* With no bid, the lot goes home to its seller. An end too big for one transaction otherwise makes each NFT that may
|
|
4127
4386
|
* be a hold one, and sends each on with a release after it (`releases`).
|
|
4387
|
+
* An NFT that is gone is never sent, whatever its kind, and the result says so (`gone: true`, and `says`, what the
|
|
4388
|
+
* end did in words): a lot gone with no bid stays where it is and the auction closes, its rent to its seller; a
|
|
4389
|
+
* sale with the lot or an NFT of the bid gone is called off, and what the project still holds goes back; for the
|
|
4390
|
+
* 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.
|
|
4391
|
+
* A hold made at the end whose NFT goes before its release is closed on the proof: its entry in `releases` is
|
|
4392
|
+
* then `{ asset, gone: true, closed }`, `closed` the signature.
|
|
4128
4393
|
*/
|
|
4129
4394
|
async endAuction(auctionKey, { holders, ...others } = NO_OPTIONS) {
|
|
4130
4395
|
only(others, ['holders'], 'endAuction');
|
|
@@ -4375,27 +4640,47 @@ export function solanaApp({
|
|
|
4375
4640
|
|
|
4376
4641
|
/**
|
|
4377
4642
|
* 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
|
-
*
|
|
4643
|
+
* took it out of the project's hands or burned it): each hold of it (`release`), its listing (`take_listing`),
|
|
4644
|
+
* and its row on each of the project's stashes that lists it (`stash`, out), on the proof that it is gone. Anyone
|
|
4645
|
+
* 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
|
|
4646
|
+
* debt ends unpaid. The sender pays the transaction's fee only; each record's rent goes to whoever that record
|
|
4647
|
+
* names (a hold's payer, a listing's maker), never to the sender, and a stash keeps the rent its row no longer
|
|
4648
|
+
* needs (it goes to the sender only when that is one of the project's keys). With its row off a stash, no roll
|
|
4649
|
+
* draws the NFT, and a prize that names it is passed over in place of failing on an NFT that can't move.
|
|
4650
|
+
* What is left is in `waits`: a listing whose rental still runs (the program keeps a running rental, and the
|
|
4651
|
+
* listing closes once it has run), and a lottery's own stash that lists the NFT (`kind` 'lottery-stash', with its
|
|
4652
|
+
* `lottery`, the lottery's id): only the project's keys take a row out of that one (`unraffle`), gone or not.
|
|
4653
|
+
* Refused in words, with nothing sent, when the project still holds the NFT or nothing shows it gone.
|
|
4654
|
+
* Besides one search of the NFT's holds, this makes one search of the project's stashes, which returns every
|
|
4655
|
+
* stash of the project whole (36 bytes for each NFT a stash lists): a stash's rows have no fixed place to look in.
|
|
4656
|
+
* Returns { signature, closed: [{ kind, address }], unstashed: [{ address, id }], waits: [{ kind, address }] }:
|
|
4657
|
+
* `closed` the holds and the listing it closed, `unstashed` the project's stashes it took the row off;
|
|
4658
|
+
* `signature` is null when there was nothing to close.
|
|
4386
4659
|
*/
|
|
4387
4660
|
async closeGone(asset) {
|
|
4388
|
-
const records = await recordsOf(asset);
|
|
4389
|
-
if (!records.length) return { signature: null, closed: [], waits: [] };
|
|
4661
|
+
const [records, stashes] = [await recordsOf(asset), await stashesOf(asset)];
|
|
4662
|
+
if (!records.length && !stashes.length) return { signature: null, closed: [], unstashed: [], waits: [] };
|
|
4390
4663
|
const proof = await goneProof(asset);
|
|
4391
4664
|
if (!proof)
|
|
4392
4665
|
throw new Error(
|
|
4393
4666
|
`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
4667
|
);
|
|
4395
4668
|
const { closes, closed, waits } = await closesOf(asset, records, proof);
|
|
4396
|
-
const
|
|
4397
|
-
|
|
4398
|
-
|
|
4669
|
+
const listed = stashes.filter(x => x.kind === 'stash');
|
|
4670
|
+
// The row comes off each of the project's stashes: free, nothing moves, and where it would go is not read.
|
|
4671
|
+
const offs = listed.map(({ id }) =>
|
|
4672
|
+
loot.stashIx({ signer: wallet.address, project, id, asset, out: wallet.address, ...proof.kind }),
|
|
4673
|
+
);
|
|
4674
|
+
const left = [
|
|
4675
|
+
...waits.map(({ kind, address }) => ({ kind, address })),
|
|
4676
|
+
...stashes
|
|
4677
|
+
.filter(x => x.kind === 'lottery-stash')
|
|
4678
|
+
.map(({ kind, address, id }) => ({ kind, address, lottery: id })),
|
|
4679
|
+
];
|
|
4680
|
+
const unstashed = listed.map(({ address, id }) => ({ address, id }));
|
|
4681
|
+
const instructions = [...(closes.length ? closes : offs.length ? proof.before : []), ...offs];
|
|
4682
|
+
if (!instructions.length) return { signature: null, closed, unstashed, waits: left };
|
|
4683
|
+
return { signature: await send(instructions), closed, unstashed, waits: left };
|
|
4399
4684
|
},
|
|
4400
4685
|
|
|
4401
4686
|
// ---------------------------------------------------------------- raffles
|
|
@@ -4446,9 +4731,20 @@ export function solanaApp({
|
|
|
4446
4731
|
return send([loot.upsertIx({ owner: wallet.address, project, def: { Lottery: { id, args } }, lotteryState })]);
|
|
4447
4732
|
},
|
|
4448
4733
|
|
|
4449
|
-
/**
|
|
4734
|
+
/**
|
|
4735
|
+
* The raffle's owner takes the NFT back out of lottery `id`'s own stash once nothing promises it (the lottery
|
|
4736
|
+
* ended, or no place names it). When the project no longer holds the NFT (its collection took it back or burned
|
|
4737
|
+
* it), only its row comes off the stash, on that proof: nothing is sent, and the owner gets no NFT back. Only the
|
|
4738
|
+
* project's owner or authority may, gone or not: a lottery's own stash is theirs alone (`NotAuthority` for anyone
|
|
4739
|
+
* else; `closeGone` leaves it and says so). Returns the signature.
|
|
4740
|
+
*/
|
|
4450
4741
|
async unraffle(id, nft) {
|
|
4451
|
-
const {
|
|
4742
|
+
const {
|
|
4743
|
+
before,
|
|
4744
|
+
hasAuthority: _,
|
|
4745
|
+
gone: _g,
|
|
4746
|
+
...piece
|
|
4747
|
+
} = await leaving(nft, wallet.address, { to: wallet.address });
|
|
4452
4748
|
const lotteryState = await readMaybe('Lottery', loot.pda.lottery(project, id));
|
|
4453
4749
|
return send([
|
|
4454
4750
|
...before,
|
|
@@ -4741,6 +5037,10 @@ export function solanaApp({
|
|
|
4741
5037
|
* SOL), and a drawn lottery's unpaid places, which anyone may pay so the next round can open. [{ kind, says, … }];
|
|
4742
5038
|
* a winner's first claim of a contest of tokens carries `potUnits` and `prizeUnits`, its part as the decision wrote
|
|
4743
5039
|
* it (base units, as text). Pass one to `claim`.
|
|
5040
|
+
* A `pull`, a `contest` or an `auction` whose claim would hand out an NFT the project no longer holds is flagged
|
|
5041
|
+
* `gone` too, with `assets` (those NFTs) and `says`, what the claim does about each in words: a ticket's NFT, a
|
|
5042
|
+
* place's NFT, a contest's entry or a loser's NFT picked is not sent anywhere and its part closes; an auction with
|
|
5043
|
+
* its lot or an NFT of its winning bid gone is called off, or ended without it. Show `says` before the player signs.
|
|
4744
5044
|
*/
|
|
4745
5045
|
async claims() {
|
|
4746
5046
|
const me = wallet.address;
|
|
@@ -4776,8 +5076,8 @@ export function solanaApp({
|
|
|
4776
5076
|
const off = loot.contestOff(contest, time);
|
|
4777
5077
|
if (!(off || contest.verdict === 'Won')) continue;
|
|
4778
5078
|
const list = await client.account(loot.contestEntries(pubkey, contest));
|
|
4779
|
-
const
|
|
4780
|
-
|
|
5079
|
+
const entries = list?.owner === loot.LOOT_PROGRAM ? loot.decodeEntries(list.data) : [];
|
|
5080
|
+
const entry = entries.find(e => e.player === me);
|
|
4781
5081
|
if (!entry) continue;
|
|
4782
5082
|
const won = !off && Number(contest.winner) === Number(entry.side);
|
|
4783
5083
|
const first = won && !(entry.bits & loot.CLAIMED);
|
|
@@ -4800,11 +5100,32 @@ export function solanaApp({
|
|
|
4800
5100
|
: 'Your share of the pot.';
|
|
4801
5101
|
// A winner's part of a contest of tokens is what the decision wrote, whoever claims first (`contestWinnings`).
|
|
4802
5102
|
const winnings = first && !contest.nft ? loot.contestWinnings(contest, entry.amount) : null;
|
|
5103
|
+
// A contest of NFTs: what its claim moves, and which of those the project no longer holds (its collection
|
|
5104
|
+
// took it back or burned it). Such an NFT is not sent anywhere: the claim closes it, and says so here first.
|
|
5105
|
+
const moves = contest.nft ? await contestNfts(contest, entries, entries.indexOf(entry), undefined) : [];
|
|
5106
|
+
const lost = moves.filter(m => m.gone);
|
|
5107
|
+
const mine = lost.find(m => m.nft === entry.nft && entries[m.j] === entry);
|
|
5108
|
+
const there = [
|
|
5109
|
+
moves.some(m => entries[m.j] === entry && !m.gone) ? 'your NFT back' : '',
|
|
5110
|
+
moves.some(m => entries[m.j] !== entry && !m.gone) ? 'your pick of the losers’' : '',
|
|
5111
|
+
]
|
|
5112
|
+
.filter(Boolean)
|
|
5113
|
+
.join(', and ');
|
|
5114
|
+
const goneSays = [
|
|
5115
|
+
off ? 'The contest is off.' : '',
|
|
5116
|
+
there ? `${there[0].toUpperCase()}${there.slice(1)}.` : '',
|
|
5117
|
+
goneContestSays(
|
|
5118
|
+
mine?.nft,
|
|
5119
|
+
lost.filter(m => m !== mine).map(m => m.nft),
|
|
5120
|
+
),
|
|
5121
|
+
]
|
|
5122
|
+
.filter(Boolean)
|
|
5123
|
+
.join(' ');
|
|
4803
5124
|
out.push({
|
|
4804
5125
|
kind: 'contest',
|
|
4805
5126
|
id: contest.id,
|
|
4806
5127
|
side: entry.side,
|
|
4807
|
-
says,
|
|
5128
|
+
...(lost.length ? { gone: true, assets: lost.map(m => m.nft), says: goneSays } : { says }),
|
|
4808
5129
|
...(winnings ? { potUnits: String(winnings.pot), prizeUnits: String(winnings.prize) } : {}),
|
|
4809
5130
|
});
|
|
4810
5131
|
}
|
|
@@ -4854,32 +5175,47 @@ export function solanaApp({
|
|
|
4854
5175
|
}
|
|
4855
5176
|
const auctions = spec.auctions ?? [];
|
|
4856
5177
|
const found = await client.accounts(auctions.map(a => auctionAt(a)));
|
|
4857
|
-
auctions.
|
|
5178
|
+
for (const [i, a] of auctions.entries()) {
|
|
4858
5179
|
const x = found[i] && loot.decode('Auction', found[i].data);
|
|
4859
|
-
if (!x || time < loot.auctionEnds(x))
|
|
5180
|
+
if (!x || time < loot.auctionEnds(x)) continue;
|
|
5181
|
+
const [sold, owed] = [BigInt(x.high) > 0n, BigInt(x.proceeds_owed) > 0n];
|
|
5182
|
+
if (!(x.seller === me || (sold && !owed && x.high_bidder === me))) continue;
|
|
5183
|
+
// Its lot, or an NFT of the winning bid, that the project no longer holds (its collection took it back or
|
|
5184
|
+
// burned it): the end does not send it anywhere, and this says what the end does instead.
|
|
5185
|
+
const lot = owed ? null : (x.lot.Asset?.[0] ?? null);
|
|
5186
|
+
const bids = sold && x.nft_bids ? x.held : [];
|
|
5187
|
+
const heldOf = lot || bids.length ? await heldEach([...(lot ? [lot] : []), ...bids]) : new Map();
|
|
5188
|
+
const lostBids = bids.filter(nft => heldOf.get(nft)?.held === false);
|
|
5189
|
+
if ((lot && heldOf.get(lot)?.held === false) || lostBids.length) {
|
|
5190
|
+
const lotGone = lot && heldOf.get(lot)?.held === false;
|
|
5191
|
+
out.push({
|
|
5192
|
+
kind: 'auction',
|
|
5193
|
+
key: a.key,
|
|
5194
|
+
gone: true,
|
|
5195
|
+
assets: [...(lotGone ? [lot] : []), ...lostBids],
|
|
5196
|
+
says: goneSaleSays(a.name, x, me, lotGone ? lot : null, lostBids),
|
|
5197
|
+
});
|
|
5198
|
+
continue;
|
|
5199
|
+
}
|
|
4860
5200
|
// The lot went first (an end too wide for one transaction, or a seller's account that couldn't take the money
|
|
4861
5201
|
// 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.` });
|
|
5202
|
+
if (owed)
|
|
5203
|
+
out.push({
|
|
5204
|
+
kind: 'auction',
|
|
5205
|
+
key: a.key,
|
|
5206
|
+
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).`,
|
|
5207
|
+
});
|
|
5208
|
+
else if (x.high_bidder === me) out.push({ kind: 'auction', key: a.key, says: `You won “${a.name}”: take it.` });
|
|
4872
5209
|
// The seller's own auction, over: ending it pays them (or brings an unsold lot home).
|
|
4873
|
-
else
|
|
5210
|
+
else
|
|
4874
5211
|
out.push({
|
|
4875
5212
|
kind: 'auction',
|
|
4876
5213
|
key: a.key,
|
|
4877
|
-
says:
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
: `Your auction “${a.name}” ended with no bid: take your lot back.`,
|
|
5214
|
+
says: sold
|
|
5215
|
+
? `Your auction “${a.name}” is over: end it, and the winning bid is paid to you.`
|
|
5216
|
+
: `Your auction “${a.name}” ended with no bid: take your lot back.`,
|
|
4881
5217
|
});
|
|
4882
|
-
}
|
|
5218
|
+
}
|
|
4883
5219
|
for (const l of spec.lotteries ?? []) {
|
|
4884
5220
|
// A lottery the page names but nobody has written yet (a raffle still to open) has nothing to finish.
|
|
4885
5221
|
if (!(await client.account(loot.pda.lottery(project, l.id)))) continue;
|
|
@@ -4918,17 +5254,32 @@ export function solanaApp({
|
|
|
4918
5254
|
const parts = e.buyer === me ? loot.partsOwed(r, i) : [];
|
|
4919
5255
|
return parts.length ? [{ entry: i, parts }] : [];
|
|
4920
5256
|
});
|
|
4921
|
-
if (mine.length)
|
|
5257
|
+
if (mine.length) {
|
|
5258
|
+
// An NFT of these parts that the project no longer holds (its collection took it back or burned it) is
|
|
5259
|
+
// not sent anywhere: the claim settles its part without it, and says so here first.
|
|
5260
|
+
const { tickets, prizes } = await roundNfts(r, mine);
|
|
5261
|
+
const heldOf = tickets.length + prizes.length ? await heldEach([...tickets, ...prizes]) : new Map();
|
|
5262
|
+
const lost = list => [...new Set(list)].filter(nft => heldOf.get(nft)?.held === false);
|
|
5263
|
+
const [lostTickets, lostPrizes] = [lost(tickets), lost(prizes)];
|
|
5264
|
+
const back = r.status === 'Cancelled' || Boolean(r.terms.returns);
|
|
5265
|
+
const owed =
|
|
5266
|
+
r.status === 'Cancelled'
|
|
5267
|
+
? `Recover your ticket payment for “${l.name}”.`
|
|
5268
|
+
: `Take what you are owed in “${l.name}”.`;
|
|
4922
5269
|
out.push({
|
|
4923
5270
|
kind: 'pull',
|
|
4924
5271
|
key: l.key,
|
|
4925
5272
|
round,
|
|
4926
5273
|
pulls: mine,
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
5274
|
+
...(lostTickets.length + lostPrizes.length
|
|
5275
|
+
? {
|
|
5276
|
+
gone: true,
|
|
5277
|
+
assets: [...lostTickets, ...lostPrizes],
|
|
5278
|
+
says: `${owed} ${goneRoundSays(lostTickets, lostPrizes, back)}`,
|
|
5279
|
+
}
|
|
5280
|
+
: { says: owed }),
|
|
4931
5281
|
});
|
|
5282
|
+
}
|
|
4932
5283
|
// Anyone may pay everyone else what they are owed, and the parts owed in common (about 0.002 SOL of account
|
|
4933
5284
|
// rent for each payee who has no account for it yet, which stays with them): the last pull closes the round,
|
|
4934
5285
|
// and its rent goes back to whoever fronted it (the lottery, or its first buyer).
|
|
@@ -5033,8 +5384,15 @@ export function solanaApp({
|
|
|
5033
5384
|
* Finishes one of `claims()`, signed by this player. Returns the signature. A `release` flagged `gone` closes its
|
|
5034
5385
|
* hold on the proof that the project no longer holds the NFT: nothing is sent, the hold's rent goes back to whoever
|
|
5035
5386
|
* 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.
|
|
5387
|
+
* which loser's NFT they take (`{ take: asset }`, the NFT of a losing entry that still holds one; by default the
|
|
5388
|
+
* first found that the project still holds): only the winner can, while any are left.
|
|
5389
|
+
* An NFT a lottery's round or a contest owes that the project no longer holds (its collection took it back or
|
|
5390
|
+
* burned it) is not sent anywhere, and the claim still finishes. A ticket whose price sends it on (burned, given,
|
|
5391
|
+
* stashed) settles on the proof with nothing moved. A ticket going back to its buyer, a place's NFT and a
|
|
5392
|
+
* contest's NFT are only handed out by the program, which reads nothing first: the claim has it make a hold of
|
|
5393
|
+
* that NFT (`["hold", nft, 0]`) and closes that hold on the proof right after, in the same transaction when it
|
|
5394
|
+
* fits. The signer pays that hold's rent and gets it back at once. The program's receipts for it are those of a
|
|
5395
|
+
* hold made (`Held`, and `PrizeGiven` for a place's prize) and then `NftGone`: no `Released`.
|
|
5038
5396
|
*/
|
|
5039
5397
|
async claim(c, { take, build = false, ...others } = NO_OPTIONS) {
|
|
5040
5398
|
only(others, ['take', 'build'], 'claim');
|
|
@@ -5065,37 +5423,25 @@ export function solanaApp({
|
|
|
5065
5423
|
// lets it pick (`take`: one asset, or several; the first left by default), as many as fit.
|
|
5066
5424
|
const nfts = [];
|
|
5067
5425
|
const kinds = {};
|
|
5426
|
+
// The NFTs of this claim that are proven gone from the project, each with what shows it and who staked it.
|
|
5427
|
+
// The contest only hands an NFT out and reads nothing, so each is passed as its hold (`["hold", nft, 0]`, made
|
|
5428
|
+
// for this player, who gets it), and that hold closes on the proof right after the claim: nothing is sent.
|
|
5429
|
+
const lost = new Map();
|
|
5068
5430
|
if (contest.nft) {
|
|
5069
5431
|
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) {
|
|
5432
|
+
for (const { j, nft } of await contestNfts(contest, entries, i, take)) {
|
|
5433
|
+
const { before, hasAuthority: _, gone, ...kind } = await leaving(nft, me, { to: me });
|
|
5434
|
+
proofs[nft] = before;
|
|
5435
|
+
kinds[nft] = kind;
|
|
5436
|
+
if (gone) lost.set(nft, { kind, player: me, rentTo: entries[j].player });
|
|
5091
5437
|
nfts.push(j);
|
|
5092
|
-
await kindOf(e.nft, me);
|
|
5093
5438
|
}
|
|
5094
5439
|
// A compressed NFT's proof is long (up to 17 nodes): such a claim moves one NFT, as a v0 transaction with a
|
|
5095
5440
|
// lookup table (`send`), and the next claim takes the next.
|
|
5096
5441
|
if (nfts.length > 1 && nfts.some(j => kinds[entries[j].nft]?.compressed)) nfts.splice(1);
|
|
5097
5442
|
for (const j of nfts) given.before.push(...proofs[entries[j].nft]);
|
|
5098
5443
|
}
|
|
5444
|
+
const closes = nfts.map(j => entries[j].nft).filter(nft => lost.has(nft));
|
|
5099
5445
|
const claimed = [
|
|
5100
5446
|
computeUnitLimit(800_000),
|
|
5101
5447
|
...given.before.filter(x => x.programId !== COMPUTE_BUDGET_PROGRAM),
|
|
@@ -5109,12 +5455,14 @@ export function solanaApp({
|
|
|
5109
5455
|
player: me,
|
|
5110
5456
|
nfts: contest.nft ? nfts : undefined,
|
|
5111
5457
|
kinds,
|
|
5458
|
+
hold: closes,
|
|
5112
5459
|
collection: given.collection,
|
|
5113
5460
|
poolMints: given.poolMints,
|
|
5114
5461
|
items: given.items,
|
|
5115
5462
|
nft: given.nft,
|
|
5116
5463
|
programs,
|
|
5117
5464
|
}),
|
|
5465
|
+
...closes.map(nft => releaseOwed(nft, lost.get(nft).kind, lost.get(nft), me, g)),
|
|
5118
5466
|
];
|
|
5119
5467
|
// A prize that rolls: asked for in the claim's own transaction and settled by a key after (one prompt).
|
|
5120
5468
|
return contest.prize?.Roll && !build ? (await sendRolling(claimed)).signature : send(claimed);
|