@xgamesfun/solana 0.27.1 → 0.27.2

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 (37) hide show
  1. package/README.md +34 -17
  2. package/bin/xsolana.mjs +72 -7
  3. package/js/approve/details.mjs +1 -1
  4. package/js/approve/keyfile.mjs +242 -0
  5. package/js/approve/live-check.mjs +5 -2
  6. package/js/approve/server.mjs +94 -13
  7. package/js/approve/with-key.mjs +298 -0
  8. package/js/chain/lookup.js +11 -0
  9. package/js/chain/programs.js +49 -1
  10. package/js/chain/rpc.js +53 -6
  11. package/js/economy/check.mjs +2 -2
  12. package/js/economy/cost.mjs +20 -4
  13. package/js/economy/explain.mjs +3 -3
  14. package/js/economy/guide.mjs +2 -2
  15. package/js/economy/live.mjs +1 -1
  16. package/js/economy/plan.mjs +34 -6
  17. package/js/economy/review.mjs +1 -1
  18. package/js/economy/schema.json +1 -1
  19. package/js/economy/txsize.mjs +3 -2
  20. package/js/mcp/server.mjs +3 -3
  21. package/js/mcp/tools.mjs +50 -13
  22. package/js/play/index.js +21 -7
  23. package/package.json +5 -1
  24. package/release.json +1 -1
  25. package/skills/xsolana-economy/SKILL.md +58 -19
  26. package/skills/xsolana-economy/references/after-launch.md +43 -12
  27. package/skills/xsolana-economy/references/apps.md +1 -1
  28. package/skills/xsolana-economy/references/sentence-to-spec.md +3 -3
  29. package/skills/xsolana-economy/references/telling-the-person.md +18 -11
  30. package/skills/xsolana-economy/references/trust-and-cheating.md +4 -2
  31. package/types/approve/keyfile.d.mts +36 -0
  32. package/types/chain/lookup.d.ts +12 -0
  33. package/types/chain/programs.d.ts +22 -0
  34. package/types/chain/rpc.d.ts +15 -1
  35. package/types/economy/plan.d.mts +21 -4
  36. package/types/mcp/server.d.mts +4 -3
  37. package/types/mcp/tools.d.mts +14 -5
@@ -1,4 +1,6 @@
1
- // `xsolana approve`: a page on 127.0.0.1 where the owner of an economy approves going live in their own wallet. The
1
+ // `xsolana approve --browser`: a page on 127.0.0.1 where the owner of an economy approves going live in a browser
2
+ // wallet, served only when asked for (`xsolana approve --keypair`, js/approve/with-key.mjs, drives this same approval
3
+ // with a key file and no page). The
2
4
  // server builds each transaction (js/economy/plan.mjs), has the fresh keys a setup makes sign their part, and sends
3
5
  // only what it prepared: a wallet's answer must carry the exact message prepared for it and that wallet's valid
4
6
  // signature. Each prepared batch answers once and expires after 90 seconds. What lands is written back into the spec
@@ -11,12 +13,12 @@ import path from 'node:path';
11
13
  import { fileURLToPath } from 'node:url';
12
14
  import { spawn } from 'node:child_process';
13
15
  import { equalBytes, fromBase64, toBase64 } from '../chain/bytes.js';
14
- import { checkCluster, notMainnet, rpcClient } from '../chain/rpc.js';
16
+ import { alreadyProcessed, checkCluster, notMainnet, rpcClient } from '../chain/rpc.js';
15
17
  import { addressBytes, encodeBase58 } from '../chain/base58.js';
16
18
  import { slotHashes, tableAddress } from '../chain/lookup.js';
17
- import { SYSVAR_SLOT_HASHES } from '../chain/pda.js';
19
+ import { COMPUTE_BUDGET_PROGRAM, SYSVAR_SLOT_HASHES } from '../chain/pda.js';
18
20
  import { MINT_BYTES } from '../chain/token.js';
19
- import { compileMessage, readTransaction, wireTransaction } from '../chain/tx.js';
21
+ import { compileMessage, readMessage, readTransaction, wireTransaction } from '../chain/tx.js';
20
22
  import { accountKind, decode } from '../client/accounts.js';
21
23
  import { LOOT_PROGRAM, pda } from '../client/pda.js';
22
24
  import { check, describe } from '../economy/check.mjs';
@@ -221,7 +223,7 @@ export async function createApproval({
221
223
  if (!(e instanceof MintMismatch)) throw e;
222
224
  throw new ApproveError(
223
225
  409,
224
- `${e.message} Nothing was sent: correct ${path.basename(file)}, then run \`xsolana approve\` again.`,
226
+ `${e.message} Nothing was sent: correct ${path.basename(file)}, then run the same approve command again.`,
225
227
  );
226
228
  }
227
229
  try {
@@ -242,9 +244,16 @@ export async function createApproval({
242
244
  return trusted;
243
245
  } catch (e) {
244
246
  if (!(e instanceof LiveMismatch)) throw e;
245
- throw new ApproveError(
246
- 409,
247
- `${e.message} Nothing was sent. ${path.basename(file)}’s live section is not what the chain says: restore the file \`xsolana approve\` wrote, or remove its live section to start a new setup.`,
247
+ // Another key's project: the owner with the wrong key, or a file that came with someone else's live section
248
+ // (the two look the same from here). Anything else: the file is not what the chain says.
249
+ throw Object.assign(
250
+ new ApproveError(
251
+ 409,
252
+ e.notOwner
253
+ ? `${e.message} Nothing was sent. If the project is yours, sign with the key that owns it (its key file, or \`approve --browser\` when a browser wallet owns it): ${path.basename(file)} needs no change, and a new key can change nothing. If it is not yours (the file came from someone else with their live section in it), that section is theirs: only then is it removed, to set up a project of your own.`
254
+ : `${e.message} Nothing was sent. ${path.basename(file)}’s live section is not what the chain says: restore the file as approve wrote it. (Its live section is removed only to start a separate setup of one’s own, which leaves the live project as it is on chain.)`,
255
+ ),
256
+ { notOwner: Boolean(e.notOwner) },
248
257
  );
249
258
  }
250
259
  }
@@ -408,6 +417,9 @@ export async function createApproval({
408
417
  // authority of).
409
418
  project: extra.project ?? null,
410
419
  wallet: n.owner ?? null,
420
+ // The accounts this session made new keys for (a token's mint, the collection, the tree): their addresses are
421
+ // this session's own, new each time, so what is shown twice differs in them and in nothing else.
422
+ fresh: [...keys].map(([name, k]) => ({ name: name.replace(/^new:/, ''), address: k.address })),
411
423
  ...(extra.refused ? { refused: extra.refused } : {}),
412
424
  };
413
425
  }
@@ -474,7 +486,12 @@ export async function createApproval({
474
486
  } catch (e) {
475
487
  if (!(e instanceof ApproveError)) throw e;
476
488
  const v = view(p0, namesOf(model(spec, { now: at }), { now: at, owner: wallet }), { refused: e.message });
477
- return { ...v, remaining: p0.ok ? p0.transactions.length : 0, done: false };
489
+ return {
490
+ ...v,
491
+ ...(e.notOwner ? { notOwner: true } : {}),
492
+ remaining: p0.ok ? p0.transactions.length : 0,
493
+ done: false,
494
+ };
478
495
  }
479
496
  if (!p0.ok || !p0.transactions.length)
480
497
  return { ...view(p0, names(), { project: trusted.project }), remaining: 0, done: p0.ok };
@@ -599,9 +616,26 @@ export async function createApproval({
599
616
  try {
600
617
  await chain.send(wire);
601
618
  } catch (e) {
602
- // The node answered and refused it: it was never taken, so it can never land.
603
- if (e.code !== undefined || !chain.signatureStatus) await forget(signature);
604
- throw e;
619
+ // A refusal alone does not say it was never taken: when the answer to a first send is lost, the same
620
+ // transaction sent again is refused as already processed, and it has landed; or the second try reaches a
621
+ // node a moment behind, which refuses it for a blockhash or an account it has not seen yet. So the chain
622
+ // is asked before the signature is forgotten, and it is forgotten only when it can never land: the chain
623
+ // says it failed, or a node answered this send with a refusal and no earlier try of it went unanswered
624
+ // (`maybeTaken`). Anything else is kept, and looked up before anything is sent again.
625
+ let status = null;
626
+ let read = Boolean(chain.signatureStatus);
627
+ if (read)
628
+ try {
629
+ status = await chain.signatureStatus(signature);
630
+ } catch {
631
+ read = false;
632
+ }
633
+ const taken = landedStatus(status) || (!status?.err && (Boolean(status) || alreadyProcessed(e)));
634
+ if (!taken) {
635
+ if (!chain.signatureStatus || status?.err || (read && e.code !== undefined && !e.maybeTaken))
636
+ await forget(signature);
637
+ throw e;
638
+ }
605
639
  }
606
640
  try {
607
641
  await chain.confirm(signature, { lastValidBlockHeight: batch.lastValidBlockHeight });
@@ -635,6 +669,8 @@ export async function createApproval({
635
669
  project: b.ctx.project,
636
670
  transactions: b.wires.map(toBase64),
637
671
  says: b.batch.map(t => t.steps.map(s => s.says).join('; ')),
672
+ // each transaction's steps, by id and in words: what a signer that showed the steps first holds them against
673
+ steps: b.batch.map(t => t.steps.map(s => ({ id: s.id, says: s.says }))),
638
674
  details: b.details,
639
675
  };
640
676
  }
@@ -892,6 +928,48 @@ const sameText = (a, b) => {
892
928
  return x.length === y.length && timingSafeEqual(x, y);
893
929
  };
894
930
 
931
+ /** What a Compute Budget instruction asks for, by its first byte. */
932
+ const BUDGET_ASKS = {
933
+ 1: 'a heap size',
934
+ 2: 'a compute unit limit',
935
+ 3: 'a compute unit price (a priority fee of its own)',
936
+ 4: 'a limit on the account data loaded',
937
+ };
938
+
939
+ /**
940
+ * What a wallet did to a message it was to sign as it stood, in words for the refusal: " (it added …)" when the
941
+ * message it signed is the prepared one with instructions added (a wallet that adds its own priority fee, or checks
942
+ * of its own), " (it put another blockhash on it)", or '' when the change is anything else or can't be read.
943
+ */
944
+ export function walletChange(ours, theirs) {
945
+ try {
946
+ const [a, b] = [readMessage(ours), readMessage(theirs)];
947
+ const told = m =>
948
+ m.instructions.map(ix => ({
949
+ program: m.keys[ix.programIndex],
950
+ kind: ix.data[0],
951
+ text: `${m.keys[ix.programIndex]} ${ix.accountIndexes.map(i => m.keys[i] ?? `#${i}`).join(',')} ${toBase64(ix.data)}`,
952
+ }));
953
+ const mine = told(a);
954
+ const added = [];
955
+ let at = 0;
956
+ for (const ix of told(b)) {
957
+ if (at < mine.length && ix.text === mine[at].text) at++;
958
+ else added.push(ix);
959
+ }
960
+ if (at !== mine.length || !added.length)
961
+ return a.blockhash !== b.blockhash ? ' (it put another blockhash on it)' : '';
962
+ const what = added.map(ix =>
963
+ ix.program === COMPUTE_BUDGET_PROGRAM
964
+ ? (BUDGET_ASKS[ix.kind] ?? 'a compute budget instruction')
965
+ : `an instruction of the program ${ix.program}`,
966
+ );
967
+ return ` (it added ${[...new Set(what)].join(', ')})`;
968
+ } catch {
969
+ return '';
970
+ }
971
+ }
972
+
895
973
  /** The wallet's signed copies: each the exact prepared message, each with the wallet's valid signature. */
896
974
  function checkSigned(prepared, signed, wallet) {
897
975
  if (!Array.isArray(signed) || signed.length !== prepared.length)
@@ -906,7 +984,10 @@ function checkSigned(prepared, signed, wallet) {
906
984
  throw new ApproveError(400, 'The wallet returned something that is not a transaction.');
907
985
  }
908
986
  if (!equalBytes(mine.message, theirs.message))
909
- throw new ApproveError(400, 'The wallet changed the transaction before signing it, so nothing was sent.');
987
+ throw new ApproveError(
988
+ 400,
989
+ `The wallet changed the transaction before signing it${walletChange(mine.message, theirs.message)}, so nothing was sent: only what was prepared and shown here is ever sent. A wallet that adds to what it signs can’t approve this way: use one that signs a transaction as it stands. (A setup that has not started can be signed by a key file instead: xsolana approve --keypair. One that has started is finished by the key that started it.)`,
990
+ );
910
991
  const slot = mine.signers.indexOf(wallet);
911
992
  if (slot < 0) throw new ApproveError(400, 'This transaction was not prepared for that wallet.');
912
993
  const signature = theirs.signatures[slot];
@@ -0,0 +1,298 @@
1
+ // `xsolana approve --keypair <file>`: going live signed by a key file on this machine, with no browser and no page.
2
+ // It is the same approval the page serves (`createApproval`: the same plan, the same reads of the chain, the same
3
+ // check of every signed transaction, the same record in the spec file), driven from the terminal. What the page shows
4
+ // before its Approve button is printed here first, whole: every step with what it pays and trusts, the warnings, the
5
+ // notice and the legal flags, and then nothing is sent. It is sent when the command is run again with the code the
6
+ // printout ends with (`--yes <code>`), which stands for that printout and no other: whoever runs it (a person, or an
7
+ // agent after the person's yes) sends exactly what was shown. The command never stops to ask: an agent's terminal
8
+ // would hang on a question, or answer it.
9
+ import { createHash } from 'node:crypto';
10
+ import path from 'node:path';
11
+ import { fromBase64, toBase64 } from '../chain/bytes.js';
12
+ import { readTransaction, wireTransaction } from '../chain/tx.js';
13
+ import { readKeyFile } from './keyfile.mjs';
14
+ import { createApproval, devnetChain } from './server.mjs';
15
+
16
+ /** The notice the page has its owner tick before Approve (js/approve/page.html, "Read this first"), word for word. */
17
+ export const NOTICE = [
18
+ 'The loot program has not been independently audited. It and these tools come as is, with no warranty and no guarantee that they are safe: what is sent to the program can be lost. You use them at your own risk.',
19
+ 'Transactions can’t be undone. You are responsible for every one you approve.',
20
+ 'Making sure your economy is legal everywhere it is offered is up to you. Nothing here is legal or financial advice.',
21
+ ];
22
+
23
+ const LAMPORTS = 1_000_000_000n;
24
+ /** Lamports as SOL, to the lamport, with no trailing zeros. */
25
+ function sol(lamports) {
26
+ const n = BigInt(lamports);
27
+ const frac = String(n % LAMPORTS)
28
+ .padStart(9, '0')
29
+ .replace(/0+$/, '');
30
+ return `${n / LAMPORTS}${frac ? `.${frac}` : ''}`;
31
+ }
32
+
33
+ /** SOL as the tools write it ("5.33360349", 5.3336) in lamports. */
34
+ function solToLamports(text) {
35
+ const [whole, frac = ''] = String(text).split('.');
36
+ return BigInt(whole || '0') * LAMPORTS + BigInt(frac.padEnd(9, '0').slice(0, 9) || '0');
37
+ }
38
+
39
+ const count = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
40
+
41
+ /** The key's own signature put into its slot of a prepared transaction, as a wallet's signTransaction does. */
42
+ function signWire(key, b64) {
43
+ const t = readTransaction(fromBase64(b64));
44
+ const slot = t.signers.indexOf(key.address);
45
+ if (slot < 0) throw new Error('A transaction was prepared that this key does not sign: nothing was sent.');
46
+ const signatures = [...t.signatures];
47
+ signatures[slot] = key.sign(t.message);
48
+ return toBase64(wireTransaction(t.message, signatures));
49
+ }
50
+
51
+ /**
52
+ * A time as the steps' lines write one: to the second ("2026-11-11T08:34:11Z"), or, for one years off, by the day and
53
+ * the years it is away ("11 November 2030, 4 years from now").
54
+ */
55
+ const WRITTEN_TIME =
56
+ /\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z\b|\b\d{1,2} (?:January|February|March|April|May|June|July|August|September|October|November|December) \d{4,}\b|\b[\d,]+ years (?:from now|after go-live)\b/g;
57
+
58
+ /**
59
+ * The code that stands for one printout: the whole file (the spec, and its live section with every address it gives
60
+ * the steps: the project, the mints, the collection), the key, every step still to send with what it pays and trusts,
61
+ * and every line the person is to read first. Anything of that changing (an edit to the file, a step landing, the
62
+ * chain saying something new of a token) gives another code, so a yes is never carried over to something that was
63
+ * not shown. Two things in the lines are left out, because they are new at each run and say nothing a person can
64
+ * check: the addresses of the accounts the setup makes new, and a time counted from "publish" (the file's own words
65
+ * for it, "publish+30d", are in the code).
66
+ */
67
+ export function approvalCode(state, spec) {
68
+ let shown = JSON.stringify([
69
+ 'xsolana approve',
70
+ state.cluster,
71
+ state.program,
72
+ state.wallet,
73
+ state.project,
74
+ state.mode,
75
+ state.steps.map(s => [s.id, s.says, s.signer, s.details ?? []]),
76
+ state.warnings,
77
+ state.legal.map(f => f.says),
78
+ state.unwritten,
79
+ state.read ?? null,
80
+ ]);
81
+ // (a new account's address is made anew for each printout: it is named by what it is)
82
+ for (const f of state.fresh ?? []) shown = shown.replaceAll(f.address, `<new ${f.name}>`);
83
+ shown = shown.replace(WRITTEN_TIME, '<a time>');
84
+ // The file as it is, live section and all: what the steps are built from, whether or not a line names it.
85
+ return createHash('sha256').update(JSON.stringify(spec)).update('\n').update(shown).digest('hex').slice(0, 8);
86
+ }
87
+
88
+ /** A new account of the setup in words: "mint/gold" is the mint of the token “gold”. */
89
+ const freshName = name =>
90
+ /^mint[/:]/.test(name)
91
+ ? `the mint of the token “${name.slice(5)}”`
92
+ : name === 'collection'
93
+ ? 'the collection'
94
+ : name === 'tree'
95
+ ? 'the tree of compressed NFTs'
96
+ : name;
97
+
98
+ /** What the page shows before Approve, as lines of text. `balance`: the key's lamports, or null when not read. */
99
+ export function approvalText(state, { keyFile, balance = null }) {
100
+ const lines = [];
101
+ const kind =
102
+ state.mode === 'change'
103
+ ? 'a change to a live economy'
104
+ : state.mode === 'resume'
105
+ ? 'a setup that was cut short goes on'
106
+ : 'a new economy';
107
+ lines.push(
108
+ `“${state.name}” (${state.file}): ${kind}, on ${state.cluster}, on the shared loot program ${state.program}.`,
109
+ );
110
+ lines.push(
111
+ `Signed by the key file ${keyFile}, whose address is ${state.wallet}. The lines below call that address “your wallet”.`,
112
+ );
113
+ if (state.project) lines.push(`Project: ${state.project}`);
114
+ for (const f of state.fresh ?? [])
115
+ lines.push(
116
+ `New, made by this setup: ${freshName(f.name)}, at ${f.address} (a new address each time this is shown).`,
117
+ );
118
+ const about = state.setupSol !== null && state.mode === 'new' ? ` Going live takes about ${state.setupSol} SOL.` : '';
119
+ if (balance !== null) lines.push(`The key holds ${sol(balance)} SOL.${about}`);
120
+ else if (about) lines.push(about.trim());
121
+
122
+ lines.push('', 'Read this first');
123
+ for (const line of [...NOTICE, ...state.legal.map(f => f.says)]) lines.push(`- ${line}`);
124
+ lines.push(
125
+ 'The full text is in DISCLAIMER.md, in the package. Devnet is not real money; real money needs every point above answered first.',
126
+ );
127
+
128
+ if (state.warnings.length) {
129
+ lines.push('', 'Before you approve');
130
+ for (const w of state.warnings) lines.push(`- ${w}`);
131
+ }
132
+ if (state.landed.length) {
133
+ lines.push('', 'Landed already');
134
+ for (const l of state.landed) lines.push(`- ${l.says}`);
135
+ }
136
+ lines.push('', `To send: ${count(state.steps.length, 'step')} in ${count(state.remaining, 'transaction')}`);
137
+ state.steps.forEach((s, i) => {
138
+ const who = s.signer === 'authority' ? ` (the authority signs: ${state.authority})` : '';
139
+ lines.push(`${i + 1}. ${s.says}${who}`);
140
+ for (const d of s.details ?? []) lines.push(` - ${d}`);
141
+ });
142
+ return lines;
143
+ }
144
+
145
+ /**
146
+ * Puts the economy in `file` live with the key in `keyFile`, or goes on with a setup that was cut short, or sends a
147
+ * live economy's change. `yes`: null shows what would be sent and sends nothing (or asks `confirm`, when a caller
148
+ * that has the person in front of it gives one; the command line never does); a code sends when it is the code of
149
+ * what is shown now, and is refused otherwise; true sends without asking (for a script that has shown it its own
150
+ * way). `out(line)` prints. Returns { ok, sent, done, code }:
151
+ * `ok` false when something was refused or failed, `sent` how many transactions landed, `done` whether the file is
152
+ * recorded as live.
153
+ */
154
+ export async function approveWithKey({
155
+ file = 'xsolana.json',
156
+ keyFile,
157
+ yes = null,
158
+ confirm = null,
159
+ chain = devnetChain(),
160
+ out = line => console.log(line),
161
+ now,
162
+ keyReader = readKeyFile,
163
+ approval = createApproval,
164
+ }) {
165
+ const key = await keyReader(keyFile);
166
+ const session = await approval({ file: path.resolve(file), chain, ...(now ? { now } : {}) });
167
+ // How many transactions this run saw land (its own, and any an earlier run sent that it found landed).
168
+ const landed = () => session.state().signatures.length;
169
+ const live = () => !session.spec.live?.partial && Boolean(session.spec.live);
170
+ try {
171
+ let state = await session.stateFor(key.address);
172
+ if (state.refused) {
173
+ out(`Nothing can be sent: ${state.refused}`);
174
+ // (a key that is not the project's: said plainly, so nobody makes a new key or cuts the file to get past it)
175
+ if (state.notOwner)
176
+ out(
177
+ `This key file’s address is ${key.address}: it is not the project’s key. If the project is yours, find the key it went live with: don’t make a new one, and don’t change ${state.file} to get past this.`,
178
+ );
179
+ return { ok: false, sent: 0, done: false, code: null };
180
+ }
181
+ if (!state.remaining) {
182
+ for (const line of state.unwritten ?? []) out(line);
183
+ if (!state.done) {
184
+ for (const w of state.warnings) out(w);
185
+ out(`Nothing can be sent for ${state.file} as it is.`);
186
+ return { ok: false, sent: 0, done: false, code: null };
187
+ }
188
+ out(
189
+ state.unwritten?.length
190
+ ? `Nothing to send for “${state.name}”: the file is as it was.`
191
+ : `Nothing to send: “${state.name}” is live on ${state.cluster} as ${state.file} says.`,
192
+ );
193
+ return { ok: true, sent: 0, done: true, code: null };
194
+ }
195
+
196
+ const balance = chain.account
197
+ ? await chain.account(key.address).then(
198
+ a => BigInt(a?.lamports ?? 0),
199
+ () => null,
200
+ )
201
+ : null;
202
+ const code = approvalCode(state, session.spec);
203
+ for (const line of approvalText(state, { keyFile: key.file, balance })) out(line);
204
+ out('');
205
+ if (balance === 0n) {
206
+ out(
207
+ `Nothing was sent: the key holds no SOL. Send devnet SOL to ${key.address} first (https://faucet.solana.com gives some), then run this again.`,
208
+ );
209
+ return { ok: false, sent: 0, done: false, code };
210
+ }
211
+ // A new setup the key can't pay for would stop part way: it is not started.
212
+ const needs = state.mode === 'new' && state.setupSol !== null ? solToLamports(state.setupSol) : null;
213
+ if (balance !== null && needs !== null && balance < needs) {
214
+ out(
215
+ `Nothing was sent: the key holds ${sol(balance)} SOL, less than the about ${state.setupSol} SOL going live takes. Send ${key.address} at least ${sol(needs - balance)} SOL more, then run this again.`,
216
+ );
217
+ return { ok: false, sent: 0, done: false, code };
218
+ }
219
+ if (yes === null && confirm) {
220
+ const said = await confirm(
221
+ `Send ${count(state.remaining, 'transaction')} from ${key.address}? Type yes to send: `,
222
+ );
223
+ if (!said) {
224
+ out('Nothing was sent.');
225
+ return { ok: true, sent: 0, done: false, code };
226
+ }
227
+ } else if (yes === null) {
228
+ out(
229
+ `Nothing was sent. This is what going on would send: show it to the person whose economy it is, and when they say yes, run the same command with --yes ${code}`,
230
+ );
231
+ return { ok: true, sent: 0, done: false, code };
232
+ } else if (yes !== true && String(yes) !== code) {
233
+ out(
234
+ `Nothing was sent: ${JSON.stringify(String(yes))} is not the code of what is above (the file, the chain or what is left to send has changed since that code was given, or it was mistyped). Read the above again; its code is ${code}.`,
235
+ );
236
+ return { ok: false, sent: 0, done: false, code };
237
+ }
238
+
239
+ // Only what was shown is sent. Each batch is held against the printout before the key signs it: every step it
240
+ // carries was on the printout, in the same words. After each batch, a step that was not on the printout, or a
241
+ // line about a token the chain did not say then, stops it for another read.
242
+ const shown = new Map(state.steps.map(s => [s.id, s.says]));
243
+ const readShown = state.read ?? null;
244
+ const changed = () => {
245
+ const sent = landed();
246
+ out(
247
+ `${sent ? `Stopped after ${count(sent, 'transaction')}` : 'Nothing was sent'}: what is left to send is not what was shown above. Run the command again to read what is left, and go on from there.`,
248
+ );
249
+ return { ok: false, sent, done: false, code };
250
+ };
251
+ for (let turn = 0; state.remaining && turn < 500; turn++) {
252
+ let batch;
253
+ try {
254
+ batch = await session.prepare(key.address, state.read ?? null);
255
+ } catch (e) {
256
+ if (e.status === 403)
257
+ e.message += ' With key files: run this command again with that address’s key file (--keypair).';
258
+ throw e;
259
+ }
260
+ if (!batch.steps?.length || batch.steps.flat().some(step => shown.get(step.id) !== step.says)) return changed();
261
+ for (const says of batch.says) out(`Sending: ${says}`);
262
+ await session.complete(
263
+ key.address,
264
+ batch.id,
265
+ batch.transactions.map(b64 => signWire(key, b64)),
266
+ );
267
+ state = await session.stateFor(key.address);
268
+ if (state.refused) throw new Error(state.refused);
269
+ if (state.steps.some(s => !shown.has(s.id)) || (state.remaining && (state.read ?? null) !== readShown))
270
+ return changed();
271
+ }
272
+ for (const line of state.unwritten ?? []) out(line);
273
+ const sent = landed();
274
+ out(
275
+ live()
276
+ ? `Live on ${state.cluster}: ${count(sent, 'transaction')} landed, and ${state.file} now holds the project’s addresses (project ${session.spec.live.project}).`
277
+ : `${count(sent, 'transaction')} landed, and ${state.file} is not recorded as live yet: run the command again.`,
278
+ );
279
+ return { ok: live(), sent, done: live(), code };
280
+ } catch (e) {
281
+ const sent = landed();
282
+ // A transaction that was sent and is not known to have landed is still looked for: it is not "nothing sent".
283
+ const unknown = session.spec.live?.pending?.length ?? 0;
284
+ const so = [
285
+ sent ? `Stopped after ${count(sent, 'transaction')} landed` : unknown ? 'Stopped' : 'Nothing was sent',
286
+ unknown
287
+ ? ` (${count(unknown, 'more was', 'more were')} sent and ${unknown === 1 ? 'is' : 'are'} not known to have landed yet)`
288
+ : '',
289
+ ].join('');
290
+ out(
291
+ `${so}: ${e.message}${sent || unknown ? ` What is known is recorded in ${path.basename(file)}: run the command again to go on from where it stopped; a transaction still looked for is found first, so nothing is sent twice.` : ''}`,
292
+ );
293
+ return { ok: false, sent, done: false, code: null };
294
+ } finally {
295
+ session.close();
296
+ key.drop();
297
+ }
298
+ }
@@ -166,6 +166,17 @@ export function compileFitting({ payer, blockhash, instructions, tables = [] })
166
166
  return tables.length && sizeOf(v0) <= MAX_TRANSACTION_BYTES ? compileMessage(v0) : null;
167
167
  }
168
168
 
169
+ /**
170
+ * The message for a transaction that has two forms, `led` (the instructions with something put first for a wallet's
171
+ * sake: a compute budget) and `plain` (without it), keeping the kind of transaction `plain` alone would be: legacy
172
+ * when `plain` fits one (led when that fits too), else v0 with `tables` (led when that fits). null when neither fits.
173
+ */
174
+ export function compileKeepingKind({ payer, blockhash, led, plain, tables = [] }) {
175
+ const legacy = instructions => compileFitting({ payer, blockhash, instructions });
176
+ const loaded = instructions => compileFitting({ payer, blockhash, instructions, tables });
177
+ return legacy(led) ?? legacy(plain) ?? loaded(led) ?? loaded(plain);
178
+ }
179
+
169
180
  /**
170
181
  * The plan for sending `instructions` from `payer` as v0 with a table of their accounts made at `slot`, `first` the
171
182
  * addresses it holds first:
@@ -2,7 +2,7 @@
2
2
  // { programId: base58, keys: [{ pubkey, isSigner, isWritable }], data: Uint8Array }
3
3
  // shared by browser apps and Node, which both serialize it with chain/tx.js.
4
4
  import { addressBytes } from './base58.js';
5
- import { concat, uintLE } from './bytes.js';
5
+ import { concat, readUintLE, uintLE } from './bytes.js';
6
6
  import {
7
7
  ASSOCIATED_TOKEN_PROGRAM,
8
8
  associatedTokenAddress,
@@ -53,6 +53,54 @@ export const computeUnitPrice = microLamports => ({
53
53
  data: concat(Uint8Array.of(SET_COMPUTE_UNIT_PRICE), uintLE(microLamports, 8)),
54
54
  });
55
55
 
56
+ /** Whether `ix` sets the transaction's compute unit limit. */
57
+ export const isComputeUnitLimit = ix =>
58
+ ix.programId === COMPUTE_BUDGET_PROGRAM && ix.data[0] === SET_COMPUTE_UNIT_LIMIT;
59
+
60
+ /** Whether `ix` sets the transaction's compute unit price (its priority fee). */
61
+ export const isComputeUnitPrice = ix =>
62
+ ix.programId === COMPUTE_BUDGET_PROGRAM && ix.data[0] === SET_COMPUTE_UNIT_PRICE;
63
+
64
+ /** The compute units a transaction has for each instruction when it sets no limit, and the most it may ask for. */
65
+ const DEFAULT_UNITS_AN_INSTRUCTION = 200_000n;
66
+ const MOST_UNITS = 1_400_000n;
67
+ const MICRO_LAMPORTS = 1_000_000n;
68
+
69
+ /**
70
+ * `instructions` led by the transaction's Compute Budget instructions: its unit limit, its unit price, then any other
71
+ * (a heap frame). A limit or a price it already carries is kept as it is; the one it lacks is added (`units`,
72
+ * `microLamports` a unit). The rest keep their order.
73
+ *
74
+ * A wallet that adds its own priority fee (Phantom does) adds it only to a transaction that sets neither, so one
75
+ * built this way comes back from the wallet with the message it was given.
76
+ */
77
+ export function withComputeBudget(instructions, { units, microLamports }) {
78
+ const budget = instructions.filter(ix => ix.programId === COMPUTE_BUDGET_PROGRAM);
79
+ const limits = budget.filter(isComputeUnitLimit);
80
+ const prices = budget.filter(isComputeUnitPrice);
81
+ return [
82
+ ...(limits.length ? limits : [computeUnitLimit(units)]),
83
+ ...(prices.length ? prices : [computeUnitPrice(microLamports)]),
84
+ ...budget.filter(ix => !isComputeUnitLimit(ix) && !isComputeUnitPrice(ix)),
85
+ ...instructions.filter(ix => ix.programId !== COMPUTE_BUDGET_PROGRAM),
86
+ ];
87
+ }
88
+
89
+ /**
90
+ * The priority fee of a transaction of `instructions`, in lamports: its unit limit times its unit price (in
91
+ * micro-lamports a unit), rounded up to a whole lamport as the network does. 0n when it sets no price. With no limit
92
+ * set, the limit is taken at its most: 200,000 units for each other instruction, 1,400,000 at most.
93
+ */
94
+ export function priorityFee(instructions) {
95
+ const price = instructions.find(isComputeUnitPrice);
96
+ if (!price) return 0n;
97
+ const limit = instructions.find(isComputeUnitLimit);
98
+ const others = BigInt(instructions.filter(ix => ix.programId !== COMPUTE_BUDGET_PROGRAM).length);
99
+ const byDefault = DEFAULT_UNITS_AN_INSTRUCTION * others;
100
+ const units = limit ? readUintLE(limit.data, 1, 4) : byDefault < MOST_UNITS ? byDefault : MOST_UNITS;
101
+ return (units * readUintLE(price.data, 1, 8) + MICRO_LAMPORTS - 1n) / MICRO_LAMPORTS;
102
+ }
103
+
56
104
  /** Create `owner`'s token account for `mint` unless it already exists; `payer` pays the rent. */
57
105
  export function createAssociatedTokenAccountIdempotent(payer, owner, mint, tokenProgram = TOKEN_PROGRAM) {
58
106
  return {