@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,46 @@
|
|
|
1
|
+
// Compressed NFTs (Bubblegum v2) as loot takes them: a leaf as an indexer gives it, read into the shape `proveLeafIx`
|
|
2
|
+
// and the routes take. The chain keeps only a tree's root, changelog and canopy; the leaf and its proof come from an
|
|
3
|
+
// indexer (DAS `getAsset` and `getAssetProof`). Browser-safe.
|
|
4
|
+
import { decodeBase58 } from '../chain/base58.js';
|
|
5
|
+
|
|
6
|
+
/** mpl-account-compression's tree header: type, version, max buffer size (u32), max depth (u32), authority, slot, padding. */
|
|
7
|
+
const HEADER = 56;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* How many of a proof's nodes a transaction passes for this tree: the levels its canopy does not keep. `treeData` is
|
|
11
|
+
* the tree account's bytes.
|
|
12
|
+
*/
|
|
13
|
+
export function proofLength(treeData) {
|
|
14
|
+
const u32 = (at) => treeData[at] | (treeData[at + 1] << 8) | (treeData[at + 2] << 16) | (treeData[at + 3] * 2 ** 24);
|
|
15
|
+
const [buffer, depth] = [u32(2), u32(6)];
|
|
16
|
+
const canopyAt = HEADER + 24 + buffer * (32 * depth + 40) + 32 * depth + 40;
|
|
17
|
+
const canopy = Math.max(0, treeData.length - canopyAt);
|
|
18
|
+
const cached = Math.log2(canopy / 32 + 2) - 1;
|
|
19
|
+
return Math.max(0, depth - Math.round(cached));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const bytes = (value) => (value == null ? null : typeof value === 'string' ? decodeBase58(value) : Uint8Array.from(value));
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A compressed NFT from an indexer's answers: `asset` (DAS `getAsset`), `proof` (DAS `getAssetProof`) and the tree
|
|
26
|
+
* account's bytes (`treeData`, for how much of the proof to pass). Returns { id, tree, nonce, owner, delegate,
|
|
27
|
+
* collection (or null), dataHash, creatorHash, assetDataHash (or null), flags, root, proof: [node, …] }. The proof
|
|
28
|
+
* must be against the tree's current root when it lands: fetch it just before sending.
|
|
29
|
+
*/
|
|
30
|
+
export function compressedFromDas({ asset, proof, treeData }) {
|
|
31
|
+
const c = asset.compression;
|
|
32
|
+
return {
|
|
33
|
+
id: asset.id,
|
|
34
|
+
tree: c.tree,
|
|
35
|
+
nonce: BigInt(c.leaf_id),
|
|
36
|
+
owner: asset.ownership.owner,
|
|
37
|
+
delegate: asset.ownership.delegate ?? asset.ownership.owner,
|
|
38
|
+
collection: asset.grouping?.find((g) => g.group_key === 'collection')?.group_value ?? null,
|
|
39
|
+
dataHash: bytes(c.data_hash),
|
|
40
|
+
creatorHash: bytes(c.creator_hash),
|
|
41
|
+
assetDataHash: bytes(c.asset_data_hash ?? null),
|
|
42
|
+
flags: Number(c.flags ?? 0),
|
|
43
|
+
root: bytes(proof.root),
|
|
44
|
+
proof: proof.proof.slice(0, proofLength(treeData)),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
// Grants: the bytes every voucher signer signs and `claim_grant` rebuilds, the approved list's Merkle tree, the
|
|
2
|
+
// Ed25519 precompile instruction that carries a voucher's signature, and the claim itself. Browser-safe.
|
|
3
|
+
//
|
|
4
|
+
// voucherBytes "loot-grant-v5" | sha256(program (32) | grant (32) | incarnation (u64 LE) | wallet (32) | person_hash (32) |
|
|
5
|
+
// expires (i64 LE) | count (u32 LE) | (round (16) | amount (u64 LE)) × count): a wallet's wins of several rounds
|
|
6
|
+
// listLeaf sha256("loot-list-v3" | wallet (32) | person_hash (32) | round (16) | amount (u64 LE)): a wallet's win of one round
|
|
7
|
+
//
|
|
8
|
+
// `amount`: the tokens a round gives, up to the grant's fixed amount (a rank's reward, a share); 0 for the grant's
|
|
9
|
+
// prize as it is. A round pays a wallet once per grant incarnation, whoever signs it again, whenever.
|
|
10
|
+
// keyLeaf sha256("loot-key-v1" | voucher key (32)): a key under a grant's key root (a promo code's)
|
|
11
|
+
// merkleRoot a node = sha256(min(a, b) | max(a, b)) (byte-wise); an odd node at a level goes up unchanged
|
|
12
|
+
// merkleProof the siblings from the leaf up (a promoted level adds none)
|
|
13
|
+
//
|
|
14
|
+
// A voucher's `person_hash` names one person across wallets (who decides that is the signer: a play host that saw
|
|
15
|
+
// a linked account, the project's own server). 32 zero bytes when there is none. Fixed vectors of these bytes are in
|
|
16
|
+
// the tests (js/test/client.test.mjs).
|
|
17
|
+
import { addressBytes } from '../chain/base58.js';
|
|
18
|
+
import { compareBytes, concat, fromHex, intLE, readUintLE, toBigInt, uintLE, utf8 } from '../chain/bytes.js';
|
|
19
|
+
import { meta } from '../chain/programs.js';
|
|
20
|
+
import { sha256 } from '../chain/sha256.js';
|
|
21
|
+
import { recordKey16 } from './accounts.js';
|
|
22
|
+
import { ASSOCIATED_TOKEN_PROGRAM, gateAccounts, lootIx, prizeMetas, prizeNonce, tailOf } from './ix.js';
|
|
23
|
+
import { LOOT_PROGRAM, pda } from './pda.js';
|
|
24
|
+
import IDL from './idl/loot.js';
|
|
25
|
+
|
|
26
|
+
export const VOUCHER_DOMAIN = 'loot-grant-v5';
|
|
27
|
+
export const LIST_DOMAIN = 'loot-list-v3';
|
|
28
|
+
export const KEY_DOMAIN = 'loot-key-v1';
|
|
29
|
+
/** What a voucher key signs: the prefix (13 bytes), then the sha256 of the voucher's body. */
|
|
30
|
+
export const VOUCHER_BYTES = 45;
|
|
31
|
+
/** A grant claim's compute limit: its rounds' records and prizes can pass the 200,000 a transaction has by default. */
|
|
32
|
+
export const CLAIM_COMPUTE_UNITS = 400_000;
|
|
33
|
+
export const ZERO_HASH = new Uint8Array(32);
|
|
34
|
+
export const ED25519_PROGRAM = 'Ed25519SigVerify111111111111111111111111111';
|
|
35
|
+
/** The seconds of a UTC day (the IDL's `DAY_SECONDS`): a limit's window of a day (`PerWallet.window`). */
|
|
36
|
+
const DAY_SECONDS = Number(IDL.constants.find((c) => c.name === 'DAY_SECONDS').value);
|
|
37
|
+
/** The day a window of `DAY_SECONDS` counts in, from the unix epoch. */
|
|
38
|
+
export const dayOf = (unixSeconds) => Math.floor(Number(unixSeconds) / DAY_SECONDS);
|
|
39
|
+
|
|
40
|
+
/** 32 bytes from a base58 address, 32 bytes or 64 hex characters. */
|
|
41
|
+
function bytes32(value, what) {
|
|
42
|
+
if (value instanceof Uint8Array) {
|
|
43
|
+
if (value.length !== 32) throw new RangeError(`${what} must be 32 bytes, not ${value.length}`);
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
if (typeof value === 'string' && /^[0-9a-f]{64}$/i.test(value)) return fromHex(value);
|
|
47
|
+
if (typeof value === 'string') return addressBytes(value);
|
|
48
|
+
throw new TypeError(`${what} must be an address or 32 bytes`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const personBytes = (value) => (value === null || value === undefined || value === '' ? ZERO_HASH : bytes32(value, 'person_hash'));
|
|
52
|
+
|
|
53
|
+
function roundBytes(value) {
|
|
54
|
+
const out = value instanceof Uint8Array ? value : typeof value === 'string' && /^[0-9a-f]{32}$/i.test(value) ? fromHex(value) : null;
|
|
55
|
+
if (!out || out.length !== 16) throw new RangeError('round must be 16 bytes (32 hex characters)');
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** An i64 little-endian (two's complement), as the program reads `expires`. */
|
|
60
|
+
export const i64LE = (value) => intLE(value, 8);
|
|
61
|
+
|
|
62
|
+
/** A claim's rounds: [{ id (16 bytes or 32 hex characters), amount }], or one `round` and its `amount`. */
|
|
63
|
+
const roundsOf = (rounds, round, amount) => (rounds ?? [{ id: round, amount }]).map((r) => ({ id: roundBytes(r.id ?? r.round ?? new Uint8Array(16)), amount: toBigInt(r.amount ?? 0) }));
|
|
64
|
+
|
|
65
|
+
/** A voucher's body, which its key signs the sha256 of (`voucherBytes`). */
|
|
66
|
+
export function voucherBody({ program = LOOT_PROGRAM, grant, incarnation, wallet, personHash, expires, rounds, round, amount = 0n }) {
|
|
67
|
+
const list = roundsOf(rounds, round, amount);
|
|
68
|
+
return concat(bytes32(program, 'program'), bytes32(grant, 'grant'), uintLE(incarnation, 8), bytes32(wallet, 'wallet'), personBytes(personHash), i64LE(expires), uintLE(list.length, 4), ...list.flatMap((r) => [r.id, uintLE(r.amount, 8)]));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The 45 bytes a voucher key signs and `claim_grant` rebuilds from its own accounts and arguments: the prefix, then the
|
|
73
|
+
* sha256 of the voucher's body (`voucherBody`), so a voucher of many rounds is as short to check as one of one. `rounds`
|
|
74
|
+
* ([{ id, amount }], or one `round` and its `amount`) names the wins (the project round, draw or giveaway each was won
|
|
75
|
+
* in): the program pays a wallet each round once per grant incarnation.
|
|
76
|
+
*/
|
|
77
|
+
export const voucherBytes = (fields) => concat(utf8(VOUCHER_DOMAIN), sha256(voucherBody(fields)));
|
|
78
|
+
|
|
79
|
+
/** The fields back from a voucher's body (`voucherBody`; addresses as bytes, rounds as [{ id, amount }]). */
|
|
80
|
+
export function readVoucher(body) {
|
|
81
|
+
if (!(body instanceof Uint8Array) || body.length < 148) throw new RangeError('a voucher body is at least 148 bytes');
|
|
82
|
+
const at = (i, len) => body.slice(i, i + len);
|
|
83
|
+
let expires = readUintLE(body, 136, 8);
|
|
84
|
+
if (expires >= 1n << 63n) expires -= 1n << 64n;
|
|
85
|
+
const count = Number(readUintLE(body, 144, 4));
|
|
86
|
+
if (body.length !== 148 + 24 * count) throw new RangeError('a voucher body holds 24 bytes a round');
|
|
87
|
+
const rounds = Array.from({ length: count }, (_, i) => ({ id: at(148 + 24 * i, 16), amount: readUintLE(body, 164 + 24 * i, 8) }));
|
|
88
|
+
return { program: at(0, 32), grant: at(32, 32), incarnation: readUintLE(body, 64, 8), wallet: at(72, 32), personHash: at(104, 32), expires, rounds };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** A voucher key's leaf under a grant's `key_root`. */
|
|
92
|
+
export const keyLeaf = (key) => sha256(concat(utf8(KEY_DOMAIN), bytes32(key, 'key')));
|
|
93
|
+
|
|
94
|
+
/** A list entry's round as the leaf names it: a number (a spec's `round`) in the first 8 of 16 bytes; 0 is zeros. */
|
|
95
|
+
export const listRound = (n = 0) => {
|
|
96
|
+
const out = new Uint8Array(16);
|
|
97
|
+
out.set(uintLE(n, 8));
|
|
98
|
+
return out;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* A leaf of an approved list: `wallet` won `round` (16 bytes or 32 hex characters; zeros when not given). A round is
|
|
103
|
+
* paid once, so a list can name the winners of many rounds, and a wallet's earlier wins stay claimable when a new
|
|
104
|
+
* list adds later ones.
|
|
105
|
+
*/
|
|
106
|
+
export const listLeaf = (wallet, personHash, round, amount = 0n) => sha256(concat(utf8(LIST_DOMAIN), bytes32(wallet, 'wallet'), personBytes(personHash), roundBytes(round ?? new Uint8Array(16)), uintLE(amount, 8)));
|
|
107
|
+
|
|
108
|
+
/** The prefix of a wallet's leaf in a list gate (`gateListLeaf`). */
|
|
109
|
+
export const GATE_LIST_DOMAIN = 'loot-list-v1';
|
|
110
|
+
/** A wallet's leaf in a list gate (`Gate::List`): sha256("loot-list-v1" | wallet). Its tree is a list's
|
|
111
|
+
* (`merkleRoot`, `merkleProof`), and its proof's nodes go where an NFT shown would (`gateAccounts`'s `listProof`). */
|
|
112
|
+
export const gateListLeaf = (wallet) => sha256(concat(utf8(GATE_LIST_DOMAIN), bytes32(wallet, 'wallet')));
|
|
113
|
+
|
|
114
|
+
/** The parent of two nodes: sha256 of the smaller then the larger. */
|
|
115
|
+
export const parentOf = (a, b) => (compareBytes(a, b) <= 0 ? sha256(concat(a, b)) : sha256(concat(b, a)));
|
|
116
|
+
|
|
117
|
+
function levelsOf(leaves) {
|
|
118
|
+
if (!leaves.length) throw new RangeError('a list needs at least one wallet');
|
|
119
|
+
const levels = [leaves.map((l) => bytes32(l, 'leaf'))];
|
|
120
|
+
while (levels.at(-1).length > 1) {
|
|
121
|
+
const level = levels.at(-1);
|
|
122
|
+
const next = [];
|
|
123
|
+
for (let i = 0; i < level.length; i += 2) next.push(i + 1 < level.length ? parentOf(level[i], level[i + 1]) : level[i]);
|
|
124
|
+
levels.push(next);
|
|
125
|
+
}
|
|
126
|
+
return levels;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The root of the leaves, in the order given. Whoever builds the list keeps that order. */
|
|
130
|
+
export const merkleRoot = (leaves) => levelsOf(leaves).at(-1)[0];
|
|
131
|
+
|
|
132
|
+
/** The siblings of leaf `index`, from the bottom up. */
|
|
133
|
+
export function merkleProof(leaves, index) {
|
|
134
|
+
const levels = levelsOf(leaves);
|
|
135
|
+
if (!Number.isInteger(index) || index < 0 || index >= levels[0].length) throw new RangeError('no leaf at that index');
|
|
136
|
+
const proof = [];
|
|
137
|
+
let i = index;
|
|
138
|
+
for (const level of levels.slice(0, -1)) {
|
|
139
|
+
const sibling = i % 2 ? i - 1 : i + 1;
|
|
140
|
+
if (sibling < level.length) proof.push(level[sibling]);
|
|
141
|
+
i = Math.floor(i / 2);
|
|
142
|
+
}
|
|
143
|
+
return proof;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* A list gate's tree (`Gate::List`) for `wallets`, in the order given: its `root`, its `depth`, and `proofOf(wallet)`.
|
|
148
|
+
* The gate reads exactly `depth` nodes, so the leaves are filled up to a power of two with a leaf no wallet has (all
|
|
149
|
+
* zeros): every wallet's proof is then `depth` nodes long.
|
|
150
|
+
*/
|
|
151
|
+
export function gateList(wallets) {
|
|
152
|
+
if (!wallets.length) throw new RangeError('a list needs at least one wallet');
|
|
153
|
+
const leaves = wallets.map((w) => gateListLeaf(w));
|
|
154
|
+
let size = 1;
|
|
155
|
+
while (size < leaves.length) size *= 2;
|
|
156
|
+
// Filled with a leaf no wallet has (all zeros): a wallet's leaf is a hash, so nobody can prove a filler.
|
|
157
|
+
while (leaves.length < size) leaves.push(new Uint8Array(32));
|
|
158
|
+
return {
|
|
159
|
+
root: merkleRoot(leaves),
|
|
160
|
+
depth: Math.log2(size),
|
|
161
|
+
proofOf: (wallet) => {
|
|
162
|
+
const at = wallets.indexOf(wallet);
|
|
163
|
+
if (at < 0) throw new RangeError(`${wallet} is not on the list`);
|
|
164
|
+
return merkleProof(leaves, at);
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Whether `proof` leads from `leaf` to `root`. */
|
|
170
|
+
export function verifyProof(leaf, proof, root) {
|
|
171
|
+
let node = bytes32(leaf, 'leaf');
|
|
172
|
+
for (const sibling of proof) node = parentOf(node, bytes32(sibling, 'proof'));
|
|
173
|
+
return compareBytes(node, bytes32(root, 'root')) === 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The Ed25519 precompile instruction for one signature, everything in its own data: count 1, padding, seven u16
|
|
178
|
+
* offsets with instruction index 0xffff (this instruction), then the key at 16, the signature at 48, the message at
|
|
179
|
+
* 112. `claim_grant` reads it through the instructions sysvar, in the instruction right before itself.
|
|
180
|
+
*/
|
|
181
|
+
export function ed25519Ix({ publicKey, signature, message }) {
|
|
182
|
+
const key = bytes32(publicKey, 'publicKey');
|
|
183
|
+
if (!(signature instanceof Uint8Array) || signature.length !== 64) throw new RangeError('signature must be 64 bytes');
|
|
184
|
+
const keyAt = 16;
|
|
185
|
+
const signatureAt = keyAt + 32;
|
|
186
|
+
const messageAt = signatureAt + 64;
|
|
187
|
+
const u16 = (n) => uintLE(n, 2);
|
|
188
|
+
const THIS = 0xffff;
|
|
189
|
+
const data = concat(Uint8Array.of(1, 0), u16(signatureAt), u16(THIS), u16(keyAt), u16(THIS), u16(messageAt), u16(message.length), u16(THIS), key, signature, message);
|
|
190
|
+
return { programId: ED25519_PROGRAM, keys: [], data };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** The kinds of a grant's limit records (`limitKey`). */
|
|
194
|
+
const KINDS = Object.freeze({ wallet: 'wallet', person: 'person', badge: 'badge', code: 'code' });
|
|
195
|
+
|
|
196
|
+
/** The key of a grant's limit record of `kind` ('wallet', 'person', 'badge', 'code') for `subject` (an address or 32
|
|
197
|
+
* bytes): sha256(kind | subject)[..16]. */
|
|
198
|
+
export const limitKey = (kind, subject) => recordKey16(utf8(KINDS[kind] ?? kind), bytes32(subject, 'subject'));
|
|
199
|
+
|
|
200
|
+
/** The key of a wallet's record of a round a grant paid it: sha256(round | wallet)[..16]. */
|
|
201
|
+
export const roundKey = (round, wallet) => recordKey16(roundBytes(round), bytes32(wallet, 'wallet'));
|
|
202
|
+
|
|
203
|
+
/** The shard a record's key falls in, of a grant's `2^bits`: the top `bits` bits of its first byte. */
|
|
204
|
+
export const shardOf = (key, bits) => (Number(bits) ? key[0] >> (8 - Number(bits)) : 0);
|
|
205
|
+
|
|
206
|
+
/** The rounds shard a wallet's paid rounds are kept in: its own limit record's. */
|
|
207
|
+
export const walletShard = (grant, wallet) => shardOf(limitKey('wallet', wallet), grant.shard_bits);
|
|
208
|
+
|
|
209
|
+
/** Whether a grant paid `wallet` for `round`, from the wallet's decoded rounds shard (`decodeList`, null for none). */
|
|
210
|
+
export const roundPaid = (list, wallet, round) => Boolean(list?.records.some((r) => compareBytes(r, roundKey(round, wallet)) === 0));
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The fewest shard bits (0 to 8) that keep each of a grant's lists under ~100,000 records over its life: `wallets` ×
|
|
214
|
+
* the rounds each may claim (`rounds`). A 10,000-wallet airdrop takes 0 (one shard).
|
|
215
|
+
*/
|
|
216
|
+
export function shardBitsFor(wallets, rounds = 1) {
|
|
217
|
+
const records = Number(wallets) * Math.max(1, Number(rounds));
|
|
218
|
+
let bits = 0;
|
|
219
|
+
while (bits < 8 && records / 2 ** bits > 100_000) bits += 1;
|
|
220
|
+
return bits;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* The claim of grant `id` for `wallet`, sent and signed by `signer` (anyone: the play host, the owner's server, the
|
|
225
|
+
* player). The grant's funds pay its records through the signer; what they can't cover, the signer pays. The signer pays
|
|
226
|
+
* the player fee, and a roll's order, with its own SOL. `grant` is the decoded Grant.
|
|
227
|
+
*
|
|
228
|
+
* A voucher claim goes right after `ed25519Ix` in the same transaction, `voucherKey` the key that signed it (a key under
|
|
229
|
+
* the key root carries its proof as `proofs[0]`); a list claim carries a proof for each round (`proofs`); an open grant
|
|
230
|
+
* (no keys, no key root, no list) names no round and pays one prize (`count`: the wallet's claims before it, for its
|
|
231
|
+
* roll's nonce). `rounds` ([{ id, amount }], or one `round` and its `amount`) are the wins it names: each paid once, so
|
|
232
|
+
* one already paid is skipped (`paidRounds`: the wallet's decoded rounds shard, `decodeList`, to leave out its prize's
|
|
233
|
+
* accounts). What passes its gate (`shown`, or `listProof` for a list gate) comes first, then each shard of the grant's
|
|
234
|
+
* claims it counts in, then each round's prize (`grant.gives`, `prizeMetas`): `poolMint` for tokens from a pool, `item`
|
|
235
|
+
* (the decoded Item) for a collectible or a compressed NFT of one, `nft` for an NFT of a stash; round `i`'s roll at
|
|
236
|
+
* `prizeRollNonce(nonce + i)` (`nonce`: `prizeNonce` of the grant, the wallet and its rounds by default), its box and
|
|
237
|
+
* order only (`askRollIx` then asks for it, in the same transaction when it fits). Send it with a compute limit of
|
|
238
|
+
* `CLAIM_COMPUTE_UNITS`.
|
|
239
|
+
*/
|
|
240
|
+
export function claimGrantIx({ project, id, grant, wallet, signer, count = 0, rounds, round, expires, personHash, amount = 0n, proofs, proof = [], paidRounds = null, poolMint, item, nft, shown, listProof, programs, voucherKey, nonce, extra = [] }) {
|
|
241
|
+
if (!signer) throw new Error('A claim needs its signer: whoever sends it');
|
|
242
|
+
// Every claim is its winning wallet's own: the wallet signs it, whatever proves it (a voucher, a list, an open
|
|
243
|
+
// grant), so nobody claims for a player (WalletMustSign). `signer` pays the fees and what the claim opens: the
|
|
244
|
+
// wallet itself, or anyone who pays for it.
|
|
245
|
+
const address = pda.grant(project, id);
|
|
246
|
+
const person = personBytes(personHash);
|
|
247
|
+
const listed = Uint8Array.from(grant.list_root ?? []).some((b) => b !== 0);
|
|
248
|
+
// A claim by voucher or list names its rounds; an open one names none.
|
|
249
|
+
const named = Boolean(voucherKey) || listed || rounds != null || round != null;
|
|
250
|
+
const list = named ? roundsOf(rounds, round ?? new Uint8Array(16), amount) : [];
|
|
251
|
+
// The limit records it counts in: the wallet's, a person's a voucher names, the NFT an open claim of an open grant
|
|
252
|
+
// gated by one shows (a badge), a voucher key's; each shard of the grant's claims once, by shard number.
|
|
253
|
+
const bits = Number(grant.shard_bits ?? 0);
|
|
254
|
+
const keys = [limitKey('wallet', wallet)];
|
|
255
|
+
if (person.some((b) => b !== 0) && Number(grant.limits?.per_person ?? 0) > 0) keys.push(limitKey('person', person));
|
|
256
|
+
const badge = typeof shown === 'string' ? shown : shown?.asset ?? shown?.mint ?? shown?.compressed;
|
|
257
|
+
if (!named && grant.open && grant.gate?.Holds && badge) keys.push(limitKey('badge', badge));
|
|
258
|
+
if (voucherKey && Number(grant.limits?.per_key ?? 0) > 0) keys.push(limitKey('code', voucherKey));
|
|
259
|
+
const shards = [...new Set(keys.map((k) => shardOf(k, bits)))].sort((x, y) => x - y).map((shard) => meta(pda.claims(address, grant.incarnation, shard), false, true));
|
|
260
|
+
nonce = toBigInt(nonce ?? (named ? prizeNonce(address, wallet, ...list.map((r) => r.id)) : prizeNonce(address, wallet, toBigInt(count))));
|
|
261
|
+
// Each round not paid yet gives the grant's prize, or the amount of its tokens the round names, in order; collectibles
|
|
262
|
+
// of one item each at its next serial.
|
|
263
|
+
const gives = grant.gives;
|
|
264
|
+
const pool = gives.Tokens?.from.Pool?.[0];
|
|
265
|
+
const itemId = gives.Item?.[0] ?? gives.Compressed?.item;
|
|
266
|
+
if (pool !== undefined && !poolMint) throw new Error('A payout claim needs its pool’s mint (poolMint), read from the pool');
|
|
267
|
+
const seen = new Set();
|
|
268
|
+
const owed = (named ? list.map((r, i) => ({ ...r, i })) : [{ amount: 0n, i: 0 }]).filter((r) => {
|
|
269
|
+
if (!named) return true;
|
|
270
|
+
const hex = Array.from(r.id, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
271
|
+
if (seen.has(hex) || roundPaid(paidRounds, wallet, r.id)) return false;
|
|
272
|
+
seen.add(hex);
|
|
273
|
+
return true;
|
|
274
|
+
});
|
|
275
|
+
const prizes = owed.flatMap((r, made) => prizeMetas({
|
|
276
|
+
project, prize: r.amount > 0n && gives.Tokens ? { Tokens: { ...gives.Tokens, amount: r.amount } } : gives, winner: wallet, nonce: BigInt.asUintN(64, nonce + BigInt(r.i)), programs,
|
|
277
|
+
poolMints: pool !== undefined ? { [pool]: poolMint } : {}, items: itemId !== undefined ? { [itemId]: item } : {}, nft, made,
|
|
278
|
+
}));
|
|
279
|
+
proofs ??= listed && !voucherKey ? [proof] : proof.length ? [proof] : [];
|
|
280
|
+
return lootIx(
|
|
281
|
+
'claim_grant',
|
|
282
|
+
{ rounds: list, expires: toBigInt(expires ?? 0), person_hash: person, proofs: proofs.map((p) => p.map((x) => bytes32(x, 'proof'))), nonce },
|
|
283
|
+
{
|
|
284
|
+
project, grant: address, signer, wallet,
|
|
285
|
+
rounds: named ? pda.rounds(address, grant.incarnation, shardOf(keys[0], bits)) : null,
|
|
286
|
+
token_program: undefined, token_2022_program: undefined, associated_token_program: ASSOCIATED_TOKEN_PROGRAM,
|
|
287
|
+
},
|
|
288
|
+
{ remaining: [...gateAccounts(project, { gate: grant.gate, wallet, shown, listProof }), ...shards, ...prizes, ...tailOf(extra)], signer: ['wallet'] },
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Anyone closes shard `shard` of a grant's rounds (`rounds`) or claims once the grant is closed or made again
|
|
294
|
+
* (`incarnation` the one the shard is of): its rent goes to the project's `owner`.
|
|
295
|
+
*/
|
|
296
|
+
export const closeShardIx = ({ project, id, incarnation, rounds = false, shard, owner }) => {
|
|
297
|
+
const grant = pda.grant(project, id);
|
|
298
|
+
return lootIx('close_shard', { id, incarnation: toBigInt(incarnation), rounds, shard }, { project, grant, shard: (rounds ? pda.rounds : pda.claims)(grant, incarnation, shard), owner });
|
|
299
|
+
};
|