@xgamesfun/solana 0.27.1 → 0.28.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 (39) 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/client/idl/loot.js +1 -1
  12. package/js/client/idl/loot.json +2 -2
  13. package/js/economy/check.mjs +2 -2
  14. package/js/economy/cost.mjs +20 -4
  15. package/js/economy/explain.mjs +3 -3
  16. package/js/economy/guide.mjs +2 -2
  17. package/js/economy/live.mjs +1 -1
  18. package/js/economy/plan.mjs +34 -6
  19. package/js/economy/review.mjs +1 -1
  20. package/js/economy/schema.json +1 -1
  21. package/js/economy/txsize.mjs +3 -2
  22. package/js/mcp/server.mjs +3 -3
  23. package/js/mcp/tools.mjs +50 -13
  24. package/js/play/index.js +21 -7
  25. package/package.json +5 -1
  26. package/release.json +6 -6
  27. package/skills/xsolana-economy/SKILL.md +58 -19
  28. package/skills/xsolana-economy/references/after-launch.md +43 -12
  29. package/skills/xsolana-economy/references/apps.md +1 -1
  30. package/skills/xsolana-economy/references/sentence-to-spec.md +3 -3
  31. package/skills/xsolana-economy/references/telling-the-person.md +18 -11
  32. package/skills/xsolana-economy/references/trust-and-cheating.md +4 -2
  33. package/types/approve/keyfile.d.mts +36 -0
  34. package/types/chain/lookup.d.ts +12 -0
  35. package/types/chain/programs.d.ts +22 -0
  36. package/types/chain/rpc.d.ts +15 -1
  37. package/types/economy/plan.d.mts +21 -4
  38. package/types/mcp/server.d.mts +4 -3
  39. package/types/mcp/tools.d.mts +14 -5
package/js/chain/rpc.js CHANGED
@@ -2,6 +2,7 @@
2
2
  // (devnet: about 100 requests per 10 s per IP, 40 for any one method), so 429s are retried with backoff, and so are a
3
3
  // gateway's passing failures (502, 503, 504).
4
4
  import { fromBase64, toBase64 } from './bytes.js';
5
+ import { transactionId } from './tx.js';
5
6
 
6
7
  // The options of a call made without any: read-only, and typed so the generated declarations take any option.
7
8
  const NO_OPTIONS = Object.freeze(/** @type {Record<string, any>} */ ({}));
@@ -21,11 +22,19 @@ const BEHIND = [-32005, -32016];
21
22
 
22
23
  /** A JSON-RPC failure, with the node's own message (and logs for a failed simulation). */
23
24
  export class RpcError extends Error {
24
- constructor(message, { code, logs } = NO_OPTIONS) {
25
+ /**
26
+ * `code`: the node's own error code, when a node answered. `logs`: a failed dry run's program logs. `err`: the
27
+ * node's own name for why a transaction was refused ("BlockhashNotFound", "AlreadyProcessed", or the program's
28
+ * error as the chain writes it). `maybeTaken`: a send whose earlier try got no answer, so a node may have taken the
29
+ * transaction although the last try was refused.
30
+ */
31
+ constructor(message, { code, logs, err, maybeTaken } = NO_OPTIONS) {
25
32
  super(message);
26
33
  this.name = 'RpcError';
27
34
  this.code = code;
28
35
  this.logs = logs;
36
+ this.err = err;
37
+ this.maybeTaken = Boolean(maybeTaken);
29
38
  }
30
39
  }
31
40
 
@@ -72,10 +81,27 @@ export function programError(err, logs) {
72
81
  return last ?? (typeof err === 'string' ? err : JSON.stringify(err));
73
82
  }
74
83
 
84
+ /**
85
+ * Whether a send was refused because the cluster has this very transaction already (it was sent before): the node's
86
+ * own word for it (`err`), or, from a node that gives only a sentence, its sentence. Never a program's log line: a
87
+ * dry run that failed inside a program is that program's refusal, whatever its words.
88
+ */
89
+ export const alreadyProcessed = error =>
90
+ error?.err === 'AlreadyProcessed' ||
91
+ (error?.err === undefined &&
92
+ !error?.logs?.length &&
93
+ /this transaction has already been processed/i.test(String(error?.message ?? '')));
94
+
95
+ /** Whether a send's refusal is the dry run not knowing the transaction's blockhash. */
96
+ const blockhashNotFound = error =>
97
+ error?.data?.err === 'BlockhashNotFound' || /blockhash not found/i.test(String(error?.message ?? ''));
98
+
75
99
  /** A client for one RPC URL. `fetchImpl` and `sleepImpl` are for tests. */
76
100
  export function rpcClient(url, { fetchImpl = globalThis.fetch, sleepImpl = sleep } = {}) {
77
101
  let id = 0;
78
102
  async function call(method, params = []) {
103
+ // A try that got no answer (the connection dropped, a gateway failed): a node may have taken what it carried.
104
+ let unanswered = false;
79
105
  for (let attempt = 0; ; attempt++) {
80
106
  let response;
81
107
  try {
@@ -86,6 +112,7 @@ export function rpcClient(url, { fetchImpl = globalThis.fetch, sleepImpl = sleep
86
112
  signal: AbortSignal.timeout(TIMEOUT_MS),
87
113
  });
88
114
  } catch (error) {
115
+ unanswered = true;
89
116
  if (attempt < RETRIES) {
90
117
  await sleepImpl(500 * 2 ** attempt);
91
118
  continue;
@@ -98,6 +125,7 @@ export function rpcClient(url, { fetchImpl = globalThis.fetch, sleepImpl = sleep
98
125
  continue;
99
126
  }
100
127
  // Sending the same signed transaction again is safe: it has one signature, so it lands once at most.
128
+ if (PASSING.includes(response.status)) unanswered = true;
101
129
  if (PASSING.includes(response.status) && attempt < RETRIES) {
102
130
  await sleepImpl(500 * 2 ** attempt);
103
131
  continue;
@@ -115,13 +143,25 @@ export function rpcClient(url, { fetchImpl = globalThis.fetch, sleepImpl = sleep
115
143
  await sleepImpl(500 * 2 ** attempt);
116
144
  continue;
117
145
  }
146
+ // A send refused in its dry run for a blockhash this node has not seen yet ("Blockhash not found"): the node
147
+ // that gave the blockhash was a moment ahead of this one. Asked again, it (or another) knows it. A blockhash
148
+ // that is truly gone is refused the same way each time, and the last refusal is the answer.
149
+ if (method === 'sendTransaction' && blockhashNotFound(body?.error) && attempt < RETRIES) {
150
+ await sleepImpl(500 * 2 ** attempt);
151
+ continue;
152
+ }
118
153
  // A send whose preflight failed ("Transaction simulation failed: … custom program error: 0x17b5") carries the
119
154
  // logs, where the program says why in words.
120
155
  const logs = body?.error?.data?.logs;
121
156
  const said = logs?.length
122
157
  ? `The network refused this in a dry run: ${programError(body.error.data.err, logs)}`
123
158
  : message;
124
- throw new RpcError(said, { code: body?.error?.code, logs });
159
+ throw new RpcError(said, {
160
+ code: body?.error?.code,
161
+ logs,
162
+ err: body?.error?.data?.err,
163
+ maybeTaken: method === 'sendTransaction' && unanswered,
164
+ });
125
165
  }
126
166
  return body.result;
127
167
  }
@@ -211,10 +251,17 @@ export function rpcClient(url, { fetchImpl = globalThis.fetch, sleepImpl = sleep
211
251
  };
212
252
  },
213
253
  async send(wire) {
214
- return call('sendTransaction', [
215
- toBase64(wire),
216
- { encoding: 'base64', preflightCommitment: 'confirmed', maxRetries: 3 },
217
- ]);
254
+ try {
255
+ return await call('sendTransaction', [
256
+ toBase64(wire),
257
+ { encoding: 'base64', preflightCommitment: 'confirmed', maxRetries: 3 },
258
+ ]);
259
+ } catch (e) {
260
+ // A transaction the cluster has taken already is refused as already processed: that is what a second send
261
+ // gets when the answer to the first was lost on its way back. It was sent; its signature is its first.
262
+ if (alreadyProcessed(e)) return transactionId(wire);
263
+ throw e;
264
+ }
218
265
  },
219
266
  signatureStatus,
220
267
  /** Wait until a sent transaction is confirmed; throws with the program's reason when it failed. */