@xgamesfun/solana 0.24.0 → 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 +94 -51
- package/bin/xsolana.mjs +23 -8
- package/examples/blind-forge.json +2 -4
- package/examples/coin-flip-arena.json +2 -5
- package/examples/duel-arena.json +1 -4
- package/examples/fair-presale.json +1 -3
- package/examples/hero-trials.json +14 -2
- 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/nft-raffle.json +1 -0
- package/examples/pack-league.json +108 -0
- package/examples/partner-vault.json +19 -3
- package/examples/sacrifice-raffle.json +2 -10
- package/examples/shelter-fair.json +1 -0
- package/examples/stakers-draw.json +3 -7
- package/examples/stride-sneakers.json +164 -0
- package/examples/trade-in-shop.json +2 -3
- package/js/approve/details.mjs +117 -48
- 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 +276 -74
- package/js/chain/base58.js +7 -1
- package/js/chain/borsh.js +15 -11
- package/js/chain/bytes.js +11 -8
- package/js/chain/core.js +21 -5
- package/js/chain/curve.js +1 -1
- package/js/chain/lookup.js +118 -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 +40 -14
- 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 +270 -80
- package/js/client/compressed.js +4 -3
- package/js/client/grant.js +165 -40
- package/js/client/idl/loot.js +1 -1
- package/js/client/idl/loot.json +104 -16
- package/js/client/ix.js +2121 -457
- package/js/client/pda.js +20 -18
- package/js/client/policy.js +12 -5
- package/js/client/random.js +1 -1
- package/js/client/sizes.js +220 -65
- package/js/client/traits.js +14 -5
- package/js/connect/connect.mjs +59 -17
- package/js/economy/check.mjs +3873 -690
- package/js/economy/cost.mjs +380 -56
- package/js/economy/examples.mjs +176 -35
- package/js/economy/explain.mjs +461 -110
- package/js/economy/guide.mjs +282 -59
- package/js/economy/legal.mjs +12 -6
- package/js/economy/live.mjs +60 -19
- package/js/economy/model.mjs +320 -97
- package/js/economy/plan.mjs +1022 -203
- package/js/economy/schema.json +127 -369
- package/js/economy/secrets.mjs +17 -7
- package/js/economy/simulate.mjs +771 -178
- 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 +293 -80
- package/js/play/index.js +1960 -461
- package/js/play/keys.js +35 -9
- 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 +30 -16
- package/skills/xsolana-economy/references/after-launch.md +30 -20
- package/skills/xsolana-economy/references/apps.md +47 -36
- package/skills/xsolana-economy/references/sentence-to-spec.md +148 -129
- package/skills/xsolana-economy/references/telling-the-person.md +9 -6
- package/types/chain/core.d.ts +1 -1
- package/types/chain/lookup.d.ts +1 -1
- package/types/client/accounts.d.ts +24 -3
- package/types/client/grant.d.ts +2 -2
- package/types/client/idl/loot.d.ts +22 -0
- package/types/client/ix.d.ts +54 -45
- package/types/client/pda.d.ts +3 -1
- package/types/economy/guide.d.mts +5 -0
- package/types/economy/model.d.mts +48 -3
- package/types/economy/plan.d.mts +44 -2
- package/types/play/index.d.ts +8 -19
- 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
|
}
|
|
@@ -43,13 +43,13 @@ function encodeScalar(type, value, out) {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/** A variant's field list: named ({ name, type }) or tuple (bare types). */
|
|
46
|
-
const isNamed =
|
|
46
|
+
const isNamed = fields => typeof fields[0] === 'object' && fields[0] !== null && 'name' in fields[0];
|
|
47
47
|
|
|
48
48
|
function encodeDefined(idl, name, value, out) {
|
|
49
49
|
const def = typeDef(idl, name).type;
|
|
50
50
|
if (def.kind === 'enum') {
|
|
51
51
|
const variantName = typeof value === 'string' ? value : Object.keys(value ?? {})[0];
|
|
52
|
-
const index = def.variants.findIndex(
|
|
52
|
+
const index = def.variants.findIndex(v => v.name === variantName);
|
|
53
53
|
if (index < 0) throw new Error(`${name} has no variant ${String(variantName)}`);
|
|
54
54
|
out.push(Uint8Array.of(index));
|
|
55
55
|
const fields = def.variants[index].fields ?? [];
|
|
@@ -71,7 +71,9 @@ function encodeDefined(idl, name, value, out) {
|
|
|
71
71
|
* a number past a byte) is refused rather than written as other bytes.
|
|
72
72
|
*/
|
|
73
73
|
function byteArray(value, length) {
|
|
74
|
-
const bytes =
|
|
74
|
+
const bytes =
|
|
75
|
+
value instanceof Uint8Array ||
|
|
76
|
+
(Array.isArray(value) && value.every(b => Number.isInteger(b) && b >= 0 && b <= 255));
|
|
75
77
|
if (!bytes) throw new TypeError(`Expected ${length} bytes: a Uint8Array or an array of integers from 0 to 255`);
|
|
76
78
|
if (value.length !== length) throw new Error(`Expected ${length} bytes, got ${value.length}`);
|
|
77
79
|
return Uint8Array.from(value);
|
|
@@ -114,7 +116,7 @@ export function encodeBorsh(idl, type, value) {
|
|
|
114
116
|
|
|
115
117
|
/** An instruction's data: its discriminator, then each argument in IDL order. */
|
|
116
118
|
export function encodeInstruction(idl, name, args) {
|
|
117
|
-
const ix = idl.instructions.find(
|
|
119
|
+
const ix = idl.instructions.find(i => i.name === name);
|
|
118
120
|
if (!ix) throw new Error(`Unknown instruction ${name}`);
|
|
119
121
|
const out = [Uint8Array.from(ix.discriminator)];
|
|
120
122
|
for (const arg of ix.args) {
|
|
@@ -126,7 +128,7 @@ export function encodeInstruction(idl, name, args) {
|
|
|
126
128
|
|
|
127
129
|
/** An account's bytes as the program writes them: its discriminator, then its fields. */
|
|
128
130
|
export function encodeAccount(idl, name, value) {
|
|
129
|
-
const account = idl.accounts.find(
|
|
131
|
+
const account = idl.accounts.find(a => a.name === name);
|
|
130
132
|
if (!account) throw new Error(`Unknown account type ${name}`);
|
|
131
133
|
return concat(Uint8Array.from(account.discriminator), encodeBorsh(idl, { defined: { name } }, value));
|
|
132
134
|
}
|
|
@@ -218,11 +220,12 @@ function decodeAt(idl, type, data, at) {
|
|
|
218
220
|
/** One value of `type` at `offset`: [value, next offset]. */
|
|
219
221
|
export const decodeBorsh = (idl, type, data, offset = 0) => decodeAt(idl, type, data, offset);
|
|
220
222
|
|
|
221
|
-
const startsWith = (data, prefix) =>
|
|
223
|
+
const startsWith = (data, prefix) =>
|
|
224
|
+
data.length >= prefix.length && equalBytes(data.subarray(0, prefix.length), Uint8Array.from(prefix));
|
|
222
225
|
|
|
223
226
|
/** An account of the named type, after checking its discriminator. Bytes after the fields are left alone. */
|
|
224
227
|
export function decodeAccount(idl, name, data) {
|
|
225
|
-
const account = idl.accounts.find(
|
|
228
|
+
const account = idl.accounts.find(a => a.name === name);
|
|
226
229
|
if (!account) throw new Error(`Unknown account type ${name}`);
|
|
227
230
|
if (!startsWith(data, account.discriminator)) throw new Error(`Not a ${name} account`);
|
|
228
231
|
return decodeDefined(idl, name, data, account.discriminator.length)[0];
|
|
@@ -230,7 +233,7 @@ export function decodeAccount(idl, name, data) {
|
|
|
230
233
|
|
|
231
234
|
/** Whether the data starts with the named account's discriminator. */
|
|
232
235
|
export function isAccount(idl, name, data) {
|
|
233
|
-
const account = idl.accounts.find(
|
|
236
|
+
const account = idl.accounts.find(a => a.name === name);
|
|
234
237
|
return Boolean(account) && startsWith(data, account.discriminator);
|
|
235
238
|
}
|
|
236
239
|
|
|
@@ -263,8 +266,9 @@ export function decodeEvents(idl, logs) {
|
|
|
263
266
|
} catch {
|
|
264
267
|
continue;
|
|
265
268
|
}
|
|
266
|
-
const event = (idl.events ?? []).find(
|
|
267
|
-
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] });
|
|
268
272
|
}
|
|
269
273
|
return out;
|
|
270
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 {
|
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
|
|
|
@@ -229,24 +276,47 @@ async function freshSlot(rpc, payer) {
|
|
|
229
276
|
* plain sentence when it can't be sent. Returns { signature (the last transaction's), table (its address, or null),
|
|
230
277
|
* steps (the signatures of the transactions before it) }.
|
|
231
278
|
*/
|
|
232
|
-
export async function sendFitting({
|
|
233
|
-
|
|
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 => {
|
|
234
291
|
const { blockhash, lastValidBlockHeight } = await rpc.latestBlockhash();
|
|
235
292
|
const message = build(blockhash);
|
|
236
293
|
const signature = await rpc.send(await sign(message));
|
|
237
294
|
await rpc.confirm?.(signature, { lastValidBlockHeight });
|
|
238
295
|
return signature;
|
|
239
296
|
};
|
|
240
|
-
const plain =
|
|
297
|
+
const plain = list => sendOne(bh => compileMessage({ payer, blockhash: bh, instructions: list }));
|
|
241
298
|
const { blockhash } = await rpc.latestBlockhash();
|
|
242
299
|
const plan = sendPlan({ payer, blockhash, instructions, alone });
|
|
243
|
-
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
|
+
);
|
|
244
304
|
if (plan.legacy) return { signature: await plain(instructions), table: null, steps: [] };
|
|
245
305
|
// Standing tables (a project's, made at go-live; each still active) that hold enough of its accounts: one v0
|
|
246
306
|
// transaction with them, and no table of its own.
|
|
247
307
|
const fits = standing.length && compileFitting({ payer, blockhash, instructions, tables: standing });
|
|
248
|
-
if (fits && fits[0] === 0x80)
|
|
249
|
-
|
|
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
|
+
);
|
|
250
320
|
const steps = [];
|
|
251
321
|
const proofs = async () => {
|
|
252
322
|
for (const ix of plan.first) steps.push(await plain([...plan.budget, ix]));
|
|
@@ -264,16 +334,28 @@ export async function sendFitting({ rpc, payer, instructions, sign, table = true
|
|
|
264
334
|
let usable = false;
|
|
265
335
|
for (let i = 0; i < 40 && !usable; i++) {
|
|
266
336
|
held = readTable(await rpc.account(made.table.address));
|
|
267
|
-
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
|
+
);
|
|
268
342
|
if (!usable) await sleep(400);
|
|
269
343
|
}
|
|
270
|
-
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
|
+
);
|
|
271
348
|
// Sent before the slot passes the table's last addition, the transaction would be refused for addresses it can't load.
|
|
272
|
-
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
|
+
);
|
|
273
353
|
await proofs();
|
|
274
354
|
const tables = [{ address: made.table.address, addresses: held.addresses }];
|
|
275
355
|
const message = compileFitting({ payer, blockhash, instructions: made.final, tables });
|
|
276
356
|
if (!message || message[0] !== 0x80) throw new Error('This transaction doesn’t fit even with its lookup table.');
|
|
277
|
-
const signature = await sendOne(
|
|
357
|
+
const signature = await sendOne(bh =>
|
|
358
|
+
compileMessage({ payer, blockhash: bh, instructions: made.final, version: 0, lookupTables: tables }),
|
|
359
|
+
);
|
|
278
360
|
return { signature, table: made.table.address, steps };
|
|
279
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),
|