@xgamesfun/solana 0.20.1 → 0.25.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 +95 -52
- package/bin/xsolana.mjs +23 -8
- package/examples/blind-forge.json +2 -4
- package/examples/coin-flip-arena.json +4 -12
- package/examples/duel-arena.json +1 -4
- package/examples/elements-set.json +2 -9
- package/examples/expedition-guild.json +1 -8
- package/examples/fair-presale.json +1 -3
- package/examples/guild-missions.json +3 -10
- package/examples/halving-seasons.json +4 -9
- package/examples/hero-trials.json +15 -6
- package/examples/holder-den.json +38 -0
- package/examples/holders-drop.json +105 -0
- package/examples/ideas.json +788 -788
- package/examples/match-predictions.json +24 -0
- package/examples/merge-2048.json +5 -5
- package/examples/nft-raffle.json +1 -0
- package/examples/pack-league.json +108 -0
- package/examples/paper-hands-farm.json +4 -16
- package/examples/partner-vault.json +20 -7
- package/examples/sacrifice-raffle.json +2 -10
- package/examples/shelter-fair.json +1 -0
- package/examples/stakers-draw.json +4 -15
- package/examples/star-fleet.json +1 -8
- package/examples/starforce.json +6 -6
- package/examples/stride-sneakers.json +164 -0
- package/examples/trade-in-shop.json +2 -3
- package/js/approve/details.mjs +122 -52
- package/js/approve/ed25519.mjs +5 -1
- package/js/approve/live-check.mjs +83 -26
- package/js/approve/page.html +217 -78
- package/js/approve/page.js +43 -15
- package/js/approve/server.mjs +290 -73
- package/js/chain/base58.js +7 -1
- package/js/chain/borsh.js +24 -11
- package/js/chain/bytes.js +11 -8
- package/js/chain/core.js +35 -7
- package/js/chain/curve.js +1 -1
- package/js/chain/lookup.js +124 -36
- package/js/chain/mint.js +6 -2
- package/js/chain/pda.js +2 -2
- package/js/chain/programs.js +6 -6
- package/js/chain/rpc.js +44 -16
- package/js/chain/token.js +13 -2
- package/js/chain/token2022.js +55 -16
- package/js/chain/tx.js +32 -22
- package/js/client/accounts.js +653 -131
- package/js/client/compressed.js +4 -3
- package/js/client/grant.js +267 -83
- package/js/client/idl/loot.js +1 -1
- package/js/client/idl/loot.json +2406 -2176
- package/js/client/index.js +1 -0
- package/js/client/ix.js +3059 -664
- package/js/client/pda.js +39 -33
- package/js/client/policy.js +52 -24
- package/js/client/random.js +70 -0
- package/js/client/sizes.js +414 -16
- package/js/client/traits.js +14 -5
- package/js/connect/connect.mjs +59 -17
- package/js/economy/check.mjs +4153 -725
- package/js/economy/cost.mjs +403 -64
- package/js/economy/examples.mjs +176 -35
- package/js/economy/explain.mjs +466 -111
- package/js/economy/guide.mjs +296 -72
- package/js/economy/legal.mjs +12 -6
- package/js/economy/live.mjs +83 -19
- package/js/economy/model.mjs +462 -124
- package/js/economy/plan.mjs +1306 -219
- package/js/economy/schema.json +390 -484
- package/js/economy/secrets.mjs +17 -7
- package/js/economy/simulate.mjs +883 -223
- package/js/economy/txsize.mjs +15 -3
- package/js/economy/validate.mjs +61 -24
- package/js/mcp/server.mjs +42 -14
- package/js/mcp/tools.mjs +307 -88
- package/js/play/index.js +2864 -619
- package/js/play/keys.js +193 -0
- package/js/results/index.js +223 -30
- package/js/results/server.js +230 -0
- package/js/results/wire.js +64 -16
- package/package.json +6 -4
- package/release.json +5 -5
- package/skills/xsolana-economy/SKILL.md +39 -21
- package/skills/xsolana-economy/references/after-launch.md +58 -38
- package/skills/xsolana-economy/references/apps.md +97 -50
- package/skills/xsolana-economy/references/sentence-to-spec.md +148 -121
- package/skills/xsolana-economy/references/telling-the-person.md +61 -17
- package/types/chain/core.d.ts +4 -2
- package/types/chain/lookup.d.ts +5 -2
- package/types/chain/rpc.d.ts +3 -1
- package/types/client/accounts.d.ts +217 -56
- package/types/client/grant.d.ts +72 -27
- package/types/client/idl/loot.d.ts +602 -549
- package/types/client/index.d.ts +1 -0
- package/types/client/ix.d.ts +3884 -247
- package/types/client/pda.d.ts +22 -16
- 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/guide.d.mts +5 -0
- package/types/economy/live.d.mts +16 -1
- package/types/economy/model.d.mts +151 -38
- package/types/economy/plan.d.mts +237 -194
- package/types/economy/simulate.d.mts +21 -13
- package/types/mcp/tools.d.mts +21 -13
- package/types/play/index.d.ts +453 -53
- package/types/play/keys.d.ts +51 -0
- package/types/results/index.d.ts +7 -6
- package/types/results/server.d.ts +126 -0
- package/types/results/wire.d.ts +1 -1
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,63 +27,274 @@ 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]));
|
|
42
50
|
|
|
43
51
|
/** The kind of a loot account from its first byte, or null. */
|
|
44
|
-
export const accountKind =
|
|
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
|
+
{
|
|
60
|
+
name: 'Round',
|
|
61
|
+
type: { kind: 'enum', variants: ['Open', 'Drawing', 'Drawn', 'Cancelled'].map(name => ({ name })) },
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'TokenPrize',
|
|
65
|
+
type: {
|
|
66
|
+
kind: 'struct',
|
|
67
|
+
fields: [
|
|
68
|
+
{ name: 'run', type: 'u16' },
|
|
69
|
+
{ name: 'mint', type: 'pubkey' },
|
|
70
|
+
{ name: 'each', type: 'u64' },
|
|
71
|
+
{ name: 'sent', type: 'u64' },
|
|
72
|
+
{ name: 'arrived', type: 'u64' },
|
|
73
|
+
{ name: 'minted', type: 'bool' },
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'Opened',
|
|
79
|
+
type: {
|
|
80
|
+
kind: 'struct',
|
|
81
|
+
fields: [
|
|
82
|
+
{ name: 'mint', type: 'pubkey' },
|
|
83
|
+
{ name: 'by', type: 'pubkey' },
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: 'LotteryRound',
|
|
89
|
+
type: {
|
|
90
|
+
kind: 'struct',
|
|
91
|
+
fields: [
|
|
92
|
+
['project', 'pubkey'],
|
|
93
|
+
['lottery', 'pubkey'],
|
|
94
|
+
['id', 'u32'],
|
|
95
|
+
['incarnation', 'u64'],
|
|
96
|
+
['round', 'u32'],
|
|
97
|
+
['bump', 'u8'],
|
|
98
|
+
['opener', 'pubkey'],
|
|
99
|
+
['lent', 'u64'],
|
|
100
|
+
['head', 'u32'],
|
|
101
|
+
['status', { defined: { name: 'Round' } }],
|
|
102
|
+
// The terms, as the lottery's revision had them at the round's first purchase; whether its entries keep what
|
|
103
|
+
// arrived (a payment mint with a transfer fee, or a share the buyer chooses, paid as they buy).
|
|
104
|
+
['revision', 'u32'],
|
|
105
|
+
['terms', { defined: { name: 'LotteryTerms' } }],
|
|
106
|
+
['fee', 'bool'],
|
|
107
|
+
// The totals, split at each purchase: what each share paid in common takes, the entries that own a part of
|
|
108
|
+
// their own, and the parts owed now.
|
|
109
|
+
['sold', 'u64'],
|
|
110
|
+
['to_pot', 'u64'],
|
|
111
|
+
['common', { vec: 'u64' }],
|
|
112
|
+
['owns', 'u32'],
|
|
113
|
+
['owed', 'u32'],
|
|
114
|
+
// The draw, and the money the ask fixed: the pot's (what the round owes the pot back: `pot_owed`).
|
|
115
|
+
['asked_first', 'u64'],
|
|
116
|
+
['asked_slot', 'u64'],
|
|
117
|
+
['first_asked_at', 'i64'],
|
|
118
|
+
['asked_at', 'i64'],
|
|
119
|
+
['answer', { array: ['u8', 32] }],
|
|
120
|
+
['first', 'u64'],
|
|
121
|
+
['pot_total', 'u64'],
|
|
122
|
+
['pot_sent', 'u64'],
|
|
123
|
+
['pot_arrived', 'u64'],
|
|
124
|
+
['pot_owed', 'u64'],
|
|
125
|
+
['paid', 'bytes'],
|
|
126
|
+
['prizes', { vec: { defined: { name: 'TokenPrize' } } }],
|
|
127
|
+
['accounts', { vec: { defined: { name: 'Opened' } } }],
|
|
128
|
+
// The NFT of the lottery's own stash each place that holds or draws one got, in place order, and how many.
|
|
129
|
+
['nfts', { vec: 'pubkey' }],
|
|
130
|
+
['assigned', 'u32'],
|
|
131
|
+
].map(([name, type]) => ({ name, type })),
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: 'ContestEntry',
|
|
136
|
+
type: {
|
|
137
|
+
kind: 'struct',
|
|
138
|
+
fields: [
|
|
139
|
+
['player', 'pubkey'],
|
|
140
|
+
['side', 'u16'],
|
|
141
|
+
['amount', 'u64'],
|
|
142
|
+
['bits', 'u8'],
|
|
143
|
+
['nft', 'pubkey'],
|
|
144
|
+
['place', 'u32'],
|
|
145
|
+
['taken', 'u32'],
|
|
146
|
+
].map(([name, type]) => ({ name, type })),
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
];
|
|
150
|
+
const IDLX = {
|
|
151
|
+
...IDL,
|
|
152
|
+
types: [...IDL.types, ...RECORDS],
|
|
153
|
+
accounts: [...IDL.accounts, { name: 'LotteryRound', discriminator: [TAGS.LotteryRound] }],
|
|
154
|
+
};
|
|
45
155
|
|
|
46
|
-
/** A loot account of the named kind (one the IDL describes). Throws when the data is not one. */
|
|
47
|
-
export const decode = (name, data) => decodeAccount(
|
|
156
|
+
/** A loot account of the named kind (one the IDL describes, or a `LotteryRound`). Throws when the data is not one. */
|
|
157
|
+
export const decode = (name, data) => decodeAccount(IDLX, name, data);
|
|
48
158
|
|
|
49
159
|
/** Any loot account: { kind, value }, or null for data that is no loot account. */
|
|
50
160
|
export function decodeAny(data) {
|
|
51
161
|
const kind = accountKind(data);
|
|
52
162
|
if (!kind) return null;
|
|
53
|
-
if (kind === '
|
|
163
|
+
if (kind === 'Profile') return { kind, value: decodeProfile(data) };
|
|
164
|
+
if (kind === 'List') return { kind, value: decodeList(data) };
|
|
165
|
+
if (kind === 'Ends') return { kind, value: decodeEnds(data) };
|
|
166
|
+
if (kind === 'LotteryRound') return { kind, value: decodeRound(data) };
|
|
54
167
|
if (kind === 'Sheet') return { kind, value: decodeSheet(data) };
|
|
168
|
+
if (kind === 'Table')
|
|
169
|
+
return data[37] === PAYLOAD.Nft ? { kind: 'Stash', value: decodeStash(data) } : { kind, value: decodeTable(data) };
|
|
55
170
|
if (kind === 'Proven') return { kind, value: decodeProven(data) };
|
|
56
171
|
if (kind === 'Traits') return { kind, value: decodeTraits(data) };
|
|
57
|
-
if (kind === 'Stash') return { kind, value: decodeStash(data) };
|
|
58
172
|
return { kind, value: decode(kind, data) };
|
|
59
173
|
}
|
|
60
174
|
|
|
175
|
+
// ---------------------------------------------------------------- lists of records (M1)
|
|
176
|
+
|
|
61
177
|
/**
|
|
62
|
-
* A
|
|
63
|
-
* a grant
|
|
64
|
-
* bytes
|
|
65
|
-
* was paid once and can never be paid again.
|
|
178
|
+
* A list of records (tag 26, `list.rs`): { width, records } in their order: a wallet's counts
|
|
179
|
+
* (`["counts", project, wallet]`) and a grant's claims are limit records (28 bytes, `readLimit`), a grant's rounds 16
|
|
180
|
+
* bytes, a contest's entries 43 or 83 (`decodeEntries`).
|
|
66
181
|
*/
|
|
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
|
-
|
|
182
|
+
export function decodeList(data) {
|
|
183
|
+
if (!(data instanceof Uint8Array) || data[0] !== TAGS.List || data.length < 7) throw new Error('Not a list account');
|
|
184
|
+
const width = Number(readUintLE(data, 1, 2));
|
|
185
|
+
const count = Number(readUintLE(data, 3, 4));
|
|
186
|
+
if (!width || data.length !== 7 + width * count) throw new Error('List account does not match its records');
|
|
187
|
+
return { width, records: Array.from({ length: count }, (_, i) => data.subarray(7 + width * i, 7 + width * (i + 1))) };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* A farm's record of when its seats stop earning (`["ends", farm]`, tag 29; `Farm.keeps_ends`): its times in order, each
|
|
192
|
+
* `{ at, rewardPerWeight, weight, seats }`: the time (Unix seconds), `reward_per_weight` there once the farm has walked past
|
|
193
|
+
* it, the weight that stops earning then, and the seats that still need the record.
|
|
194
|
+
*/
|
|
195
|
+
export function decodeEnds(data) {
|
|
196
|
+
if (!(data instanceof Uint8Array) || data[0] !== TAGS.Ends || data.length < 7)
|
|
197
|
+
throw new Error('Not a record of ends');
|
|
198
|
+
const width = Number(readUintLE(data, 1, 2));
|
|
199
|
+
const count = Number(readUintLE(data, 3, 4));
|
|
200
|
+
if (width !== END_BYTES || data.length !== 7 + width * count)
|
|
201
|
+
throw new Error('The record of ends does not match its records');
|
|
202
|
+
return Array.from({ length: count }, (_, i) => {
|
|
203
|
+
const at = 7 + width * i;
|
|
204
|
+
let time = 0n;
|
|
205
|
+
for (let b = 0; b < 8; b++) time = (time << 8n) | BigInt(data[at + b]);
|
|
206
|
+
return {
|
|
207
|
+
at: time,
|
|
208
|
+
rewardPerWeight: readUintLE(data, at + 8, 16),
|
|
209
|
+
weight: readUintLE(data, at + 24, 16),
|
|
210
|
+
seats: Number(readUintLE(data, at + 40, 4)),
|
|
211
|
+
};
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** The first 16 bytes of the sha256 of `parts` (addresses, bytes or u64s): a record's key in a list. */
|
|
216
|
+
export const recordKey16 = (...parts) =>
|
|
217
|
+
sha256(
|
|
218
|
+
concat(...parts.map(p => (typeof p === 'string' ? addressBytes(p) : p instanceof Uint8Array ? p : uintLE(p, 8)))),
|
|
219
|
+
).subarray(0, 16);
|
|
220
|
+
|
|
221
|
+
/** The key of a wallet's count for a box, a recipe (incarnation 0) or a lottery (its incarnation), in its counts. */
|
|
222
|
+
export const countKey = (record, incarnation = 0) => recordKey16(record, BigInt(incarnation));
|
|
223
|
+
|
|
224
|
+
/** What the limit record at `key` counts in a decoded list (`decodeList`, or null for none): { count, window, inWindow }. */
|
|
225
|
+
export function readLimit(list, key) {
|
|
226
|
+
const record = list?.records.find(r => compareBytes(r.subarray(0, 16), key) === 0);
|
|
227
|
+
const n = i => (record ? Number(readUintLE(record, 16 + 4 * i, 4)) : 0);
|
|
228
|
+
return { count: n(0), window: n(1), inWindow: n(2) };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** A fetched list account, or null for none (or an empty System account someone sent SOL to, which the program opens
|
|
232
|
+
* as the list). Any other owner or layout is refused. */
|
|
233
|
+
export function readListAccount(account) {
|
|
234
|
+
if (account == null) return null;
|
|
235
|
+
if (
|
|
236
|
+
account.owner === SYSTEM_PROGRAM &&
|
|
237
|
+
account.data instanceof Uint8Array &&
|
|
238
|
+
account.data.length === 0 &&
|
|
239
|
+
!account.executable
|
|
240
|
+
)
|
|
241
|
+
return null;
|
|
242
|
+
if (account.owner !== LOOT_PROGRAM || account.executable) throw new Error('The list has an invalid owner.');
|
|
243
|
+
return decodeList(account.data);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* A contest's entries (`["entries", contest, incarnation, 0]`), in the list's order (by player): [{ player, side,
|
|
248
|
+
* amount (0 once paid or refunded; 1 while a contest of NFTs holds its NFT), bits (`CLAIMED`, `ENDED`), nft, place (how
|
|
249
|
+
* many entered its side before it), taken (the losers' NFTs a winner took) }]. A claim names an entry by its index here.
|
|
250
|
+
*/
|
|
251
|
+
export function decodeEntries(data) {
|
|
252
|
+
const { width, records } = decodeList(data);
|
|
253
|
+
return records.map(r => {
|
|
254
|
+
const full = new Uint8Array(83);
|
|
255
|
+
full.set(r.subarray(0, Math.min(width, 83)));
|
|
256
|
+
return decodeBorsh(IDLX, { defined: { name: 'ContestEntry' } }, full)[0];
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** A winner's first claim came (its prize given or forgone); a winner ended its picks early. */
|
|
261
|
+
export const CLAIMED = 1;
|
|
262
|
+
export const ENDED = 2;
|
|
263
|
+
|
|
264
|
+
/** How many losers' NFTs a winner of a contest of NFTs takes: the share fixed at the decision, one more for an early
|
|
265
|
+
* place on its side. */
|
|
266
|
+
export const shareOf = (contest, entry) =>
|
|
267
|
+
Number(contest.share) + (Number(entry.place) < Number(contest.extras) ? 1 : 0);
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* What a winning entry that put in `amount` takes with its first claim of a decided contest of tokens: its part of what
|
|
271
|
+
* the decision wrote (`pot_won`, the pot once the shares were taken; `prize_won`, a split prize's vault), by what it put
|
|
272
|
+
* in against its side's total. The winners claimed before it took `won × claimed ÷ total`, so the parts add up exactly
|
|
273
|
+
* whatever the order: `{ pot, prize }` (the prize part is 0 when the claim forgoes it). Tokens sent to the pot after the
|
|
274
|
+
* decision are nobody's.
|
|
275
|
+
*/
|
|
276
|
+
export function contestWinnings(contest, amount) {
|
|
277
|
+
const total = BigInt(contest.sides[contest.winner] ?? 0) || 1n;
|
|
278
|
+
const claimed = total - BigInt(contest.unclaimed);
|
|
279
|
+
const part = won => (BigInt(won) * (claimed + BigInt(amount))) / total - (BigInt(won) * claimed) / total;
|
|
280
|
+
return { pot: part(contest.pot_won), prize: part(contest.prize_won) };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** What a count allows `n` more of at `now` under `limit` ({ total, per_window, window }, 0 is no limit; a lottery's
|
|
284
|
+
* window is its round plus one, given as `window`). */
|
|
285
|
+
export function countAllows(
|
|
286
|
+
count,
|
|
287
|
+
limit,
|
|
288
|
+
now,
|
|
289
|
+
n = 1,
|
|
290
|
+
window = Number(limit.window) ? Math.floor(Number(now) / Number(limit.window)) + 1 : 0,
|
|
291
|
+
) {
|
|
292
|
+
const total = (count?.count ?? 0) + n;
|
|
293
|
+
const inWindow = (count && count.window === window ? count.inWindow : 0) + n;
|
|
294
|
+
return (
|
|
295
|
+
(!Number(limit.total) || total <= Number(limit.total)) &&
|
|
296
|
+
(!Number(limit.per_window) || inWindow <= Number(limit.per_window))
|
|
297
|
+
);
|
|
83
298
|
}
|
|
84
299
|
|
|
85
300
|
/**
|
|
@@ -88,12 +303,29 @@ export function decodeReceipt(data) {
|
|
|
88
303
|
*/
|
|
89
304
|
export function decodeSheet(data) {
|
|
90
305
|
if (data == null || data.length === 0) return null;
|
|
91
|
-
if (!(data instanceof Uint8Array) || data[0] !== TAGS.Sheet || data.length < 69)
|
|
306
|
+
if (!(data instanceof Uint8Array) || data[0] !== TAGS.Sheet || data.length < 69)
|
|
307
|
+
throw new Error('Not a Sheet account');
|
|
308
|
+
const { attributes, head } = attributesAt(data, 'Sheet');
|
|
309
|
+
return { project: head(1), nft: head(33), attributes };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** A player's profile (`["profile", project, wallet]`): { project, wallet, attributes }, what the project keeps about
|
|
313
|
+
* the wallet (its XP, level, rank). Its layout is a Sheet's, under its own tag. */
|
|
314
|
+
export function decodeProfile(data) {
|
|
315
|
+
if (data == null || data.length === 0) return null;
|
|
316
|
+
if (!(data instanceof Uint8Array) || data[0] !== TAGS.Profile || data.length < 69)
|
|
317
|
+
throw new Error('Not a Profile account');
|
|
318
|
+
const { attributes, head } = attributesAt(data, 'Profile');
|
|
319
|
+
return { project: head(1), wallet: head(33), attributes };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** The attributes after a Sheet's or a profile's two addresses, read with every length checked against the data. */
|
|
323
|
+
function attributesAt(data, name) {
|
|
92
324
|
let at = 65;
|
|
93
325
|
// Every length is checked against the data: a count or a text longer than what is there is a truncated account.
|
|
94
326
|
const text = () => {
|
|
95
327
|
const n = Number(readUintLE(data, at, 4));
|
|
96
|
-
if (at + 4 + n > data.length) throw new Error(
|
|
328
|
+
if (at + 4 + n > data.length) throw new Error(`${name} account is truncated`);
|
|
97
329
|
const value = new TextDecoder().decode(data.subarray(at + 4, at + 4 + n));
|
|
98
330
|
at += 4 + n;
|
|
99
331
|
return value;
|
|
@@ -101,19 +333,68 @@ export function decodeSheet(data) {
|
|
|
101
333
|
const count = Number(readUintLE(data, at, 4));
|
|
102
334
|
at += 4;
|
|
103
335
|
// Each attribute takes 8 bytes at least (two empty texts).
|
|
104
|
-
if (count * 8 > data.length - at) throw new Error(
|
|
336
|
+
if (count * 8 > data.length - at) throw new Error(`${name} account is truncated`);
|
|
105
337
|
const attributes = Array.from({ length: count }, () => ({ key: text(), value: text() }));
|
|
106
|
-
return {
|
|
338
|
+
return { attributes, end: at, head: from => encodeBase58(data.subarray(from, from + 32)) };
|
|
107
339
|
}
|
|
108
340
|
|
|
109
|
-
/**
|
|
341
|
+
/** What a table's rows carry (its payload byte). */
|
|
342
|
+
export const PAYLOAD = Object.freeze({ Number: 0, Label: 1, Nft: 2 });
|
|
343
|
+
const TABLE_HEAD = 51;
|
|
344
|
+
|
|
345
|
+
/** 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. */
|
|
346
|
+
export function tableRowAt(len, r) {
|
|
347
|
+
const block = 8 + 64 * len;
|
|
348
|
+
const group = 8 + 64 * block;
|
|
349
|
+
return TABLE_HEAD + Math.floor(r / 4096) * group + 8 + (Math.floor(r / 64) % 64) * block + 8 + (r % 64) * len;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* A table (`["table", project, id]`, `table.rs`): { project, id, payload ('Number' | 'Label'), sealed, count, total
|
|
354
|
+
* (the weight in stock), rows: [{ weight, left, label }] }. A row's `left` is `NO_LIMIT` (4294967295) for a stock that
|
|
355
|
+
* never runs out.
|
|
356
|
+
*/
|
|
357
|
+
export function decodeTable(data) {
|
|
358
|
+
if (data == null || data.length === 0) return null;
|
|
359
|
+
if (!(data instanceof Uint8Array) || data[0] !== TAGS.Table || data.length < TABLE_HEAD)
|
|
360
|
+
throw new Error('Not a Table account');
|
|
361
|
+
const payload = Object.keys(PAYLOAD).find(k => PAYLOAD[k] === data[37]);
|
|
362
|
+
if (!payload || payload === 'Nft') throw new Error('Not a Table account');
|
|
363
|
+
const count = Number(readUintLE(data, 39, 4));
|
|
364
|
+
const head = {
|
|
365
|
+
project: encodeBase58(data.subarray(1, 33)),
|
|
366
|
+
id: Number(readUintLE(data, 33, 4)),
|
|
367
|
+
payload,
|
|
368
|
+
sealed: data[38] === 1,
|
|
369
|
+
count,
|
|
370
|
+
total: readUintLE(data, 43, 8),
|
|
371
|
+
};
|
|
372
|
+
const len = payload === 'Label' ? 40 : 8;
|
|
373
|
+
if (count && tableRowAt(len, count - 1) + len > data.length) throw new Error('Table account is truncated');
|
|
374
|
+
const rows = Array.from({ length: count }, (_, r) => {
|
|
375
|
+
const at = tableRowAt(len, r);
|
|
376
|
+
const label =
|
|
377
|
+
payload === 'Label' ? new TextDecoder().decode(data.subarray(at + 8, at + 40)).replace(/\0+$/, '') : '';
|
|
378
|
+
return { weight: Number(readUintLE(data, at, 4)), left: Number(readUintLE(data, at + 4, 4)), label };
|
|
379
|
+
});
|
|
380
|
+
return { ...head, rows };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** A stash (`["stash", project, id]`, or a lottery's own): { project, id, nfts, weights }, the NFTs the project holds to
|
|
384
|
+
* give away and the weight each is drawn by, as a table of NFTs keeps them (a row: its weight, then the NFT). */
|
|
110
385
|
export function decodeStash(data) {
|
|
111
386
|
if (data == null || data.length === 0) return null;
|
|
112
|
-
if (!(data instanceof Uint8Array) || data[0] !== TAGS.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
387
|
+
if (!(data instanceof Uint8Array) || data[0] !== TAGS.Table || data[37] !== PAYLOAD.Nft || data.length < TABLE_HEAD)
|
|
388
|
+
throw new Error('Not a Stash account');
|
|
389
|
+
const count = Number(readUintLE(data, 39, 4));
|
|
390
|
+
if (count && tableRowAt(36, count - 1) + 36 > data.length) throw new Error('Stash account is truncated');
|
|
391
|
+
const rows = Array.from({ length: count }, (_, r) => tableRowAt(36, r));
|
|
392
|
+
return {
|
|
393
|
+
project: encodeBase58(data.subarray(1, 33)),
|
|
394
|
+
id: Number(readUintLE(data, 33, 4)),
|
|
395
|
+
nfts: rows.map(at => encodeBase58(data.subarray(at + 4, at + 36))),
|
|
396
|
+
weights: rows.map(at => Number(readUintLE(data, at, 4))),
|
|
397
|
+
};
|
|
117
398
|
}
|
|
118
399
|
|
|
119
400
|
/**
|
|
@@ -124,9 +405,10 @@ export function decodeStash(data) {
|
|
|
124
405
|
export function decodeProven(data) {
|
|
125
406
|
// Its shortest: the tag, five addresses, the root, data hash and creator hash, no asset data hash, the flags and
|
|
126
407
|
// nonce, and its payer.
|
|
127
|
-
if (!(data instanceof Uint8Array) || data[0] !== TAGS.Proven || data.length < 1 + 32 * 8 + 1 + 1 + 8 + 32)
|
|
128
|
-
|
|
129
|
-
const
|
|
408
|
+
if (!(data instanceof Uint8Array) || data[0] !== TAGS.Proven || data.length < 1 + 32 * 8 + 1 + 1 + 8 + 32)
|
|
409
|
+
throw new Error('Not a Proven account');
|
|
410
|
+
const key = at => encodeBase58(data.subarray(at, at + 32));
|
|
411
|
+
const none = k => (k === NO_ADDRESS ? null : k);
|
|
130
412
|
const root = 1 + 32 * 5;
|
|
131
413
|
let at = root + 32 * 3; // the five addresses, then the root, data hash and creator hash
|
|
132
414
|
if (data[at] !== 0 && data[at] !== 1) throw new Error('Proven account has an unknown asset data hash flag');
|
|
@@ -135,15 +417,27 @@ export function decodeProven(data) {
|
|
|
135
417
|
if (at + 1 + 8 + 32 > data.length) throw new Error('Proven account is truncated');
|
|
136
418
|
const flags = data[at];
|
|
137
419
|
return {
|
|
138
|
-
asset: key(1),
|
|
139
|
-
|
|
420
|
+
asset: key(1),
|
|
421
|
+
tree: key(33),
|
|
422
|
+
owner: none(key(65)),
|
|
423
|
+
delegate: none(key(97)),
|
|
424
|
+
collection: none(key(129)),
|
|
425
|
+
flags,
|
|
426
|
+
nonce: readUintLE(data, at + 1, 8),
|
|
427
|
+
root: toHex(data.subarray(root, root + 32)),
|
|
428
|
+
payer: key(at + 9),
|
|
140
429
|
};
|
|
141
430
|
}
|
|
142
431
|
|
|
143
432
|
/** The traits a project vouches for (`["traits", project, key]`): { project, key, root (hex) }. */
|
|
144
433
|
export function decodeTraits(data) {
|
|
145
|
-
if (!(data instanceof Uint8Array) || data[0] !== TAGS.Traits || data.length < 97)
|
|
146
|
-
|
|
434
|
+
if (!(data instanceof Uint8Array) || data[0] !== TAGS.Traits || data.length < 97)
|
|
435
|
+
throw new Error('Not a Traits account');
|
|
436
|
+
return {
|
|
437
|
+
project: encodeBase58(data.subarray(1, 33)),
|
|
438
|
+
key: encodeBase58(data.subarray(33, 65)),
|
|
439
|
+
root: toHex(data.subarray(65, 97)),
|
|
440
|
+
};
|
|
147
441
|
}
|
|
148
442
|
|
|
149
443
|
/**
|
|
@@ -155,11 +449,11 @@ export function decodeTokenMetadata(data) {
|
|
|
155
449
|
if (!(data instanceof Uint8Array) || data[0] !== 4) return null;
|
|
156
450
|
let at = 33;
|
|
157
451
|
const key = () => encodeBase58(data.subarray(at, (at += 32)));
|
|
158
|
-
const skip =
|
|
452
|
+
const skip = n => (at += n);
|
|
159
453
|
const str = () => skip(4 + Number(readUintLE(data, at, 4)));
|
|
160
454
|
const some = () => at < data.length && data[at++] === 1;
|
|
161
455
|
const mint = key();
|
|
162
|
-
str(), str(), str(), skip(2);
|
|
456
|
+
(str(), str(), str(), skip(2));
|
|
163
457
|
if (some()) skip(34 * Number(readUintLE(data, at, 4)) + 4);
|
|
164
458
|
skip(2);
|
|
165
459
|
if (some()) skip(1); // edition nonce
|
|
@@ -180,92 +474,302 @@ export function decodeTokenMetadata(data) {
|
|
|
180
474
|
return { mint, collection, programmable, ruleSet };
|
|
181
475
|
}
|
|
182
476
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
477
|
+
// ---------------------------------------------------------------- a lottery's rounds (M3)
|
|
478
|
+
//
|
|
479
|
+
// A round is a record of its own (`pda.round`), opened by its first purchase: a header with a copy of the terms, the
|
|
480
|
+
// totals, what it still owes and the draw, then one entry a purchase. The ask (`draw_lottery`) fixes its money and moves
|
|
481
|
+
// the lottery on; after the answer, `pull` pays one part at a time, in any order, and the pull that leaves nothing owed
|
|
482
|
+
// closes it.
|
|
483
|
+
|
|
484
|
+
/** An entry's word: its tickets' end in the low 48 bits, its own part paid in the top bit. */
|
|
485
|
+
const END_BITS = (1n << 48n) - 1n;
|
|
486
|
+
export const OWN_PAID = 1n << 63n;
|
|
487
|
+
|
|
488
|
+
/** The mint a lottery's tickets are paid in (`terms`: a Lottery's or a round's): its ticket mint, burned, or `mint`. */
|
|
489
|
+
export const paymentMint = terms => (ticketsBurn(terms) ? terms.ticket_mint : terms.mint);
|
|
490
|
+
/** Whether a lottery's tickets are burned: they are paid in a ticket mint of its own. */
|
|
491
|
+
export const ticketsBurn = terms => Boolean(terms.ticket_mint) && terms.ticket_mint !== NO_ADDRESS;
|
|
492
|
+
|
|
493
|
+
/** The most pots one lottery keeps (`MAX_POTS`): a pot's id is below it. */
|
|
494
|
+
export const MAX_POTS = Number(IDL.constants.find(c => c.name === 'MAX_POTS').value);
|
|
495
|
+
/** The pot a lottery's places are paid from (`terms.pays_from`: `{ Pot: [id] }`), or null when each round pays from its
|
|
496
|
+
* own ticket money (`{ Round: { rest } }`). */
|
|
497
|
+
export const potIdOf = terms => (terms?.pays_from?.Pot ? Number(terms.pays_from.Pot[0]) : null);
|
|
498
|
+
/** Where what a round's places leave goes (`Part::Common` past the shares): its pot, or the `rest` a round paid from
|
|
499
|
+
* its own money names (`lotteryAddress`: the lottery's). */
|
|
500
|
+
export const leftoverTo = (lotteryAddress, terms) =>
|
|
501
|
+
potIdOf(terms) != null ? { Account: [pda.lotteryPot(lotteryAddress, potIdOf(terms))] } : terms.pays_from.Round.rest;
|
|
502
|
+
/** The ids of the pots a lottery opened (`Lottery::pots`, one bit an id), lowest first: a close empties each. */
|
|
503
|
+
export const lotteryPots = lottery =>
|
|
504
|
+
Array.from({ length: MAX_POTS }, (_, id) => id).filter(id => (Number(lottery.pots ?? 0) >> id) & 1);
|
|
190
505
|
|
|
191
|
-
/**
|
|
192
|
-
export
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
506
|
+
/** A share the buyer chooses (a referrer, a collectible's holder): paid out of the price as they buy, from their own account. */
|
|
507
|
+
export const chosenAtPurchase = share => Boolean(share.to?.Chosen);
|
|
508
|
+
|
|
509
|
+
/** The bytes of one entry of a round: buyer, word, what arrived (`fee`: a fee mint, or a chosen share), the NFT ticket. */
|
|
510
|
+
export const entryWidth = (fee, nft) => 40 + (fee ? 8 : 0) + (nft ? 32 : 0);
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* A lottery round (`["round", lottery, incarnation, round]`): its header (`status` 'Open' | 'Drawing' | 'Drawn' |
|
|
514
|
+
* 'Cancelled', the terms it copied, `sold`, `owed`, the draw and the money the ask fixed, `nfts` the NFT of the
|
|
515
|
+
* lottery's own stash each such place got), and `entries`, one a purchase: { buyer, start, end (its tickets), paid (its
|
|
516
|
+
* own part's bit), amount (what reached the round), nft (its NFT ticket) }.
|
|
517
|
+
*/
|
|
518
|
+
export function decodeRound(data) {
|
|
519
|
+
const r = decode('LotteryRound', data);
|
|
520
|
+
const at = Number(r.head);
|
|
521
|
+
const width = Number(readUintLE(data, at, 2));
|
|
522
|
+
const count = Number(readUintLE(data, at + 2, 4));
|
|
523
|
+
if (width !== entryWidth(r.fee, r.terms.nft_ticket != null) || data.length !== at + 6 + width * count)
|
|
524
|
+
throw new Error('LotteryRound entries do not match its header');
|
|
525
|
+
let start = 0n;
|
|
526
|
+
const entries = Array.from({ length: count }, (_, i) => {
|
|
527
|
+
const e = data.subarray(at + 6 + width * i, at + 6 + width * (i + 1));
|
|
528
|
+
const word = readUintLE(e, 32, 8);
|
|
529
|
+
const end = word & END_BITS;
|
|
530
|
+
let k = 40;
|
|
531
|
+
const take = n => e.subarray((k += n) - n, k);
|
|
532
|
+
const amount = r.fee ? readUintLE(take(8), 0, 8) : (end - start) * BigInt(r.terms.price);
|
|
533
|
+
const entry = {
|
|
534
|
+
buyer: encodeBase58(e.subarray(0, 32)),
|
|
535
|
+
start,
|
|
536
|
+
end,
|
|
537
|
+
paid: word & ~END_BITS,
|
|
538
|
+
amount,
|
|
539
|
+
nft: r.terms.nft_ticket ? encodeBase58(take(32)) : NO_ADDRESS,
|
|
540
|
+
};
|
|
541
|
+
start = end;
|
|
542
|
+
return entry;
|
|
543
|
+
});
|
|
544
|
+
return { ...r, entries };
|
|
197
545
|
}
|
|
198
546
|
|
|
199
|
-
|
|
547
|
+
/** How many places a lottery's (or a round's) `places` are, every run counted (`Place.times`). */
|
|
548
|
+
export const placeCount = places => places.reduce((n, p) => n + Number(p.times ?? 1), 0);
|
|
549
|
+
|
|
550
|
+
/** The places of a round that have a winner: every place, but no more than tickets. */
|
|
551
|
+
export const roundWinners = r => Math.min(placeCount(r.terms.places), Number(r.sold));
|
|
200
552
|
|
|
201
|
-
/** The
|
|
202
|
-
export
|
|
203
|
-
|
|
553
|
+
/** The ticket that wins `place` of a drawn round: even steps around the list from first place's ticket. */
|
|
554
|
+
export function ticketOfPlace(r, place) {
|
|
555
|
+
const sold = BigInt(r.sold);
|
|
556
|
+
const ahead = BigInt(place) * (sold / BigInt(roundWinners(r) || 1));
|
|
557
|
+
const toEnd = sold - BigInt(r.first);
|
|
558
|
+
return ahead < toEnd ? BigInt(r.first) + ahead : ahead - toEnd;
|
|
559
|
+
}
|
|
204
560
|
|
|
205
|
-
/**
|
|
206
|
-
export
|
|
561
|
+
/** The run of places that holds `place`, and its first place. */
|
|
562
|
+
export function runOf(r, place) {
|
|
563
|
+
let first = 0;
|
|
564
|
+
for (let run = 0; run < r.terms.places.length; run++) {
|
|
565
|
+
if (place < first + Number(r.terms.places[run].times)) return [run, first];
|
|
566
|
+
first += Number(r.terms.places[run].times);
|
|
567
|
+
}
|
|
568
|
+
return [r.terms.places.length, first];
|
|
569
|
+
}
|
|
207
570
|
|
|
208
|
-
/** The places of
|
|
209
|
-
export function
|
|
210
|
-
const
|
|
211
|
-
|
|
571
|
+
/** The places of run `run` with a winner: [first, end). */
|
|
572
|
+
export function runPlaces(r, run) {
|
|
573
|
+
const first = placeCount(r.terms.places.slice(0, run));
|
|
574
|
+
const w = roundWinners(r);
|
|
575
|
+
return [Math.min(first, w), Math.min(first + Number(r.terms.places[run].times), w)];
|
|
212
576
|
}
|
|
213
577
|
|
|
214
|
-
/** The
|
|
215
|
-
export function
|
|
216
|
-
const
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
578
|
+
/** The places of run `run` whose ticket is in `start..end` (an entry's), in order. */
|
|
579
|
+
export function placesIn(r, run, start, end) {
|
|
580
|
+
const [from, to] = runPlaces(r, run);
|
|
581
|
+
const out = [];
|
|
582
|
+
if (from >= to || BigInt(start) >= BigInt(end)) return out;
|
|
583
|
+
const sold = BigInt(r.sold);
|
|
584
|
+
const step = sold / BigInt(roundWinners(r));
|
|
585
|
+
const d = (BigInt(start) + sold - BigInt(r.first)) % sold;
|
|
586
|
+
const len = BigInt(end) - BigInt(start);
|
|
587
|
+
for (const [a, b] of [
|
|
588
|
+
[d, d + len < sold ? d + len : sold],
|
|
589
|
+
[0n, d + len > sold ? d + len - sold : 0n],
|
|
590
|
+
]) {
|
|
591
|
+
let p = Number((a + step - 1n) / step);
|
|
592
|
+
for (p = Math.max(p, from); p < to && BigInt(p) * step < b; p++) out.push(p);
|
|
593
|
+
}
|
|
594
|
+
return out.sort((x, y) => x - y);
|
|
221
595
|
}
|
|
222
596
|
|
|
223
|
-
/**
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
597
|
+
/** What one place of a pass-through amount takes: `clamp(sent − before, 0, amount) × arrived / sent` (none of nothing sent). */
|
|
598
|
+
const through = (sent, arrived, before, amount) => {
|
|
599
|
+
if (sent === 0n) return 0n;
|
|
600
|
+
const part = sent > before ? (sent - before < amount ? sent - before : amount) : 0n;
|
|
601
|
+
return (part * arrived) / sent;
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* What place `place` (counted from the first place of `runs`) takes of a pass-through amount, as the program splits it:
|
|
606
|
+
* runs of `[a, n]` (n places taking `a` each), `sent` what the places are owed and `arrived` what the round holds for
|
|
607
|
+
* them; the last place with a part takes what the others leave.
|
|
608
|
+
*/
|
|
609
|
+
export function splitPart(runs, sent, arrived, place) {
|
|
610
|
+
[sent, arrived] = [BigInt(sent), BigInt(arrived)];
|
|
611
|
+
let [c, at, last, paid, before] = [0n, 0, null, 0n, 0n];
|
|
612
|
+
for (const [a, n] of runs) {
|
|
613
|
+
const m = a === 0n ? 0 : Math.min(Number((sent > c ? sent - c + a - 1n : 0n) / a), n);
|
|
614
|
+
if (m > 0) {
|
|
615
|
+
const full = through(sent, arrived, 0n, a);
|
|
616
|
+
[last, before] = [at + m - 1, paid + BigInt(m - 1) * full];
|
|
617
|
+
paid = before + through(sent, arrived, c + BigInt(m - 1) * a, a);
|
|
618
|
+
}
|
|
619
|
+
[c, at] = [c + a * BigInt(n), at + n];
|
|
229
620
|
}
|
|
230
|
-
return
|
|
621
|
+
if (last === place) return arrived > before ? arrived - before : 0n;
|
|
622
|
+
[c, at] = [0n, 0];
|
|
623
|
+
for (const [a, n] of runs) {
|
|
624
|
+
if (place < at + n) return through(sent, arrived, c + BigInt(place - at) * a, a);
|
|
625
|
+
[c, at] = [c + a * BigInt(n), at + n];
|
|
626
|
+
}
|
|
627
|
+
return 0n;
|
|
231
628
|
}
|
|
232
629
|
|
|
233
|
-
/**
|
|
234
|
-
|
|
630
|
+
/** A share in basis points of an amount, rounded down. */
|
|
631
|
+
const bpsOf = (amount, bps) => (BigInt(amount) * BigInt(bps)) / 10_000n;
|
|
235
632
|
|
|
236
|
-
/**
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
633
|
+
/** Whether place `place`'s part is paid: its pot part's bit (every place's first), or with `prize` its prize's (after
|
|
634
|
+
* every place's pot bit). */
|
|
635
|
+
export const placePaid = (r, place, prize) => {
|
|
636
|
+
const at = place + (prize ? placeCount(r.terms.places) : 0);
|
|
637
|
+
return Boolean((r.paid[at >> 3] ?? 0) & (1 << (at & 7)));
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
/** Entry `i`'s places in run `run` not paid yet of its pot part or (`prize`) its token prize, lowest first, each with
|
|
641
|
+
* what it takes: [{ place, amount }]. A pull pays all of them. */
|
|
642
|
+
function unpaidPlaces(r, i, run, prize) {
|
|
643
|
+
const e = r.entries[i];
|
|
644
|
+
const k = prize ? r.prizes.findIndex(p => Number(p.run) === run) : -1;
|
|
645
|
+
if (prize && k < 0) return [];
|
|
646
|
+
const runs =
|
|
647
|
+
k >= 0
|
|
648
|
+
? [[BigInt(r.prizes[k].each), runPlaces(r, run)[1] - runPlaces(r, run)[0]]]
|
|
649
|
+
: r.terms.places.map((p, j) => [bpsOf(r.pot_total, p.bps), runPlaces(r, j)[1] - runPlaces(r, j)[0]]);
|
|
650
|
+
const [sent, arrived, from] =
|
|
651
|
+
k >= 0 ? [r.prizes[k].sent, r.prizes[k].arrived, runPlaces(r, run)[0]] : [r.pot_sent, r.pot_arrived, 0];
|
|
652
|
+
return placesIn(r, run, e.start, e.end)
|
|
653
|
+
.filter(p => !placePaid(r, p, prize))
|
|
654
|
+
.map(place => ({ place, amount: splitPart(runs, sent, arrived, place - from) }));
|
|
242
655
|
}
|
|
243
656
|
|
|
244
|
-
/**
|
|
245
|
-
export const
|
|
657
|
+
/** What entry `i`'s places in run `run` take and are not paid yet, of its pot part or (`prize`) its token prize. */
|
|
658
|
+
export const placesOwed = (r, i, run, prize) => unpaidPlaces(r, i, run, prize).reduce((sum, p) => sum + p.amount, 0n);
|
|
659
|
+
|
|
660
|
+
/** How many of the round's owed parts a pull of `{ Pot: [run] }` or a token `{ Prize: [run] }` for entry `i` settles:
|
|
661
|
+
* its places there that take more than nothing (one pull pays them all). */
|
|
662
|
+
export const placesPulled = (r, i, run, prize) => unpaidPlaces(r, i, run, prize).filter(p => p.amount > 0n).length;
|
|
663
|
+
|
|
664
|
+
/** Whether run `run` of a round's places gives tokens (its prize is fixed at the ask, a `TokenPrize`). */
|
|
665
|
+
export const tokenRun = (r, run) => Boolean(r.terms.places[run]?.prize?.Tokens);
|
|
246
666
|
|
|
247
|
-
/**
|
|
248
|
-
export const
|
|
667
|
+
/** A place whose prize is an NFT of the lottery's own stash (`Held`, `Raffled`): assigned by the pulls, in place order. */
|
|
668
|
+
export const ownStash = place =>
|
|
669
|
+
Boolean(place.prize?.Held) || place.prize === 'Raffled' || place.prize?.Raffled !== undefined;
|
|
670
|
+
|
|
671
|
+
/** Entry `i`'s places in run `run` whose prize (not tokens) is not given yet, lowest first. */
|
|
672
|
+
export const prizePlacesOwed = (r, i, run) => {
|
|
673
|
+
const e = r.entries[i];
|
|
674
|
+
return placesIn(r, run, e.start, e.end).filter(p => !placePaid(r, p, true));
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
/** 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). */
|
|
678
|
+
export function nftSlot(r, place) {
|
|
679
|
+
let [k, first] = [0, 0];
|
|
680
|
+
for (const p of r.terms.places) {
|
|
681
|
+
if (place < first + Number(p.times)) return k + place - first;
|
|
682
|
+
if (ownStash(p)) k += Number(p.times);
|
|
683
|
+
first += Number(p.times);
|
|
684
|
+
}
|
|
685
|
+
return k;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/** How many places of a round win an NFT of the lottery's own stash (every such place with a winner). */
|
|
689
|
+
export function stashPlacesWon(r) {
|
|
690
|
+
const w = roundWinners(r);
|
|
691
|
+
let [first, won] = [0, 0];
|
|
692
|
+
for (const p of r.terms.places) {
|
|
693
|
+
if (ownStash(p)) won += Math.max(0, Math.min(first + Number(p.times), w) - first);
|
|
694
|
+
first += Number(p.times);
|
|
695
|
+
}
|
|
696
|
+
return won;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/** Whether entry `i`'s places in run `run` (an NFT of the lottery's own stash) wait for their NFTs to be taken out
|
|
700
|
+
* (`LotteryRound::assign`): a pull with the stash alone does it, and `r.nfts` then names each. */
|
|
701
|
+
export function awaitsNfts(r, i, run) {
|
|
702
|
+
if (!ownStash(r.terms.places[run])) return false;
|
|
703
|
+
const places = prizePlacesOwed(r, i, run);
|
|
704
|
+
return places.some(p => nftSlot(r, p) >= Number(r.assigned));
|
|
705
|
+
}
|
|
249
706
|
|
|
250
707
|
/**
|
|
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.
|
|
708
|
+
* The parts entry `i` of a round is owed now, in the order a keeper pulls them (`pullIx`'s `part`): its own ('Own': a
|
|
709
|
+
* refund, a no-loss ticket's price, its NFT ticket back or where its price sends it), and with `places`, its places'
|
|
710
|
+
* parts of the pot ({ Pot: [run] }) and prizes ({ Prize: [run] }). Parts owed in common (a share, the pot's, a pool's
|
|
711
|
+
* prize back) are no entry's: `commonOwed`.
|
|
257
712
|
*/
|
|
258
|
-
export function
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
if (
|
|
265
|
-
if (
|
|
266
|
-
|
|
713
|
+
export function partsOwed(r, i, { places = true } = {}) {
|
|
714
|
+
const e = r.entries[i];
|
|
715
|
+
const parts = [];
|
|
716
|
+
const drawn = r.status === 'Drawn';
|
|
717
|
+
const back = r.status === 'Cancelled' || (r.terms.returns && drawn);
|
|
718
|
+
const nft = e.nft !== NO_ADDRESS;
|
|
719
|
+
if ((e.amount > 0n || nft) && (back || (nft && drawn)) && !(e.paid & OWN_PAID)) parts.push('Own');
|
|
720
|
+
if (!places || !drawn) return parts;
|
|
721
|
+
for (let run = 0; run < r.terms.places.length; run++) {
|
|
722
|
+
const prize = r.terms.places[run].prize;
|
|
723
|
+
if (placesOwed(r, i, run, false) > 0n) parts.push({ Pot: [run] });
|
|
724
|
+
if (tokenRun(r, run) ? placesOwed(r, i, run, true) > 0n : prize != null && prizePlacesOwed(r, i, run).length > 0)
|
|
725
|
+
parts.push({ Prize: [run] });
|
|
726
|
+
}
|
|
727
|
+
return parts;
|
|
267
728
|
}
|
|
268
729
|
|
|
730
|
+
/**
|
|
731
|
+
* The parts of a round owed in common, of no entry (`{ Common: [i] }`): each share paid in common once drawn (one that
|
|
732
|
+
* burns is burned at the close), then what its places leave, owed to its pot or to `rest` (`i` = the shares' count), then after a
|
|
733
|
+
* cancel each pool's token prize back to its pool (past it).
|
|
734
|
+
*/
|
|
735
|
+
export function commonOwed(r) {
|
|
736
|
+
const n = r.terms.shares.length;
|
|
737
|
+
const out = [];
|
|
738
|
+
if (r.status === 'Drawn')
|
|
739
|
+
r.terms.shares.forEach((s, i) => s.to !== 'Burn' && BigInt(r.common[i] ?? 0) > 0n && out.push({ Common: [i] }));
|
|
740
|
+
// What the places leave, to the pot or to `rest`: a burned rest stays for the close to burn, and is no part owed.
|
|
741
|
+
if (
|
|
742
|
+
(r.status === 'Drawn' || r.status === 'Cancelled') &&
|
|
743
|
+
BigInt(r.pot_owed) > 0n &&
|
|
744
|
+
leftoverTo(r.lottery, r.terms) !== 'Burn'
|
|
745
|
+
)
|
|
746
|
+
out.push({ Common: [n] });
|
|
747
|
+
if (r.status === 'Cancelled')
|
|
748
|
+
r.prizes.forEach((p, k) => !p.minted && BigInt(p.arrived) > 0n && out.push({ Common: [n + 1 + k] }));
|
|
749
|
+
return out;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/** The entry of a round holding `ticket`. */
|
|
753
|
+
export const entryOf = (r, ticket) => r.entries.findIndex(e => e.start <= BigInt(ticket) && BigInt(ticket) < e.end);
|
|
754
|
+
|
|
755
|
+
/** Whether a round is done and owes nothing: the next pull closes it, given its close's accounts. */
|
|
756
|
+
export const roundFinished = r => (r.status === 'Drawn' || r.status === 'Cancelled') && Number(r.owed) === 0;
|
|
757
|
+
|
|
758
|
+
/** Whether a lottery's place wins something: a part of what the round pays from, a prize, or both. */
|
|
759
|
+
export const placeSet = place => Number(place.bps) > 0 || place.prize != null;
|
|
760
|
+
|
|
761
|
+
/** The address of a lottery's current round's record (there or not). */
|
|
762
|
+
export const roundAddress = (lotteryAddress, lottery) => pda.round(lotteryAddress, lottery.incarnation, lottery.round);
|
|
763
|
+
|
|
764
|
+
/** Whether a lottery (decoded) drew once and has ended. */
|
|
765
|
+
export const lotteryEnded = lottery => Number(lottery.every) === 0 && Number(lottery.round) > 0;
|
|
766
|
+
|
|
767
|
+
/** The round a lottery's close, its write again and its own stash's moves name (`lottery::check_idle`): its current
|
|
768
|
+
* round's, or once a lottery that draws once has ended, round 0's (which keeps its stash's NFTs until its places took
|
|
769
|
+
* them out). */
|
|
770
|
+
export const idleRound = (lotteryAddress, lottery) =>
|
|
771
|
+
pda.round(lotteryAddress, lottery.incarnation, lotteryEnded(lottery) ? 0 : lottery.round);
|
|
772
|
+
|
|
269
773
|
/**
|
|
270
774
|
* Who rents a listed NFT at `now` (Unix seconds), from its decoded Listing (or null for none): the renter's wallet while
|
|
271
775
|
* a rental runs, else null (a sale listing, or a rental that is over).
|
|
@@ -275,13 +779,19 @@ export function renterOf(listing, now) {
|
|
|
275
779
|
return BigInt(now) < BigInt(listing.until) && listing.user !== SYSTEM_PROGRAM ? listing.user : null;
|
|
276
780
|
}
|
|
277
781
|
|
|
782
|
+
/** Whether a schedule pays every run due when it is sent late (`schedule::catches_up`): only a sealed fixed amount. */
|
|
783
|
+
export const scheduleCatchesUp = schedule => Boolean(schedule.action?.from?.Vest && schedule.action?.amount?.Fixed);
|
|
784
|
+
|
|
278
785
|
/**
|
|
279
|
-
* How many runs of a schedule are due at `now` (Unix seconds, the chain's clock, never the computer's)
|
|
280
|
-
*
|
|
281
|
-
*
|
|
786
|
+
* How many runs of a schedule are due at `now` (Unix seconds, the chain's clock, never the computer's). One `run_schedule`
|
|
787
|
+
* makes them all. A sealed schedule's fixed amount pays each of them (`scheduleCatchesUp`; up to its `upto`, the runs made
|
|
788
|
+
* after it: `schedule.runs` plus these, and a long backlog takes a few calls). Any other run pays once, for the latest
|
|
789
|
+
* run due, and those before it lapse. A sealed schedule past its last run is due once whenever its vault holds something
|
|
790
|
+
* (`vaultBalance`), which goes where it always went.
|
|
282
791
|
*/
|
|
283
792
|
export function runsDue(schedule, now, vaultBalance = 0n) {
|
|
284
|
-
const runs = Number(schedule.runs),
|
|
793
|
+
const runs = Number(schedule.runs),
|
|
794
|
+
max = Number(schedule.max_runs);
|
|
285
795
|
if (max > 0 && runs >= max) return schedule.action?.from?.Vest && BigInt(vaultBalance) > 0n ? 1 : 0;
|
|
286
796
|
if (!schedule.active || BigInt(now) < BigInt(schedule.next_at)) return 0;
|
|
287
797
|
const due = Number((BigInt(now) - BigInt(schedule.next_at)) / BigInt(schedule.every)) + 1;
|
|
@@ -299,20 +809,27 @@ export function runsDue(schedule, now, vaultBalance = 0n) {
|
|
|
299
809
|
*/
|
|
300
810
|
export function receipts(tx, err) {
|
|
301
811
|
if (Array.isArray(tx)) {
|
|
302
|
-
if (err === undefined)
|
|
812
|
+
if (err === undefined)
|
|
813
|
+
throw new Error(
|
|
814
|
+
"loot.receipts(logs, err) needs the transaction's meta.err (null when it succeeded): a failed transaction keeps its logs",
|
|
815
|
+
);
|
|
303
816
|
return err === null ? decodeEvents(IDL, tx) : [];
|
|
304
817
|
}
|
|
305
818
|
const meta = tx?.meta;
|
|
306
|
-
if (!meta || meta.err === undefined)
|
|
819
|
+
if (!meta || meta.err === undefined)
|
|
820
|
+
throw new Error(
|
|
821
|
+
'loot.receipts(tx) needs the transaction getTransaction returns, with its meta.err and meta.logMessages',
|
|
822
|
+
);
|
|
307
823
|
return meta.err === null ? decodeEvents(IDL, meta.logMessages ?? []) : [];
|
|
308
824
|
}
|
|
309
825
|
|
|
310
826
|
/** The program's errors by code: { code: { name, msg } }. */
|
|
311
|
-
export const ERRORS = Object.freeze(Object.fromEntries(IDL.errors.map(
|
|
827
|
+
export const ERRORS = Object.freeze(Object.fromEntries(IDL.errors.map(e => [e.code, { name: e.name, msg: e.msg }])));
|
|
312
828
|
|
|
313
829
|
/**
|
|
314
830
|
* The loot error in a failed transaction's logs ("custom program error: 0x…", or Anchor's "Error Code: …" line), or
|
|
315
|
-
* null
|
|
831
|
+
* null: { code, name, msg }, `msg` the IDL's sentence (the program's own log names the error only: its text is
|
|
832
|
+
* compiled out). Only loot's own: another program the transaction calls (Bubblegum, the VRF program, …) has codes of its own
|
|
316
833
|
* 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
834
|
* The first program that fails is where the error came from; the programs that called it fail after it with the same
|
|
318
835
|
* code.
|
|
@@ -339,7 +856,12 @@ export function lootError(logs) {
|
|
|
339
856
|
// 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
857
|
if (running.length && running.at(-1) !== LOOT_PROGRAM) continue;
|
|
341
858
|
const named = /Error Code: (\w+)\. Error Number: (\d+)\. Error Message: (.+?)\.?$/.exec(line);
|
|
342
|
-
if (named)
|
|
859
|
+
if (named)
|
|
860
|
+
return {
|
|
861
|
+
code: Number(named[2]),
|
|
862
|
+
name: named[1],
|
|
863
|
+
msg: ERRORS[Number(named[2])]?.name === named[1] ? ERRORS[Number(named[2])].msg : named[3],
|
|
864
|
+
};
|
|
343
865
|
const m = /custom program error: 0x([0-9a-f]+)/i.exec(line);
|
|
344
866
|
if (m && ERRORS[parseInt(m[1], 16)]) return { code: parseInt(m[1], 16), ...ERRORS[parseInt(m[1], 16)] };
|
|
345
867
|
}
|