@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,73 @@
|
|
|
1
|
+
// Base58 (Bitcoin alphabet): the text form of every Solana address and signature.
|
|
2
|
+
|
|
3
|
+
const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
4
|
+
const INDEX = new Map([...ALPHABET].map((c, i) => [c, i]));
|
|
5
|
+
/** An address is 32 bytes: 32 to 44 base58 characters. */
|
|
6
|
+
const ADDRESS_TEXT = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
|
7
|
+
const ADDRESS_BYTES = 32;
|
|
8
|
+
/** A signature is 64 bytes: 64 to 88 base58 characters. */
|
|
9
|
+
const SIGNATURE_TEXT = /^[1-9A-HJ-NP-Za-km-z]{64,88}$/;
|
|
10
|
+
|
|
11
|
+
/** The address of 32 zero bytes: what an address field holds when it names no account (the System program's address too). */
|
|
12
|
+
export const NO_ADDRESS = '11111111111111111111111111111111';
|
|
13
|
+
|
|
14
|
+
export function encodeBase58(bytes) {
|
|
15
|
+
const digits = [];
|
|
16
|
+
for (const byte of bytes) {
|
|
17
|
+
let carry = byte;
|
|
18
|
+
for (let i = 0; i < digits.length; i++) {
|
|
19
|
+
carry += digits[i] << 8;
|
|
20
|
+
digits[i] = carry % 58;
|
|
21
|
+
carry = Math.floor(carry / 58);
|
|
22
|
+
}
|
|
23
|
+
while (carry > 0) {
|
|
24
|
+
digits.push(carry % 58);
|
|
25
|
+
carry = Math.floor(carry / 58);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
let zeros = '';
|
|
29
|
+
for (const byte of bytes) {
|
|
30
|
+
if (byte !== 0) break;
|
|
31
|
+
zeros += '1';
|
|
32
|
+
}
|
|
33
|
+
return zeros + digits.reverse().map((d) => ALPHABET[d]).join('');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function decodeBase58(text) {
|
|
37
|
+
const bytes = [];
|
|
38
|
+
for (const char of text) {
|
|
39
|
+
const value = INDEX.get(char);
|
|
40
|
+
if (value === undefined) throw new Error(`Not base58: ${JSON.stringify(char)}`);
|
|
41
|
+
let carry = value;
|
|
42
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
43
|
+
carry += bytes[i] * 58;
|
|
44
|
+
bytes[i] = carry & 0xff;
|
|
45
|
+
carry >>= 8;
|
|
46
|
+
}
|
|
47
|
+
while (carry > 0) {
|
|
48
|
+
bytes.push(carry & 0xff);
|
|
49
|
+
carry >>= 8;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
let zeros = 0;
|
|
53
|
+
while (text[zeros] === '1') zeros++;
|
|
54
|
+
return Uint8Array.from([...new Array(zeros).fill(0), ...bytes.reverse()]);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Whether the text is a Solana address: base58 that decodes to exactly 32 bytes. */
|
|
58
|
+
export function isAddress(text) {
|
|
59
|
+
if (typeof text !== 'string' || !ADDRESS_TEXT.test(text)) return false;
|
|
60
|
+
return decodeBase58(text).length === ADDRESS_BYTES;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Whether the text is a transaction signature: base58 that decodes to exactly 64 bytes. */
|
|
64
|
+
export function isSignature(text) {
|
|
65
|
+
if (typeof text !== 'string' || !SIGNATURE_TEXT.test(text)) return false;
|
|
66
|
+
return decodeBase58(text).length === 64;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The 32 bytes of an address; throws on anything else. */
|
|
70
|
+
export function addressBytes(address) {
|
|
71
|
+
if (!isAddress(address)) throw new Error(`Not a Solana address: ${String(address).slice(0, 60)}`);
|
|
72
|
+
return decodeBase58(address);
|
|
73
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
// A small Borsh codec driven by an Anchor IDL, so neither side needs Anchor. It covers what the loot program and the
|
|
2
|
+
// accounts this kit reads use: bool, u8–u128, i32, i64, pubkey, string, fixed arrays, vec, option, defined structs and
|
|
3
|
+
// enums, with or without data. Plain Uint8Array, never Node's Buffer, so it runs in browser apps and Node alike.
|
|
4
|
+
//
|
|
5
|
+
// Values: addresses are base58 strings, u64/u128/i64 bigints (u8–u32 numbers), byte arrays Uint8Arrays. An enum is
|
|
6
|
+
// its variant's name ('Nothing'), or for a variant with data { Name: [values] } (tuple fields) or
|
|
7
|
+
// { Name: { field: value } } (named fields). Account and event discriminators are whatever length the IDL gives (loot
|
|
8
|
+
// tags its accounts with one byte).
|
|
9
|
+
import { addressBytes, encodeBase58 } from './base58.js';
|
|
10
|
+
import { concat, equalBytes, fromBase64, fromUtf8, intLE, readUintLE, uintLE, utf8 } from './bytes.js';
|
|
11
|
+
|
|
12
|
+
const UINT = { u8: 1, u16: 2, u32: 4, u64: 8, u128: 16 };
|
|
13
|
+
const INT = { i32: 4, i64: 8 };
|
|
14
|
+
const ADDRESS_BYTES = 32;
|
|
15
|
+
|
|
16
|
+
function typeDef(idl, name) {
|
|
17
|
+
const def = idl.types.find((t) => t.name === name);
|
|
18
|
+
if (!def) throw new Error(`Unknown IDL type ${name}`);
|
|
19
|
+
return def;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function readIntLE(data, at, size) {
|
|
23
|
+
const v = BigInt(readUintLE(data, at, size));
|
|
24
|
+
const bits = BigInt(size * 8);
|
|
25
|
+
const signed = v >= 1n << (bits - 1n) ? v - (1n << bits) : v;
|
|
26
|
+
// An i32 is a number, as u8 to u32 are; an i64 a bigint.
|
|
27
|
+
return size <= 4 ? Number(signed) : signed;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function encodeScalar(type, value, out) {
|
|
31
|
+
if (UINT[type]) out.push(uintLE(value, UINT[type]));
|
|
32
|
+
else if (INT[type]) out.push(intLE(value, INT[type]));
|
|
33
|
+
else if (type === 'bool') out.push(Uint8Array.of(value ? 1 : 0));
|
|
34
|
+
else if (type === 'pubkey') out.push(addressBytes(String(value)));
|
|
35
|
+
else if (type === 'string') {
|
|
36
|
+
const bytes = utf8(String(value));
|
|
37
|
+
out.push(uintLE(bytes.length, 4), bytes);
|
|
38
|
+
} else if (type === 'bytes') {
|
|
39
|
+
// A list of bytes (`Vec<u8>`): its length, then the bytes.
|
|
40
|
+
const bytes = Uint8Array.from(value);
|
|
41
|
+
out.push(uintLE(bytes.length, 4), bytes);
|
|
42
|
+
} else throw new Error(`Unsupported IDL type ${type}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A variant's field list: named ({ name, type }) or tuple (bare types). */
|
|
46
|
+
const isNamed = (fields) => typeof fields[0] === 'object' && fields[0] !== null && 'name' in fields[0];
|
|
47
|
+
|
|
48
|
+
function encodeDefined(idl, name, value, out) {
|
|
49
|
+
const def = typeDef(idl, name).type;
|
|
50
|
+
if (def.kind === 'enum') {
|
|
51
|
+
const variantName = typeof value === 'string' ? value : Object.keys(value ?? {})[0];
|
|
52
|
+
const index = def.variants.findIndex((v) => v.name === variantName);
|
|
53
|
+
if (index < 0) throw new Error(`${name} has no variant ${String(variantName)}`);
|
|
54
|
+
out.push(Uint8Array.of(index));
|
|
55
|
+
const fields = def.variants[index].fields ?? [];
|
|
56
|
+
if (!fields.length) return;
|
|
57
|
+
const payload = typeof value === 'string' ? null : value[variantName];
|
|
58
|
+
if (payload == null) throw new Error(`${name}.${variantName} needs its data`);
|
|
59
|
+
if (isNamed(fields)) for (const f of fields) encodeInto(idl, f.type, payload[f.name], out);
|
|
60
|
+
else fields.forEach((f, i) => encodeInto(idl, f, payload[i], out));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
for (const field of def.fields) {
|
|
64
|
+
if (!(field.name in value)) throw new Error(`${name}.${field.name} is required`);
|
|
65
|
+
encodeInto(idl, field.type, value[field.name], out);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* A `[u8; N]`: a Uint8Array, or an array of integers from 0 to 255, of exactly `length` bytes. Anything else (a string,
|
|
71
|
+
* a number past a byte) is refused rather than written as other bytes.
|
|
72
|
+
*/
|
|
73
|
+
function byteArray(value, length) {
|
|
74
|
+
const bytes = value instanceof Uint8Array || (Array.isArray(value) && value.every((b) => Number.isInteger(b) && b >= 0 && b <= 255));
|
|
75
|
+
if (!bytes) throw new TypeError(`Expected ${length} bytes: a Uint8Array or an array of integers from 0 to 255`);
|
|
76
|
+
if (value.length !== length) throw new Error(`Expected ${length} bytes, got ${value.length}`);
|
|
77
|
+
return Uint8Array.from(value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function encodeInto(idl, type, value, out) {
|
|
81
|
+
if (typeof type === 'string') return encodeScalar(type, value, out);
|
|
82
|
+
if ('array' in type) {
|
|
83
|
+
const [inner, length] = type.array;
|
|
84
|
+
if (inner === 'u8') {
|
|
85
|
+
out.push(byteArray(value, length));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (value.length !== length) throw new Error(`Expected ${length} items, got ${value.length}`);
|
|
89
|
+
for (const item of Array.from(value)) encodeInto(idl, inner, item, out);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if ('vec' in type) {
|
|
93
|
+
out.push(uintLE(value.length, 4));
|
|
94
|
+
for (const item of value) encodeInto(idl, type.vec, item, out);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if ('option' in type) {
|
|
98
|
+
if (value === null || value === undefined) out.push(Uint8Array.of(0));
|
|
99
|
+
else {
|
|
100
|
+
out.push(Uint8Array.of(1));
|
|
101
|
+
encodeInto(idl, type.option, value, out);
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
encodeDefined(idl, type.defined.name, value, out);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The Borsh bytes of one value of `type`. */
|
|
109
|
+
export function encodeBorsh(idl, type, value) {
|
|
110
|
+
const out = [];
|
|
111
|
+
encodeInto(idl, type, value, out);
|
|
112
|
+
return concat(...out);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** An instruction's data: its discriminator, then each argument in IDL order. */
|
|
116
|
+
export function encodeInstruction(idl, name, args) {
|
|
117
|
+
const ix = idl.instructions.find((i) => i.name === name);
|
|
118
|
+
if (!ix) throw new Error(`Unknown instruction ${name}`);
|
|
119
|
+
const out = [Uint8Array.from(ix.discriminator)];
|
|
120
|
+
for (const arg of ix.args) {
|
|
121
|
+
if (!(arg.name in args)) throw new Error(`${name}: missing argument ${arg.name}`);
|
|
122
|
+
encodeInto(idl, arg.type, args[arg.name], out);
|
|
123
|
+
}
|
|
124
|
+
return concat(...out);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** An account's bytes as the program writes them: its discriminator, then its fields. */
|
|
128
|
+
export function encodeAccount(idl, name, value) {
|
|
129
|
+
const account = idl.accounts.find((a) => a.name === name);
|
|
130
|
+
if (!account) throw new Error(`Unknown account type ${name}`);
|
|
131
|
+
return concat(Uint8Array.from(account.discriminator), encodeBorsh(idl, { defined: { name } }, value));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function decodeScalar(type, data, at) {
|
|
135
|
+
const size = UINT[type] ?? INT[type];
|
|
136
|
+
if (size && at + size > data.length) throw new RangeError('read past the end of the data');
|
|
137
|
+
if (INT[type]) return [readIntLE(data, at, size), at + size];
|
|
138
|
+
if (size >= 8) return [readUintLE(data, at, size), at + size];
|
|
139
|
+
if (size) return [Number(readUintLE(data, at, size)), at + size];
|
|
140
|
+
if (type === 'bool') {
|
|
141
|
+
if (at >= data.length) throw new RangeError('read past the end of the data');
|
|
142
|
+
return [data[at] !== 0, at + 1];
|
|
143
|
+
}
|
|
144
|
+
if (type === 'pubkey') {
|
|
145
|
+
if (at + ADDRESS_BYTES > data.length) throw new RangeError('read past the end of the data');
|
|
146
|
+
return [encodeBase58(data.subarray(at, at + ADDRESS_BYTES)), at + ADDRESS_BYTES];
|
|
147
|
+
}
|
|
148
|
+
if (type === 'string') {
|
|
149
|
+
const length = Number(readUintLE(data, at, 4));
|
|
150
|
+
if (at + 4 + length > data.length) throw new RangeError('string runs past the end of the data');
|
|
151
|
+
return [fromUtf8(data.subarray(at + 4, at + 4 + length)), at + 4 + length];
|
|
152
|
+
}
|
|
153
|
+
if (type === 'bytes') {
|
|
154
|
+
const length = Number(readUintLE(data, at, 4));
|
|
155
|
+
if (at + 4 + length > data.length) throw new RangeError('bytes run past the end of the data');
|
|
156
|
+
return [data.slice(at + 4, at + 4 + length), at + 4 + length];
|
|
157
|
+
}
|
|
158
|
+
throw new Error(`Unsupported IDL type ${type}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function decodeList(idl, inner, length, data, at) {
|
|
162
|
+
const items = [];
|
|
163
|
+
let offset = at;
|
|
164
|
+
for (let i = 0; i < length; i++) {
|
|
165
|
+
const [item, next] = decodeAt(idl, inner, data, offset);
|
|
166
|
+
items.push(item);
|
|
167
|
+
offset = next;
|
|
168
|
+
}
|
|
169
|
+
return [items, offset];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function decodeDefined(idl, name, data, at) {
|
|
173
|
+
const def = typeDef(idl, name).type;
|
|
174
|
+
if (def.kind === 'enum') {
|
|
175
|
+
const variant = def.variants[data[at]];
|
|
176
|
+
if (!variant) throw new Error(`${name}: unknown variant ${data[at]}`);
|
|
177
|
+
const fields = variant.fields ?? [];
|
|
178
|
+
if (!fields.length) return [variant.name, at + 1];
|
|
179
|
+
let offset = at + 1;
|
|
180
|
+
const named = isNamed(fields);
|
|
181
|
+
const payload = named ? {} : [];
|
|
182
|
+
fields.forEach((f, i) => {
|
|
183
|
+
let value;
|
|
184
|
+
[value, offset] = decodeAt(idl, named ? f.type : f, data, offset);
|
|
185
|
+
if (named) payload[f.name] = value;
|
|
186
|
+
else payload[i] = value;
|
|
187
|
+
});
|
|
188
|
+
return [{ [variant.name]: payload }, offset];
|
|
189
|
+
}
|
|
190
|
+
const record = {};
|
|
191
|
+
let offset = at;
|
|
192
|
+
for (const field of def.fields) [record[field.name], offset] = decodeAt(idl, field.type, data, offset);
|
|
193
|
+
return [record, offset];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function decodeAt(idl, type, data, at) {
|
|
197
|
+
if (typeof type === 'string') return decodeScalar(type, data, at);
|
|
198
|
+
if ('array' in type) {
|
|
199
|
+
const [inner, length] = type.array;
|
|
200
|
+
if (inner === 'u8') {
|
|
201
|
+
if (at + length > data.length) throw new RangeError('read past the end of the data');
|
|
202
|
+
return [data.slice(at, at + length), at + length];
|
|
203
|
+
}
|
|
204
|
+
return decodeList(idl, inner, length, data, at);
|
|
205
|
+
}
|
|
206
|
+
if ('vec' in type) {
|
|
207
|
+
const length = Number(readUintLE(data, at, 4));
|
|
208
|
+
if (length > data.length) throw new RangeError('vec longer than the data');
|
|
209
|
+
return decodeList(idl, type.vec, length, data, at + 4);
|
|
210
|
+
}
|
|
211
|
+
if ('option' in type) {
|
|
212
|
+
if (at >= data.length) throw new RangeError('read past the end of the data');
|
|
213
|
+
return data[at] === 0 ? [null, at + 1] : decodeAt(idl, type.option, data, at + 1);
|
|
214
|
+
}
|
|
215
|
+
return decodeDefined(idl, type.defined.name, data, at);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** One value of `type` at `offset`: [value, next offset]. */
|
|
219
|
+
export const decodeBorsh = (idl, type, data, offset = 0) => decodeAt(idl, type, data, offset);
|
|
220
|
+
|
|
221
|
+
const startsWith = (data, prefix) => data.length >= prefix.length && equalBytes(data.subarray(0, prefix.length), Uint8Array.from(prefix));
|
|
222
|
+
|
|
223
|
+
/** An account of the named type, after checking its discriminator. Bytes after the fields are left alone. */
|
|
224
|
+
export function decodeAccount(idl, name, data) {
|
|
225
|
+
const account = idl.accounts.find((a) => a.name === name);
|
|
226
|
+
if (!account) throw new Error(`Unknown account type ${name}`);
|
|
227
|
+
if (!startsWith(data, account.discriminator)) throw new Error(`Not a ${name} account`);
|
|
228
|
+
return decodeDefined(idl, name, data, account.discriminator.length)[0];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Whether the data starts with the named account's discriminator. */
|
|
232
|
+
export function isAccount(idl, name, data) {
|
|
233
|
+
const account = idl.accounts.find((a) => a.name === name);
|
|
234
|
+
return Boolean(account) && startsWith(data, account.discriminator);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The program's Anchor events in a transaction's logs (`Program data: <base64 of discriminator + Borsh>`):
|
|
239
|
+
* [{ name, data }]. Only lines the program itself printed count: the log names which program each line comes from
|
|
240
|
+
* (`Program <id> invoke [n]` … `success`), and any program can print a line that looks like an event. Logs alone
|
|
241
|
+
* cannot say whether the transaction succeeded (the runtime can fail it after every instruction logged success), so
|
|
242
|
+
* the caller decides that from the transaction's `meta.err` and reads events only when it is null.
|
|
243
|
+
*/
|
|
244
|
+
export function decodeEvents(idl, logs) {
|
|
245
|
+
const out = [];
|
|
246
|
+
const stack = [];
|
|
247
|
+
for (const line of logs ?? []) {
|
|
248
|
+
const invoke = /^Program (\w+) invoke \[\d+\]$/.exec(line);
|
|
249
|
+
if (invoke) {
|
|
250
|
+
stack.push(invoke[1]);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (/^Program \w+ failed/.test(line)) return [];
|
|
254
|
+
if (/^Program \w+ success$/.test(line)) {
|
|
255
|
+
stack.pop();
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
const m = /^Program data: (.+)$/.exec(line);
|
|
259
|
+
if (!m || stack[stack.length - 1] !== idl.address) continue;
|
|
260
|
+
let bytes;
|
|
261
|
+
try {
|
|
262
|
+
bytes = fromBase64(m[1]);
|
|
263
|
+
} catch {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const event = (idl.events ?? []).find((e) => startsWith(bytes, e.discriminator));
|
|
267
|
+
if (event) out.push({ name: event.name, data: decodeDefined(idl, event.name, bytes, event.discriminator.length)[0] });
|
|
268
|
+
}
|
|
269
|
+
return out;
|
|
270
|
+
}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// Byte helpers shared by browser apps and Node. Plain Uint8Array, never Node's Buffer, so they run in a browser page
|
|
2
|
+
// too.
|
|
3
|
+
|
|
4
|
+
const encoder = new TextEncoder();
|
|
5
|
+
const decoder = new TextDecoder();
|
|
6
|
+
/** btoa/atob take strings of at most this many code units comfortably. */
|
|
7
|
+
const BASE64_CHUNK = 0x8000;
|
|
8
|
+
|
|
9
|
+
/** UTF-8 bytes of a string. */
|
|
10
|
+
export const utf8 = (text) => encoder.encode(text);
|
|
11
|
+
|
|
12
|
+
/** A string from UTF-8 bytes. */
|
|
13
|
+
export const fromUtf8 = (bytes) => decoder.decode(bytes);
|
|
14
|
+
|
|
15
|
+
/** The parts, one after another. */
|
|
16
|
+
export function concat(...parts) {
|
|
17
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
18
|
+
let at = 0;
|
|
19
|
+
for (const part of parts) {
|
|
20
|
+
out.set(part, at);
|
|
21
|
+
at += part.length;
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Lexicographic order of two byte strings (like Buffer.compare): -1, 0 or 1. */
|
|
27
|
+
export function compareBytes(a, b) {
|
|
28
|
+
const n = Math.min(a.length, b.length);
|
|
29
|
+
for (let i = 0; i < n; i++) if (a[i] !== b[i]) return a[i] < b[i] ? -1 : 1;
|
|
30
|
+
return a.length === b.length ? 0 : a.length < b.length ? -1 : 1;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Whether two byte strings are equal. */
|
|
34
|
+
export function equalBytes(a, b) {
|
|
35
|
+
if (!a || !b || a.length !== b.length) return false;
|
|
36
|
+
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function toBase64(bytes) {
|
|
41
|
+
let text = '';
|
|
42
|
+
for (let i = 0; i < bytes.length; i += BASE64_CHUNK) text += String.fromCharCode(...bytes.subarray(i, i + BASE64_CHUNK));
|
|
43
|
+
return btoa(text);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Standard base64 to bytes; throws on anything that is not base64. */
|
|
47
|
+
export function fromBase64(text) {
|
|
48
|
+
if (typeof text !== 'string' || !/^[A-Za-z0-9+/]*={0,2}$/.test(text)) throw new Error('Not base64');
|
|
49
|
+
return Uint8Array.from(atob(text), (c) => c.charCodeAt(0));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const toHex = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
53
|
+
|
|
54
|
+
export function fromHex(text) {
|
|
55
|
+
if (typeof text !== 'string' || text.length % 2 || /[^0-9a-f]/i.test(text)) throw new Error('Not hex');
|
|
56
|
+
const out = new Uint8Array(text.length / 2);
|
|
57
|
+
for (let i = 0; i < out.length; i++) out[i] = parseInt(text.slice(i * 2, i * 2 + 2), 16);
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Little-endian unsigned integer of `size` bytes, as Borsh and the Solana wire format write them. `value` is a bigint,
|
|
63
|
+
* or a number that is a safe integer: a larger number has already lost its low digits, so it is refused rather than
|
|
64
|
+
* written as some other value.
|
|
65
|
+
*/
|
|
66
|
+
export function uintLE(value, size) {
|
|
67
|
+
const out = new Uint8Array(size);
|
|
68
|
+
let rest = toBigInt(value);
|
|
69
|
+
if (rest < 0n || rest >= 1n << BigInt(size * 8)) throw new RangeError(`${value} does not fit in ${size} bytes`);
|
|
70
|
+
for (let i = 0; i < size; i++) {
|
|
71
|
+
out[i] = Number(rest & 0xffn);
|
|
72
|
+
rest >>= 8n;
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Little-endian signed (two's complement) integer of `size` bytes, as Borsh writes an i32 or an i64. `value` as
|
|
79
|
+
* `uintLE` takes it; one outside the signed range is refused rather than wrapped.
|
|
80
|
+
*/
|
|
81
|
+
export function intLE(value, size) {
|
|
82
|
+
const v = toBigInt(value);
|
|
83
|
+
const bits = BigInt(size * 8);
|
|
84
|
+
if (v < -(1n << (bits - 1n)) || v >= 1n << (bits - 1n)) throw new RangeError(`${value} does not fit in a signed ${size}-byte integer`);
|
|
85
|
+
return uintLE(v < 0n ? v + (1n << bits) : v, size);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* `value` as a bigint: a bigint, a number that is a safe integer, or a string of digits. A number past 2^53 has already
|
|
90
|
+
* lost its low digits, so it is refused rather than turned into some other value.
|
|
91
|
+
*/
|
|
92
|
+
export function toBigInt(value) {
|
|
93
|
+
if (typeof value === 'number' && !Number.isSafeInteger(value)) throw new RangeError(`${value} is not a safe integer: pass it as a bigint`);
|
|
94
|
+
return BigInt(value);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** A little-endian unsigned integer of `size` bytes at `offset`, as a bigint. */
|
|
98
|
+
export function readUintLE(bytes, offset, size) {
|
|
99
|
+
if (offset < 0 || offset + size > bytes.length) throw new RangeError('read past the end of the data');
|
|
100
|
+
let value = 0n;
|
|
101
|
+
for (let i = size - 1; i >= 0; i--) value = (value << 8n) | BigInt(bytes[offset + i]);
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Solana's compact-u16 length prefix. */
|
|
106
|
+
export function compactU16(value) {
|
|
107
|
+
if (!Number.isInteger(value) || value < 0 || value > 0xffff) throw new RangeError('compact-u16 out of range');
|
|
108
|
+
const out = [];
|
|
109
|
+
let rest = value;
|
|
110
|
+
for (;;) {
|
|
111
|
+
const low = rest & 0x7f;
|
|
112
|
+
rest >>= 7;
|
|
113
|
+
if (rest === 0) {
|
|
114
|
+
out.push(low);
|
|
115
|
+
return Uint8Array.from(out);
|
|
116
|
+
}
|
|
117
|
+
out.push(low | 0x80);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* A compact-u16 at `offset`: its value and how many bytes it took. Like Solana, it refuses a value past 0xffff and a
|
|
123
|
+
* longer form of a value that has a shorter one.
|
|
124
|
+
*/
|
|
125
|
+
export function readCompactU16(bytes, offset) {
|
|
126
|
+
let value = 0;
|
|
127
|
+
for (let size = 0; size < 3; size++) {
|
|
128
|
+
const byte = bytes[offset + size];
|
|
129
|
+
if (byte === undefined) throw new RangeError('truncated length prefix');
|
|
130
|
+
if (size > 0 && byte === 0) throw new RangeError('length prefix is longer than it needs to be');
|
|
131
|
+
value |= (byte & 0x7f) << (7 * size);
|
|
132
|
+
if ((byte & 0x80) === 0) {
|
|
133
|
+
if (value > 0xffff) throw new RangeError('length prefix past 0xffff');
|
|
134
|
+
return { value, size: size + 1 };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
throw new RangeError('length prefix too long');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Random bytes from the platform's CSPRNG (WebCrypto in browsers and Node 24). */
|
|
141
|
+
export const randomBytes = (n) => globalThis.crypto.getRandomValues(new Uint8Array(n));
|
package/js/chain/core.js
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// Metaplex Core: a project's collection (created by its owner's wallet, with the loot project as update delegate) and
|
|
2
|
+
// reading a Core asset (owner, collection, name, uri, attributes, freeze state). The layouts are Core's own, checked
|
|
3
|
+
// against the real Core program in the tests.
|
|
4
|
+
import { addressBytes, encodeBase58 } from './base58.js';
|
|
5
|
+
import { concat, fromUtf8, readUintLE, uintLE, utf8 } from './bytes.js';
|
|
6
|
+
import { MPL_CORE_PROGRAM, SYSTEM_PROGRAM } from './pda.js';
|
|
7
|
+
import { meta } from './programs.js';
|
|
8
|
+
|
|
9
|
+
export { MPL_CORE_PROGRAM };
|
|
10
|
+
/** MPL Core `CreateCollectionV2`. */
|
|
11
|
+
const CREATE_COLLECTION_V2 = 21;
|
|
12
|
+
const PLUGIN = { Royalties: 0, FreezeDelegate: 1, UpdateDelegate: 4, PermanentFreezeDelegate: 5, Attributes: 6, BubblegumV2: 15 };
|
|
13
|
+
const ASSET_V1 = 1;
|
|
14
|
+
const COLLECTION_V1 = 5;
|
|
15
|
+
const PLUGIN_HEADER = 3;
|
|
16
|
+
const UPDATE_AUTHORITY_COLLECTION = 2;
|
|
17
|
+
|
|
18
|
+
/** A Borsh string as Core writes it. */
|
|
19
|
+
const coreString = (text) => {
|
|
20
|
+
const bytes = utf8(text);
|
|
21
|
+
return concat(uintLE(bytes.length, 4), bytes);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A project's Core collection. The creator is its update authority and royalty receiver; the loot project's address
|
|
26
|
+
* (`project`, for example `pda.project(creator, 0)` from the client) is an update delegate so the program can mint into it. `compressed`:
|
|
27
|
+
* it also carries Bubblegum's plugin, so compressed NFTs can be minted into it. The collection address signs too (a
|
|
28
|
+
* fresh key).
|
|
29
|
+
*/
|
|
30
|
+
export function createCollectionInstruction({ collection, creator, name, uri, royaltyBps = 500, project, compressed = false }) {
|
|
31
|
+
if (!project) throw new Error('createCollectionInstruction needs the loot project that mints into the collection (`project`).');
|
|
32
|
+
const data = concat(
|
|
33
|
+
Uint8Array.of(CREATE_COLLECTION_V2),
|
|
34
|
+
coreString(name),
|
|
35
|
+
coreString(uri),
|
|
36
|
+
Uint8Array.of(1), // plugins: Some
|
|
37
|
+
uintLE(compressed ? 3 : 2, 4),
|
|
38
|
+
// Royalties { basis_points, creators: [creator 100%], rule_set: None }, authority: default.
|
|
39
|
+
Uint8Array.of(PLUGIN.Royalties),
|
|
40
|
+
uintLE(royaltyBps, 2),
|
|
41
|
+
uintLE(1, 4),
|
|
42
|
+
addressBytes(creator),
|
|
43
|
+
Uint8Array.of(100, 0, 0),
|
|
44
|
+
// UpdateDelegate { additional_delegates: [the project] }, authority: default.
|
|
45
|
+
Uint8Array.of(PLUGIN.UpdateDelegate),
|
|
46
|
+
uintLE(1, 4),
|
|
47
|
+
addressBytes(project),
|
|
48
|
+
Uint8Array.of(0),
|
|
49
|
+
// BubblegumV2 {}, authority: default.
|
|
50
|
+
compressed ? Uint8Array.of(PLUGIN.BubblegumV2, 0) : new Uint8Array(0),
|
|
51
|
+
Uint8Array.of(0), // external plugin adapters: None
|
|
52
|
+
);
|
|
53
|
+
return {
|
|
54
|
+
programId: MPL_CORE_PROGRAM,
|
|
55
|
+
keys: [
|
|
56
|
+
meta(collection, true, true),
|
|
57
|
+
meta(creator, false, false),
|
|
58
|
+
meta(creator, true, true),
|
|
59
|
+
meta(SYSTEM_PROGRAM, false, false),
|
|
60
|
+
],
|
|
61
|
+
data,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function reader(data, start) {
|
|
66
|
+
let at = start;
|
|
67
|
+
const need = (n) => {
|
|
68
|
+
if (at + n > data.length) throw new RangeError('read past the end of the account');
|
|
69
|
+
};
|
|
70
|
+
return {
|
|
71
|
+
get at() {
|
|
72
|
+
return at;
|
|
73
|
+
},
|
|
74
|
+
skip(n) {
|
|
75
|
+
at += n;
|
|
76
|
+
},
|
|
77
|
+
byte() {
|
|
78
|
+
need(1);
|
|
79
|
+
return data[at++];
|
|
80
|
+
},
|
|
81
|
+
u32() {
|
|
82
|
+
need(4);
|
|
83
|
+
const v = Number(readUintLE(data, at, 4));
|
|
84
|
+
at += 4;
|
|
85
|
+
return v;
|
|
86
|
+
},
|
|
87
|
+
u64() {
|
|
88
|
+
need(8);
|
|
89
|
+
const v = readUintLE(data, at, 8);
|
|
90
|
+
at += 8;
|
|
91
|
+
return v;
|
|
92
|
+
},
|
|
93
|
+
address() {
|
|
94
|
+
need(32);
|
|
95
|
+
const v = encodeBase58(data.subarray(at, at + 32));
|
|
96
|
+
at += 32;
|
|
97
|
+
return v;
|
|
98
|
+
},
|
|
99
|
+
text() {
|
|
100
|
+
const n = this.u32();
|
|
101
|
+
need(n);
|
|
102
|
+
const v = fromUtf8(data.subarray(at, at + n));
|
|
103
|
+
at += n;
|
|
104
|
+
return v;
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function readAttributes(data, offset) {
|
|
110
|
+
const r = reader(data, offset + 1);
|
|
111
|
+
const out = {};
|
|
112
|
+
const count = r.u32();
|
|
113
|
+
for (let i = 0; i < count; i++) {
|
|
114
|
+
const key = r.text();
|
|
115
|
+
out[key] = r.text();
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Decode a Core AssetV1: { owner, collection, name, uri, attributes, frozen, soulbound }. */
|
|
121
|
+
export function parseCoreAsset(data) {
|
|
122
|
+
if (data[0] !== ASSET_V1) throw new Error('Not a Core asset');
|
|
123
|
+
const r = reader(data, 1);
|
|
124
|
+
const owner = r.address();
|
|
125
|
+
const authorityKind = r.byte();
|
|
126
|
+
const authority = authorityKind === 0 ? null : r.address();
|
|
127
|
+
const asset = {
|
|
128
|
+
owner,
|
|
129
|
+
collection: authorityKind === UPDATE_AUTHORITY_COLLECTION ? authority : null,
|
|
130
|
+
name: r.text(),
|
|
131
|
+
uri: r.text(),
|
|
132
|
+
attributes: {},
|
|
133
|
+
frozen: false,
|
|
134
|
+
soulbound: false,
|
|
135
|
+
};
|
|
136
|
+
r.skip(data[r.at] === 1 ? 9 : 1); // seq: Option<u64>
|
|
137
|
+
if (r.at >= data.length || data[r.at] !== PLUGIN_HEADER) return asset;
|
|
138
|
+
r.skip(1);
|
|
139
|
+
const registry = reader(data, Number(r.u64()) + 1);
|
|
140
|
+
const plugins = registry.u32();
|
|
141
|
+
for (let i = 0; i < plugins; i++) {
|
|
142
|
+
const type = registry.byte();
|
|
143
|
+
if (registry.byte() === 3) registry.skip(32);
|
|
144
|
+
const offset = Number(registry.u64());
|
|
145
|
+
if (type === PLUGIN.Attributes) asset.attributes = readAttributes(data, offset);
|
|
146
|
+
if (type === PLUGIN.PermanentFreezeDelegate) asset.soulbound = data[offset + 1] === 1;
|
|
147
|
+
if (type === PLUGIN.FreezeDelegate) asset.frozen ||= data[offset + 1] === 1;
|
|
148
|
+
}
|
|
149
|
+
asset.frozen ||= asset.soulbound;
|
|
150
|
+
return asset;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Decode the start of a Core CollectionV1: { updateAuthority, name, uri, numMinted, currentSize, bubblegum (whether it
|
|
154
|
+
* carries Bubblegum's plugin, BubblegumV2: compressed NFTs can be minted into it) }. */
|
|
155
|
+
export function parseCoreCollection(data) {
|
|
156
|
+
if (data[0] !== COLLECTION_V1) throw new Error('Not a Core collection');
|
|
157
|
+
const r = reader(data, 1);
|
|
158
|
+
const updateAuthority = r.address();
|
|
159
|
+
const name = r.text();
|
|
160
|
+
const uri = r.text();
|
|
161
|
+
const out = { updateAuthority, name, uri, numMinted: r.u32(), currentSize: r.u32(), bubblegum: false };
|
|
162
|
+
if (r.at >= data.length || data[r.at] !== PLUGIN_HEADER) return out;
|
|
163
|
+
r.skip(1);
|
|
164
|
+
const registry = reader(data, Number(r.u64()) + 1);
|
|
165
|
+
const plugins = registry.u32();
|
|
166
|
+
for (let i = 0; i < plugins; i++) {
|
|
167
|
+
const type = registry.byte();
|
|
168
|
+
if (registry.byte() === 3) registry.skip(32);
|
|
169
|
+
registry.u64();
|
|
170
|
+
if (type === PLUGIN.BubblegumV2) out.bubblegum = true;
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** getProgramAccounts filters for Core assets owned by `owner` in `collection` (no indexer needed). */
|
|
176
|
+
export const ownedAssetFilters = (owner, collection) => [
|
|
177
|
+
{ memcmp: { offset: 0, bytes: encodeBase58(Uint8Array.of(ASSET_V1)) } },
|
|
178
|
+
{ memcmp: { offset: 1, bytes: owner } },
|
|
179
|
+
{ memcmp: { offset: 33, bytes: encodeBase58(Uint8Array.of(UPDATE_AUTHORITY_COLLECTION)) } },
|
|
180
|
+
{ memcmp: { offset: 34, bytes: collection } },
|
|
181
|
+
];
|