@xgamesfun/solana 0.0.0-stage → 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/DISCLAIMER.md +41 -0
- package/LICENSE +202 -0
- package/NOTICE +13 -0
- package/README.md +172 -2
- package/bin/xsolana.mjs +189 -0
- package/examples/artwork-auction.json +51 -0
- package/examples/blind-forge.json +128 -0
- package/examples/coin-flip-arena.json +103 -0
- package/examples/duel-arena.json +137 -0
- package/examples/elements-set.json +274 -0
- package/examples/event-badges.json +67 -0
- package/examples/expedition-guild.json +154 -0
- package/examples/fair-presale.json +48 -0
- package/examples/falling-drop.json +90 -0
- package/examples/guild-missions.json +137 -0
- package/examples/halving-seasons.json +107 -0
- package/examples/hero-trials.json +93 -0
- package/examples/ideas.json +789 -0
- package/examples/level-ladder.json +65 -0
- package/examples/merge-2048.json +128 -0
- package/examples/nft-raffle.json +33 -0
- package/examples/paper-hands-farm.json +44 -0
- package/examples/partner-vault.json +112 -0
- package/examples/sacrifice-raffle.json +118 -0
- package/examples/scorched-treasury.json +84 -0
- package/examples/shelter-fair.json +164 -0
- package/examples/spark-pity.json +153 -0
- package/examples/stakers-draw.json +118 -0
- package/examples/star-fleet.json +150 -0
- package/examples/starforce.json +116 -0
- package/examples/stud-farm.json +134 -0
- package/examples/team-vesting.json +70 -0
- package/examples/trade-in-shop.json +168 -0
- package/js/approve/details.mjs +263 -0
- package/js/approve/ed25519.mjs +36 -0
- package/js/approve/live-check.mjs +144 -0
- package/js/approve/page.html +86 -0
- package/js/approve/page.js +203 -0
- package/js/approve/server.mjs +726 -0
- package/js/chain/base58.js +73 -0
- package/js/chain/borsh.js +270 -0
- package/js/chain/bytes.js +141 -0
- package/js/chain/core.js +181 -0
- package/js/chain/curve.js +52 -0
- package/js/chain/lookup.js +279 -0
- package/js/chain/mint.js +96 -0
- package/js/chain/pda.js +66 -0
- package/js/chain/programs.js +102 -0
- package/js/chain/rpc.js +219 -0
- package/js/chain/sha256.js +68 -0
- package/js/chain/token.js +85 -0
- package/js/chain/token2022.js +247 -0
- package/js/chain/tx.js +224 -0
- package/js/client/accounts.js +679 -0
- package/js/client/compressed.js +46 -0
- package/js/client/grant.js +299 -0
- package/js/client/idl/loot.js +2 -0
- package/js/client/idl/loot.json +17206 -0
- package/js/client/index.js +10 -0
- package/js/client/ix.js +2887 -0
- package/js/client/pda.js +114 -0
- package/js/client/policy.js +69 -0
- package/js/client/random.js +70 -0
- package/js/client/sizes.js +297 -0
- package/js/client/traits.js +44 -0
- package/js/connect/connect.mjs +182 -0
- package/js/economy/check.mjs +2236 -0
- package/js/economy/cost.mjs +185 -0
- package/js/economy/examples.mjs +114 -0
- package/js/economy/explain.mjs +239 -0
- package/js/economy/guide.mjs +232 -0
- package/js/economy/legal.mjs +15 -0
- package/js/economy/live.mjs +130 -0
- package/js/economy/model.mjs +698 -0
- package/js/economy/plan.mjs +1155 -0
- package/js/economy/schema.json +3071 -0
- package/js/economy/secrets.mjs +60 -0
- package/js/economy/simulate.mjs +1019 -0
- package/js/economy/txsize.mjs +45 -0
- package/js/economy/validate.mjs +85 -0
- package/js/index.js +10 -0
- package/js/mcp/server.mjs +113 -0
- package/js/mcp/tools.mjs +485 -0
- package/js/play/index.js +2855 -0
- package/js/play/keys.js +167 -0
- package/js/results/index.js +184 -0
- package/js/results/wire.js +90 -0
- package/package.json +100 -4
- package/release.json +8 -0
- package/skills/xsolana-economy/SKILL.md +109 -0
- package/skills/xsolana-economy/references/after-launch.md +124 -0
- package/skills/xsolana-economy/references/apps.md +193 -0
- package/skills/xsolana-economy/references/sentence-to-spec.md +180 -0
- package/skills/xsolana-economy/references/telling-the-person.md +134 -0
- package/types/chain/base58.d.ts +10 -0
- package/types/chain/borsh.d.ts +23 -0
- package/types/chain/bytes.d.ts +45 -0
- package/types/chain/core.d.ts +52 -0
- package/types/chain/curve.d.ts +1 -0
- package/types/chain/lookup.d.ts +194 -0
- package/types/chain/mint.d.ts +25 -0
- package/types/chain/pda.d.ts +25 -0
- package/types/chain/programs.d.ts +65 -0
- package/types/chain/rpc.d.ts +68 -0
- package/types/chain/sha256.d.ts +1 -0
- package/types/chain/token.d.ts +141 -0
- package/types/chain/token2022.d.ts +53 -0
- package/types/chain/tx.d.ts +60 -0
- package/types/client/accounts.d.ts +318 -0
- package/types/client/compressed.d.ts +29 -0
- package/types/client/grant.d.ts +170 -0
- package/types/client/idl/loot.d.ts +4498 -0
- package/types/client/index.d.ts +9 -0
- package/types/client/ix.d.ts +5315 -0
- package/types/client/pda.d.ts +95 -0
- package/types/client/policy.d.ts +48 -0
- package/types/client/random.d.ts +26 -0
- package/types/client/sizes.d.ts +101 -0
- package/types/client/traits.d.ts +12 -0
- package/types/economy/check.d.mts +100 -0
- package/types/economy/cost.d.mts +69 -0
- package/types/economy/examples.d.mts +27 -0
- package/types/economy/explain.d.mts +19 -0
- package/types/economy/guide.d.mts +140 -0
- package/types/economy/legal.d.mts +12 -0
- package/types/economy/live.d.mts +124 -0
- package/types/economy/model.d.mts +431 -0
- package/types/economy/plan.d.mts +1014 -0
- package/types/economy/secrets.d.mts +4 -0
- package/types/economy/simulate.d.mts +164 -0
- package/types/economy/txsize.d.mts +17 -0
- package/types/economy/validate.d.mts +3 -0
- package/types/index.d.ts +9 -0
- package/types/mcp/tools.d.mts +635 -0
- package/types/play/index.d.ts +880 -0
- package/types/play/keys.d.ts +51 -0
- package/types/results/index.d.ts +155 -0
- package/types/results/wire.d.ts +122 -0
|
@@ -0,0 +1,679 @@
|
|
|
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
|
|
4
|
+
// IDL's shapes (chain/borsh.js).
|
|
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';
|
|
8
|
+
import { sha256 } from '../chain/sha256.js';
|
|
9
|
+
import { SYSTEM_PROGRAM } from '../chain/pda.js';
|
|
10
|
+
import { LOOT_PROGRAM, pda } from './pda.js';
|
|
11
|
+
import IDL from './idl/loot.js';
|
|
12
|
+
|
|
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. */
|
|
19
|
+
export const TAGS = Object.freeze({
|
|
20
|
+
Platform: 1,
|
|
21
|
+
Project: 2,
|
|
22
|
+
Item: 3,
|
|
23
|
+
LootBox: 4,
|
|
24
|
+
Order: 5,
|
|
25
|
+
Schedule: 6,
|
|
26
|
+
Lottery: 7,
|
|
27
|
+
Recipe: 8,
|
|
28
|
+
Hold: 9,
|
|
29
|
+
Contest: 10,
|
|
30
|
+
Sheet: 12,
|
|
31
|
+
Traits: 13,
|
|
32
|
+
LotteryRound: 16,
|
|
33
|
+
Farm: 17,
|
|
34
|
+
Seat: 18,
|
|
35
|
+
Proven: 19,
|
|
36
|
+
Auction: 20,
|
|
37
|
+
Offer: 22,
|
|
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,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const BY_TAG = Object.fromEntries(Object.entries(TAGS).map(([name, tag]) => [tag, name]));
|
|
50
|
+
|
|
51
|
+
/** The kind of a loot account from its first byte, or null. */
|
|
52
|
+
export const accountKind = (data) => (data?.length ? BY_TAG[data[0]] ?? null : null);
|
|
53
|
+
|
|
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);
|
|
90
|
+
|
|
91
|
+
/** Any loot account: { kind, value }, or null for data that is no loot account. */
|
|
92
|
+
export function decodeAny(data) {
|
|
93
|
+
const kind = accountKind(data);
|
|
94
|
+
if (!kind) return null;
|
|
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) };
|
|
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) };
|
|
101
|
+
if (kind === 'Proven') return { kind, value: decodeProven(data) };
|
|
102
|
+
if (kind === 'Traits') return { kind, value: decodeTraits(data) };
|
|
103
|
+
return { kind, value: decode(kind, data) };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---------------------------------------------------------------- lists of records (M1)
|
|
107
|
+
|
|
108
|
+
/**
|
|
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`).
|
|
112
|
+
*/
|
|
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));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* A Sheet (`["sheet", project, nft]`): what the project wrote about an NFT it has no authority over. Returns { project, nft,
|
|
193
|
+
* attributes: [{ key, value }] }. A Sheet never written reads as none: pass `null` data for no account.
|
|
194
|
+
*/
|
|
195
|
+
export function decodeSheet(data) {
|
|
196
|
+
if (data == null || data.length === 0) return null;
|
|
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) {
|
|
213
|
+
let at = 65;
|
|
214
|
+
// Every length is checked against the data: a count or a text longer than what is there is a truncated account.
|
|
215
|
+
const text = () => {
|
|
216
|
+
const n = Number(readUintLE(data, at, 4));
|
|
217
|
+
if (at + 4 + n > data.length) throw new Error(`${name} account is truncated`);
|
|
218
|
+
const value = new TextDecoder().decode(data.subarray(at + 4, at + 4 + n));
|
|
219
|
+
at += 4 + n;
|
|
220
|
+
return value;
|
|
221
|
+
};
|
|
222
|
+
const count = Number(readUintLE(data, at, 4));
|
|
223
|
+
at += 4;
|
|
224
|
+
// Each attribute takes 8 bytes at least (two empty texts).
|
|
225
|
+
if (count * 8 > data.length - at) throw new Error(`${name} account is truncated`);
|
|
226
|
+
const attributes = Array.from({ length: count }, () => ({ key: text(), value: text() }));
|
|
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;
|
|
239
|
+
}
|
|
240
|
+
|
|
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). */
|
|
265
|
+
export function decodeStash(data) {
|
|
266
|
+
if (data == null || data.length === 0) return null;
|
|
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))) };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* A compressed NFT's record (`["leaf", asset]`, `prove_leaf`): { asset, tree, owner (null once burned), delegate,
|
|
276
|
+
* collection (or null), flags, nonce, root (hex: the tree's root it holds for), payer }. Routes read it only while the
|
|
277
|
+
* tree's root is still `root`; after that anyone may close it (`closeLeafIx`).
|
|
278
|
+
*/
|
|
279
|
+
export function decodeProven(data) {
|
|
280
|
+
// Its shortest: the tag, five addresses, the root, data hash and creator hash, no asset data hash, the flags and
|
|
281
|
+
// nonce, and its payer.
|
|
282
|
+
if (!(data instanceof Uint8Array) || data[0] !== TAGS.Proven || data.length < 1 + 32 * 8 + 1 + 1 + 8 + 32) throw new Error('Not a Proven account');
|
|
283
|
+
const key = (at) => encodeBase58(data.subarray(at, at + 32));
|
|
284
|
+
const none = (k) => (k === NO_ADDRESS ? null : k);
|
|
285
|
+
const root = 1 + 32 * 5;
|
|
286
|
+
let at = root + 32 * 3; // the five addresses, then the root, data hash and creator hash
|
|
287
|
+
if (data[at] !== 0 && data[at] !== 1) throw new Error('Proven account has an unknown asset data hash flag');
|
|
288
|
+
at += data[at] === 1 ? 33 : 1; // the asset data hash
|
|
289
|
+
// With an asset data hash, the flags, nonce and payer come 32 bytes later: the account must hold them too.
|
|
290
|
+
if (at + 1 + 8 + 32 > data.length) throw new Error('Proven account is truncated');
|
|
291
|
+
const flags = data[at];
|
|
292
|
+
return {
|
|
293
|
+
asset: key(1), tree: key(33), owner: none(key(65)), delegate: none(key(97)), collection: none(key(129)),
|
|
294
|
+
flags, nonce: readUintLE(data, at + 1, 8), root: toHex(data.subarray(root, root + 32)), payer: key(at + 9),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** The traits a project vouches for (`["traits", project, key]`): { project, key, root (hex) }. */
|
|
299
|
+
export function decodeTraits(data) {
|
|
300
|
+
if (!(data instanceof Uint8Array) || data[0] !== TAGS.Traits || data.length < 97) throw new Error('Not a Traits account');
|
|
301
|
+
return { project: encodeBase58(data.subarray(1, 33)), key: encodeBase58(data.subarray(33, 65)), root: toHex(data.subarray(65, 97)) };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* A Token Metadata account (`MetadataV1`), read as far as a farm needs: { mint, collection (the one it is verified
|
|
306
|
+
* in, or null), programmable (a pNFT), ruleSet (a pNFT's, or null) }. Null for anything else. Older accounts end
|
|
307
|
+
* early: what is not there reads as none.
|
|
308
|
+
*/
|
|
309
|
+
export function decodeTokenMetadata(data) {
|
|
310
|
+
if (!(data instanceof Uint8Array) || data[0] !== 4) return null;
|
|
311
|
+
let at = 33;
|
|
312
|
+
const key = () => encodeBase58(data.subarray(at, (at += 32)));
|
|
313
|
+
const skip = (n) => (at += n);
|
|
314
|
+
const str = () => skip(4 + Number(readUintLE(data, at, 4)));
|
|
315
|
+
const some = () => at < data.length && data[at++] === 1;
|
|
316
|
+
const mint = key();
|
|
317
|
+
str(), str(), str(), skip(2);
|
|
318
|
+
if (some()) skip(34 * Number(readUintLE(data, at, 4)) + 4);
|
|
319
|
+
skip(2);
|
|
320
|
+
if (some()) skip(1); // edition nonce
|
|
321
|
+
const programmable = some() && [4, 5].includes(data[at++]); // a pNFT, or a pNFT's print
|
|
322
|
+
let collection = null;
|
|
323
|
+
if (some()) {
|
|
324
|
+
const verified = data[at++] === 1;
|
|
325
|
+
const address = key();
|
|
326
|
+
if (verified) collection = address;
|
|
327
|
+
}
|
|
328
|
+
if (some()) skip(17); // uses
|
|
329
|
+
if (some()) skip(9); // collection details
|
|
330
|
+
let ruleSet = null;
|
|
331
|
+
if (some()) {
|
|
332
|
+
skip(1); // ProgrammableConfig::V1
|
|
333
|
+
if (some()) ruleSet = key();
|
|
334
|
+
}
|
|
335
|
+
return { mint, collection, programmable, ruleSet };
|
|
336
|
+
}
|
|
337
|
+
|
|
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;
|
|
348
|
+
|
|
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 };
|
|
385
|
+
}
|
|
386
|
+
|
|
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);
|
|
389
|
+
|
|
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));
|
|
392
|
+
|
|
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
|
+
}
|
|
400
|
+
|
|
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];
|
|
409
|
+
}
|
|
410
|
+
|
|
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)];
|
|
416
|
+
}
|
|
417
|
+
|
|
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);
|
|
430
|
+
}
|
|
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;
|
|
465
|
+
}
|
|
466
|
+
|
|
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;
|
|
496
|
+
|
|
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;
|
|
518
|
+
}
|
|
519
|
+
|
|
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
|
+
}
|
|
530
|
+
|
|
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
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
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`.
|
|
544
|
+
*/
|
|
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;
|
|
573
|
+
}
|
|
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
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Who rents a listed NFT at `now` (Unix seconds), from its decoded Listing (or null for none): the renter's wallet while
|
|
597
|
+
* a rental runs, else null (a sale listing, or a rental that is over).
|
|
598
|
+
*/
|
|
599
|
+
export function renterOf(listing, now) {
|
|
600
|
+
if (!listing || !listing.deal?.Rent) return null;
|
|
601
|
+
return BigInt(now) < BigInt(listing.until) && listing.user !== SYSTEM_PROGRAM ? listing.user : null;
|
|
602
|
+
}
|
|
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
|
+
|
|
607
|
+
/**
|
|
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.
|
|
613
|
+
*/
|
|
614
|
+
export function runsDue(schedule, now, vaultBalance = 0n) {
|
|
615
|
+
const runs = Number(schedule.runs), max = Number(schedule.max_runs);
|
|
616
|
+
if (max > 0 && runs >= max) return schedule.action?.from?.Vest && BigInt(vaultBalance) > 0n ? 1 : 0;
|
|
617
|
+
if (!schedule.active || BigInt(now) < BigInt(schedule.next_at)) return 0;
|
|
618
|
+
const due = Number((BigInt(now) - BigInt(schedule.next_at)) / BigInt(schedule.every)) + 1;
|
|
619
|
+
return max > 0 ? Math.min(due, max - runs) : due;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// ---------------------------------------------------------------- receipts
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* The receipts (Anchor events) of a transaction that succeeded: [{ name, data }]. Pass what getTransaction returns
|
|
626
|
+
* (`{ meta: { err, logMessages } }`), or the logs and the transaction's `meta.err`. Success is `meta.err === null` and
|
|
627
|
+
* nothing else: a transaction can fail after every instruction logged success (the runtime's rent check, for one),
|
|
628
|
+
* and its logs still hold every receipt, so a transaction with an error gives none. A backend delivers from a
|
|
629
|
+
* finalized transaction's receipts, once per order or reference.
|
|
630
|
+
*/
|
|
631
|
+
export function receipts(tx, err) {
|
|
632
|
+
if (Array.isArray(tx)) {
|
|
633
|
+
if (err === undefined) throw new Error("loot.receipts(logs, err) needs the transaction's meta.err (null when it succeeded): a failed transaction keeps its logs");
|
|
634
|
+
return err === null ? decodeEvents(IDL, tx) : [];
|
|
635
|
+
}
|
|
636
|
+
const meta = tx?.meta;
|
|
637
|
+
if (!meta || meta.err === undefined) throw new Error('loot.receipts(tx) needs the transaction getTransaction returns, with its meta.err and meta.logMessages');
|
|
638
|
+
return meta.err === null ? decodeEvents(IDL, meta.logMessages ?? []) : [];
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** The program's errors by code: { code: { name, msg } }. */
|
|
642
|
+
export const ERRORS = Object.freeze(Object.fromEntries(IDL.errors.map((e) => [e.code, { name: e.name, msg: e.msg }])));
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* The loot error in a failed transaction's logs ("custom program error: 0x…", or Anchor's "Error Code: …" line), or
|
|
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
|
|
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.
|
|
649
|
+
* The first program that fails is where the error came from; the programs that called it fail after it with the same
|
|
650
|
+
* code.
|
|
651
|
+
*/
|
|
652
|
+
export function lootError(logs) {
|
|
653
|
+
const running = [];
|
|
654
|
+
for (const line of logs ?? []) {
|
|
655
|
+
const invoked = /^Program (\w+) invoke \[\d+\]/.exec(line);
|
|
656
|
+
if (invoked) {
|
|
657
|
+
running.push(invoked[1]);
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
const failed = /^Program (\w+) failed: (.*)$/.exec(line);
|
|
661
|
+
if (failed) {
|
|
662
|
+
if (failed[1] !== LOOT_PROGRAM) return null;
|
|
663
|
+
const m = /custom program error: 0x([0-9a-f]+)/i.exec(failed[2]);
|
|
664
|
+
const code = m ? parseInt(m[1], 16) : null;
|
|
665
|
+
return code !== null && ERRORS[code] ? { code, ...ERRORS[code] } : null;
|
|
666
|
+
}
|
|
667
|
+
if (/^Program (\w+) success/.test(line)) {
|
|
668
|
+
running.pop();
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
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.
|
|
672
|
+
if (running.length && running.at(-1) !== LOOT_PROGRAM) continue;
|
|
673
|
+
const named = /Error Code: (\w+)\. Error Number: (\d+)\. Error Message: (.+?)\.?$/.exec(line);
|
|
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] };
|
|
675
|
+
const m = /custom program error: 0x([0-9a-f]+)/i.exec(line);
|
|
676
|
+
if (m && ERRORS[parseInt(m[1], 16)]) return { code: parseInt(m[1], 16), ...ERRORS[parseInt(m[1], 16)] };
|
|
677
|
+
}
|
|
678
|
+
return null;
|
|
679
|
+
}
|