@xgamesfun/solana 0.25.4 → 0.26.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 +8 -8
- package/js/approve/details.mjs +52 -2
- package/js/approve/live-check.mjs +0 -15
- package/js/approve/server.mjs +5 -4
- package/js/chain/token2022.js +3 -2
- package/js/client/accounts.js +67 -25
- package/js/client/grant.js +7 -2
- package/js/client/idl/loot.js +1 -1
- package/js/client/idl/loot.json +716 -183
- package/js/client/ix.js +617 -113
- package/js/client/sizes.js +210 -64
- package/js/economy/check.mjs +691 -261
- package/js/economy/cost.mjs +2 -4
- package/js/economy/explain.mjs +16 -2
- package/js/economy/guide.mjs +12 -12
- package/js/economy/model.mjs +133 -14
- package/js/economy/plan.mjs +68 -19
- package/js/economy/review.mjs +50 -8
- package/js/economy/schema.json +73 -29
- package/js/economy/simulate.mjs +116 -13
- package/js/mcp/tools.mjs +2 -2
- package/js/play/index.js +855 -296
- package/package.json +1 -1
- package/release.json +5 -5
- package/skills/xsolana-economy/SKILL.md +12 -4
- package/skills/xsolana-economy/references/after-launch.md +1 -1
- package/skills/xsolana-economy/references/apps.md +11 -2
- package/skills/xsolana-economy/references/sentence-to-spec.md +3 -3
- package/skills/xsolana-economy/references/telling-the-person.md +22 -4
- package/skills/xsolana-economy/references/trust-and-cheating.md +15 -5
- package/types/chain/token2022.d.ts +3 -2
- package/types/client/accounts.d.ts +34 -11
- package/types/client/idl/loot.d.ts +25 -4
- package/types/client/ix.d.ts +217 -45
- package/types/client/sizes.d.ts +42 -8
- package/types/economy/check.d.mts +11 -2
- package/types/economy/cost.d.mts +1 -0
- package/types/economy/live.d.mts +2 -0
- package/types/economy/model.d.mts +75 -12
- package/types/economy/plan.d.mts +31 -2
- package/types/economy/simulate.d.mts +12 -1
- package/types/mcp/server.d.mts +12 -1
- package/types/mcp/tools.d.mts +12 -1
- package/types/play/index.d.ts +210 -144
package/README.md
CHANGED
|
@@ -64,7 +64,7 @@ codex plugin add xsolana@xgamesfun
|
|
|
64
64
|
|
|
65
65
|
**claude.ai, ChatGPT or Claude Desktop:** add `https://xgames.fun/mcp` as a custom connector. Nothing to install.
|
|
66
66
|
|
|
67
|
-
**Cursor, Cline, opencode or any MCP client:** run `npx -y --package=@xgamesfun/solana@0.
|
|
67
|
+
**Cursor, Cline, opencode or any MCP client:** run `npx -y --package=@xgamesfun/solana@0.26.1 xsolana mcp`.
|
|
68
68
|
|
|
69
69
|
**Any agent that reads skills** (Copilot, Gemini CLI, Windsurf…): `npx skills add XGamesFun/solana-kit`.
|
|
70
70
|
|
|
@@ -75,19 +75,19 @@ are in [docs/harnesses.md](docs/harnesses.md).
|
|
|
75
75
|
|
|
76
76
|
```bash
|
|
77
77
|
# find a worked example close to your idea
|
|
78
|
-
npx -y @xgamesfun/solana@0.
|
|
78
|
+
npx -y @xgamesfun/solana@0.26.1 examples shop with loot boxes
|
|
79
79
|
|
|
80
80
|
# does it work? where does the money go? what does it cost?
|
|
81
|
-
npx -y @xgamesfun/solana@0.
|
|
82
|
-
npx -y @xgamesfun/solana@0.
|
|
83
|
-
npx -y @xgamesfun/solana@0.
|
|
81
|
+
npx -y @xgamesfun/solana@0.26.1 check xsolana.json
|
|
82
|
+
npx -y @xgamesfun/solana@0.26.1 simulate xsolana.json
|
|
83
|
+
npx -y @xgamesfun/solana@0.26.1 cost xsolana.json
|
|
84
84
|
|
|
85
85
|
# the whole economy in plain words, and what a cheater could get
|
|
86
|
-
npx -y @xgamesfun/solana@0.
|
|
87
|
-
npx -y @xgamesfun/solana@0.
|
|
86
|
+
npx -y @xgamesfun/solana@0.26.1 explain xsolana.json
|
|
87
|
+
npx -y @xgamesfun/solana@0.26.1 review xsolana.json
|
|
88
88
|
|
|
89
89
|
# go live, approved in your own wallet
|
|
90
|
-
npx -y @xgamesfun/solana@0.
|
|
90
|
+
npx -y @xgamesfun/solana@0.26.1 approve xsolana.json
|
|
91
91
|
```
|
|
92
92
|
|
|
93
93
|
Each command names an exact version, so you always run the release you meant to. `xsolana help` lists them all.
|
package/js/approve/details.mjs
CHANGED
|
@@ -6,10 +6,31 @@ import { decodeBorsh } from '../chain/borsh.js';
|
|
|
6
6
|
import { equalBytes, toHex } from '../chain/bytes.js';
|
|
7
7
|
import { accountKind, decode, IDL, instructionOf, keyLeaf, merkleRoot } from '../client/index.js';
|
|
8
8
|
import { LOOT_PROGRAM, pda } from '../client/pda.js';
|
|
9
|
+
import { dateWords, FAR_SECONDS } from '../economy/check.mjs';
|
|
9
10
|
import { fromUnits } from '../economy/model.mjs';
|
|
10
11
|
import { grantArgs } from '../economy/plan.mjs';
|
|
11
12
|
|
|
12
13
|
const NOBODY = '11111111111111111111111111111111';
|
|
14
|
+
/** What places of tokens from a short pool get, by their `when_short`. */
|
|
15
|
+
const SHORT = {
|
|
16
|
+
InOrder: 'whole prizes, first place first, while the pool covers one',
|
|
17
|
+
InProportion: 'every place the same share of its prize',
|
|
18
|
+
Nothing: 'no place gets anything, and the pool keeps it',
|
|
19
|
+
};
|
|
20
|
+
/** What a far time is, by the field that holds it. */
|
|
21
|
+
const FAR = {
|
|
22
|
+
draws_at: 'The first draw',
|
|
23
|
+
ends_at: 'The auction ends',
|
|
24
|
+
result_by: 'The result is due',
|
|
25
|
+
next_at: 'The first run',
|
|
26
|
+
};
|
|
27
|
+
/** A number of seconds in words: "1 day", "36 hours", "90 seconds". */
|
|
28
|
+
const duration = v => {
|
|
29
|
+
const n = Number(v);
|
|
30
|
+
if (n % 86_400 === 0) return `${n / 86_400} day${n === 86_400 ? '' : 's'}`;
|
|
31
|
+
if (n % 3_600 === 0) return `${n / 3_600} hours`;
|
|
32
|
+
return `${n} seconds`;
|
|
33
|
+
};
|
|
13
34
|
const ZERO = new Uint8Array(32);
|
|
14
35
|
|
|
15
36
|
/** An instruction's accounts by name, nested groups flattened, in the order the program takes them. */
|
|
@@ -32,11 +53,12 @@ function readLootInstruction(ix) {
|
|
|
32
53
|
* connected; `treasury`: the treasury's address when it is known (else "your wallet" stands for it, as the setup
|
|
33
54
|
* makes it).
|
|
34
55
|
*/
|
|
35
|
-
export function namesOf(m, { owner = null, treasury = null } = {}) {
|
|
56
|
+
export function namesOf(m, { owner = null, treasury = null, now = null } = {}) {
|
|
36
57
|
return {
|
|
37
58
|
m,
|
|
38
59
|
owner,
|
|
39
60
|
treasury,
|
|
61
|
+
now,
|
|
40
62
|
pools: new Map(m.pools.map(p => [p.index, p.key])),
|
|
41
63
|
stashes: new Map(m.stashes.map(s => [s.id, s.name ?? s.key])),
|
|
42
64
|
grants: new Map(m.grants.map(g => [g.id, g])),
|
|
@@ -151,6 +173,8 @@ function readDetails(reads, names) {
|
|
|
151
173
|
: treasury()
|
|
152
174
|
: treasury();
|
|
153
175
|
const to = v => (v === 'Burn' ? 'burned' : `to ${where(v)}`);
|
|
176
|
+
// A time more than three years after now: the program takes it, and the owner sees its date before signing.
|
|
177
|
+
const far = v => Number(v) - (names.now ?? Math.floor(Date.now() / 1000)) > FAR_SECONDS;
|
|
154
178
|
const isTo = v =>
|
|
155
179
|
v === 'Burn' ||
|
|
156
180
|
v === 'Treasury' ||
|
|
@@ -176,7 +200,33 @@ function readDetails(reads, names) {
|
|
|
176
200
|
? `Places paid from: its pot ${v.Pot[0]}, kept from round to round (what anyone sends it, yield and what earlier rounds left all count)`
|
|
177
201
|
: `Places paid from: each round’s own ticket money alone; what they leave is ${v.Round.rest === 'Burn' ? 'burned' : `sent to ${where(v.Round.rest)}`}`,
|
|
178
202
|
);
|
|
179
|
-
} else if (k === '
|
|
203
|
+
} else if (k === 'when_out' && v === 'GiveNothing')
|
|
204
|
+
lines.push(
|
|
205
|
+
'When an outcome is out: a draw on it gives nothing, and a paid draw is not refunded (the box keeps selling)',
|
|
206
|
+
);
|
|
207
|
+
else if (k === 'closes_before' && BigInt(v ?? 0) > 0n)
|
|
208
|
+
lines.push(`Sales close ${duration(v)} before each draw`);
|
|
209
|
+
else if (k === 'time_weighted' && v === true)
|
|
210
|
+
lines.push(
|
|
211
|
+
'Tickets weighed by time: each purchase counts its tickets times the seconds left until sales close, so a ticket bought early has a better chance',
|
|
212
|
+
);
|
|
213
|
+
else if (k === 'places' && Array.isArray(v)) {
|
|
214
|
+
// What places of tokens from a pool get when it is short, read once when the draw is asked.
|
|
215
|
+
const short = [...new Set(v.filter(p => p?.prize?.Tokens?.from?.Pool).map(p => p.when_short))];
|
|
216
|
+
if (short.length)
|
|
217
|
+
lines.push(
|
|
218
|
+
`If a pool its places are paid from holds less than they win: ${short.map(x => SHORT[x] ?? x).join('; ')}`,
|
|
219
|
+
);
|
|
220
|
+
walk(v);
|
|
221
|
+
} else if (['draws_at', 'ends_at', 'result_by', 'next_at'].includes(k) && far(v))
|
|
222
|
+
lines.push(
|
|
223
|
+
`${FAR[k]} on ${dateWords(Number(v))}, ${Math.floor((Number(v) - (names.now ?? Math.floor(Date.now() / 1000))) / 31_536_000)} years from now: what waits on it is locked until then. Check the unit before approving`,
|
|
224
|
+
);
|
|
225
|
+
else if (k === 'extend' && read.name === 'open_auction' && Number(v) > FAR_SECONDS)
|
|
226
|
+
lines.push(
|
|
227
|
+
`A late bid moves the end by ${duration(v)}: one bid holds the lot and itself that long. Check the unit before approving`,
|
|
228
|
+
);
|
|
229
|
+
else if (k === 'to' && isTo(v))
|
|
180
230
|
lines.push(v === 'Burn' ? 'Burns the tokens it takes' : `Sends tokens to ${where(v)}`);
|
|
181
231
|
else if (k === 'Give' && Array.isArray(v) && typeof v[0] === 'string')
|
|
182
232
|
lines.push(`Gives the NFT to ${address(v[0])}`);
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
import { encodeBase58 } from '../chain/base58.js';
|
|
7
7
|
import { parseCoreCollection, MPL_CORE_PROGRAM } from '../chain/core.js';
|
|
8
8
|
import { parseMint } from '../chain/mint.js';
|
|
9
|
-
import { mayStartFrozen } from '../chain/token2022.js';
|
|
10
9
|
import { accountKind, decode } from '../client/accounts.js';
|
|
11
10
|
import { BUBBLEGUM_PROGRAM, LOOT_PROGRAM, pda } from '../client/pda.js';
|
|
12
11
|
import { SYSTEM_PROGRAM } from '../chain/pda.js';
|
|
@@ -128,16 +127,6 @@ export class MintMismatch extends Error {}
|
|
|
128
127
|
*/
|
|
129
128
|
export async function checkMints({ m, chain }) {
|
|
130
129
|
const named = [...m.tokens.values()].filter(t => t.mint && !t.mint.startsWith('@'));
|
|
131
|
-
// The routes that read a mint's default account state once, as they open, and refuse one that can start frozen.
|
|
132
|
-
const once = new Map();
|
|
133
|
-
const reads = (key, what) => key && !once.has(key) && once.set(key, what);
|
|
134
|
-
for (const l of m.lotteries)
|
|
135
|
-
(reads(l.currency ?? m.currencyKey, `the lottery ${l.name ?? l.key}`),
|
|
136
|
-
reads(l.ticketCurrency, `the lottery ${l.name ?? l.key}`));
|
|
137
|
-
for (const c of m.contests) reads(c.currency ?? m.currencyKey, `the contest ${c.name ?? c.key}`);
|
|
138
|
-
for (const a of m.auctions) reads(a.currency ?? m.currencyKey, `the auction ${a.name ?? a.key}`);
|
|
139
|
-
for (const s of m.schedules.filter(x => x.sealed))
|
|
140
|
-
reads(s.currency ?? m.currencyKey, `the sealed schedule ${s.name ?? s.key}`);
|
|
141
130
|
for (const t of named) {
|
|
142
131
|
const what = `the token ${t.symbol ?? t.key} (“${t.key}”)`;
|
|
143
132
|
if (t.wsol) {
|
|
@@ -162,10 +151,6 @@ export async function checkMints({ m, chain }) {
|
|
|
162
151
|
throw new MintMismatch(
|
|
163
152
|
`The spec says ${what} is a ${t.program} token; its mint ${t.mint} is ${mint.program}. Write "program": "${mint.program}".`,
|
|
164
153
|
);
|
|
165
|
-
if (once.has(t.key) && mayStartFrozen(account))
|
|
166
|
-
throw new MintMismatch(
|
|
167
|
-
`The mint ${t.mint} of ${what} has a default account state that starts new accounts frozen, or that its freeze authority can switch to frozen, so ${once.get(t.key)} would be refused as it opens (the program refuses such a mint wherever a refund or a payout goes into an account opened for it). Use another token there.`,
|
|
168
|
-
);
|
|
169
154
|
}
|
|
170
155
|
await checkTreasury({ m, chain });
|
|
171
156
|
}
|
package/js/approve/server.mjs
CHANGED
|
@@ -221,6 +221,7 @@ export async function createApproval({
|
|
|
221
221
|
*/
|
|
222
222
|
const names = () =>
|
|
223
223
|
namesOf(model(spec, { now: at }), {
|
|
224
|
+
now: at,
|
|
224
225
|
treasury: madeSoFar(live())?.treasury ?? model(spec, { now: at }).treasuryWallet ?? null,
|
|
225
226
|
});
|
|
226
227
|
/**
|
|
@@ -232,7 +233,7 @@ export async function createApproval({
|
|
|
232
233
|
async function readLive(p) {
|
|
233
234
|
if (!spec.live?.published || typeof chain.account !== 'function') return;
|
|
234
235
|
// The names a published list or key list goes by: the published spec's, each held against the root on chain.
|
|
235
|
-
const n = namesOf(model(spec.live.published, { now: at }), { treasury: p.ctx.treasury });
|
|
236
|
+
const n = namesOf(model(spec.live.published, { now: at }), { now: at, treasury: p.ctx.treasury });
|
|
236
237
|
const was = new Map();
|
|
237
238
|
for (const t of p.transactions) {
|
|
238
239
|
for (const [i, step] of t.steps.entries()) {
|
|
@@ -381,14 +382,14 @@ export async function createApproval({
|
|
|
381
382
|
trusted = await trust(wallet);
|
|
382
383
|
} catch (e) {
|
|
383
384
|
if (!(e instanceof ApproveError)) throw e;
|
|
384
|
-
const v = view(p0, namesOf(model(spec, { now: at }), { owner: wallet }), { refused: e.message });
|
|
385
|
+
const v = view(p0, namesOf(model(spec, { now: at }), { now: at, owner: wallet }), { refused: e.message });
|
|
385
386
|
return { ...v, remaining: p0.ok ? p0.transactions.length : 0, done: false };
|
|
386
387
|
}
|
|
387
388
|
if (!p0.ok || !p0.transactions.length)
|
|
388
389
|
return { ...view(p0, names(), { project: trusted.project }), remaining: 0, done: p0.ok };
|
|
389
390
|
const p = await build(trusted.owner);
|
|
390
391
|
await readLive(p);
|
|
391
|
-
const v = view(p, namesOf(model(spec, { now: at }), { owner: wallet, treasury: p.ctx.treasury }), {
|
|
392
|
+
const v = view(p, namesOf(model(spec, { now: at }), { now: at, owner: wallet, treasury: p.ctx.treasury }), {
|
|
392
393
|
project: p.ctx.project,
|
|
393
394
|
});
|
|
394
395
|
return { ...v, remaining: p.transactions.length, done: false };
|
|
@@ -435,7 +436,7 @@ export async function createApproval({
|
|
|
435
436
|
return wireTransaction(message, sigs);
|
|
436
437
|
});
|
|
437
438
|
// What each transaction pays to whom, read back from the very instructions the wallet signs.
|
|
438
|
-
const n = namesOf(model(spec, { now: at }), { owner: wallet, treasury: p.ctx.treasury });
|
|
439
|
+
const n = namesOf(model(spec, { now: at }), { now: at, owner: wallet, treasury: p.ctx.treasury });
|
|
439
440
|
// Every line names the project it goes to first: always the wallet's own, or the one it is the authority of.
|
|
440
441
|
const details = batch.map(t => [
|
|
441
442
|
`Project: ${p.ctx.project}${p.ctx.owner === wallet ? ' (your own)' : ` (owned by ${p.ctx.owner})`}`,
|
package/js/chain/token2022.js
CHANGED
|
@@ -60,8 +60,9 @@ export function hookProgramOf(mintData) {
|
|
|
60
60
|
/**
|
|
61
61
|
* Whether a mint's new accounts start frozen, or can be made to (as the program judges it): a Token-2022
|
|
62
62
|
* mint carrying `DefaultAccountState` set to frozen, or set to anything while it has a freeze authority, which can switch
|
|
63
|
-
* it to frozen whenever it likes.
|
|
64
|
-
*
|
|
63
|
+
* it to frozen whenever it likes. The program takes such a mint everywhere: it is the project's choice. A refund or a
|
|
64
|
+
* payout opened into an account that starts frozen does not land until its issuer thaws it, so the tools warn the
|
|
65
|
+
* project, and a bidder or a borrower before they sign.
|
|
65
66
|
*/
|
|
66
67
|
export function mayStartFrozen({ owner, data }) {
|
|
67
68
|
if (
|
package/js/client/accounts.js
CHANGED
|
@@ -107,6 +107,9 @@ const RECORDS = [
|
|
|
107
107
|
// The totals, split at each purchase: what each share paid in common takes, the entries that own a part of
|
|
108
108
|
// their own, and the parts owed now.
|
|
109
109
|
['sold', 'u64'],
|
|
110
|
+
// In a weighted round (`time_weighted`), the weight sold: each purchase's tickets times the seconds from it
|
|
111
|
+
// to the close of sales. Null: each ticket weighs one, and the draw is over `sold`.
|
|
112
|
+
['weight', { option: 'u128' }],
|
|
110
113
|
['to_pot', 'u64'],
|
|
111
114
|
['common', { vec: 'u64' }],
|
|
112
115
|
['owns', 'u32'],
|
|
@@ -117,7 +120,8 @@ const RECORDS = [
|
|
|
117
120
|
['first_asked_at', 'i64'],
|
|
118
121
|
['asked_at', 'i64'],
|
|
119
122
|
['answer', { array: ['u8', 32] }],
|
|
120
|
-
|
|
123
|
+
// First place's point: below the draw's total (`roundTotal`), so in a weighted round a point of its weight.
|
|
124
|
+
['first', 'u128'],
|
|
121
125
|
['pot_total', 'u64'],
|
|
122
126
|
['pot_sent', 'u64'],
|
|
123
127
|
['pot_arrived', 'u64'],
|
|
@@ -506,55 +510,94 @@ export const lotteryPots = lottery =>
|
|
|
506
510
|
/** A share the buyer chooses (a referrer, a collectible's holder): paid out of the price as they buy, from their own account. */
|
|
507
511
|
export const chosenAtPurchase = share => Boolean(share.to?.Chosen);
|
|
508
512
|
|
|
509
|
-
/** The bytes of one entry of a round: buyer, word, what arrived (`fee`: a fee mint,
|
|
510
|
-
|
|
513
|
+
/** The bytes of one entry of a round: buyer, word, what arrived (`fee`: a fee mint, a chosen share, or a weighted
|
|
514
|
+
* round of paid tickets), where its weight ends (`weighted`: a round that weighs its tickets by time), the NFT ticket. */
|
|
515
|
+
export const entryWidth = (fee, nft, weighted = false) => 40 + (fee ? 8 : 0) + (weighted ? 16 : 0) + (nft ? 32 : 0);
|
|
516
|
+
|
|
517
|
+
/** What a round's draw picks a point below: the weight sold in a weighted round, the tickets sold otherwise. */
|
|
518
|
+
export const roundTotal = r => (r.weight != null ? BigInt(r.weight) : BigInt(r.sold));
|
|
511
519
|
|
|
512
520
|
/**
|
|
513
521
|
* A lottery round (`["round", lottery, incarnation, round]`): its header (`status` 'Open' | 'Drawing' | 'Drawn' |
|
|
514
|
-
* 'Cancelled', the terms it copied, `sold`, `
|
|
515
|
-
* lottery's own stash each such place got), and `entries`, one a
|
|
516
|
-
*
|
|
522
|
+
* 'Cancelled', the terms it copied, `sold`, `weight` (null in a round that doesn't weigh its tickets), `owed`, the draw
|
|
523
|
+
* and the money the ask fixed, `nfts` the NFT of the lottery's own stash each such place got), and `entries`, one a
|
|
524
|
+
* purchase: { buyer, start, end (its slice of the draw: its tickets, or in a weighted round its weight), tickets (how
|
|
525
|
+
* many it bought), paid (its own part's bit), amount (what reached the round), nft (its NFT ticket) }.
|
|
517
526
|
*/
|
|
518
527
|
export function decodeRound(data) {
|
|
519
528
|
const r = decode('LotteryRound', data);
|
|
520
529
|
const at = Number(r.head);
|
|
521
530
|
const width = Number(readUintLE(data, at, 2));
|
|
522
531
|
const count = Number(readUintLE(data, at + 2, 4));
|
|
523
|
-
|
|
532
|
+
const weighted = r.weight != null;
|
|
533
|
+
if (width !== entryWidth(r.fee, r.terms.nft_ticket != null, weighted) || data.length !== at + 6 + width * count)
|
|
524
534
|
throw new Error('LotteryRound entries do not match its header');
|
|
525
535
|
let start = 0n;
|
|
536
|
+
let sold = 0n;
|
|
526
537
|
const entries = Array.from({ length: count }, (_, i) => {
|
|
527
538
|
const e = data.subarray(at + 6 + width * i, at + 6 + width * (i + 1));
|
|
528
539
|
const word = readUintLE(e, 32, 8);
|
|
529
|
-
|
|
540
|
+
// Its tickets end at the word's low 48 bits; its slice of the draw ends there too, or at its weight's end.
|
|
541
|
+
const last = word & END_BITS;
|
|
542
|
+
const tickets = last - sold;
|
|
530
543
|
let k = 40;
|
|
531
544
|
const take = n => e.subarray((k += n) - n, k);
|
|
532
|
-
const amount = r.fee ? readUintLE(take(8), 0, 8) :
|
|
545
|
+
const amount = r.fee ? readUintLE(take(8), 0, 8) : tickets * BigInt(r.terms.price);
|
|
546
|
+
const end = weighted ? readUintLE(take(16), 0, 16) : last;
|
|
533
547
|
const entry = {
|
|
534
548
|
buyer: encodeBase58(e.subarray(0, 32)),
|
|
535
549
|
start,
|
|
536
550
|
end,
|
|
551
|
+
tickets,
|
|
537
552
|
paid: word & ~END_BITS,
|
|
538
553
|
amount,
|
|
539
554
|
nft: r.terms.nft_ticket ? encodeBase58(take(32)) : NO_ADDRESS,
|
|
540
555
|
};
|
|
541
556
|
start = end;
|
|
557
|
+
sold = last;
|
|
542
558
|
return entry;
|
|
543
559
|
});
|
|
544
560
|
return { ...r, entries };
|
|
545
561
|
}
|
|
546
562
|
|
|
563
|
+
/** When a lottery's sales close (`terms`: a Lottery's or a round's): `closes_before` seconds before its draw. */
|
|
564
|
+
export const salesCloseAt = terms => BigInt(terms.draws_at) - BigInt(terms.closes_before ?? 0);
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* What a purchase of `count` tickets at `now` (Unix seconds, the chain's clock) weighs in a round's draw: one a ticket,
|
|
568
|
+
* or in a lottery that weighs tickets by time (`terms.time_weighted`) each ticket the seconds from the purchase to the
|
|
569
|
+
* close of sales (`salesCloseAt`). 0 once sales have closed: nothing more is sold. The same tickets bought later weigh less.
|
|
570
|
+
*/
|
|
571
|
+
export function ticketWeight(terms, count, now) {
|
|
572
|
+
const left = salesCloseAt(terms) - BigInt(now);
|
|
573
|
+
if (left <= 0n) return 0n;
|
|
574
|
+
return BigInt(count) * (terms.time_weighted ? left : 1n);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* A buyer's chance before they buy: { weight, total, share } of a purchase of `count` tickets at `now` into `round` (its
|
|
579
|
+
* decoded record, null when nobody bought into it yet; `terms` the lottery's then): what it would weigh, the round's
|
|
580
|
+
* weight with it, and its share of that (a number from 0 to 1). Each place is drawn over the round's whole weight, so a
|
|
581
|
+
* share is the purchase's chance at one place as things stand: later purchases make it smaller.
|
|
582
|
+
*/
|
|
583
|
+
export function purchaseOdds(round, terms, count, now) {
|
|
584
|
+
const weight = ticketWeight(round?.terms ?? terms, count, now);
|
|
585
|
+
const total = (round ? roundTotal(round) : 0n) + weight;
|
|
586
|
+
return { weight, total, share: total > 0n ? Number((weight * 1_000_000n) / total) / 1_000_000 : 0 };
|
|
587
|
+
}
|
|
588
|
+
|
|
547
589
|
/** How many places a lottery's (or a round's) `places` are, every run counted (`Place.times`). */
|
|
548
590
|
export const placeCount = places => places.reduce((n, p) => n + Number(p.times ?? 1), 0);
|
|
549
591
|
|
|
550
592
|
/** The places of a round that have a winner: every place, but no more than tickets. */
|
|
551
593
|
export const roundWinners = r => Math.min(placeCount(r.terms.places), Number(r.sold));
|
|
552
594
|
|
|
553
|
-
/** The
|
|
595
|
+
/** The point that wins `place` of a drawn round: even steps of the draw's total around the list from first place's
|
|
596
|
+
* (a ticket's number, or in a weighted round a point of its weight). */
|
|
554
597
|
export function ticketOfPlace(r, place) {
|
|
555
|
-
const
|
|
556
|
-
const ahead = BigInt(place) * (
|
|
557
|
-
const toEnd =
|
|
598
|
+
const total = roundTotal(r);
|
|
599
|
+
const ahead = BigInt(place) * (total / BigInt(roundWinners(r) || 1));
|
|
600
|
+
const toEnd = total - BigInt(r.first);
|
|
558
601
|
return ahead < toEnd ? BigInt(r.first) + ahead : ahead - toEnd;
|
|
559
602
|
}
|
|
560
603
|
|
|
@@ -575,18 +618,18 @@ export function runPlaces(r, run) {
|
|
|
575
618
|
return [Math.min(first, w), Math.min(first + Number(r.terms.places[run].times), w)];
|
|
576
619
|
}
|
|
577
620
|
|
|
578
|
-
/** The places of run `run` whose
|
|
621
|
+
/** The places of run `run` whose point is in `start..end` (an entry's slice of the draw), in order. */
|
|
579
622
|
export function placesIn(r, run, start, end) {
|
|
580
623
|
const [from, to] = runPlaces(r, run);
|
|
581
624
|
const out = [];
|
|
582
625
|
if (from >= to || BigInt(start) >= BigInt(end)) return out;
|
|
583
|
-
const
|
|
584
|
-
const step =
|
|
585
|
-
const d = (BigInt(start) +
|
|
626
|
+
const total = roundTotal(r);
|
|
627
|
+
const step = total / BigInt(roundWinners(r));
|
|
628
|
+
const d = (BigInt(start) + total - BigInt(r.first)) % total;
|
|
586
629
|
const len = BigInt(end) - BigInt(start);
|
|
587
630
|
for (const [a, b] of [
|
|
588
|
-
[d, d + len <
|
|
589
|
-
[0n, d + len >
|
|
631
|
+
[d, d + len < total ? d + len : total],
|
|
632
|
+
[0n, d + len > total ? d + len - total : 0n],
|
|
590
633
|
]) {
|
|
591
634
|
let p = Number((a + step - 1n) / step);
|
|
592
635
|
for (p = Math.max(p, from); p < to && BigInt(p) * step < b; p++) out.push(p);
|
|
@@ -761,14 +804,13 @@ export const placeSet = place => Number(place.bps) > 0 || place.prize != null;
|
|
|
761
804
|
/** The address of a lottery's current round's record (there or not). */
|
|
762
805
|
export const roundAddress = (lotteryAddress, lottery) => pda.round(lotteryAddress, lottery.incarnation, lottery.round);
|
|
763
806
|
|
|
764
|
-
/** Whether a lottery (decoded) drew
|
|
765
|
-
export const lotteryEnded = lottery => Number(lottery.every) === 0 && Number(lottery.round) > 0;
|
|
807
|
+
/** Whether a lottery (decoded) drew its last round and has ended: it draws once (`every` 0) and is past round `last`. */
|
|
808
|
+
export const lotteryEnded = lottery => Number(lottery.every) === 0 && Number(lottery.round) > Number(lottery.last ?? 0);
|
|
766
809
|
|
|
767
|
-
/** The round a lottery's close, its write again and its own stash's moves name (`
|
|
768
|
-
* round's, or once
|
|
769
|
-
* them out). */
|
|
810
|
+
/** The round a lottery's close, its write again and its own stash's moves name (`Lottery::waits_on`): its current
|
|
811
|
+
* round's, or once it has ended, its last round's (which keeps its stash's NFTs until its places took them out). */
|
|
770
812
|
export const idleRound = (lotteryAddress, lottery) =>
|
|
771
|
-
pda.round(lotteryAddress, lottery.incarnation, lotteryEnded(lottery) ?
|
|
813
|
+
pda.round(lotteryAddress, lottery.incarnation, lotteryEnded(lottery) ? lottery.last : lottery.round);
|
|
772
814
|
|
|
773
815
|
/**
|
|
774
816
|
* Who rents a listed NFT at `now` (Unix seconds), from its decoded Listing (or null for none): the renter's wallet while
|
package/js/client/grant.js
CHANGED
|
@@ -19,7 +19,7 @@ import { compareBytes, concat, fromHex, intLE, readUintLE, toBigInt, uintLE, utf
|
|
|
19
19
|
import { meta } from '../chain/programs.js';
|
|
20
20
|
import { sha256 } from '../chain/sha256.js';
|
|
21
21
|
import { recordKey16 } from './accounts.js';
|
|
22
|
-
import { ASSOCIATED_TOKEN_PROGRAM, gateAccounts, lootIx, prizeMetas, prizeNonce, tailOf } from './ix.js';
|
|
22
|
+
import { ASSOCIATED_TOKEN_PROGRAM, gateAccounts, lootIx, prizeMetas, prizeNonce, shownFor, tailOf } from './ix.js';
|
|
23
23
|
import { LOOT_PROGRAM, pda } from './pda.js';
|
|
24
24
|
import IDL from './idl/loot.js';
|
|
25
25
|
|
|
@@ -400,7 +400,12 @@ export function claimGrantIx({
|
|
|
400
400
|
},
|
|
401
401
|
{
|
|
402
402
|
remaining: [
|
|
403
|
-
...gateAccounts(project, {
|
|
403
|
+
...gateAccounts(project, {
|
|
404
|
+
gate: grant.gate,
|
|
405
|
+
wallet,
|
|
406
|
+
shown: shownFor(project, grant.gate, wallet, shown),
|
|
407
|
+
listProof,
|
|
408
|
+
}),
|
|
404
409
|
...shards,
|
|
405
410
|
...prizes,
|
|
406
411
|
...tailOf(extra),
|