@xgamesfun/solana 0.0.0-stage → 0.20.1

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 (134) hide show
  1. package/DISCLAIMER.md +41 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +13 -0
  4. package/README.md +172 -2
  5. package/bin/xsolana.mjs +189 -0
  6. package/examples/artwork-auction.json +51 -0
  7. package/examples/blind-forge.json +128 -0
  8. package/examples/coin-flip-arena.json +108 -0
  9. package/examples/duel-arena.json +137 -0
  10. package/examples/elements-set.json +281 -0
  11. package/examples/event-badges.json +67 -0
  12. package/examples/expedition-guild.json +161 -0
  13. package/examples/fair-presale.json +48 -0
  14. package/examples/falling-drop.json +90 -0
  15. package/examples/guild-missions.json +144 -0
  16. package/examples/halving-seasons.json +112 -0
  17. package/examples/hero-trials.json +96 -0
  18. package/examples/ideas.json +789 -0
  19. package/examples/level-ladder.json +65 -0
  20. package/examples/merge-2048.json +128 -0
  21. package/examples/nft-raffle.json +33 -0
  22. package/examples/paper-hands-farm.json +56 -0
  23. package/examples/partner-vault.json +115 -0
  24. package/examples/sacrifice-raffle.json +118 -0
  25. package/examples/scorched-treasury.json +84 -0
  26. package/examples/shelter-fair.json +164 -0
  27. package/examples/spark-pity.json +153 -0
  28. package/examples/stakers-draw.json +125 -0
  29. package/examples/star-fleet.json +157 -0
  30. package/examples/starforce.json +116 -0
  31. package/examples/stud-farm.json +134 -0
  32. package/examples/team-vesting.json +70 -0
  33. package/examples/trade-in-shop.json +168 -0
  34. package/js/approve/details.mjs +262 -0
  35. package/js/approve/ed25519.mjs +36 -0
  36. package/js/approve/live-check.mjs +144 -0
  37. package/js/approve/page.html +86 -0
  38. package/js/approve/page.js +203 -0
  39. package/js/approve/server.mjs +711 -0
  40. package/js/chain/base58.js +73 -0
  41. package/js/chain/borsh.js +261 -0
  42. package/js/chain/bytes.js +141 -0
  43. package/js/chain/core.js +169 -0
  44. package/js/chain/curve.js +52 -0
  45. package/js/chain/lookup.js +273 -0
  46. package/js/chain/mint.js +96 -0
  47. package/js/chain/pda.js +66 -0
  48. package/js/chain/programs.js +102 -0
  49. package/js/chain/rpc.js +217 -0
  50. package/js/chain/sha256.js +68 -0
  51. package/js/chain/token.js +85 -0
  52. package/js/chain/token2022.js +247 -0
  53. package/js/chain/tx.js +224 -0
  54. package/js/client/accounts.js +347 -0
  55. package/js/client/compressed.js +46 -0
  56. package/js/client/grant.js +240 -0
  57. package/js/client/idl/loot.js +2 -0
  58. package/js/client/idl/loot.json +17064 -0
  59. package/js/client/index.js +9 -0
  60. package/js/client/ix.js +2156 -0
  61. package/js/client/pda.js +110 -0
  62. package/js/client/policy.js +48 -0
  63. package/js/client/sizes.js +54 -0
  64. package/js/client/traits.js +44 -0
  65. package/js/connect/connect.mjs +182 -0
  66. package/js/economy/check.mjs +1991 -0
  67. package/js/economy/cost.mjs +170 -0
  68. package/js/economy/examples.mjs +114 -0
  69. package/js/economy/explain.mjs +235 -0
  70. package/js/economy/guide.mjs +231 -0
  71. package/js/economy/legal.mjs +15 -0
  72. package/js/economy/live.mjs +107 -0
  73. package/js/economy/model.mjs +583 -0
  74. package/js/economy/plan.mjs +887 -0
  75. package/js/economy/schema.json +2923 -0
  76. package/js/economy/secrets.mjs +60 -0
  77. package/js/economy/simulate.mjs +952 -0
  78. package/js/economy/txsize.mjs +45 -0
  79. package/js/economy/validate.mjs +85 -0
  80. package/js/index.js +10 -0
  81. package/js/mcp/server.mjs +113 -0
  82. package/js/mcp/tools.mjs +479 -0
  83. package/js/play/index.js +2109 -0
  84. package/js/results/index.js +184 -0
  85. package/js/results/wire.js +90 -0
  86. package/package.json +100 -4
  87. package/release.json +8 -0
  88. package/skills/xsolana-economy/SKILL.md +105 -0
  89. package/skills/xsolana-economy/references/after-launch.md +114 -0
  90. package/skills/xsolana-economy/references/apps.md +157 -0
  91. package/skills/xsolana-economy/references/sentence-to-spec.md +172 -0
  92. package/skills/xsolana-economy/references/telling-the-person.md +93 -0
  93. package/types/chain/base58.d.ts +10 -0
  94. package/types/chain/borsh.d.ts +23 -0
  95. package/types/chain/bytes.d.ts +45 -0
  96. package/types/chain/core.d.ts +50 -0
  97. package/types/chain/curve.d.ts +1 -0
  98. package/types/chain/lookup.d.ts +191 -0
  99. package/types/chain/mint.d.ts +25 -0
  100. package/types/chain/pda.d.ts +25 -0
  101. package/types/chain/programs.d.ts +65 -0
  102. package/types/chain/rpc.d.ts +66 -0
  103. package/types/chain/sha256.d.ts +1 -0
  104. package/types/chain/token.d.ts +141 -0
  105. package/types/chain/token2022.d.ts +53 -0
  106. package/types/chain/tx.d.ts +60 -0
  107. package/types/client/accounts.d.ts +178 -0
  108. package/types/client/compressed.d.ts +29 -0
  109. package/types/client/grant.d.ts +125 -0
  110. package/types/client/idl/loot.d.ts +4467 -0
  111. package/types/client/index.d.ts +8 -0
  112. package/types/client/ix.d.ts +1687 -0
  113. package/types/client/pda.d.ts +91 -0
  114. package/types/client/policy.d.ts +23 -0
  115. package/types/client/sizes.d.ts +13 -0
  116. package/types/client/traits.d.ts +12 -0
  117. package/types/economy/check.d.mts +105 -0
  118. package/types/economy/cost.d.mts +68 -0
  119. package/types/economy/examples.d.mts +27 -0
  120. package/types/economy/explain.d.mts +19 -0
  121. package/types/economy/guide.d.mts +140 -0
  122. package/types/economy/legal.d.mts +12 -0
  123. package/types/economy/live.d.mts +109 -0
  124. package/types/economy/model.d.mts +363 -0
  125. package/types/economy/plan.d.mts +1013 -0
  126. package/types/economy/secrets.d.mts +4 -0
  127. package/types/economy/simulate.d.mts +156 -0
  128. package/types/economy/txsize.d.mts +17 -0
  129. package/types/economy/validate.d.mts +3 -0
  130. package/types/index.d.ts +9 -0
  131. package/types/mcp/tools.d.mts +627 -0
  132. package/types/play/index.d.ts +469 -0
  133. package/types/results/index.d.ts +155 -0
  134. package/types/results/wire.d.ts +122 -0
@@ -0,0 +1,273 @@
1
+ // Address lookup tables, and sending a transaction whatever its size. A legacy transaction holds at most 1,232 bytes,
2
+ // about 35 accounts with room for their instructions; loot's program accepts more (a settle can lock up to 64 accounts).
3
+ // A v0 transaction names each account it loads from a lookup table with one byte instead of 32. `sendFitting` sends
4
+ // legacy when it fits; otherwise the payer makes a table of the transaction's accounts first (its own transactions,
5
+ // each legacy), waits one slot for it to be usable, and sends the transaction as v0 with the table, deactivating the
6
+ // table in that same transaction (a compressed NFT's prove_leaf goes alone, after the table, only when even that
7
+ // doesn't fit: `sendPlan`). Once its deactivation slot has left the recent slot hashes (about 513 slots: on devnet about two minutes)
8
+ // the table can be closed and its rent goes back to the payer (`closeTableIx`; `tableStep` says when).
9
+ import { addressBytes, encodeBase58 } from './base58.js';
10
+ import { concat, readUintLE, uintLE } from './bytes.js';
11
+ import { COMPUTE_BUDGET_PROGRAM, findProgramAddress, SYSTEM_PROGRAM, SYSVAR_SLOT_HASHES } from './pda.js';
12
+ import { compileMessage, MAX_TRANSACTION_BYTES, transactionSize } from './tx.js';
13
+
14
+ export const LOOKUP_TABLE_PROGRAM = 'AddressLookupTab1e1111111111111111111111111';
15
+ /** A table's account: 56 bytes of header, then its addresses, 32 bytes each (at most 256). */
16
+ export const TABLE_HEADER_BYTES = 56;
17
+ export const TABLE_MAX_ADDRESSES = 256;
18
+ /** The accounts a transaction may lock. */
19
+ export const MAX_ACCOUNT_LOCKS = 64;
20
+ /** A deactivated table can be closed once its deactivation slot has left the 512 recent slot hashes. */
21
+ export const TABLE_COOLDOWN_SLOTS = 513n;
22
+ /** The deactivation slot of a table still active. */
23
+ const ACTIVE = 2n ** 64n - 1n;
24
+
25
+ const meta = (pubkey, isSigner, isWritable) => ({ pubkey, isSigner, isWritable });
26
+ const ix = (keys, data) => ({ programId: LOOKUP_TABLE_PROGRAM, keys, data });
27
+
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);
30
+
31
+ /** Makes the table `authority` owns at `slot`, its rent paid by `payer`. Returns { address, instruction }. */
32
+ export function createTableIx({ authority, payer = authority, slot }) {
33
+ 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
+ return { address, instruction };
36
+ }
37
+
38
+ /** Adds `addresses` to `table`, the rent of its growth paid by `payer`. */
39
+ 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)));
41
+
42
+ /** 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));
44
+
45
+ /** 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));
47
+
48
+ /** 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));
50
+
51
+ /**
52
+ * A table's account read back: { authority (null once frozen), deactivationSlot (null while active),
53
+ * lastExtendedSlot, addresses }, or null when `account` is not a lookup table.
54
+ */
55
+ 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;
57
+ const data = account.data;
58
+ const deactivation = readUintLE(data, 4, 8);
59
+ const addresses = [];
60
+ for (let at = TABLE_HEADER_BYTES; at + 32 <= data.length; at += 32) addresses.push(encodeBase58(data.subarray(at, at + 32)));
61
+ return {
62
+ authority: data[21] === 1 ? encodeBase58(data.subarray(22, 54)) : null,
63
+ deactivationSlot: deactivation === ACTIVE ? null : deactivation,
64
+ lastExtendedSlot: readUintLE(data, 12, 8),
65
+ addresses,
66
+ };
67
+ }
68
+
69
+ /** The filter that finds the tables `authority` owns (getProgramAccounts on LOOKUP_TABLE_PROGRAM). */
70
+ export const tablesOf = (authority) => [{ memcmp: { offset: 22, bytes: authority } }];
71
+
72
+ /**
73
+ * The slots the SlotHashes sysvar holds (its account's data: a count, then a slot and a hash each), newest first. It
74
+ * holds the last 512 slots that had a block: a table is made at one of them, and closes only once its deactivation
75
+ * slot has left them.
76
+ */
77
+ export function slotHashes(data) {
78
+ if (!data || data.length < 8) return [];
79
+ const n = Number(readUintLE(data, 0, 8));
80
+ const out = [];
81
+ for (let i = 0; i < n && 8 + 40 * (i + 1) <= data.length; i++) out.push(readUintLE(data, 8 + 40 * i, 8));
82
+ return out;
83
+ }
84
+
85
+ /** How many slots the SlotHashes sysvar holds when full. */
86
+ export const SLOT_HASHES_HELD = 512;
87
+
88
+ /**
89
+ * What is due for a table of yours at `slot`: 'close' (deactivated, and its deactivation slot has left `recent`, the
90
+ * slots SlotHashes holds: its rent comes back), 'cooling' (deactivated, not yet closable), or 'active'. A table is
91
+ * closable only when `recent` is a full read (512 slots) and its deactivation slot is older than all of them: an empty
92
+ * or short read (an RPC that answered nothing, a young chain) says cooling. Without `recent`, a table is counted
93
+ * closable 513 slots after its deactivation, which skipped slots can make too early.
94
+ */
95
+ export function tableStep(table, slot, recent = null) {
96
+ if (table.deactivationSlot === null) return 'active';
97
+ if (BigInt(slot) <= table.deactivationSlot) return 'cooling';
98
+ if (recent) return recent.length >= SLOT_HASHES_HELD && recent.every((s) => s > table.deactivationSlot) ? 'close' : 'cooling';
99
+ return BigInt(slot) > table.deactivationSlot + TABLE_COOLDOWN_SLOTS ? 'close' : 'cooling';
100
+ }
101
+
102
+ /**
103
+ * How many accounts a transaction of `instructions` from `payer` locks: every account once, its programs included. As
104
+ * v0 with a lookup table it deactivates (`planTable`), the table and the lookup-table program are two more
105
+ * (`asV0`).
106
+ */
107
+ 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);
109
+ }
110
+
111
+ /** The accounts of `instructions` a table can hold: every one that is neither a signer nor a program they call, once. */
112
+ 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));
116
+ }
117
+
118
+ const sizeOf = (input) => {
119
+ try {
120
+ return transactionSize(compileMessage(input));
121
+ } catch {
122
+ return Infinity;
123
+ }
124
+ };
125
+
126
+ /**
127
+ * The message for `instructions` from `payer`: legacy when it fits, else v0 loading what it can from `tables`
128
+ * ([{ address, addresses }]). null when neither fits.
129
+ */
130
+ export function compileFitting({ payer, blockhash, instructions, tables = [] }) {
131
+ const legacy = { payer, blockhash, instructions };
132
+ if (sizeOf(legacy) <= MAX_TRANSACTION_BYTES) return compileMessage(legacy);
133
+ const v0 = { ...legacy, version: 0, lookupTables: tables };
134
+ return tables.length && sizeOf(v0) <= MAX_TRANSACTION_BYTES ? compileMessage(v0) : null;
135
+ }
136
+
137
+ /**
138
+ * The plan for sending `instructions` from `payer` as v0 with a table of their accounts made at `slot`, `first` the
139
+ * addresses it holds first:
140
+ * { table: { address, addresses }, steps: [instructions…] (each a legacy transaction: the table's creation and as
141
+ * many additions as it takes), final: the instructions with the table's deactivation after them }. null when even that
142
+ * does not fit (more accounts than a transaction can lock, or instructions too long).
143
+ */
144
+ export function planTable({ payer, blockhash, slot, instructions, first = [] }) {
145
+ const { address, instruction: create } = createTableIx({ authority: payer, slot });
146
+ // `first` (js/play: the project's address) goes in the table's creation step, so a table whose later steps were
147
+ // never sent still shows whose it is.
148
+ const addresses = [...new Set([...first, ...tableAccounts(instructions)])];
149
+ if (addresses.length > TABLE_MAX_ADDRESSES) return null;
150
+ const table = { address, addresses };
151
+ const final = [...instructions, deactivateTableIx({ table: address, authority: payer })];
152
+ // A transaction locks at most 64 accounts, its programs and those it loads from a table included.
153
+ 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;
155
+ const steps = [];
156
+ let step = [create];
157
+ let left = addresses;
158
+ while (left.length) {
159
+ // As many addresses as fit this step's transaction.
160
+ 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--;
162
+ if (n === 0) {
163
+ // Not one address fits beside the table's creation: the creation goes alone. Not one fits a transaction of its
164
+ // own: no table can be made for this payer.
165
+ if (!step.length) return null;
166
+ steps.push(step);
167
+ step = [];
168
+ continue;
169
+ }
170
+ steps.push([...step, extendTableIx({ table: address, authority: payer, addresses: left.slice(0, n) })]);
171
+ left = left.slice(n);
172
+ step = [];
173
+ }
174
+ return { table, steps, final };
175
+ }
176
+
177
+ /**
178
+ * How `instructions` from `payer` can be sent, decided before anyone signs:
179
+ * { legacy: true } they fit a legacy transaction;
180
+ * { table: true, first: [] } they fit one v0 transaction with a lookup table of their accounts,
181
+ * a compressed NFT's prove_leaf included (one transaction: nobody can
182
+ * prove its leaf again between the proof and the move);
183
+ * { first, rest, table } only when they don't: `first`, the instructions `alone` picks (each
184
+ * compressed NFT's prove_leaf), each sent alone with the transaction's
185
+ * compute-budget instructions, after the table is made (`table`: the
186
+ * rest goes as v0 with one) or before a legacy rest.
187
+ * null when nothing fits: more accounts than a transaction locks, or instructions too long even as v0.
188
+ */
189
+ 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;
198
+ }
199
+
200
+ const pause = (ms) => new Promise((r) => setTimeout(r, ms));
201
+ /** The tables this page made, by address: two sends never name the same one, even in one slot. */
202
+ const madeHere = new Set();
203
+
204
+ /**
205
+ * A recent slot to make `payer`'s next table at: the newest of the finalized SlotHashes (the program takes only a slot
206
+ * they hold) whose table this page hasn't made and that isn't on the chain.
207
+ */
208
+ async function freshSlot(rpc, payer) {
209
+ const recent = slotHashes((await rpc.account(SYSVAR_SLOT_HASHES, 'finalized'))?.data);
210
+ for (const slot of recent) {
211
+ const [address] = tableAddress(payer, slot);
212
+ if (madeHere.has(address) || (await rpc.account(address))) continue;
213
+ madeHere.add(address);
214
+ return slot;
215
+ }
216
+ throw new Error('The network named no recent slot to make a lookup table at: try again in a moment.');
217
+ }
218
+
219
+ /**
220
+ * Sends `instructions` paid by `payer` (the only signer of a table's own transactions) and confirms it, as `sendPlan`
221
+ * decides before anything is signed: legacy when it fits; else one v0 transaction with a lookup table `payer` makes
222
+ * first (`planTable`); else the table first, then each compressed NFT's prove_leaf alone, then the rest as v0 (or the
223
+ * proofs then a legacy rest, when that fits with no table). The v0 transaction is compiled against the table as the
224
+ * 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`:
226
+ * addresses a table holds first (`planTable`). Throws a
227
+ * plain sentence when it can't be sent. Returns { signature (the last transaction's), table (its address, or null),
228
+ * steps (the signatures of the transactions before it) }.
229
+ */
230
+ export async function sendFitting({ rpc, payer, instructions, sign, table = true, alone = () => false, first = [], sleep = pause }) {
231
+ const sendOne = async (build) => {
232
+ const { blockhash, lastValidBlockHeight } = await rpc.latestBlockhash();
233
+ const message = build(blockhash);
234
+ const signature = await rpc.send(await sign(message));
235
+ await rpc.confirm?.(signature, { lastValidBlockHeight });
236
+ return signature;
237
+ };
238
+ const plain = (list) => sendOne((bh) => compileMessage({ payer, blockhash: bh, instructions: list }));
239
+ const { blockhash } = await rpc.latestBlockhash();
240
+ 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.`);
242
+ 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.`);
244
+ const steps = [];
245
+ const proofs = async () => {
246
+ for (const ix of plan.first) steps.push(await plain([...plan.budget, ix]));
247
+ };
248
+ if (!plan.table) {
249
+ await proofs();
250
+ return { signature: await plain(plan.rest), table: null, steps };
251
+ }
252
+ const made = planTable({ payer, blockhash, slot: await freshSlot(rpc, payer), instructions: plan.rest, first });
253
+ if (!made) throw new Error('This transaction is too large to send even as a v0 one with a lookup table.');
254
+ for (const step of made.steps) steps.push(await plain(step));
255
+ // A table's new addresses are usable from the slot after the one that added them; the transaction names them by
256
+ // where the chain holds them.
257
+ let held = null;
258
+ let usable = false;
259
+ for (let i = 0; i < 40 && !usable; i++) {
260
+ 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);
262
+ if (!usable) await sleep(400);
263
+ }
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.`);
265
+ // 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.`);
267
+ await proofs();
268
+ const tables = [{ address: made.table.address, addresses: held.addresses }];
269
+ const message = compileFitting({ payer, blockhash, instructions: made.final, tables });
270
+ 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 }));
272
+ return { signature, table: made.table.address, steps };
273
+ }
@@ -0,0 +1,96 @@
1
+ // What a token mint is: its program, decimals, supply and authorities, plus a name, ticker and metadata link from
2
+ // Metaplex token metadata or Token-2022's metadata extension. Used to link a token a project's owner already has and to
3
+ // price a shop in any token (classic SPL or Token-2022: the loot program takes both).
4
+ import { encodeBase58 } from './base58.js';
5
+ import { fromUtf8, readUintLE } from './bytes.js';
6
+ import { NATIVE_MINT, TOKEN_2022_PROGRAM, TOKEN_PROGRAM } from './pda.js';
7
+ import { MINT_BYTES, TOKEN_ACCOUNT_BYTES } from './token.js';
8
+
9
+ /** Where Token-2022 writes what an account with extensions is (1 a mint, 2 a token account): right after a token
10
+ * account's classic bytes. */
11
+ const ACCOUNT_TYPE_OFFSET = TOKEN_ACCOUNT_BYTES;
12
+ const ACCOUNT_TYPE_MINT = 1;
13
+ const TOKEN_METADATA_EXTENSION = 19;
14
+
15
+ function option(data, at) {
16
+ return readUintLE(data, at, 4) === 1n ? encodeBase58(data.subarray(at + 4, at + 36)) : null;
17
+ }
18
+
19
+ /** A Borsh string padded with NULs, as Metaplex stores names. */
20
+ function paddedString(data, at) {
21
+ const length = Number(readUintLE(data, at, 4));
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();
24
+ return { value, next: at + 4 + length };
25
+ }
26
+
27
+ /** Metaplex token metadata: key, update authority and mint come before name, symbol and uri. */
28
+ export function parseMetaplexMetadata(data) {
29
+ const mint = encodeBase58(data.subarray(33, 65));
30
+ const name = paddedString(data, 65);
31
+ const symbol = paddedString(data, name.next);
32
+ const uri = paddedString(data, symbol.next);
33
+ return { mint, name: name.value, symbol: symbol.value, uri: uri.value };
34
+ }
35
+
36
+ /** Token-2022's TokenMetadata extension, if the mint carries one. */
37
+ function token2022Metadata(data) {
38
+ if (data.length <= ACCOUNT_TYPE_OFFSET || data[ACCOUNT_TYPE_OFFSET] !== ACCOUNT_TYPE_MINT) return null;
39
+ let at = ACCOUNT_TYPE_OFFSET + 1;
40
+ while (at + 4 <= data.length) {
41
+ const type = Number(readUintLE(data, at, 2));
42
+ const length = Number(readUintLE(data, at + 2, 2));
43
+ at += 4;
44
+ if (type === TOKEN_METADATA_EXTENSION) {
45
+ const name = paddedString(data, at + 64);
46
+ const symbol = paddedString(data, name.next);
47
+ const uri = paddedString(data, symbol.next);
48
+ return { name: name.value, symbol: symbol.value, uri: uri.value };
49
+ }
50
+ if (type === 0) break;
51
+ at += length;
52
+ }
53
+ return null;
54
+ }
55
+
56
+ /**
57
+ * A mint account read: `owner` is the account's program. Returns null when it is not a mint: a classic mint is exactly
58
+ * 82 bytes; a Token-2022 mint is 82 bytes, or longer than a token account with its account type saying mint (a
59
+ * Token-2022 token account with extensions is longer too, and says it is an account).
60
+ * metadataData: the Metaplex metadata account's data, when there is one.
61
+ */
62
+ export function parseMint(address, owner, data, metadataData = null) {
63
+ const program = owner === TOKEN_PROGRAM ? 'spl-token' : owner === TOKEN_2022_PROGRAM ? 'token-2022' : null;
64
+ 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);
66
+ if (!mintLayout || data[45] !== 1) return null;
67
+ let meta = null;
68
+ if (metadataData) {
69
+ try {
70
+ const m = parseMetaplexMetadata(metadataData);
71
+ if (m.mint === address) meta = m;
72
+ } catch {
73
+ meta = null;
74
+ }
75
+ }
76
+ if (!meta && program === 'token-2022') {
77
+ try {
78
+ meta = token2022Metadata(data);
79
+ } catch {
80
+ meta = null;
81
+ }
82
+ }
83
+ const native = address === NATIVE_MINT;
84
+ return {
85
+ mint: address,
86
+ program,
87
+ native,
88
+ decimals: data[44],
89
+ supply: readUintLE(data, 36, 8).toString(),
90
+ mintAuthority: option(data, 0),
91
+ freezeAuthority: option(data, 46),
92
+ name: native ? 'Solana' : meta?.name || null,
93
+ symbol: native ? 'SOL' : meta?.symbol || null,
94
+ uri: meta?.uri || null,
95
+ };
96
+ }
@@ -0,0 +1,66 @@
1
+ // Program-derived addresses and associated token accounts, derived exactly as `PublicKey.findProgramAddressSync`
2
+ // derives them.
3
+ import { addressBytes, encodeBase58 } from './base58.js';
4
+ import { concat, utf8 } from './bytes.js';
5
+ import { isOnCurve } from './curve.js';
6
+ import { sha256 } from './sha256.js';
7
+
8
+ export const SYSTEM_PROGRAM = '11111111111111111111111111111111';
9
+ export const TOKEN_PROGRAM = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA';
10
+ export const TOKEN_2022_PROGRAM = 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb';
11
+ export const ASSOCIATED_TOKEN_PROGRAM = 'ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL';
12
+ export const COMPUTE_BUDGET_PROGRAM = 'ComputeBudget111111111111111111111111111111';
13
+ export const METAPLEX_METADATA_PROGRAM = 'metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s';
14
+ /** Metaplex Core: the program of Core assets and collections. */
15
+ export const MPL_CORE_PROGRAM = 'CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d';
16
+ export const SYSVAR_INSTRUCTIONS = 'Sysvar1nstructions1111111111111111111111111';
17
+ export const SYSVAR_SLOT_HASHES = 'SysvarS1otHashes111111111111111111111111111';
18
+ /** Wrapped SOL: a shop priced in SOL holds this mint, and players pay plain SOL. */
19
+ export const NATIVE_MINT = 'So11111111111111111111111111111111111111112';
20
+
21
+ const MARKER = utf8('ProgramDerivedAddress');
22
+ const MAX_SEED_BYTES = 32;
23
+ const MAX_SEEDS = 16;
24
+
25
+ /** The address for these seeds (bump included), or null when it lands on the curve (not a valid PDA). */
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');
28
+ const hash = sha256(concat(...seeds, addressBytes(programId), MARKER));
29
+ return isOnCurve(hash) ? null : encodeBase58(hash);
30
+ }
31
+
32
+ /** Addresses found already: the curve check is slow in plain BigInt, and the same few PDAs are asked for again. */
33
+ const found = new Map();
34
+
35
+ /** The canonical PDA and its bump: the highest bump whose address is off the curve. */
36
+ export function findProgramAddress(seeds, programId) {
37
+ const key = `${programId}:${seeds.map((s) => Array.from(s).join(',')).join('|')}`;
38
+ if (found.has(key)) return found.get(key);
39
+ for (let bump = 255; bump >= 0; bump--) {
40
+ const address = createProgramAddress([...seeds, Uint8Array.of(bump)], programId);
41
+ if (address) {
42
+ if (found.size >= 20_000) found.clear();
43
+ found.set(key, [address, bump]);
44
+ return [address, bump];
45
+ }
46
+ }
47
+ throw new Error('No program address for these seeds');
48
+ }
49
+
50
+ /** The PDA address alone. */
51
+ export const pdaOf = (seeds, programId) => findProgramAddress(seeds, programId)[0];
52
+
53
+ /** `owner`'s associated token account for `mint` (the owner may itself be a PDA). */
54
+ export function associatedTokenAddress(owner, mint, tokenProgram = TOKEN_PROGRAM) {
55
+ return pdaOf([addressBytes(owner), addressBytes(tokenProgram), addressBytes(mint)], ASSOCIATED_TOKEN_PROGRAM);
56
+ }
57
+
58
+ /**
59
+ * The address System's `CreateAccountWithSeed` makes from `base`, a `seed` text (32 bytes at most) and the program that
60
+ * will own it: sha256(base | seed | owner). Only `base` signs for it, so no new key is needed.
61
+ */
62
+ export function addressWithSeed(base, seed, owner) {
63
+ const bytes = utf8(seed);
64
+ if (bytes.length > MAX_SEED_BYTES) throw new Error('A seed is 32 bytes at most');
65
+ return encodeBase58(sha256(concat(addressBytes(base), bytes, addressBytes(owner))));
66
+ }
@@ -0,0 +1,102 @@
1
+ // Instructions for the built-in programs, as plain data. An instruction is
2
+ // { programId: base58, keys: [{ pubkey, isSigner, isWritable }], data: Uint8Array }
3
+ // shared by browser apps and Node, which both serialize it with chain/tx.js.
4
+ import { addressBytes } from './base58.js';
5
+ import { concat, uintLE } from './bytes.js';
6
+ import {
7
+ ASSOCIATED_TOKEN_PROGRAM,
8
+ associatedTokenAddress,
9
+ COMPUTE_BUDGET_PROGRAM,
10
+ NATIVE_MINT,
11
+ SYSTEM_PROGRAM,
12
+ TOKEN_PROGRAM,
13
+ } from './pda.js';
14
+
15
+ const SYSTEM_TRANSFER = 2;
16
+ const REQUEST_HEAP_FRAME = 1;
17
+ const SET_COMPUTE_UNIT_LIMIT = 2;
18
+ const SET_COMPUTE_UNIT_PRICE = 3;
19
+ const ATA_CREATE_IDEMPOTENT = 1;
20
+ const TOKEN_CLOSE_ACCOUNT = 9;
21
+ const TOKEN_SYNC_NATIVE = 17;
22
+
23
+ /** An account as an instruction lists it. */
24
+ export const meta = (pubkey, isSigner, isWritable) => {
25
+ addressBytes(pubkey);
26
+ return { pubkey, isSigner, isWritable };
27
+ };
28
+
29
+ export function transferLamports(from, to, lamports) {
30
+ return {
31
+ programId: SYSTEM_PROGRAM,
32
+ keys: [meta(from, true, true), meta(to, false, true)],
33
+ data: concat(uintLE(SYSTEM_TRANSFER, 4), uintLE(lamports, 8)),
34
+ };
35
+ }
36
+
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) => ({
39
+ programId: COMPUTE_BUDGET_PROGRAM,
40
+ keys: [],
41
+ data: concat(Uint8Array.of(REQUEST_HEAP_FRAME), uintLE(bytes, 4)),
42
+ });
43
+
44
+ export const computeUnitLimit = (units) => ({
45
+ programId: COMPUTE_BUDGET_PROGRAM,
46
+ keys: [],
47
+ data: concat(Uint8Array.of(SET_COMPUTE_UNIT_LIMIT), uintLE(units, 4)),
48
+ });
49
+
50
+ export const computeUnitPrice = (microLamports) => ({
51
+ programId: COMPUTE_BUDGET_PROGRAM,
52
+ keys: [],
53
+ data: concat(Uint8Array.of(SET_COMPUTE_UNIT_PRICE), uintLE(microLamports, 8)),
54
+ });
55
+
56
+ /** Create `owner`'s token account for `mint` unless it already exists; `payer` pays the rent. */
57
+ export function createAssociatedTokenAccountIdempotent(payer, owner, mint, tokenProgram = TOKEN_PROGRAM) {
58
+ return {
59
+ programId: ASSOCIATED_TOKEN_PROGRAM,
60
+ keys: [
61
+ meta(payer, true, true),
62
+ meta(associatedTokenAddress(owner, mint, tokenProgram), false, true),
63
+ meta(owner, false, false),
64
+ meta(mint, false, false),
65
+ meta(SYSTEM_PROGRAM, false, false),
66
+ meta(tokenProgram, false, false),
67
+ ],
68
+ data: Uint8Array.of(ATA_CREATE_IDEMPOTENT),
69
+ };
70
+ }
71
+
72
+ export const syncNative = (account) => ({
73
+ programId: TOKEN_PROGRAM,
74
+ keys: [meta(account, false, true)],
75
+ data: Uint8Array.of(TOKEN_SYNC_NATIVE),
76
+ });
77
+
78
+ export const closeTokenAccount = (account, destination, owner) => ({
79
+ programId: TOKEN_PROGRAM,
80
+ keys: [meta(account, false, true), meta(destination, false, true), meta(owner, true, false)],
81
+ data: Uint8Array.of(TOKEN_CLOSE_ACCOUNT),
82
+ });
83
+
84
+ /**
85
+ * A purchase from a shop priced in SOL: wrap exactly the price into the player's wrapped-SOL account first and close
86
+ * that account after, so the player pays and keeps plain SOL. Other mints pass through. Compute-budget instructions
87
+ * stay in front.
88
+ */
89
+ export function withSolWrap(player, mint, price, instructions) {
90
+ if (mint !== NATIVE_MINT) return instructions;
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);
94
+ return [
95
+ ...budget,
96
+ createAssociatedTokenAccountIdempotent(player, player, NATIVE_MINT),
97
+ transferLamports(player, wrapped, price),
98
+ syncNative(wrapped),
99
+ ...rest,
100
+ closeTokenAccount(wrapped, player, player),
101
+ ];
102
+ }