@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/chain/base58.js
CHANGED
|
@@ -30,7 +30,13 @@ export function encodeBase58(bytes) {
|
|
|
30
30
|
if (byte !== 0) break;
|
|
31
31
|
zeros += '1';
|
|
32
32
|
}
|
|
33
|
-
return
|
|
33
|
+
return (
|
|
34
|
+
zeros +
|
|
35
|
+
digits
|
|
36
|
+
.reverse()
|
|
37
|
+
.map(d => ALPHABET[d])
|
|
38
|
+
.join('')
|
|
39
|
+
);
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
export function decodeBase58(text) {
|
package/js/chain/borsh.js
CHANGED
|
@@ -14,7 +14,7 @@ const INT = { i32: 4, i64: 8 };
|
|
|
14
14
|
const ADDRESS_BYTES = 32;
|
|
15
15
|
|
|
16
16
|
function typeDef(idl, name) {
|
|
17
|
-
const def = idl.types.find(
|
|
17
|
+
const def = idl.types.find(t => t.name === name);
|
|
18
18
|
if (!def) throw new Error(`Unknown IDL type ${name}`);
|
|
19
19
|
return def;
|
|
20
20
|
}
|
|
@@ -35,17 +35,21 @@ function encodeScalar(type, value, out) {
|
|
|
35
35
|
else if (type === 'string') {
|
|
36
36
|
const bytes = utf8(String(value));
|
|
37
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);
|
|
38
42
|
} else throw new Error(`Unsupported IDL type ${type}`);
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
/** A variant's field list: named ({ name, type }) or tuple (bare types). */
|
|
42
|
-
const isNamed =
|
|
46
|
+
const isNamed = fields => typeof fields[0] === 'object' && fields[0] !== null && 'name' in fields[0];
|
|
43
47
|
|
|
44
48
|
function encodeDefined(idl, name, value, out) {
|
|
45
49
|
const def = typeDef(idl, name).type;
|
|
46
50
|
if (def.kind === 'enum') {
|
|
47
51
|
const variantName = typeof value === 'string' ? value : Object.keys(value ?? {})[0];
|
|
48
|
-
const index = def.variants.findIndex(
|
|
52
|
+
const index = def.variants.findIndex(v => v.name === variantName);
|
|
49
53
|
if (index < 0) throw new Error(`${name} has no variant ${String(variantName)}`);
|
|
50
54
|
out.push(Uint8Array.of(index));
|
|
51
55
|
const fields = def.variants[index].fields ?? [];
|
|
@@ -67,7 +71,9 @@ function encodeDefined(idl, name, value, out) {
|
|
|
67
71
|
* a number past a byte) is refused rather than written as other bytes.
|
|
68
72
|
*/
|
|
69
73
|
function byteArray(value, length) {
|
|
70
|
-
const bytes =
|
|
74
|
+
const bytes =
|
|
75
|
+
value instanceof Uint8Array ||
|
|
76
|
+
(Array.isArray(value) && value.every(b => Number.isInteger(b) && b >= 0 && b <= 255));
|
|
71
77
|
if (!bytes) throw new TypeError(`Expected ${length} bytes: a Uint8Array or an array of integers from 0 to 255`);
|
|
72
78
|
if (value.length !== length) throw new Error(`Expected ${length} bytes, got ${value.length}`);
|
|
73
79
|
return Uint8Array.from(value);
|
|
@@ -110,7 +116,7 @@ export function encodeBorsh(idl, type, value) {
|
|
|
110
116
|
|
|
111
117
|
/** An instruction's data: its discriminator, then each argument in IDL order. */
|
|
112
118
|
export function encodeInstruction(idl, name, args) {
|
|
113
|
-
const ix = idl.instructions.find(
|
|
119
|
+
const ix = idl.instructions.find(i => i.name === name);
|
|
114
120
|
if (!ix) throw new Error(`Unknown instruction ${name}`);
|
|
115
121
|
const out = [Uint8Array.from(ix.discriminator)];
|
|
116
122
|
for (const arg of ix.args) {
|
|
@@ -122,7 +128,7 @@ export function encodeInstruction(idl, name, args) {
|
|
|
122
128
|
|
|
123
129
|
/** An account's bytes as the program writes them: its discriminator, then its fields. */
|
|
124
130
|
export function encodeAccount(idl, name, value) {
|
|
125
|
-
const account = idl.accounts.find(
|
|
131
|
+
const account = idl.accounts.find(a => a.name === name);
|
|
126
132
|
if (!account) throw new Error(`Unknown account type ${name}`);
|
|
127
133
|
return concat(Uint8Array.from(account.discriminator), encodeBorsh(idl, { defined: { name } }, value));
|
|
128
134
|
}
|
|
@@ -146,6 +152,11 @@ function decodeScalar(type, data, at) {
|
|
|
146
152
|
if (at + 4 + length > data.length) throw new RangeError('string runs past the end of the data');
|
|
147
153
|
return [fromUtf8(data.subarray(at + 4, at + 4 + length)), at + 4 + length];
|
|
148
154
|
}
|
|
155
|
+
if (type === 'bytes') {
|
|
156
|
+
const length = Number(readUintLE(data, at, 4));
|
|
157
|
+
if (at + 4 + length > data.length) throw new RangeError('bytes run past the end of the data');
|
|
158
|
+
return [data.slice(at + 4, at + 4 + length), at + 4 + length];
|
|
159
|
+
}
|
|
149
160
|
throw new Error(`Unsupported IDL type ${type}`);
|
|
150
161
|
}
|
|
151
162
|
|
|
@@ -209,11 +220,12 @@ function decodeAt(idl, type, data, at) {
|
|
|
209
220
|
/** One value of `type` at `offset`: [value, next offset]. */
|
|
210
221
|
export const decodeBorsh = (idl, type, data, offset = 0) => decodeAt(idl, type, data, offset);
|
|
211
222
|
|
|
212
|
-
const startsWith = (data, prefix) =>
|
|
223
|
+
const startsWith = (data, prefix) =>
|
|
224
|
+
data.length >= prefix.length && equalBytes(data.subarray(0, prefix.length), Uint8Array.from(prefix));
|
|
213
225
|
|
|
214
226
|
/** An account of the named type, after checking its discriminator. Bytes after the fields are left alone. */
|
|
215
227
|
export function decodeAccount(idl, name, data) {
|
|
216
|
-
const account = idl.accounts.find(
|
|
228
|
+
const account = idl.accounts.find(a => a.name === name);
|
|
217
229
|
if (!account) throw new Error(`Unknown account type ${name}`);
|
|
218
230
|
if (!startsWith(data, account.discriminator)) throw new Error(`Not a ${name} account`);
|
|
219
231
|
return decodeDefined(idl, name, data, account.discriminator.length)[0];
|
|
@@ -221,7 +233,7 @@ export function decodeAccount(idl, name, data) {
|
|
|
221
233
|
|
|
222
234
|
/** Whether the data starts with the named account's discriminator. */
|
|
223
235
|
export function isAccount(idl, name, data) {
|
|
224
|
-
const account = idl.accounts.find(
|
|
236
|
+
const account = idl.accounts.find(a => a.name === name);
|
|
225
237
|
return Boolean(account) && startsWith(data, account.discriminator);
|
|
226
238
|
}
|
|
227
239
|
|
|
@@ -254,8 +266,9 @@ export function decodeEvents(idl, logs) {
|
|
|
254
266
|
} catch {
|
|
255
267
|
continue;
|
|
256
268
|
}
|
|
257
|
-
const event = (idl.events ?? []).find(
|
|
258
|
-
if (event)
|
|
269
|
+
const event = (idl.events ?? []).find(e => startsWith(bytes, e.discriminator));
|
|
270
|
+
if (event)
|
|
271
|
+
out.push({ name: event.name, data: decodeDefined(idl, event.name, bytes, event.discriminator.length)[0] });
|
|
259
272
|
}
|
|
260
273
|
return out;
|
|
261
274
|
}
|
package/js/chain/bytes.js
CHANGED
|
@@ -7,10 +7,10 @@ const decoder = new TextDecoder();
|
|
|
7
7
|
const BASE64_CHUNK = 0x8000;
|
|
8
8
|
|
|
9
9
|
/** UTF-8 bytes of a string. */
|
|
10
|
-
export const utf8 =
|
|
10
|
+
export const utf8 = text => encoder.encode(text);
|
|
11
11
|
|
|
12
12
|
/** A string from UTF-8 bytes. */
|
|
13
|
-
export const fromUtf8 =
|
|
13
|
+
export const fromUtf8 = bytes => decoder.decode(bytes);
|
|
14
14
|
|
|
15
15
|
/** The parts, one after another. */
|
|
16
16
|
export function concat(...parts) {
|
|
@@ -39,17 +39,18 @@ export function equalBytes(a, b) {
|
|
|
39
39
|
|
|
40
40
|
export function toBase64(bytes) {
|
|
41
41
|
let text = '';
|
|
42
|
-
for (let i = 0; i < bytes.length; i += BASE64_CHUNK)
|
|
42
|
+
for (let i = 0; i < bytes.length; i += BASE64_CHUNK)
|
|
43
|
+
text += String.fromCharCode(...bytes.subarray(i, i + BASE64_CHUNK));
|
|
43
44
|
return btoa(text);
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
/** Standard base64 to bytes; throws on anything that is not base64. */
|
|
47
48
|
export function fromBase64(text) {
|
|
48
49
|
if (typeof text !== 'string' || !/^[A-Za-z0-9+/]*={0,2}$/.test(text)) throw new Error('Not base64');
|
|
49
|
-
return Uint8Array.from(atob(text),
|
|
50
|
+
return Uint8Array.from(atob(text), c => c.charCodeAt(0));
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
export const toHex =
|
|
53
|
+
export const toHex = bytes => Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
53
54
|
|
|
54
55
|
export function fromHex(text) {
|
|
55
56
|
if (typeof text !== 'string' || text.length % 2 || /[^0-9a-f]/i.test(text)) throw new Error('Not hex');
|
|
@@ -81,7 +82,8 @@ export function uintLE(value, size) {
|
|
|
81
82
|
export function intLE(value, size) {
|
|
82
83
|
const v = toBigInt(value);
|
|
83
84
|
const bits = BigInt(size * 8);
|
|
84
|
-
if (v < -(1n << (bits - 1n)) || v >= 1n << (bits - 1n))
|
|
85
|
+
if (v < -(1n << (bits - 1n)) || v >= 1n << (bits - 1n))
|
|
86
|
+
throw new RangeError(`${value} does not fit in a signed ${size}-byte integer`);
|
|
85
87
|
return uintLE(v < 0n ? v + (1n << bits) : v, size);
|
|
86
88
|
}
|
|
87
89
|
|
|
@@ -90,7 +92,8 @@ export function intLE(value, size) {
|
|
|
90
92
|
* lost its low digits, so it is refused rather than turned into some other value.
|
|
91
93
|
*/
|
|
92
94
|
export function toBigInt(value) {
|
|
93
|
-
if (typeof value === 'number' && !Number.isSafeInteger(value))
|
|
95
|
+
if (typeof value === 'number' && !Number.isSafeInteger(value))
|
|
96
|
+
throw new RangeError(`${value} is not a safe integer: pass it as a bigint`);
|
|
94
97
|
return BigInt(value);
|
|
95
98
|
}
|
|
96
99
|
|
|
@@ -138,4 +141,4 @@ export function readCompactU16(bytes, offset) {
|
|
|
138
141
|
}
|
|
139
142
|
|
|
140
143
|
/** Random bytes from the platform's CSPRNG (WebCrypto in browsers and Node 24). */
|
|
141
|
-
export const randomBytes =
|
|
144
|
+
export const randomBytes = n => globalThis.crypto.getRandomValues(new Uint8Array(n));
|
package/js/chain/core.js
CHANGED
|
@@ -9,14 +9,21 @@ import { meta } from './programs.js';
|
|
|
9
9
|
export { MPL_CORE_PROGRAM };
|
|
10
10
|
/** MPL Core `CreateCollectionV2`. */
|
|
11
11
|
const CREATE_COLLECTION_V2 = 21;
|
|
12
|
-
const PLUGIN = {
|
|
12
|
+
const PLUGIN = {
|
|
13
|
+
Royalties: 0,
|
|
14
|
+
FreezeDelegate: 1,
|
|
15
|
+
UpdateDelegate: 4,
|
|
16
|
+
PermanentFreezeDelegate: 5,
|
|
17
|
+
Attributes: 6,
|
|
18
|
+
BubblegumV2: 15,
|
|
19
|
+
};
|
|
13
20
|
const ASSET_V1 = 1;
|
|
14
21
|
const COLLECTION_V1 = 5;
|
|
15
22
|
const PLUGIN_HEADER = 3;
|
|
16
23
|
const UPDATE_AUTHORITY_COLLECTION = 2;
|
|
17
24
|
|
|
18
25
|
/** A Borsh string as Core writes it. */
|
|
19
|
-
const coreString =
|
|
26
|
+
const coreString = text => {
|
|
20
27
|
const bytes = utf8(text);
|
|
21
28
|
return concat(uintLE(bytes.length, 4), bytes);
|
|
22
29
|
};
|
|
@@ -27,8 +34,17 @@ const coreString = (text) => {
|
|
|
27
34
|
* it also carries Bubblegum's plugin, so compressed NFTs can be minted into it. The collection address signs too (a
|
|
28
35
|
* fresh key).
|
|
29
36
|
*/
|
|
30
|
-
export function createCollectionInstruction({
|
|
31
|
-
|
|
37
|
+
export function createCollectionInstruction({
|
|
38
|
+
collection,
|
|
39
|
+
creator,
|
|
40
|
+
name,
|
|
41
|
+
uri,
|
|
42
|
+
royaltyBps = 500,
|
|
43
|
+
project,
|
|
44
|
+
compressed = false,
|
|
45
|
+
}) {
|
|
46
|
+
if (!project)
|
|
47
|
+
throw new Error('createCollectionInstruction needs the loot project that mints into the collection (`project`).');
|
|
32
48
|
const data = concat(
|
|
33
49
|
Uint8Array.of(CREATE_COLLECTION_V2),
|
|
34
50
|
coreString(name),
|
|
@@ -64,7 +80,7 @@ export function createCollectionInstruction({ collection, creator, name, uri, ro
|
|
|
64
80
|
|
|
65
81
|
function reader(data, start) {
|
|
66
82
|
let at = start;
|
|
67
|
-
const need =
|
|
83
|
+
const need = n => {
|
|
68
84
|
if (at + n > data.length) throw new RangeError('read past the end of the account');
|
|
69
85
|
};
|
|
70
86
|
return {
|
|
@@ -150,14 +166,26 @@ export function parseCoreAsset(data) {
|
|
|
150
166
|
return asset;
|
|
151
167
|
}
|
|
152
168
|
|
|
153
|
-
/** Decode the start of a Core CollectionV1: { updateAuthority, name, uri, numMinted, currentSize
|
|
169
|
+
/** Decode the start of a Core CollectionV1: { updateAuthority, name, uri, numMinted, currentSize, bubblegum (whether it
|
|
170
|
+
* carries Bubblegum's plugin, BubblegumV2: compressed NFTs can be minted into it) }. */
|
|
154
171
|
export function parseCoreCollection(data) {
|
|
155
172
|
if (data[0] !== COLLECTION_V1) throw new Error('Not a Core collection');
|
|
156
173
|
const r = reader(data, 1);
|
|
157
174
|
const updateAuthority = r.address();
|
|
158
175
|
const name = r.text();
|
|
159
176
|
const uri = r.text();
|
|
160
|
-
|
|
177
|
+
const out = { updateAuthority, name, uri, numMinted: r.u32(), currentSize: r.u32(), bubblegum: false };
|
|
178
|
+
if (r.at >= data.length || data[r.at] !== PLUGIN_HEADER) return out;
|
|
179
|
+
r.skip(1);
|
|
180
|
+
const registry = reader(data, Number(r.u64()) + 1);
|
|
181
|
+
const plugins = registry.u32();
|
|
182
|
+
for (let i = 0; i < plugins; i++) {
|
|
183
|
+
const type = registry.byte();
|
|
184
|
+
if (registry.byte() === 3) registry.skip(32);
|
|
185
|
+
registry.u64();
|
|
186
|
+
if (type === PLUGIN.BubblegumV2) out.bubblegum = true;
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
161
189
|
}
|
|
162
190
|
|
|
163
191
|
/** getProgramAccounts filters for Core assets owned by `owner` in `collection` (no indexer needed). */
|
package/js/chain/curve.js
CHANGED
package/js/chain/lookup.js
CHANGED
|
@@ -26,38 +26,65 @@ const meta = (pubkey, isSigner, isWritable) => ({ pubkey, isSigner, isWritable }
|
|
|
26
26
|
const ix = (keys, data) => ({ programId: LOOKUP_TABLE_PROGRAM, keys, data });
|
|
27
27
|
|
|
28
28
|
/** The table `authority` makes at `slot` (a recent slot: the table's address names it): [address, bump]. */
|
|
29
|
-
export const tableAddress = (authority, slot) =>
|
|
29
|
+
export const tableAddress = (authority, slot) =>
|
|
30
|
+
findProgramAddress([addressBytes(authority), uintLE(slot, 8)], LOOKUP_TABLE_PROGRAM);
|
|
30
31
|
|
|
31
32
|
/** Makes the table `authority` owns at `slot`, its rent paid by `payer`. Returns { address, instruction }. */
|
|
32
33
|
export function createTableIx({ authority, payer = authority, slot }) {
|
|
33
34
|
const [address, bump] = tableAddress(authority, slot);
|
|
34
|
-
const instruction = ix(
|
|
35
|
+
const instruction = ix(
|
|
36
|
+
[
|
|
37
|
+
meta(address, false, true),
|
|
38
|
+
meta(authority, true, false),
|
|
39
|
+
meta(payer, true, true),
|
|
40
|
+
meta(SYSTEM_PROGRAM, false, false),
|
|
41
|
+
],
|
|
42
|
+
concat(uintLE(0, 4), uintLE(slot, 8), Uint8Array.of(bump)),
|
|
43
|
+
);
|
|
35
44
|
return { address, instruction };
|
|
36
45
|
}
|
|
37
46
|
|
|
38
47
|
/** Adds `addresses` to `table`, the rent of its growth paid by `payer`. */
|
|
39
48
|
export const extendTableIx = ({ table, authority, payer = authority, addresses }) =>
|
|
40
|
-
ix(
|
|
49
|
+
ix(
|
|
50
|
+
[
|
|
51
|
+
meta(table, false, true),
|
|
52
|
+
meta(authority, true, false),
|
|
53
|
+
meta(payer, true, true),
|
|
54
|
+
meta(SYSTEM_PROGRAM, false, false),
|
|
55
|
+
],
|
|
56
|
+
concat(uintLE(2, 4), uintLE(addresses.length, 8), ...addresses.map(addressBytes)),
|
|
57
|
+
);
|
|
41
58
|
|
|
42
59
|
/** Freezes `table`: nobody can add to it or close it after. */
|
|
43
|
-
export const freezeTableIx = ({ table, authority }) =>
|
|
60
|
+
export const freezeTableIx = ({ table, authority }) =>
|
|
61
|
+
ix([meta(table, false, true), meta(authority, true, false)], uintLE(1, 4));
|
|
44
62
|
|
|
45
63
|
/** Deactivates `table`: transactions can still use it until it cools down, then it can be closed. */
|
|
46
|
-
export const deactivateTableIx = ({ table, authority }) =>
|
|
64
|
+
export const deactivateTableIx = ({ table, authority }) =>
|
|
65
|
+
ix([meta(table, false, true), meta(authority, true, false)], uintLE(3, 4));
|
|
47
66
|
|
|
48
67
|
/** Closes a cooled-down `table`: its rent goes to `recipient`. */
|
|
49
|
-
export const closeTableIx = ({ table, authority, recipient = authority }) =>
|
|
68
|
+
export const closeTableIx = ({ table, authority, recipient = authority }) =>
|
|
69
|
+
ix([meta(table, false, true), meta(authority, true, false), meta(recipient, false, true)], uintLE(4, 4));
|
|
50
70
|
|
|
51
71
|
/**
|
|
52
72
|
* A table's account read back: { authority (null once frozen), deactivationSlot (null while active),
|
|
53
73
|
* lastExtendedSlot, addresses }, or null when `account` is not a lookup table.
|
|
54
74
|
*/
|
|
55
75
|
export function readTable(account) {
|
|
56
|
-
if (
|
|
76
|
+
if (
|
|
77
|
+
!account ||
|
|
78
|
+
account.owner !== LOOKUP_TABLE_PROGRAM ||
|
|
79
|
+
account.data.length < TABLE_HEADER_BYTES ||
|
|
80
|
+
readUintLE(account.data, 0, 4) !== 1n
|
|
81
|
+
)
|
|
82
|
+
return null;
|
|
57
83
|
const data = account.data;
|
|
58
84
|
const deactivation = readUintLE(data, 4, 8);
|
|
59
85
|
const addresses = [];
|
|
60
|
-
for (let at = TABLE_HEADER_BYTES; at + 32 <= data.length; at += 32)
|
|
86
|
+
for (let at = TABLE_HEADER_BYTES; at + 32 <= data.length; at += 32)
|
|
87
|
+
addresses.push(encodeBase58(data.subarray(at, at + 32)));
|
|
61
88
|
return {
|
|
62
89
|
authority: data[21] === 1 ? encodeBase58(data.subarray(22, 54)) : null,
|
|
63
90
|
deactivationSlot: deactivation === ACTIVE ? null : deactivation,
|
|
@@ -67,7 +94,7 @@ export function readTable(account) {
|
|
|
67
94
|
}
|
|
68
95
|
|
|
69
96
|
/** The filter that finds the tables `authority` owns (getProgramAccounts on LOOKUP_TABLE_PROGRAM). */
|
|
70
|
-
export const tablesOf =
|
|
97
|
+
export const tablesOf = authority => [{ memcmp: { offset: 22, bytes: authority } }];
|
|
71
98
|
|
|
72
99
|
/**
|
|
73
100
|
* The slots the SlotHashes sysvar holds (its account's data: a count, then a slot and a hash each), newest first. It
|
|
@@ -95,7 +122,8 @@ export const SLOT_HASHES_HELD = 512;
|
|
|
95
122
|
export function tableStep(table, slot, recent = null) {
|
|
96
123
|
if (table.deactivationSlot === null) return 'active';
|
|
97
124
|
if (BigInt(slot) <= table.deactivationSlot) return 'cooling';
|
|
98
|
-
if (recent)
|
|
125
|
+
if (recent)
|
|
126
|
+
return recent.length >= SLOT_HASHES_HELD && recent.every(s => s > table.deactivationSlot) ? 'close' : 'cooling';
|
|
99
127
|
return BigInt(slot) > table.deactivationSlot + TABLE_COOLDOWN_SLOTS ? 'close' : 'cooling';
|
|
100
128
|
}
|
|
101
129
|
|
|
@@ -105,17 +133,21 @@ export function tableStep(table, slot, recent = null) {
|
|
|
105
133
|
* (`asV0`).
|
|
106
134
|
*/
|
|
107
135
|
export function transactionLocks(payer, instructions, { asV0 = false } = {}) {
|
|
108
|
-
return
|
|
136
|
+
return (
|
|
137
|
+
new Set([payer, ...instructions.flatMap(i => [i.programId, ...i.keys.map(k => k.pubkey)])]).size + (asV0 ? 2 : 0)
|
|
138
|
+
);
|
|
109
139
|
}
|
|
110
140
|
|
|
111
141
|
/** The accounts of `instructions` a table can hold: every one that is neither a signer nor a program they call, once. */
|
|
112
142
|
export function tableAccounts(instructions) {
|
|
113
|
-
const signers = new Set(instructions.flatMap(
|
|
114
|
-
const programs = new Set(instructions.map(
|
|
115
|
-
return [...new Set(instructions.flatMap(
|
|
143
|
+
const signers = new Set(instructions.flatMap(i => i.keys.filter(k => k.isSigner).map(k => k.pubkey)));
|
|
144
|
+
const programs = new Set(instructions.map(i => i.programId));
|
|
145
|
+
return [...new Set(instructions.flatMap(i => i.keys.map(k => k.pubkey)))].filter(
|
|
146
|
+
a => !signers.has(a) && !programs.has(a),
|
|
147
|
+
);
|
|
116
148
|
}
|
|
117
149
|
|
|
118
|
-
const sizeOf =
|
|
150
|
+
const sizeOf = input => {
|
|
119
151
|
try {
|
|
120
152
|
return transactionSize(compileMessage(input));
|
|
121
153
|
} catch {
|
|
@@ -151,14 +183,23 @@ export function planTable({ payer, blockhash, slot, instructions, first = [] })
|
|
|
151
183
|
const final = [...instructions, deactivateTableIx({ table: address, authority: payer })];
|
|
152
184
|
// A transaction locks at most 64 accounts, its programs and those it loads from a table included.
|
|
153
185
|
if (transactionLocks(payer, final) > MAX_ACCOUNT_LOCKS) return null;
|
|
154
|
-
if (sizeOf({ payer, blockhash, instructions: final, version: 0, lookupTables: [table] }) > MAX_TRANSACTION_BYTES)
|
|
186
|
+
if (sizeOf({ payer, blockhash, instructions: final, version: 0, lookupTables: [table] }) > MAX_TRANSACTION_BYTES)
|
|
187
|
+
return null;
|
|
155
188
|
const steps = [];
|
|
156
189
|
let step = [create];
|
|
157
190
|
let left = addresses;
|
|
158
191
|
while (left.length) {
|
|
159
192
|
// As many addresses as fit this step's transaction.
|
|
160
193
|
let n = left.length;
|
|
161
|
-
while (
|
|
194
|
+
while (
|
|
195
|
+
n > 0 &&
|
|
196
|
+
sizeOf({
|
|
197
|
+
payer,
|
|
198
|
+
blockhash,
|
|
199
|
+
instructions: [...step, extendTableIx({ table: address, authority: payer, addresses: left.slice(0, n) })],
|
|
200
|
+
}) > MAX_TRANSACTION_BYTES
|
|
201
|
+
)
|
|
202
|
+
n--;
|
|
162
203
|
if (n === 0) {
|
|
163
204
|
// Not one address fits beside the table's creation: the creation goes alone. Not one fits a transaction of its
|
|
164
205
|
// own: no table can be made for this payer.
|
|
@@ -187,17 +228,23 @@ export function planTable({ payer, blockhash, slot, instructions, first = [] })
|
|
|
187
228
|
* null when nothing fits: more accounts than a transaction locks, or instructions too long even as v0.
|
|
188
229
|
*/
|
|
189
230
|
export function sendPlan({ payer, blockhash = SYSTEM_PROGRAM, instructions, alone = () => false }) {
|
|
190
|
-
if (compileFitting({ payer, blockhash, instructions }))
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
231
|
+
if (compileFitting({ payer, blockhash, instructions }))
|
|
232
|
+
return { legacy: true, first: [], budget: [], rest: instructions, table: false };
|
|
233
|
+
if (planTable({ payer, blockhash, slot: 0n, instructions }))
|
|
234
|
+
return { legacy: false, first: [], budget: [], rest: instructions, table: true };
|
|
235
|
+
const budget = instructions.filter(i => i.programId === COMPUTE_BUDGET_PROGRAM);
|
|
236
|
+
const first = instructions.filter(i => alone(i));
|
|
237
|
+
const rest = instructions.filter(i => !alone(i));
|
|
238
|
+
if (!first.length || first.some(i => !compileFitting({ payer, blockhash, instructions: [...budget, i] })))
|
|
239
|
+
return null;
|
|
240
|
+
if (compileFitting({ payer, blockhash, instructions: rest }))
|
|
241
|
+
return { legacy: false, first, budget, rest, table: false };
|
|
242
|
+
return planTable({ payer, blockhash, slot: 0n, instructions: rest })
|
|
243
|
+
? { legacy: false, first, budget, rest, table: true }
|
|
244
|
+
: null;
|
|
198
245
|
}
|
|
199
246
|
|
|
200
|
-
const pause =
|
|
247
|
+
const pause = ms => new Promise(r => setTimeout(r, ms));
|
|
201
248
|
/** The tables this page made, by address: two sends never name the same one, even in one slot. */
|
|
202
249
|
const madeHere = new Set();
|
|
203
250
|
|
|
@@ -222,25 +269,54 @@ async function freshSlot(rpc, payer) {
|
|
|
222
269
|
* first (`planTable`); else the table first, then each compressed NFT's prove_leaf alone, then the rest as v0 (or the
|
|
223
270
|
* proofs then a legacy rest, when that fits with no table). The v0 transaction is compiled against the table as the
|
|
224
271
|
* chain holds it. `sign(message)` → the signed wire transaction. `rpc`: the client's (latestBlockhash, send, confirm,
|
|
225
|
-
* account, call('getSlot')). `
|
|
272
|
+
* account, call('getSlot')). `standing`: tables already made ([{ address, addresses }], a project's, active), tried
|
|
273
|
+
* first: one v0 transaction with them when they hold enough. `table: false` sends no lookup table of its own (a throwaway
|
|
274
|
+
* key holds no rent for one); `first`:
|
|
226
275
|
* addresses a table holds first (`planTable`). Throws a
|
|
227
276
|
* plain sentence when it can't be sent. Returns { signature (the last transaction's), table (its address, or null),
|
|
228
277
|
* steps (the signatures of the transactions before it) }.
|
|
229
278
|
*/
|
|
230
|
-
export async function sendFitting({
|
|
231
|
-
|
|
279
|
+
export async function sendFitting({
|
|
280
|
+
rpc,
|
|
281
|
+
payer,
|
|
282
|
+
instructions,
|
|
283
|
+
sign,
|
|
284
|
+
table = true,
|
|
285
|
+
alone = () => false,
|
|
286
|
+
first = [],
|
|
287
|
+
standing = [],
|
|
288
|
+
sleep = pause,
|
|
289
|
+
}) {
|
|
290
|
+
const sendOne = async build => {
|
|
232
291
|
const { blockhash, lastValidBlockHeight } = await rpc.latestBlockhash();
|
|
233
292
|
const message = build(blockhash);
|
|
234
293
|
const signature = await rpc.send(await sign(message));
|
|
235
294
|
await rpc.confirm?.(signature, { lastValidBlockHeight });
|
|
236
295
|
return signature;
|
|
237
296
|
};
|
|
238
|
-
const plain =
|
|
297
|
+
const plain = list => sendOne(bh => compileMessage({ payer, blockhash: bh, instructions: list }));
|
|
239
298
|
const { blockhash } = await rpc.latestBlockhash();
|
|
240
299
|
const plan = sendPlan({ payer, blockhash, instructions, alone });
|
|
241
|
-
if (!plan)
|
|
300
|
+
if (!plan)
|
|
301
|
+
throw new Error(
|
|
302
|
+
`This transaction is too large to send even as a v0 one with a lookup table: it names ${tableAccounts(instructions).length + 1} accounts or more (a transaction locks at most 64), or its instructions are too long.`,
|
|
303
|
+
);
|
|
242
304
|
if (plan.legacy) return { signature: await plain(instructions), table: null, steps: [] };
|
|
243
|
-
|
|
305
|
+
// Standing tables (a project's, made at go-live; each still active) that hold enough of its accounts: one v0
|
|
306
|
+
// transaction with them, and no table of its own.
|
|
307
|
+
const fits = standing.length && compileFitting({ payer, blockhash, instructions, tables: standing });
|
|
308
|
+
if (fits && fits[0] === 0x80)
|
|
309
|
+
return {
|
|
310
|
+
signature: await sendOne(bh =>
|
|
311
|
+
compileMessage({ payer, blockhash: bh, instructions, version: 0, lookupTables: standing }),
|
|
312
|
+
),
|
|
313
|
+
table: null,
|
|
314
|
+
steps: [],
|
|
315
|
+
};
|
|
316
|
+
if (plan.table && !table)
|
|
317
|
+
throw new Error(
|
|
318
|
+
`This transaction takes ${sizeOf({ payer, blockhash, instructions })} bytes, more than the ${MAX_TRANSACTION_BYTES} a legacy one holds, and this key holds no rent for a lookup table.`,
|
|
319
|
+
);
|
|
244
320
|
const steps = [];
|
|
245
321
|
const proofs = async () => {
|
|
246
322
|
for (const ix of plan.first) steps.push(await plain([...plan.budget, ix]));
|
|
@@ -258,16 +334,28 @@ export async function sendFitting({ rpc, payer, instructions, sign, table = true
|
|
|
258
334
|
let usable = false;
|
|
259
335
|
for (let i = 0; i < 40 && !usable; i++) {
|
|
260
336
|
held = readTable(await rpc.account(made.table.address));
|
|
261
|
-
usable = Boolean(
|
|
337
|
+
usable = Boolean(
|
|
338
|
+
held &&
|
|
339
|
+
made.table.addresses.every(a => held.addresses.includes(a)) &&
|
|
340
|
+
BigInt(await rpc.call('getSlot', [{ commitment: 'confirmed' }])) > held.lastExtendedSlot,
|
|
341
|
+
);
|
|
262
342
|
if (!usable) await sleep(400);
|
|
263
343
|
}
|
|
264
|
-
if (!held || !made.table.addresses.every(
|
|
344
|
+
if (!held || !made.table.addresses.every(a => held.addresses.includes(a)))
|
|
345
|
+
throw new Error(
|
|
346
|
+
`The lookup table ${made.table.address} doesn't hold this transaction's accounts yet: try again in a moment.`,
|
|
347
|
+
);
|
|
265
348
|
// Sent before the slot passes the table's last addition, the transaction would be refused for addresses it can't load.
|
|
266
|
-
if (!usable)
|
|
349
|
+
if (!usable)
|
|
350
|
+
throw new Error(
|
|
351
|
+
`The lookup table ${made.table.address} is made, but the network has not moved past the slot it was last added to: try again in a moment.`,
|
|
352
|
+
);
|
|
267
353
|
await proofs();
|
|
268
354
|
const tables = [{ address: made.table.address, addresses: held.addresses }];
|
|
269
355
|
const message = compileFitting({ payer, blockhash, instructions: made.final, tables });
|
|
270
356
|
if (!message || message[0] !== 0x80) throw new Error('This transaction doesn’t fit even with its lookup table.');
|
|
271
|
-
const signature = await sendOne(
|
|
357
|
+
const signature = await sendOne(bh =>
|
|
358
|
+
compileMessage({ payer, blockhash: bh, instructions: made.final, version: 0, lookupTables: tables }),
|
|
359
|
+
);
|
|
272
360
|
return { signature, table: made.table.address, steps };
|
|
273
361
|
}
|
package/js/chain/mint.js
CHANGED
|
@@ -20,7 +20,9 @@ function option(data, at) {
|
|
|
20
20
|
function paddedString(data, at) {
|
|
21
21
|
const length = Number(readUintLE(data, at, 4));
|
|
22
22
|
if (at + 4 + length > data.length) throw new RangeError('string past the end');
|
|
23
|
-
const value = fromUtf8(data.subarray(at + 4, at + 4 + length))
|
|
23
|
+
const value = fromUtf8(data.subarray(at + 4, at + 4 + length))
|
|
24
|
+
.replaceAll('\0', '')
|
|
25
|
+
.trim();
|
|
24
26
|
return { value, next: at + 4 + length };
|
|
25
27
|
}
|
|
26
28
|
|
|
@@ -62,7 +64,9 @@ function token2022Metadata(data) {
|
|
|
62
64
|
export function parseMint(address, owner, data, metadataData = null) {
|
|
63
65
|
const program = owner === TOKEN_PROGRAM ? 'spl-token' : owner === TOKEN_2022_PROGRAM ? 'token-2022' : null;
|
|
64
66
|
if (!program) return null;
|
|
65
|
-
const mintLayout =
|
|
67
|
+
const mintLayout =
|
|
68
|
+
data.length === MINT_BYTES ||
|
|
69
|
+
(program === 'token-2022' && data.length > ACCOUNT_TYPE_OFFSET && data[ACCOUNT_TYPE_OFFSET] === ACCOUNT_TYPE_MINT);
|
|
66
70
|
if (!mintLayout || data[45] !== 1) return null;
|
|
67
71
|
let meta = null;
|
|
68
72
|
if (metadataData) {
|
package/js/chain/pda.js
CHANGED
|
@@ -24,7 +24,7 @@ const MAX_SEEDS = 16;
|
|
|
24
24
|
|
|
25
25
|
/** The address for these seeds (bump included), or null when it lands on the curve (not a valid PDA). */
|
|
26
26
|
export function createProgramAddress(seeds, programId) {
|
|
27
|
-
if (seeds.length > MAX_SEEDS || seeds.some(
|
|
27
|
+
if (seeds.length > MAX_SEEDS || seeds.some(s => s.length > MAX_SEED_BYTES)) throw new Error('Invalid PDA seeds');
|
|
28
28
|
const hash = sha256(concat(...seeds, addressBytes(programId), MARKER));
|
|
29
29
|
return isOnCurve(hash) ? null : encodeBase58(hash);
|
|
30
30
|
}
|
|
@@ -34,7 +34,7 @@ const found = new Map();
|
|
|
34
34
|
|
|
35
35
|
/** The canonical PDA and its bump: the highest bump whose address is off the curve. */
|
|
36
36
|
export function findProgramAddress(seeds, programId) {
|
|
37
|
-
const key = `${programId}:${seeds.map(
|
|
37
|
+
const key = `${programId}:${seeds.map(s => Array.from(s).join(',')).join('|')}`;
|
|
38
38
|
if (found.has(key)) return found.get(key);
|
|
39
39
|
for (let bump = 255; bump >= 0; bump--) {
|
|
40
40
|
const address = createProgramAddress([...seeds, Uint8Array.of(bump)], programId);
|
package/js/chain/programs.js
CHANGED
|
@@ -35,19 +35,19 @@ export function transferLamports(from, to, lamports) {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/** Compute Budget `RequestHeapFrame`: the transaction's programs get a heap of `bytes` (a multiple of 1 KiB, 32 KiB to 256 KiB). */
|
|
38
|
-
export const requestHeapFrame =
|
|
38
|
+
export const requestHeapFrame = bytes => ({
|
|
39
39
|
programId: COMPUTE_BUDGET_PROGRAM,
|
|
40
40
|
keys: [],
|
|
41
41
|
data: concat(Uint8Array.of(REQUEST_HEAP_FRAME), uintLE(bytes, 4)),
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
-
export const computeUnitLimit =
|
|
44
|
+
export const computeUnitLimit = units => ({
|
|
45
45
|
programId: COMPUTE_BUDGET_PROGRAM,
|
|
46
46
|
keys: [],
|
|
47
47
|
data: concat(Uint8Array.of(SET_COMPUTE_UNIT_LIMIT), uintLE(units, 4)),
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
export const computeUnitPrice =
|
|
50
|
+
export const computeUnitPrice = microLamports => ({
|
|
51
51
|
programId: COMPUTE_BUDGET_PROGRAM,
|
|
52
52
|
keys: [],
|
|
53
53
|
data: concat(Uint8Array.of(SET_COMPUTE_UNIT_PRICE), uintLE(microLamports, 8)),
|
|
@@ -69,7 +69,7 @@ export function createAssociatedTokenAccountIdempotent(payer, owner, mint, token
|
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
export const syncNative =
|
|
72
|
+
export const syncNative = account => ({
|
|
73
73
|
programId: TOKEN_PROGRAM,
|
|
74
74
|
keys: [meta(account, false, true)],
|
|
75
75
|
data: Uint8Array.of(TOKEN_SYNC_NATIVE),
|
|
@@ -89,8 +89,8 @@ export const closeTokenAccount = (account, destination, owner) => ({
|
|
|
89
89
|
export function withSolWrap(player, mint, price, instructions) {
|
|
90
90
|
if (mint !== NATIVE_MINT) return instructions;
|
|
91
91
|
const wrapped = associatedTokenAddress(player, NATIVE_MINT);
|
|
92
|
-
const budget = instructions.filter(
|
|
93
|
-
const rest = instructions.filter(
|
|
92
|
+
const budget = instructions.filter(ix => ix.programId === COMPUTE_BUDGET_PROGRAM);
|
|
93
|
+
const rest = instructions.filter(ix => ix.programId !== COMPUTE_BUDGET_PROGRAM);
|
|
94
94
|
return [
|
|
95
95
|
...budget,
|
|
96
96
|
createAssociatedTokenAccountIdempotent(player, player, NATIVE_MINT),
|