@xgamesfun/solana 0.20.1 → 0.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -10
- package/examples/coin-flip-arena.json +2 -7
- package/examples/elements-set.json +2 -9
- package/examples/expedition-guild.json +1 -8
- package/examples/guild-missions.json +3 -10
- package/examples/halving-seasons.json +4 -9
- package/examples/hero-trials.json +1 -4
- package/examples/merge-2048.json +5 -5
- package/examples/paper-hands-farm.json +4 -16
- package/examples/partner-vault.json +1 -4
- package/examples/stakers-draw.json +1 -8
- package/examples/star-fleet.json +1 -8
- package/examples/starforce.json +6 -6
- package/js/approve/details.mjs +8 -7
- package/js/approve/server.mjs +20 -5
- package/js/chain/borsh.js +9 -0
- package/js/chain/core.js +14 -2
- package/js/chain/lookup.js +8 -2
- package/js/chain/rpc.js +4 -2
- package/js/client/accounts.js +448 -116
- package/js/client/grant.js +125 -66
- package/js/client/idl/loot.js +1 -1
- package/js/client/idl/loot.json +2360 -2218
- package/js/client/index.js +1 -0
- package/js/client/ix.js +1204 -473
- package/js/client/pda.js +20 -16
- package/js/client/policy.js +44 -23
- package/js/client/random.js +70 -0
- package/js/client/sizes.js +255 -12
- package/js/economy/check.mjs +455 -210
- package/js/economy/cost.mjs +45 -30
- package/js/economy/examples.mjs +2 -2
- package/js/economy/explain.mjs +24 -20
- package/js/economy/guide.mjs +31 -30
- package/js/economy/live.mjs +30 -7
- package/js/economy/model.mjs +171 -56
- package/js/economy/plan.mjs +380 -112
- package/js/economy/schema.json +291 -143
- package/js/economy/simulate.mjs +154 -87
- package/js/mcp/tools.mjs +14 -8
- package/js/play/index.js +1121 -375
- package/js/play/keys.js +167 -0
- package/package.json +1 -1
- package/release.json +5 -5
- package/skills/xsolana-economy/SKILL.md +15 -11
- package/skills/xsolana-economy/references/after-launch.md +37 -27
- package/skills/xsolana-economy/references/apps.md +55 -19
- package/skills/xsolana-economy/references/sentence-to-spec.md +30 -22
- package/skills/xsolana-economy/references/telling-the-person.md +52 -11
- package/types/chain/core.d.ts +3 -1
- package/types/chain/lookup.d.ts +5 -2
- package/types/chain/rpc.d.ts +3 -1
- package/types/client/accounts.d.ts +195 -55
- package/types/client/grant.d.ts +72 -27
- package/types/client/idl/loot.d.ts +590 -559
- package/types/client/index.d.ts +1 -0
- package/types/client/ix.d.ts +3862 -234
- package/types/client/pda.d.ts +19 -15
- package/types/client/policy.d.ts +39 -14
- package/types/client/random.d.ts +26 -0
- package/types/client/sizes.d.ts +93 -5
- package/types/economy/check.d.mts +5 -10
- package/types/economy/cost.d.mts +2 -1
- package/types/economy/live.d.mts +16 -1
- package/types/economy/model.d.mts +105 -37
- package/types/economy/plan.d.mts +193 -192
- package/types/economy/simulate.d.mts +21 -13
- package/types/mcp/tools.d.mts +21 -13
- package/types/play/index.d.ts +450 -39
- package/types/play/keys.d.ts +51 -0
package/js/client/accounts.js
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
// Reading the loot program's accounts and receipts. Every account starts with a one-byte tag (1 to
|
|
2
|
-
// describes most of them, and the ones it doesn't (a
|
|
1
|
+
// Reading the loot program's accounts and receipts. Every account starts with a one-byte tag (1 to 29); the IDL
|
|
2
|
+
// describes most of them, and the ones it doesn't (a list of records, a lottery round, a sheet, a stash, …) are read
|
|
3
|
+
// here by hand. Values come back in the
|
|
3
4
|
// IDL's shapes (chain/borsh.js).
|
|
4
|
-
import { encodeBase58, NO_ADDRESS } from '../chain/base58.js';
|
|
5
|
-
import { decodeAccount, decodeEvents } from '../chain/borsh.js';
|
|
6
|
-
import { compareBytes,
|
|
5
|
+
import { addressBytes, encodeBase58, NO_ADDRESS } from '../chain/base58.js';
|
|
6
|
+
import { decodeAccount, decodeBorsh, decodeEvents } from '../chain/borsh.js';
|
|
7
|
+
import { compareBytes, concat, readUintLE, toHex, uintLE } from '../chain/bytes.js';
|
|
7
8
|
import { sha256 } from '../chain/sha256.js';
|
|
8
9
|
import { SYSTEM_PROGRAM } from '../chain/pda.js';
|
|
9
|
-
import { LOOT_PROGRAM } from './pda.js';
|
|
10
|
+
import { LOOT_PROGRAM, pda } from './pda.js';
|
|
10
11
|
import IDL from './idl/loot.js';
|
|
11
|
-
import { MAX_RECEIPT_ROUNDS, RECEIPT_BASE_BYTES, RECEIPT_ROUND_BYTES } from './sizes.js';
|
|
12
|
-
export { MAX_RECEIPT_ROUNDS } from './sizes.js';
|
|
13
12
|
|
|
14
|
-
/**
|
|
13
|
+
/** The bytes of one time in a farm's record of ends (`decodeEnds`): rent its writer pays, not refunded when a seat leaves. */
|
|
14
|
+
export const END_BYTES = 44;
|
|
15
|
+
/** The bytes of a farm's record of ends with no time in it, its owner's rent at `open_farm`. */
|
|
16
|
+
export const ENDS_HEAD_BYTES = 7;
|
|
17
|
+
|
|
18
|
+
/** Account tags: each account's first byte, as the program writes it; every list of records (`decodeList`) is 26. */
|
|
15
19
|
export const TAGS = Object.freeze({
|
|
16
20
|
Platform: 1,
|
|
17
21
|
Project: 2,
|
|
@@ -23,19 +27,23 @@ export const TAGS = Object.freeze({
|
|
|
23
27
|
Recipe: 8,
|
|
24
28
|
Hold: 9,
|
|
25
29
|
Contest: 10,
|
|
26
|
-
Entry: 11,
|
|
27
30
|
Sheet: 12,
|
|
28
31
|
Traits: 13,
|
|
29
|
-
|
|
30
|
-
Receipt: 15,
|
|
31
|
-
LotteryContribution: 16,
|
|
32
|
+
LotteryRound: 16,
|
|
32
33
|
Farm: 17,
|
|
33
34
|
Seat: 18,
|
|
34
35
|
Proven: 19,
|
|
35
36
|
Auction: 20,
|
|
36
|
-
Stash: 21,
|
|
37
37
|
Offer: 22,
|
|
38
38
|
Listing: 23,
|
|
39
|
+
Profile: 24,
|
|
40
|
+
Grant: 27,
|
|
41
|
+
// A table, and a stash: a table of NFTs (21 was a stash's own before 0.22).
|
|
42
|
+
Table: 25,
|
|
43
|
+
// A list of records: a wallet's counts, a grant's claims or rounds, a contest's entries.
|
|
44
|
+
List: 26,
|
|
45
|
+
// A farm's record of when its seats stop earning (`decodeEnds`).
|
|
46
|
+
Ends: 29,
|
|
39
47
|
});
|
|
40
48
|
|
|
41
49
|
const BY_TAG = Object.fromEntries(Object.entries(TAGS).map(([name, tag]) => [tag, name]));
|
|
@@ -43,43 +51,141 @@ const BY_TAG = Object.fromEntries(Object.entries(TAGS).map(([name, tag]) => [tag
|
|
|
43
51
|
/** The kind of a loot account from its first byte, or null. */
|
|
44
52
|
export const accountKind = (data) => (data?.length ? BY_TAG[data[0]] ?? null : null);
|
|
45
53
|
|
|
46
|
-
/**
|
|
47
|
-
|
|
54
|
+
/**
|
|
55
|
+
* The records the program writes by hand, as IDL types: a lottery round's header and what it keeps (`LotteryRound`,
|
|
56
|
+
* tag 16), and a contest's entry. The client reads them with the same Borsh codec as any account.
|
|
57
|
+
*/
|
|
58
|
+
const RECORDS = [
|
|
59
|
+
{ name: 'Round', type: { kind: 'enum', variants: ['Open', 'Drawing', 'Drawn', 'Cancelled'].map((name) => ({ name })) } },
|
|
60
|
+
{ name: 'TokenPrize', type: { kind: 'struct', fields: [{ name: 'run', type: 'u16' }, { name: 'mint', type: 'pubkey' }, { name: 'each', type: 'u64' }, { name: 'sent', type: 'u64' }, { name: 'arrived', type: 'u64' }, { name: 'minted', type: 'bool' }] } },
|
|
61
|
+
{ name: 'Opened', type: { kind: 'struct', fields: [{ name: 'mint', type: 'pubkey' }, { name: 'by', type: 'pubkey' }] } },
|
|
62
|
+
{
|
|
63
|
+
name: 'LotteryRound',
|
|
64
|
+
type: {
|
|
65
|
+
kind: 'struct',
|
|
66
|
+
fields: [
|
|
67
|
+
['project', 'pubkey'], ['lottery', 'pubkey'], ['id', 'u32'], ['incarnation', 'u64'], ['round', 'u32'], ['bump', 'u8'], ['opener', 'pubkey'], ['lent', 'u64'], ['head', 'u32'],
|
|
68
|
+
['status', { defined: { name: 'Round' } }],
|
|
69
|
+
// The terms, as the lottery's revision had them at the round's first purchase; whether its entries keep what
|
|
70
|
+
// arrived (a payment mint with a transfer fee, or a share the buyer chooses, paid as they buy).
|
|
71
|
+
['revision', 'u32'], ['terms', { defined: { name: 'LotteryTerms' } }], ['fee', 'bool'],
|
|
72
|
+
// The totals, split at each purchase: what each share paid in common takes, the entries that own a part of
|
|
73
|
+
// their own, and the parts owed now.
|
|
74
|
+
['sold', 'u64'], ['to_pot', 'u64'], ['common', { vec: 'u64' }], ['owns', 'u32'], ['owed', 'u32'],
|
|
75
|
+
// The draw, and the money the ask fixed: the pot's (what the round owes the pot back: `pot_owed`).
|
|
76
|
+
['asked_first', 'u64'], ['asked_slot', 'u64'], ['first_asked_at', 'i64'], ['asked_at', 'i64'], ['answer', { array: ['u8', 32] }], ['first', 'u64'],
|
|
77
|
+
['pot_total', 'u64'], ['pot_sent', 'u64'], ['pot_arrived', 'u64'], ['pot_owed', 'u64'], ['paid', 'bytes'],
|
|
78
|
+
['prizes', { vec: { defined: { name: 'TokenPrize' } } }], ['accounts', { vec: { defined: { name: 'Opened' } } }],
|
|
79
|
+
// The NFT of the lottery's own stash each place that holds or draws one got, in place order, and how many.
|
|
80
|
+
['nfts', { vec: 'pubkey' }], ['assigned', 'u32'],
|
|
81
|
+
].map(([name, type]) => ({ name, type })),
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{ name: 'ContestEntry', type: { kind: 'struct', fields: [['player', 'pubkey'], ['side', 'u16'], ['amount', 'u64'], ['bits', 'u8'], ['nft', 'pubkey'], ['place', 'u32'], ['taken', 'u32']].map(([name, type]) => ({ name, type })) } },
|
|
85
|
+
];
|
|
86
|
+
const IDLX = { ...IDL, types: [...IDL.types, ...RECORDS], accounts: [...IDL.accounts, { name: 'LotteryRound', discriminator: [TAGS.LotteryRound] }] };
|
|
87
|
+
|
|
88
|
+
/** A loot account of the named kind (one the IDL describes, or a `LotteryRound`). Throws when the data is not one. */
|
|
89
|
+
export const decode = (name, data) => decodeAccount(IDLX, name, data);
|
|
48
90
|
|
|
49
91
|
/** Any loot account: { kind, value }, or null for data that is no loot account. */
|
|
50
92
|
export function decodeAny(data) {
|
|
51
93
|
const kind = accountKind(data);
|
|
52
94
|
if (!kind) return null;
|
|
53
|
-
if (kind === '
|
|
95
|
+
if (kind === 'Profile') return { kind, value: decodeProfile(data) };
|
|
96
|
+
if (kind === 'List') return { kind, value: decodeList(data) };
|
|
97
|
+
if (kind === 'Ends') return { kind, value: decodeEnds(data) };
|
|
98
|
+
if (kind === 'LotteryRound') return { kind, value: decodeRound(data) };
|
|
54
99
|
if (kind === 'Sheet') return { kind, value: decodeSheet(data) };
|
|
100
|
+
if (kind === 'Table') return data[37] === PAYLOAD.Nft ? { kind: 'Stash', value: decodeStash(data) } : { kind, value: decodeTable(data) };
|
|
55
101
|
if (kind === 'Proven') return { kind, value: decodeProven(data) };
|
|
56
102
|
if (kind === 'Traits') return { kind, value: decodeTraits(data) };
|
|
57
|
-
if (kind === 'Stash') return { kind, value: decodeStash(data) };
|
|
58
103
|
return { kind, value: decode(kind, data) };
|
|
59
104
|
}
|
|
60
105
|
|
|
106
|
+
// ---------------------------------------------------------------- lists of records (M1)
|
|
107
|
+
|
|
61
108
|
/**
|
|
62
|
-
* A
|
|
63
|
-
* a grant
|
|
64
|
-
* bytes
|
|
65
|
-
* was paid once and can never be paid again.
|
|
109
|
+
* A list of records (tag 26, `list.rs`): { width, records } in their order: a wallet's counts
|
|
110
|
+
* (`["counts", project, wallet]`) and a grant's claims are limit records (28 bytes, `readLimit`), a grant's rounds 16
|
|
111
|
+
* bytes, a contest's entries 43 or 83 (`decodeEntries`).
|
|
66
112
|
*/
|
|
67
|
-
export function
|
|
68
|
-
if (!(data instanceof Uint8Array) || data[0] !== TAGS.
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
if (data.length !==
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
113
|
+
export function decodeList(data) {
|
|
114
|
+
if (!(data instanceof Uint8Array) || data[0] !== TAGS.List || data.length < 7) throw new Error('Not a list account');
|
|
115
|
+
const width = Number(readUintLE(data, 1, 2));
|
|
116
|
+
const count = Number(readUintLE(data, 3, 4));
|
|
117
|
+
if (!width || data.length !== 7 + width * count) throw new Error('List account does not match its records');
|
|
118
|
+
return { width, records: Array.from({ length: count }, (_, i) => data.subarray(7 + width * i, 7 + width * (i + 1))) };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* A farm's record of when its seats stop earning (`["ends", farm]`, tag 29; `Farm.keeps_ends`): its times in order, each
|
|
123
|
+
* `{ at, rewardPerWeight, weight, seats }`: the time (Unix seconds), `reward_per_weight` there once the farm has walked past
|
|
124
|
+
* it, the weight that stops earning then, and the seats that still need the record.
|
|
125
|
+
*/
|
|
126
|
+
export function decodeEnds(data) {
|
|
127
|
+
if (!(data instanceof Uint8Array) || data[0] !== TAGS.Ends || data.length < 7) throw new Error('Not a record of ends');
|
|
128
|
+
const width = Number(readUintLE(data, 1, 2));
|
|
129
|
+
const count = Number(readUintLE(data, 3, 4));
|
|
130
|
+
if (width !== END_BYTES || data.length !== 7 + width * count) throw new Error('The record of ends does not match its records');
|
|
131
|
+
return Array.from({ length: count }, (_, i) => {
|
|
132
|
+
const at = 7 + width * i;
|
|
133
|
+
let time = 0n;
|
|
134
|
+
for (let b = 0; b < 8; b++) time = (time << 8n) | BigInt(data[at + b]);
|
|
135
|
+
return { at: time, rewardPerWeight: readUintLE(data, at + 8, 16), weight: readUintLE(data, at + 24, 16), seats: Number(readUintLE(data, at + 40, 4)) };
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The first 16 bytes of the sha256 of `parts` (addresses, bytes or u64s): a record's key in a list. */
|
|
140
|
+
export const recordKey16 = (...parts) => sha256(concat(...parts.map((p) => (typeof p === 'string' ? addressBytes(p) : p instanceof Uint8Array ? p : uintLE(p, 8))))).subarray(0, 16);
|
|
141
|
+
|
|
142
|
+
/** The key of a wallet's count for a box, a recipe (incarnation 0) or a lottery (its incarnation), in its counts. */
|
|
143
|
+
export const countKey = (record, incarnation = 0) => recordKey16(record, BigInt(incarnation));
|
|
144
|
+
|
|
145
|
+
/** What the limit record at `key` counts in a decoded list (`decodeList`, or null for none): { count, window, inWindow }. */
|
|
146
|
+
export function readLimit(list, key) {
|
|
147
|
+
const record = list?.records.find((r) => compareBytes(r.subarray(0, 16), key) === 0);
|
|
148
|
+
const n = (i) => (record ? Number(readUintLE(record, 16 + 4 * i, 4)) : 0);
|
|
149
|
+
return { count: n(0), window: n(1), inWindow: n(2) };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** A fetched list account, or null for none (or an empty System account someone sent SOL to, which the program opens
|
|
153
|
+
* as the list). Any other owner or layout is refused. */
|
|
154
|
+
export function readListAccount(account) {
|
|
155
|
+
if (account == null) return null;
|
|
156
|
+
if (account.owner === SYSTEM_PROGRAM && account.data instanceof Uint8Array && account.data.length === 0 && !account.executable) return null;
|
|
157
|
+
if (account.owner !== LOOT_PROGRAM || account.executable) throw new Error('The list has an invalid owner.');
|
|
158
|
+
return decodeList(account.data);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* A contest's entries (`["entries", contest, incarnation, 0]`), in the list's order (by player): [{ player, side,
|
|
163
|
+
* amount (0 once paid or refunded; 1 while a contest of NFTs holds its NFT), bits (`CLAIMED`, `ENDED`), nft, place (how
|
|
164
|
+
* many entered its side before it), taken (the losers' NFTs a winner took) }]. A claim names an entry by its index here.
|
|
165
|
+
*/
|
|
166
|
+
export function decodeEntries(data) {
|
|
167
|
+
const { width, records } = decodeList(data);
|
|
168
|
+
return records.map((r) => {
|
|
169
|
+
const full = new Uint8Array(83);
|
|
170
|
+
full.set(r.subarray(0, Math.min(width, 83)));
|
|
171
|
+
return decodeBorsh(IDLX, { defined: { name: 'ContestEntry' } }, full)[0];
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** A winner's first claim came (its prize given or forgone); a winner ended its picks early. */
|
|
176
|
+
export const CLAIMED = 1;
|
|
177
|
+
export const ENDED = 2;
|
|
178
|
+
|
|
179
|
+
/** How many losers' NFTs a winner of a contest of NFTs takes: the share fixed at the decision, one more for an early
|
|
180
|
+
* place on its side. */
|
|
181
|
+
export const shareOf = (contest, entry) => Number(contest.share) + (Number(entry.place) < Number(contest.extras) ? 1 : 0);
|
|
182
|
+
|
|
183
|
+
/** What a count allows `n` more of at `now` under `limit` ({ total, per_window, window }, 0 is no limit; a lottery's
|
|
184
|
+
* window is its round plus one, given as `window`). */
|
|
185
|
+
export function countAllows(count, limit, now, n = 1, window = Number(limit.window) ? Math.floor(Number(now) / Number(limit.window)) + 1 : 0) {
|
|
186
|
+
const total = (count?.count ?? 0) + n;
|
|
187
|
+
const inWindow = (count && count.window === window ? count.inWindow : 0) + n;
|
|
188
|
+
return (!Number(limit.total) || total <= Number(limit.total)) && (!Number(limit.per_window) || inWindow <= Number(limit.per_window));
|
|
83
189
|
}
|
|
84
190
|
|
|
85
191
|
/**
|
|
@@ -89,11 +195,26 @@ export function decodeReceipt(data) {
|
|
|
89
195
|
export function decodeSheet(data) {
|
|
90
196
|
if (data == null || data.length === 0) return null;
|
|
91
197
|
if (!(data instanceof Uint8Array) || data[0] !== TAGS.Sheet || data.length < 69) throw new Error('Not a Sheet account');
|
|
198
|
+
const { attributes, head } = attributesAt(data, 'Sheet');
|
|
199
|
+
return { project: head(1), nft: head(33), attributes };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** A player's profile (`["profile", project, wallet]`): { project, wallet, attributes }, what the project keeps about
|
|
203
|
+
* the wallet (its XP, level, rank). Its layout is a Sheet's, under its own tag. */
|
|
204
|
+
export function decodeProfile(data) {
|
|
205
|
+
if (data == null || data.length === 0) return null;
|
|
206
|
+
if (!(data instanceof Uint8Array) || data[0] !== TAGS.Profile || data.length < 69) throw new Error('Not a Profile account');
|
|
207
|
+
const { attributes, head } = attributesAt(data, 'Profile');
|
|
208
|
+
return { project: head(1), wallet: head(33), attributes };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** The attributes after a Sheet's or a profile's two addresses, read with every length checked against the data. */
|
|
212
|
+
function attributesAt(data, name) {
|
|
92
213
|
let at = 65;
|
|
93
214
|
// Every length is checked against the data: a count or a text longer than what is there is a truncated account.
|
|
94
215
|
const text = () => {
|
|
95
216
|
const n = Number(readUintLE(data, at, 4));
|
|
96
|
-
if (at + 4 + n > data.length) throw new Error(
|
|
217
|
+
if (at + 4 + n > data.length) throw new Error(`${name} account is truncated`);
|
|
97
218
|
const value = new TextDecoder().decode(data.subarray(at + 4, at + 4 + n));
|
|
98
219
|
at += 4 + n;
|
|
99
220
|
return value;
|
|
@@ -101,19 +222,53 @@ export function decodeSheet(data) {
|
|
|
101
222
|
const count = Number(readUintLE(data, at, 4));
|
|
102
223
|
at += 4;
|
|
103
224
|
// Each attribute takes 8 bytes at least (two empty texts).
|
|
104
|
-
if (count * 8 > data.length - at) throw new Error(
|
|
225
|
+
if (count * 8 > data.length - at) throw new Error(`${name} account is truncated`);
|
|
105
226
|
const attributes = Array.from({ length: count }, () => ({ key: text(), value: text() }));
|
|
106
|
-
return {
|
|
227
|
+
return { attributes, end: at, head: (from) => encodeBase58(data.subarray(from, from + 32)) };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** What a table's rows carry (its payload byte). */
|
|
231
|
+
export const PAYLOAD = Object.freeze({ Number: 0, Label: 1, Nft: 2 });
|
|
232
|
+
const TABLE_HEAD = 51;
|
|
233
|
+
|
|
234
|
+
/** Where row `r` of a table of `len`-byte rows is: groups of 64 blocks of 64 rows, each led by its weight in stock. */
|
|
235
|
+
export function tableRowAt(len, r) {
|
|
236
|
+
const block = 8 + 64 * len;
|
|
237
|
+
const group = 8 + 64 * block;
|
|
238
|
+
return TABLE_HEAD + Math.floor(r / 4096) * group + 8 + (Math.floor(r / 64) % 64) * block + 8 + (r % 64) * len;
|
|
107
239
|
}
|
|
108
240
|
|
|
109
|
-
/**
|
|
241
|
+
/**
|
|
242
|
+
* A table (`["table", project, id]`, `table.rs`): { project, id, payload ('Number' | 'Label'), sealed, count, total
|
|
243
|
+
* (the weight in stock), rows: [{ weight, left, label }] }. A row's `left` is `NO_LIMIT` (4294967295) for a stock that
|
|
244
|
+
* never runs out.
|
|
245
|
+
*/
|
|
246
|
+
export function decodeTable(data) {
|
|
247
|
+
if (data == null || data.length === 0) return null;
|
|
248
|
+
if (!(data instanceof Uint8Array) || data[0] !== TAGS.Table || data.length < TABLE_HEAD) throw new Error('Not a Table account');
|
|
249
|
+
const payload = Object.keys(PAYLOAD).find((k) => PAYLOAD[k] === data[37]);
|
|
250
|
+
if (!payload || payload === 'Nft') throw new Error('Not a Table account');
|
|
251
|
+
const count = Number(readUintLE(data, 39, 4));
|
|
252
|
+
const head = { project: encodeBase58(data.subarray(1, 33)), id: Number(readUintLE(data, 33, 4)), payload, sealed: data[38] === 1, count, total: readUintLE(data, 43, 8) };
|
|
253
|
+
const len = payload === 'Label' ? 40 : 8;
|
|
254
|
+
if (count && tableRowAt(len, count - 1) + len > data.length) throw new Error('Table account is truncated');
|
|
255
|
+
const rows = Array.from({ length: count }, (_, r) => {
|
|
256
|
+
const at = tableRowAt(len, r);
|
|
257
|
+
const label = payload === 'Label' ? new TextDecoder().decode(data.subarray(at + 8, at + 40)).replace(/\0+$/, '') : '';
|
|
258
|
+
return { weight: Number(readUintLE(data, at, 4)), left: Number(readUintLE(data, at + 4, 4)), label };
|
|
259
|
+
});
|
|
260
|
+
return { ...head, rows };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** A stash (`["stash", project, id]`, or a lottery's own): { project, id, nfts, weights }, the NFTs the project holds to
|
|
264
|
+
* give away and the weight each is drawn by, as a table of NFTs keeps them (a row: its weight, then the NFT). */
|
|
110
265
|
export function decodeStash(data) {
|
|
111
266
|
if (data == null || data.length === 0) return null;
|
|
112
|
-
if (!(data instanceof Uint8Array) || data[0] !== TAGS.
|
|
113
|
-
const count = Number(readUintLE(data,
|
|
114
|
-
if (
|
|
115
|
-
const
|
|
116
|
-
return { project: encodeBase58(data.subarray(1, 33)), id: Number(readUintLE(data, 33, 4)), nfts };
|
|
267
|
+
if (!(data instanceof Uint8Array) || data[0] !== TAGS.Table || data[37] !== PAYLOAD.Nft || data.length < TABLE_HEAD) throw new Error('Not a Stash account');
|
|
268
|
+
const count = Number(readUintLE(data, 39, 4));
|
|
269
|
+
if (count && tableRowAt(36, count - 1) + 36 > data.length) throw new Error('Stash account is truncated');
|
|
270
|
+
const rows = Array.from({ length: count }, (_, r) => tableRowAt(36, r));
|
|
271
|
+
return { project: encodeBase58(data.subarray(1, 33)), id: Number(readUintLE(data, 33, 4)), nfts: rows.map((at) => encodeBase58(data.subarray(at + 4, at + 36))), weights: rows.map((at) => Number(readUintLE(data, at, 4))) };
|
|
117
272
|
}
|
|
118
273
|
|
|
119
274
|
/**
|
|
@@ -180,92 +335,263 @@ export function decodeTokenMetadata(data) {
|
|
|
180
335
|
return { mint, collection, programmable, ruleSet };
|
|
181
336
|
}
|
|
182
337
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
338
|
+
// ---------------------------------------------------------------- a lottery's rounds (M3)
|
|
339
|
+
//
|
|
340
|
+
// A round is a record of its own (`pda.round`), opened by its first purchase: a header with a copy of the terms, the
|
|
341
|
+
// totals, what it still owes and the draw, then one entry a purchase. The ask (`draw_lottery`) fixes its money and moves
|
|
342
|
+
// the lottery on; after the answer, `pull` pays one part at a time, in any order, and the pull that leaves nothing owed
|
|
343
|
+
// closes it.
|
|
344
|
+
|
|
345
|
+
/** An entry's word: its tickets' end in the low 48 bits, its own part paid in the top bit. */
|
|
346
|
+
const END_BITS = (1n << 48n) - 1n;
|
|
347
|
+
export const OWN_PAID = 1n << 63n;
|
|
190
348
|
|
|
191
|
-
/**
|
|
192
|
-
export
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
349
|
+
/** The mint a lottery's tickets are paid in (`terms`: a Lottery's or a round's): its ticket mint, burned, or its pot's. */
|
|
350
|
+
export const paymentMint = (terms) => (ticketsBurn(terms) ? terms.ticket_mint : terms.mint);
|
|
351
|
+
/** Whether a lottery's tickets are burned: they are paid in a ticket mint of its own. */
|
|
352
|
+
export const ticketsBurn = (terms) => Boolean(terms.ticket_mint) && terms.ticket_mint !== NO_ADDRESS;
|
|
353
|
+
|
|
354
|
+
/** A share the buyer chooses (a referrer, a collectible's holder): paid out of the price as they buy, from their own account. */
|
|
355
|
+
export const chosenAtPurchase = (share) => Boolean(share.to?.Chosen);
|
|
356
|
+
|
|
357
|
+
/** The bytes of one entry of a round: buyer, word, what arrived (`fee`: a fee mint, or a chosen share), the NFT ticket. */
|
|
358
|
+
export const entryWidth = (fee, nft) => 40 + (fee ? 8 : 0) + (nft ? 32 : 0);
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* A lottery round (`["round", lottery, incarnation, round]`): its header (`status` 'Open' | 'Drawing' | 'Drawn' |
|
|
362
|
+
* 'Cancelled', the terms it copied, `sold`, `owed`, the draw and the money the ask fixed, `nfts` the NFT of the
|
|
363
|
+
* lottery's own stash each such place got), and `entries`, one a purchase: { buyer, start, end (its tickets), paid (its
|
|
364
|
+
* own part's bit), amount (what reached the round), nft (its NFT ticket) }.
|
|
365
|
+
*/
|
|
366
|
+
export function decodeRound(data) {
|
|
367
|
+
const r = decode('LotteryRound', data);
|
|
368
|
+
const at = Number(r.head);
|
|
369
|
+
const width = Number(readUintLE(data, at, 2));
|
|
370
|
+
const count = Number(readUintLE(data, at + 2, 4));
|
|
371
|
+
if (width !== entryWidth(r.fee, r.terms.nft_ticket != null) || data.length !== at + 6 + width * count) throw new Error('LotteryRound entries do not match its header');
|
|
372
|
+
let start = 0n;
|
|
373
|
+
const entries = Array.from({ length: count }, (_, i) => {
|
|
374
|
+
const e = data.subarray(at + 6 + width * i, at + 6 + width * (i + 1));
|
|
375
|
+
const word = readUintLE(e, 32, 8);
|
|
376
|
+
const end = word & END_BITS;
|
|
377
|
+
let k = 40;
|
|
378
|
+
const take = (n) => e.subarray((k += n) - n, k);
|
|
379
|
+
const amount = r.fee ? readUintLE(take(8), 0, 8) : (end - start) * BigInt(r.terms.price);
|
|
380
|
+
const entry = { buyer: encodeBase58(e.subarray(0, 32)), start, end, paid: word & ~END_BITS, amount, nft: r.terms.nft_ticket ? encodeBase58(take(32)) : NO_ADDRESS };
|
|
381
|
+
start = end;
|
|
382
|
+
return entry;
|
|
383
|
+
});
|
|
384
|
+
return { ...r, entries };
|
|
197
385
|
}
|
|
198
386
|
|
|
199
|
-
|
|
387
|
+
/** How many places a lottery's (or a round's) `places` are, every run counted (`Place.times`). */
|
|
388
|
+
export const placeCount = (places) => places.reduce((n, p) => n + Number(p.times ?? 1), 0);
|
|
200
389
|
|
|
201
|
-
/** The
|
|
202
|
-
export const
|
|
203
|
-
contributions.find(({ value }) => BigInt(value.first_ticket) <= BigInt(ticket) && BigInt(ticket) < BigInt(value.end)) ?? null;
|
|
390
|
+
/** The places of a round that have a winner: every place, but no more than tickets. */
|
|
391
|
+
export const roundWinners = (r) => Math.min(placeCount(r.terms.places), Number(r.sold));
|
|
204
392
|
|
|
205
|
-
/**
|
|
206
|
-
export
|
|
393
|
+
/** The ticket that wins `place` of a drawn round: even steps around the list from first place's ticket. */
|
|
394
|
+
export function ticketOfPlace(r, place) {
|
|
395
|
+
const sold = BigInt(r.sold);
|
|
396
|
+
const ahead = BigInt(place) * (sold / BigInt(roundWinners(r) || 1));
|
|
397
|
+
const toEnd = sold - BigInt(r.first);
|
|
398
|
+
return ahead < toEnd ? BigInt(r.first) + ahead : ahead - toEnd;
|
|
399
|
+
}
|
|
207
400
|
|
|
208
|
-
/** The
|
|
209
|
-
export function
|
|
210
|
-
|
|
211
|
-
|
|
401
|
+
/** The run of places that holds `place`, and its first place. */
|
|
402
|
+
export function runOf(r, place) {
|
|
403
|
+
let first = 0;
|
|
404
|
+
for (let run = 0; run < r.terms.places.length; run++) {
|
|
405
|
+
if (place < first + Number(r.terms.places[run].times)) return [run, first];
|
|
406
|
+
first += Number(r.terms.places[run].times);
|
|
407
|
+
}
|
|
408
|
+
return [r.terms.places.length, first];
|
|
212
409
|
}
|
|
213
410
|
|
|
214
|
-
/** The
|
|
215
|
-
export function
|
|
216
|
-
const
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
const toEnd = sold - BigInt(lottery.first_ticket);
|
|
220
|
-
return ahead < toEnd ? BigInt(lottery.first_ticket) + ahead : ahead - toEnd;
|
|
411
|
+
/** The places of run `run` with a winner: [first, end). */
|
|
412
|
+
export function runPlaces(r, run) {
|
|
413
|
+
const first = placeCount(r.terms.places.slice(0, run));
|
|
414
|
+
const w = roundWinners(r);
|
|
415
|
+
return [Math.min(first, w), Math.min(first + Number(r.terms.places[run].times), w)];
|
|
221
416
|
}
|
|
222
417
|
|
|
223
|
-
/** The places of
|
|
224
|
-
export function
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
418
|
+
/** The places of run `run` whose ticket is in `start..end` (an entry's), in order. */
|
|
419
|
+
export function placesIn(r, run, start, end) {
|
|
420
|
+
const [from, to] = runPlaces(r, run);
|
|
421
|
+
const out = [];
|
|
422
|
+
if (from >= to || BigInt(start) >= BigInt(end)) return out;
|
|
423
|
+
const sold = BigInt(r.sold);
|
|
424
|
+
const step = sold / BigInt(roundWinners(r));
|
|
425
|
+
const d = (BigInt(start) + sold - BigInt(r.first)) % sold;
|
|
426
|
+
const len = BigInt(end) - BigInt(start);
|
|
427
|
+
for (const [a, b] of [[d, d + len < sold ? d + len : sold], [0n, d + len > sold ? d + len - sold : 0n]]) {
|
|
428
|
+
let p = Number((a + step - 1n) / step);
|
|
429
|
+
for (p = Math.max(p, from); p < to && BigInt(p) * step < b; p++) out.push(p);
|
|
229
430
|
}
|
|
230
|
-
return
|
|
431
|
+
return out.sort((x, y) => x - y);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** What one place of a pass-through amount takes: `clamp(sent − before, 0, amount) × arrived / sent` (none of nothing sent). */
|
|
435
|
+
const through = (sent, arrived, before, amount) => {
|
|
436
|
+
if (sent === 0n) return 0n;
|
|
437
|
+
const part = sent > before ? (sent - before < amount ? sent - before : amount) : 0n;
|
|
438
|
+
return (part * arrived) / sent;
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* What place `place` (counted from the first place of `runs`) takes of a pass-through amount, as the program splits it:
|
|
443
|
+
* runs of `[a, n]` (n places taking `a` each), `sent` what the places are owed and `arrived` what the round holds for
|
|
444
|
+
* them; the last place with a part takes what the others leave.
|
|
445
|
+
*/
|
|
446
|
+
export function splitPart(runs, sent, arrived, place) {
|
|
447
|
+
[sent, arrived] = [BigInt(sent), BigInt(arrived)];
|
|
448
|
+
let [c, at, last, paid, before] = [0n, 0, null, 0n, 0n];
|
|
449
|
+
for (const [a, n] of runs) {
|
|
450
|
+
const m = a === 0n ? 0 : Math.min(Number((sent > c ? sent - c + a - 1n : 0n) / a), n);
|
|
451
|
+
if (m > 0) {
|
|
452
|
+
const full = through(sent, arrived, 0n, a);
|
|
453
|
+
[last, before] = [at + m - 1, paid + BigInt(m - 1) * full];
|
|
454
|
+
paid = before + through(sent, arrived, c + BigInt(m - 1) * a, a);
|
|
455
|
+
}
|
|
456
|
+
[c, at] = [c + a * BigInt(n), at + n];
|
|
457
|
+
}
|
|
458
|
+
if (last === place) return arrived > before ? arrived - before : 0n;
|
|
459
|
+
[c, at] = [0n, 0];
|
|
460
|
+
for (const [a, n] of runs) {
|
|
461
|
+
if (place < at + n) return through(sent, arrived, c + BigInt(place - at) * a, a);
|
|
462
|
+
[c, at] = [c + a * BigInt(n), at + n];
|
|
463
|
+
}
|
|
464
|
+
return 0n;
|
|
231
465
|
}
|
|
232
466
|
|
|
233
|
-
/**
|
|
234
|
-
|
|
467
|
+
/** A share in basis points of an amount, rounded down. */
|
|
468
|
+
const bpsOf = (amount, bps) => (BigInt(amount) * BigInt(bps)) / 10_000n;
|
|
469
|
+
|
|
470
|
+
/** Whether place `place`'s part is paid: its pot part's bit (every place's first), or with `prize` its prize's (after
|
|
471
|
+
* every place's pot bit). */
|
|
472
|
+
export const placePaid = (r, place, prize) => {
|
|
473
|
+
const at = place + (prize ? placeCount(r.terms.places) : 0);
|
|
474
|
+
return Boolean((r.paid[at >> 3] ?? 0) & (1 << (at & 7)));
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
/** Entry `i`'s places in run `run` not paid yet of its pot part or (`prize`) its token prize, lowest first, each with
|
|
478
|
+
* what it takes: [{ place, amount }]. A pull pays all of them. */
|
|
479
|
+
function unpaidPlaces(r, i, run, prize) {
|
|
480
|
+
const e = r.entries[i];
|
|
481
|
+
const k = prize ? r.prizes.findIndex((p) => Number(p.run) === run) : -1;
|
|
482
|
+
if (prize && k < 0) return [];
|
|
483
|
+
const runs = k >= 0
|
|
484
|
+
? [[BigInt(r.prizes[k].each), runPlaces(r, run)[1] - runPlaces(r, run)[0]]]
|
|
485
|
+
: r.terms.places.map((p, j) => [bpsOf(r.pot_total, p.bps), runPlaces(r, j)[1] - runPlaces(r, j)[0]]);
|
|
486
|
+
const [sent, arrived, from] = k >= 0 ? [r.prizes[k].sent, r.prizes[k].arrived, runPlaces(r, run)[0]] : [r.pot_sent, r.pot_arrived, 0];
|
|
487
|
+
return placesIn(r, run, e.start, e.end).filter((p) => !placePaid(r, p, prize)).map((place) => ({ place, amount: splitPart(runs, sent, arrived, place - from) }));
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** What entry `i`'s places in run `run` take and are not paid yet, of its pot part or (`prize`) its token prize. */
|
|
491
|
+
export const placesOwed = (r, i, run, prize) => unpaidPlaces(r, i, run, prize).reduce((sum, p) => sum + p.amount, 0n);
|
|
492
|
+
|
|
493
|
+
/** How many of the round's owed parts a pull of `{ Pot: [run] }` or a token `{ Prize: [run] }` for entry `i` settles:
|
|
494
|
+
* its places there that take more than nothing (one pull pays them all). */
|
|
495
|
+
export const placesPulled = (r, i, run, prize) => unpaidPlaces(r, i, run, prize).filter((p) => p.amount > 0n).length;
|
|
235
496
|
|
|
236
|
-
/**
|
|
237
|
-
export
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
497
|
+
/** Whether run `run` of a round's places gives tokens (its prize is fixed at the ask, a `TokenPrize`). */
|
|
498
|
+
export const tokenRun = (r, run) => Boolean(r.terms.places[run]?.prize?.Tokens);
|
|
499
|
+
|
|
500
|
+
/** A place whose prize is an NFT of the lottery's own stash (`Held`, `Raffled`): assigned by the pulls, in place order. */
|
|
501
|
+
export const ownStash = (place) => Boolean(place.prize?.Held) || place.prize === 'Raffled' || place.prize?.Raffled !== undefined;
|
|
502
|
+
|
|
503
|
+
/** Entry `i`'s places in run `run` whose prize (not tokens) is not given yet, lowest first. */
|
|
504
|
+
export const prizePlacesOwed = (r, i, run) => {
|
|
505
|
+
const e = r.entries[i];
|
|
506
|
+
return placesIn(r, run, e.start, e.end).filter((p) => !placePaid(r, p, true));
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
/** Where in `r.nfts` the NFT of `place` is (a place of a run that holds or draws one of the lottery's own stash). */
|
|
510
|
+
export function nftSlot(r, place) {
|
|
511
|
+
let [k, first] = [0, 0];
|
|
512
|
+
for (const p of r.terms.places) {
|
|
513
|
+
if (place < first + Number(p.times)) return k + place - first;
|
|
514
|
+
if (ownStash(p)) k += Number(p.times);
|
|
515
|
+
first += Number(p.times);
|
|
516
|
+
}
|
|
517
|
+
return k;
|
|
242
518
|
}
|
|
243
519
|
|
|
244
|
-
/**
|
|
245
|
-
export
|
|
520
|
+
/** How many places of a round win an NFT of the lottery's own stash (every such place with a winner). */
|
|
521
|
+
export function stashPlacesWon(r) {
|
|
522
|
+
const w = roundWinners(r);
|
|
523
|
+
let [first, won] = [0, 0];
|
|
524
|
+
for (const p of r.terms.places) {
|
|
525
|
+
if (ownStash(p)) won += Math.max(0, Math.min(first + Number(p.times), w) - first);
|
|
526
|
+
first += Number(p.times);
|
|
527
|
+
}
|
|
528
|
+
return won;
|
|
529
|
+
}
|
|
246
530
|
|
|
247
|
-
/**
|
|
248
|
-
|
|
531
|
+
/** Whether entry `i`'s places in run `run` (an NFT of the lottery's own stash) wait for their NFTs to be taken out
|
|
532
|
+
* (`LotteryRound::assign`): a pull with the stash alone does it, and `r.nfts` then names each. */
|
|
533
|
+
export function awaitsNfts(r, i, run) {
|
|
534
|
+
if (!ownStash(r.terms.places[run])) return false;
|
|
535
|
+
const places = prizePlacesOwed(r, i, run);
|
|
536
|
+
return places.some((p) => nftSlot(r, p) >= Number(r.assigned));
|
|
537
|
+
}
|
|
249
538
|
|
|
250
539
|
/**
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
* prize of the round is given);
|
|
256
|
-
* 'pay' its places' shares of the pot, once every contribution of the round is finished.
|
|
540
|
+
* The parts entry `i` of a round is owed now, in the order a keeper pulls them (`pullIx`'s `part`): its own ('Own': a
|
|
541
|
+
* refund, a no-loss ticket's price, its NFT ticket back or where its price sends it), and with `places`, its places'
|
|
542
|
+
* parts of the pot ({ Pot: [run] }) and prizes ({ Prize: [run] }). Parts owed in common (a share, the pot's, a pool's
|
|
543
|
+
* prize back) are no entry's: `commonOwed`.
|
|
257
544
|
*/
|
|
258
|
-
export function
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
if (
|
|
265
|
-
if (
|
|
266
|
-
|
|
545
|
+
export function partsOwed(r, i, { places = true } = {}) {
|
|
546
|
+
const e = r.entries[i];
|
|
547
|
+
const parts = [];
|
|
548
|
+
const drawn = r.status === 'Drawn';
|
|
549
|
+
const back = r.status === 'Cancelled' || (r.terms.returns && drawn);
|
|
550
|
+
const nft = e.nft !== NO_ADDRESS;
|
|
551
|
+
if ((e.amount > 0n || nft) && (back || (nft && drawn)) && !(e.paid & OWN_PAID)) parts.push('Own');
|
|
552
|
+
if (!places || !drawn) return parts;
|
|
553
|
+
for (let run = 0; run < r.terms.places.length; run++) {
|
|
554
|
+
const prize = r.terms.places[run].prize;
|
|
555
|
+
if (placesOwed(r, i, run, false) > 0n) parts.push({ Pot: [run] });
|
|
556
|
+
if (tokenRun(r, run) ? placesOwed(r, i, run, true) > 0n : prize != null && prizePlacesOwed(r, i, run).length > 0) parts.push({ Prize: [run] });
|
|
557
|
+
}
|
|
558
|
+
return parts;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* The parts of a round owed in common, of no entry (`{ Common: [i] }`): each share paid in common once drawn (one that
|
|
563
|
+
* burns is burned at the close), then what the round owes the lottery's pot (`i` = the shares' count), then after a
|
|
564
|
+
* cancel each pool's token prize back to its pool (past it).
|
|
565
|
+
*/
|
|
566
|
+
export function commonOwed(r) {
|
|
567
|
+
const n = r.terms.shares.length;
|
|
568
|
+
const out = [];
|
|
569
|
+
if (r.status === 'Drawn') r.terms.shares.forEach((s, i) => s.to !== 'Burn' && BigInt(r.common[i] ?? 0) > 0n && out.push({ Common: [i] }));
|
|
570
|
+
if ((r.status === 'Drawn' || r.status === 'Cancelled') && BigInt(r.pot_owed) > 0n) out.push({ Common: [n] });
|
|
571
|
+
if (r.status === 'Cancelled') r.prizes.forEach((p, k) => !p.minted && BigInt(p.arrived) > 0n && out.push({ Common: [n + 1 + k] }));
|
|
572
|
+
return out;
|
|
267
573
|
}
|
|
268
574
|
|
|
575
|
+
/** The entry of a round holding `ticket`. */
|
|
576
|
+
export const entryOf = (r, ticket) => r.entries.findIndex((e) => e.start <= BigInt(ticket) && BigInt(ticket) < e.end);
|
|
577
|
+
|
|
578
|
+
/** Whether a round is done and owes nothing: the next pull closes it, given its close's accounts. */
|
|
579
|
+
export const roundFinished = (r) => (r.status === 'Drawn' || r.status === 'Cancelled') && Number(r.owed) === 0;
|
|
580
|
+
|
|
581
|
+
/** Whether a lottery's place wins something: a share of the pot, a prize, or both. */
|
|
582
|
+
export const placeSet = (place) => Number(place.bps) > 0 || place.prize != null;
|
|
583
|
+
|
|
584
|
+
/** The address of a lottery's current round's record (there or not). */
|
|
585
|
+
export const roundAddress = (lotteryAddress, lottery) => pda.round(lotteryAddress, lottery.incarnation, lottery.round);
|
|
586
|
+
|
|
587
|
+
/** Whether a lottery (decoded) drew once and has ended. */
|
|
588
|
+
export const lotteryEnded = (lottery) => Number(lottery.every) === 0 && Number(lottery.round) > 0;
|
|
589
|
+
|
|
590
|
+
/** The round a lottery's close, its write again and its own stash's moves name (`lottery::check_idle`): its current
|
|
591
|
+
* round's, or once a lottery that draws once has ended, round 0's (which keeps its stash's NFTs until its places took
|
|
592
|
+
* them out). */
|
|
593
|
+
export const idleRound = (lotteryAddress, lottery) => pda.round(lotteryAddress, lottery.incarnation, lotteryEnded(lottery) ? 0 : lottery.round);
|
|
594
|
+
|
|
269
595
|
/**
|
|
270
596
|
* Who rents a listed NFT at `now` (Unix seconds), from its decoded Listing (or null for none): the renter's wallet while
|
|
271
597
|
* a rental runs, else null (a sale listing, or a rental that is over).
|
|
@@ -275,10 +601,15 @@ export function renterOf(listing, now) {
|
|
|
275
601
|
return BigInt(now) < BigInt(listing.until) && listing.user !== SYSTEM_PROGRAM ? listing.user : null;
|
|
276
602
|
}
|
|
277
603
|
|
|
604
|
+
/** Whether a schedule pays every run due when it is sent late (`schedule::catches_up`): only a sealed fixed amount. */
|
|
605
|
+
export const scheduleCatchesUp = (schedule) => Boolean(schedule.action?.from?.Vest && schedule.action?.amount?.Fixed);
|
|
606
|
+
|
|
278
607
|
/**
|
|
279
|
-
* How many runs of a schedule are due at `now` (Unix seconds, the chain's clock, never the computer's)
|
|
280
|
-
*
|
|
281
|
-
*
|
|
608
|
+
* How many runs of a schedule are due at `now` (Unix seconds, the chain's clock, never the computer's). One `run_schedule`
|
|
609
|
+
* makes them all. A sealed schedule's fixed amount pays each of them (`scheduleCatchesUp`; up to its `upto`, the runs made
|
|
610
|
+
* after it: `schedule.runs` plus these, and a long backlog takes a few calls). Any other run pays once, for the latest
|
|
611
|
+
* run due, and those before it lapse. A sealed schedule past its last run is due once whenever its vault holds something
|
|
612
|
+
* (`vaultBalance`), which goes where it always went.
|
|
282
613
|
*/
|
|
283
614
|
export function runsDue(schedule, now, vaultBalance = 0n) {
|
|
284
615
|
const runs = Number(schedule.runs), max = Number(schedule.max_runs);
|
|
@@ -312,7 +643,8 @@ export const ERRORS = Object.freeze(Object.fromEntries(IDL.errors.map((e) => [e.
|
|
|
312
643
|
|
|
313
644
|
/**
|
|
314
645
|
* The loot error in a failed transaction's logs ("custom program error: 0x…", or Anchor's "Error Code: …" line), or
|
|
315
|
-
* null
|
|
646
|
+
* null: { code, name, msg }, `msg` the IDL's sentence (the program's own log names the error only: its text is
|
|
647
|
+
* compiled out). Only loot's own: another program the transaction calls (Bubblegum, the VRF program, …) has codes of its own
|
|
316
648
|
* that may be the same numbers, so an error is loot's only when loot is the program that logged it or failed with it.
|
|
317
649
|
* The first program that fails is where the error came from; the programs that called it fail after it with the same
|
|
318
650
|
* code.
|
|
@@ -339,7 +671,7 @@ export function lootError(logs) {
|
|
|
339
671
|
// A line that names no program (a log line, or an error's own text) is the running program's, when it is known.
|
|
340
672
|
if (running.length && running.at(-1) !== LOOT_PROGRAM) continue;
|
|
341
673
|
const named = /Error Code: (\w+)\. Error Number: (\d+)\. Error Message: (.+?)\.?$/.exec(line);
|
|
342
|
-
if (named) return { code: Number(named[2]), name: named[1], msg: named[3] };
|
|
674
|
+
if (named) return { code: Number(named[2]), name: named[1], msg: ERRORS[Number(named[2])]?.name === named[1] ? ERRORS[Number(named[2])].msg : named[3] };
|
|
343
675
|
const m = /custom program error: 0x([0-9a-f]+)/i.exec(line);
|
|
344
676
|
if (m && ERRORS[parseInt(m[1], 16)]) return { code: parseInt(m[1], 16), ...ERRORS[parseInt(m[1], 16)] };
|
|
345
677
|
}
|