@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.
Files changed (112) hide show
  1. package/README.md +95 -52
  2. package/bin/xsolana.mjs +23 -8
  3. package/examples/blind-forge.json +2 -4
  4. package/examples/coin-flip-arena.json +4 -12
  5. package/examples/duel-arena.json +1 -4
  6. package/examples/elements-set.json +2 -9
  7. package/examples/expedition-guild.json +1 -8
  8. package/examples/fair-presale.json +1 -3
  9. package/examples/guild-missions.json +3 -10
  10. package/examples/halving-seasons.json +4 -9
  11. package/examples/hero-trials.json +15 -6
  12. package/examples/holder-den.json +38 -0
  13. package/examples/holders-drop.json +105 -0
  14. package/examples/ideas.json +788 -788
  15. package/examples/match-predictions.json +24 -0
  16. package/examples/merge-2048.json +5 -5
  17. package/examples/nft-raffle.json +1 -0
  18. package/examples/pack-league.json +108 -0
  19. package/examples/paper-hands-farm.json +4 -16
  20. package/examples/partner-vault.json +20 -7
  21. package/examples/sacrifice-raffle.json +2 -10
  22. package/examples/shelter-fair.json +1 -0
  23. package/examples/stakers-draw.json +4 -15
  24. package/examples/star-fleet.json +1 -8
  25. package/examples/starforce.json +6 -6
  26. package/examples/stride-sneakers.json +164 -0
  27. package/examples/trade-in-shop.json +2 -3
  28. package/js/approve/details.mjs +122 -52
  29. package/js/approve/ed25519.mjs +5 -1
  30. package/js/approve/live-check.mjs +83 -26
  31. package/js/approve/page.html +217 -78
  32. package/js/approve/page.js +43 -15
  33. package/js/approve/server.mjs +290 -73
  34. package/js/chain/base58.js +7 -1
  35. package/js/chain/borsh.js +24 -11
  36. package/js/chain/bytes.js +11 -8
  37. package/js/chain/core.js +35 -7
  38. package/js/chain/curve.js +1 -1
  39. package/js/chain/lookup.js +124 -36
  40. package/js/chain/mint.js +6 -2
  41. package/js/chain/pda.js +2 -2
  42. package/js/chain/programs.js +6 -6
  43. package/js/chain/rpc.js +44 -16
  44. package/js/chain/token.js +13 -2
  45. package/js/chain/token2022.js +55 -16
  46. package/js/chain/tx.js +32 -22
  47. package/js/client/accounts.js +653 -131
  48. package/js/client/compressed.js +4 -3
  49. package/js/client/grant.js +267 -83
  50. package/js/client/idl/loot.js +1 -1
  51. package/js/client/idl/loot.json +2406 -2176
  52. package/js/client/index.js +1 -0
  53. package/js/client/ix.js +3059 -664
  54. package/js/client/pda.js +39 -33
  55. package/js/client/policy.js +52 -24
  56. package/js/client/random.js +70 -0
  57. package/js/client/sizes.js +414 -16
  58. package/js/client/traits.js +14 -5
  59. package/js/connect/connect.mjs +59 -17
  60. package/js/economy/check.mjs +4153 -725
  61. package/js/economy/cost.mjs +403 -64
  62. package/js/economy/examples.mjs +176 -35
  63. package/js/economy/explain.mjs +466 -111
  64. package/js/economy/guide.mjs +296 -72
  65. package/js/economy/legal.mjs +12 -6
  66. package/js/economy/live.mjs +83 -19
  67. package/js/economy/model.mjs +462 -124
  68. package/js/economy/plan.mjs +1306 -219
  69. package/js/economy/schema.json +390 -484
  70. package/js/economy/secrets.mjs +17 -7
  71. package/js/economy/simulate.mjs +883 -223
  72. package/js/economy/txsize.mjs +15 -3
  73. package/js/economy/validate.mjs +61 -24
  74. package/js/mcp/server.mjs +42 -14
  75. package/js/mcp/tools.mjs +307 -88
  76. package/js/play/index.js +2864 -619
  77. package/js/play/keys.js +193 -0
  78. package/js/results/index.js +223 -30
  79. package/js/results/server.js +230 -0
  80. package/js/results/wire.js +64 -16
  81. package/package.json +6 -4
  82. package/release.json +5 -5
  83. package/skills/xsolana-economy/SKILL.md +39 -21
  84. package/skills/xsolana-economy/references/after-launch.md +58 -38
  85. package/skills/xsolana-economy/references/apps.md +97 -50
  86. package/skills/xsolana-economy/references/sentence-to-spec.md +148 -121
  87. package/skills/xsolana-economy/references/telling-the-person.md +61 -17
  88. package/types/chain/core.d.ts +4 -2
  89. package/types/chain/lookup.d.ts +5 -2
  90. package/types/chain/rpc.d.ts +3 -1
  91. package/types/client/accounts.d.ts +217 -56
  92. package/types/client/grant.d.ts +72 -27
  93. package/types/client/idl/loot.d.ts +602 -549
  94. package/types/client/index.d.ts +1 -0
  95. package/types/client/ix.d.ts +3884 -247
  96. package/types/client/pda.d.ts +22 -16
  97. package/types/client/policy.d.ts +39 -14
  98. package/types/client/random.d.ts +26 -0
  99. package/types/client/sizes.d.ts +93 -5
  100. package/types/economy/check.d.mts +5 -10
  101. package/types/economy/cost.d.mts +2 -1
  102. package/types/economy/guide.d.mts +5 -0
  103. package/types/economy/live.d.mts +16 -1
  104. package/types/economy/model.d.mts +151 -38
  105. package/types/economy/plan.d.mts +237 -194
  106. package/types/economy/simulate.d.mts +21 -13
  107. package/types/mcp/tools.d.mts +21 -13
  108. package/types/play/index.d.ts +453 -53
  109. package/types/play/keys.d.ts +51 -0
  110. package/types/results/index.d.ts +7 -6
  111. package/types/results/server.d.ts +126 -0
  112. package/types/results/wire.d.ts +1 -1
@@ -30,7 +30,13 @@ export function encodeBase58(bytes) {
30
30
  if (byte !== 0) break;
31
31
  zeros += '1';
32
32
  }
33
- return zeros + digits.reverse().map((d) => ALPHABET[d]).join('');
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((t) => t.name === name);
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 = (fields) => typeof fields[0] === 'object' && fields[0] !== null && 'name' in fields[0];
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((v) => v.name === variantName);
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 = value instanceof Uint8Array || (Array.isArray(value) && value.every((b) => Number.isInteger(b) && b >= 0 && b <= 255));
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((i) => i.name === name);
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((a) => a.name === name);
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) => data.length >= prefix.length && equalBytes(data.subarray(0, prefix.length), Uint8Array.from(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((a) => a.name === name);
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((a) => a.name === name);
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((e) => startsWith(bytes, e.discriminator));
258
- if (event) out.push({ name: event.name, data: decodeDefined(idl, event.name, bytes, event.discriminator.length)[0] });
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 = (text) => encoder.encode(text);
10
+ export const utf8 = text => encoder.encode(text);
11
11
 
12
12
  /** A string from UTF-8 bytes. */
13
- export const fromUtf8 = (bytes) => decoder.decode(bytes);
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) text += String.fromCharCode(...bytes.subarray(i, 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), (c) => c.charCodeAt(0));
50
+ return Uint8Array.from(atob(text), c => c.charCodeAt(0));
50
51
  }
51
52
 
52
- export const toHex = (bytes) => Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
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)) throw new RangeError(`${value} does not fit in a signed ${size}-byte integer`);
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)) throw new RangeError(`${value} is not a safe integer: pass it as a bigint`);
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 = (n) => globalThis.crypto.getRandomValues(new Uint8Array(n));
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 = { Royalties: 0, FreezeDelegate: 1, UpdateDelegate: 4, PermanentFreezeDelegate: 5, Attributes: 6, BubblegumV2: 15 };
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 = (text) => {
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({ 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`).');
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 = (n) => {
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
- return { updateAuthority, name, uri, numMinted: r.u32(), currentSize: r.u32() };
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
@@ -9,7 +9,7 @@ const P = 2n ** 255n - 19n;
9
9
  const D = 37095705934669439343138083508754565189542113879843219016388785533085940283555n;
10
10
  const Y_MASK = (1n << 255n) - 1n;
11
11
 
12
- const mod = (a) => {
12
+ const mod = a => {
13
13
  const r = a % P;
14
14
  return r >= 0n ? r : r + P;
15
15
  };
@@ -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) => findProgramAddress([addressBytes(authority), uintLE(slot, 8)], LOOKUP_TABLE_PROGRAM);
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([meta(address, false, true), meta(authority, true, false), meta(payer, true, true), meta(SYSTEM_PROGRAM, false, false)], concat(uintLE(0, 4), uintLE(slot, 8), Uint8Array.of(bump)));
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([meta(table, false, true), meta(authority, true, false), meta(payer, true, true), meta(SYSTEM_PROGRAM, false, false)], concat(uintLE(2, 4), uintLE(addresses.length, 8), ...addresses.map(addressBytes)));
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 }) => ix([meta(table, false, true), meta(authority, true, false)], uintLE(1, 4));
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 }) => ix([meta(table, false, true), meta(authority, true, false)], uintLE(3, 4));
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 }) => ix([meta(table, false, true), meta(authority, true, false), meta(recipient, false, true)], uintLE(4, 4));
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 (!account || account.owner !== LOOKUP_TABLE_PROGRAM || account.data.length < TABLE_HEADER_BYTES || readUintLE(account.data, 0, 4) !== 1n) return null;
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) addresses.push(encodeBase58(data.subarray(at, 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 = (authority) => [{ memcmp: { offset: 22, bytes: authority } }];
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) return recent.length >= SLOT_HASHES_HELD && recent.every((s) => s > table.deactivationSlot) ? 'close' : 'cooling';
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 new Set([payer, ...instructions.flatMap((i) => [i.programId, ...i.keys.map((k) => k.pubkey)])]).size + (asV0 ? 2 : 0);
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((i) => i.keys.filter((k) => k.isSigner).map((k) => k.pubkey)));
114
- const programs = new Set(instructions.map((i) => i.programId));
115
- return [...new Set(instructions.flatMap((i) => i.keys.map((k) => k.pubkey)))].filter((a) => !signers.has(a) && !programs.has(a));
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 = (input) => {
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) return null;
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 (n > 0 && sizeOf({ payer, blockhash, instructions: [...step, extendTableIx({ table: address, authority: payer, addresses: left.slice(0, n) })] }) > MAX_TRANSACTION_BYTES) n--;
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 })) return { legacy: true, first: [], budget: [], rest: instructions, table: false };
191
- if (planTable({ payer, blockhash, slot: 0n, instructions })) return { legacy: false, first: [], budget: [], rest: instructions, table: true };
192
- const budget = instructions.filter((i) => i.programId === COMPUTE_BUDGET_PROGRAM);
193
- const first = instructions.filter((i) => alone(i));
194
- const rest = instructions.filter((i) => !alone(i));
195
- if (!first.length || first.some((i) => !compileFitting({ payer, blockhash, instructions: [...budget, i] }))) return null;
196
- if (compileFitting({ payer, blockhash, instructions: rest })) return { legacy: false, first, budget, rest, table: false };
197
- return planTable({ payer, blockhash, slot: 0n, instructions: rest }) ? { legacy: false, first, budget, rest, table: true } : null;
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 = (ms) => new Promise((r) => setTimeout(r, ms));
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')). `table: false` sends no lookup table (a throwaway key holds no rent for one); `first`:
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({ rpc, payer, instructions, sign, table = true, alone = () => false, first = [], sleep = pause }) {
231
- const sendOne = async (build) => {
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 = (list) => sendOne((bh) => compileMessage({ payer, blockhash: bh, instructions: list }));
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) throw new Error(`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.`);
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
- if (plan.table && !table) throw new Error(`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.`);
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(held && made.table.addresses.every((a) => held.addresses.includes(a)) && BigInt(await rpc.call('getSlot', [{ commitment: 'confirmed' }])) > held.lastExtendedSlot);
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((a) => held.addresses.includes(a))) throw new Error(`The lookup table ${made.table.address} doesn't hold this transaction's accounts yet: try again in a moment.`);
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) throw new Error(`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.`);
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((bh) => compileMessage({ payer, blockhash: bh, instructions: made.final, version: 0, lookupTables: tables }));
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)).replaceAll('\0', '').trim();
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 = data.length === MINT_BYTES || (program === 'token-2022' && data.length > ACCOUNT_TYPE_OFFSET && data[ACCOUNT_TYPE_OFFSET] === ACCOUNT_TYPE_MINT);
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((s) => s.length > MAX_SEED_BYTES)) throw new Error('Invalid PDA seeds');
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((s) => Array.from(s).join(',')).join('|')}`;
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);
@@ -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 = (bytes) => ({
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 = (units) => ({
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 = (microLamports) => ({
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 = (account) => ({
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((ix) => ix.programId === COMPUTE_BUDGET_PROGRAM);
93
- const rest = instructions.filter((ix) => ix.programId !== COMPUTE_BUDGET_PROGRAM);
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),