x402-proxy 0.10.7 → 0.10.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -1
- package/README.md +7 -1
- package/dist/Credential-COZQnr1-.js +2055 -0
- package/dist/Mcp-CrCEqLqO.js +10 -0
- package/dist/Sse-ChldYgU7.js +9742 -0
- package/dist/Sse-kCB38G56.js +16482 -0
- package/dist/accounts-DsuvWwph.js +232 -0
- package/dist/accounts-DzvAlQRn.js +5 -0
- package/dist/accounts-IG-Cmrwy.js +229 -0
- package/dist/api-CUzmQvTQ.js +2802 -0
- package/dist/auth-DTzQmnZ_.js +1196 -0
- package/dist/bin/cli.js +585 -242
- package/dist/ccip-Bx-zoUCJ.js +240 -0
- package/dist/ccip-C2k1DD1T.js +153 -0
- package/dist/ccip-C6CQOJYv.js +152 -0
- package/dist/ccip-RZzsZ5Mv.js +156 -0
- package/dist/chain-CafcHffR.js +1997 -0
- package/dist/chain-DwfP5RGZ.js +1968 -0
- package/dist/chunk-DBEY4PJZ.js +16 -0
- package/dist/chunk-DjEMn6fM.js +36 -0
- package/dist/client-Blw2V7LF.js +657 -0
- package/dist/client-C37gWJOZ.js +102 -0
- package/dist/client-CEc4NYAA.js +6388 -0
- package/dist/client-CVDTUY0l.js +5152 -0
- package/dist/config-BUQsit4s.js +3 -0
- package/dist/config-DR1Fs_wL.js +6600 -0
- package/dist/{config-D9wIR3xc.js → config-rvKA3SYT.js} +10 -5
- package/dist/decodeFunctionData-DuFcwhC_.js +4510 -0
- package/dist/decodeFunctionData-JPOUdvil.js +4394 -0
- package/dist/derive-DNUl8LU9.js +9109 -0
- package/dist/dist-C2YO6HSQ.js +6581 -0
- package/dist/dist-DM5_F3r5.js +4 -0
- package/dist/dist-DxJCYyL5.js +1388 -0
- package/dist/hashTypedData-BHmP9dBd.js +859 -0
- package/dist/hashTypedData-CtEdfx4y.js +846 -0
- package/dist/helpers-CuUSw-tH.js +7125 -0
- package/dist/hmac-59IlS_by.js +648 -0
- package/dist/http-BAtucMbS.js +2060 -0
- package/dist/index.d.ts +1903 -9
- package/dist/index.js +18006 -50
- package/dist/index.node-CxkL0OFh.js +3592 -0
- package/dist/index.node-DvmeuZBj.js +3 -0
- package/dist/isAddressEqual-BLrd1Hg1.js +9 -0
- package/dist/isAddressEqual-DsAqfQOD.js +10 -0
- package/dist/localBatchGatewayRequest-C-RPJyDO.js +6260 -0
- package/dist/localBatchGatewayRequest-DOdQ9bR7.js +93 -0
- package/dist/localBatchGatewayRequest-DQkbZaSy.js +6261 -0
- package/dist/parseUnits-CApwcKSD.js +49 -0
- package/dist/parseUnits-cMO2udMe.js +48 -0
- package/dist/schemas-BxMFYNbH.js +1270 -0
- package/dist/secp256k1-BZpiyffY.js +2525 -0
- package/dist/secp256k1-BjenrLl5.js +1877 -0
- package/dist/secp256k1-CLPUX17u.js +3 -0
- package/dist/sendRawTransactionSync-DvSkhZtW.js +3612 -0
- package/dist/server-CSq0IuUq.js +565 -0
- package/dist/setup-BY4J49Lv.js +1110 -0
- package/dist/setup-wMOAgrsN.js +3 -0
- package/dist/sha256-FAs0qeni.js +17 -0
- package/dist/sha3-CYkWM8Xa.js +195 -0
- package/dist/sha3-DbMJRJ3C.js +194 -0
- package/dist/sse-B4LLqBQm.js +408 -0
- package/dist/status-Bu23RjW6.js +3 -0
- package/dist/{status-DihAcUSC.js → status-X21VnGUO.js} +16 -15
- package/dist/stdio-BADqxZdZ.js +85 -0
- package/dist/streamableHttp-BHkJypcI.js +358 -0
- package/dist/tempo-3nttrxgQ.js +17 -0
- package/dist/tempo-DER0P-ul.js +18 -0
- package/dist/types-BEKUz-Mf.js +1240 -0
- package/dist/types-DatK5vR5.js +3 -0
- package/dist/utils-BYjkXZDF.js +444 -0
- package/dist/utils-SeGHMW9O.js +445 -0
- package/dist/wallet-DKVlrR1S.js +3 -0
- package/dist/wallet-DSyht15_.js +17759 -0
- package/package.json +18 -71
- package/dist/config-B_upkJeK.js +0 -66
- package/dist/config-Be35NM5s.js +0 -3
- package/dist/config-J1m-CWXT.js +0 -27
- package/dist/derive-CL6e8K0Z.js +0 -81
- package/dist/openclaw/plugin.d.ts +0 -15
- package/dist/openclaw/plugin.js +0 -2067
- package/dist/openclaw.plugin.json +0 -93
- package/dist/setup-CNyMLnM-.js +0 -197
- package/dist/setup-DTIxPe58.js +0 -3
- package/dist/status-DZlJ4pS7.js +0 -3
- package/dist/wallet-B0S-rma9.js +0 -544
- package/dist/wallet-DBrVZJqe.js +0 -3
- package/openclaw.plugin.json +0 -93
- package/skills/SKILL.md +0 -183
- package/skills/references/library.md +0 -85
- package/skills/references/openclaw-plugin.md +0 -145
package/dist/openclaw/plugin.js
DELETED
|
@@ -1,2067 +0,0 @@
|
|
|
1
|
-
import { n as getHistoryPath, r as loadWalletFile } from "../config-B_upkJeK.js";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
import { SOLANA_ERROR__RPC__TRANSPORT_HTTP_ERROR, address, appendTransactionMessageInstructions, createDefaultRpcTransport, createKeyPairSignerFromBytes, createSolanaRpc, createSolanaRpcFromTransport, createTransactionMessage, getAddressEncoder, getBase64EncodedWireTransaction, getProgramDerivedAddress, isSolanaError, mainnet, partiallySignTransactionMessageWithSigners, pipe, setTransactionMessageComputeUnitLimit, setTransactionMessageComputeUnitPrice, setTransactionMessageFeePayer, setTransactionMessageLifetimeUsingBlockhash } from "@solana/kit";
|
|
5
|
-
import { decodePaymentResponseHeader, wrapFetchWithPayment, x402Client } from "@x402/fetch";
|
|
6
|
-
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
7
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
8
|
-
import { TOKEN_PROGRAM_ADDRESS, findAssociatedTokenPda, getTransferCheckedInstruction } from "@solana-program/token";
|
|
9
|
-
import { ed25519 } from "@noble/curves/ed25519.js";
|
|
10
|
-
import { base58 } from "@scure/base";
|
|
11
|
-
import "@x402/evm";
|
|
12
|
-
import "@x402/evm/exact/client";
|
|
13
|
-
import "viem";
|
|
14
|
-
import { privateKeyToAccount } from "viem/accounts";
|
|
15
|
-
import "viem/chains";
|
|
16
|
-
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
17
|
-
import { hmac } from "@noble/hashes/hmac.js";
|
|
18
|
-
import { sha512 } from "@noble/hashes/sha2.js";
|
|
19
|
-
import { keccak_256 } from "@noble/hashes/sha3.js";
|
|
20
|
-
import { HDKey } from "@scure/bip32";
|
|
21
|
-
import { mnemonicToSeedSync } from "@scure/bip39";
|
|
22
|
-
import "@scure/bip39/wordlists/english.js";
|
|
23
|
-
import { Type } from "@sinclair/typebox";
|
|
24
|
-
import { buildCommand } from "@stricli/core";
|
|
25
|
-
import pc from "picocolors";
|
|
26
|
-
//#region src/handler.ts
|
|
27
|
-
/**
|
|
28
|
-
* Extract the on-chain transaction signature from an x402 payment response header.
|
|
29
|
-
*/
|
|
30
|
-
function extractTxSignature(response) {
|
|
31
|
-
const x402Header = response.headers.get("PAYMENT-RESPONSE") ?? response.headers.get("X-PAYMENT-RESPONSE");
|
|
32
|
-
if (x402Header) try {
|
|
33
|
-
return decodePaymentResponseHeader(x402Header).transaction ?? void 0;
|
|
34
|
-
} catch {}
|
|
35
|
-
const mppHeader = response.headers.get("Payment-Receipt");
|
|
36
|
-
if (mppHeader) try {
|
|
37
|
-
return JSON.parse(Buffer.from(mppHeader, "base64url").toString()).reference ?? void 0;
|
|
38
|
-
} catch {
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Create an x402 proxy handler that wraps fetch with automatic payment.
|
|
44
|
-
*/
|
|
45
|
-
function createX402ProxyHandler(opts) {
|
|
46
|
-
const { client } = opts;
|
|
47
|
-
const paymentQueue = [];
|
|
48
|
-
client.onAfterPaymentCreation(async (hookCtx) => {
|
|
49
|
-
const raw = hookCtx.selectedRequirements.amount;
|
|
50
|
-
paymentQueue.push({
|
|
51
|
-
protocol: "x402",
|
|
52
|
-
network: hookCtx.selectedRequirements.network,
|
|
53
|
-
payTo: hookCtx.selectedRequirements.payTo,
|
|
54
|
-
amount: raw,
|
|
55
|
-
asset: hookCtx.selectedRequirements.asset
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
return {
|
|
59
|
-
x402Fetch: wrapFetchWithPayment(globalThis.fetch, client),
|
|
60
|
-
shiftPayment: () => paymentQueue.shift()
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
const TEMPO_NETWORK = "eip155:4217";
|
|
64
|
-
/**
|
|
65
|
-
* Create an MPP proxy handler using mppx client.
|
|
66
|
-
* Dynamically imports mppx/client to keep startup fast.
|
|
67
|
-
*/
|
|
68
|
-
async function createMppProxyHandler(opts) {
|
|
69
|
-
const { Mppx, tempo } = await import("mppx/client");
|
|
70
|
-
const { privateKeyToAccount } = await import("viem/accounts");
|
|
71
|
-
const { saveSession, clearSession } = await import("../config-B_upkJeK.js").then((n) => n.t);
|
|
72
|
-
const account = privateKeyToAccount(opts.evmKey);
|
|
73
|
-
const maxDeposit = opts.maxDeposit ?? "1";
|
|
74
|
-
const paymentQueue = [];
|
|
75
|
-
let lastChallengeAmount;
|
|
76
|
-
const debug = process.env.X402_PROXY_DEBUG === "1";
|
|
77
|
-
const mppx = Mppx.create({
|
|
78
|
-
methods: [tempo({
|
|
79
|
-
account,
|
|
80
|
-
maxDeposit
|
|
81
|
-
})],
|
|
82
|
-
polyfill: false,
|
|
83
|
-
onChallenge: async (challenge) => {
|
|
84
|
-
const req = challenge.request;
|
|
85
|
-
if (req.amount) lastChallengeAmount = (Number(req.amount) / 10 ** (req.decimals ?? 6)).toString();
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
const payerAddress = account.address;
|
|
89
|
-
function injectPayerHeader(init) {
|
|
90
|
-
const existing = init?.headers instanceof Headers ? Object.fromEntries(init.headers.entries()) : init?.headers ?? {};
|
|
91
|
-
return {
|
|
92
|
-
...init,
|
|
93
|
-
headers: {
|
|
94
|
-
...existing,
|
|
95
|
-
"X-Payer-Address": payerAddress
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
let session;
|
|
100
|
-
let persistedChannelId;
|
|
101
|
-
return {
|
|
102
|
-
async fetch(input, init) {
|
|
103
|
-
const response = await mppx.fetch(typeof input === "string" ? input : input.toString(), injectPayerHeader(init));
|
|
104
|
-
const receiptHeader = response.headers.get("Payment-Receipt");
|
|
105
|
-
if (receiptHeader) {
|
|
106
|
-
try {
|
|
107
|
-
const receipt = JSON.parse(Buffer.from(receiptHeader, "base64url").toString());
|
|
108
|
-
paymentQueue.push({
|
|
109
|
-
protocol: "mpp",
|
|
110
|
-
network: TEMPO_NETWORK,
|
|
111
|
-
amount: lastChallengeAmount,
|
|
112
|
-
receipt
|
|
113
|
-
});
|
|
114
|
-
} catch {
|
|
115
|
-
paymentQueue.push({
|
|
116
|
-
protocol: "mpp",
|
|
117
|
-
network: TEMPO_NETWORK,
|
|
118
|
-
amount: lastChallengeAmount
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
lastChallengeAmount = void 0;
|
|
122
|
-
}
|
|
123
|
-
return response;
|
|
124
|
-
},
|
|
125
|
-
async sse(input, init) {
|
|
126
|
-
session ??= tempo.session({
|
|
127
|
-
account,
|
|
128
|
-
maxDeposit
|
|
129
|
-
});
|
|
130
|
-
const url = typeof input === "string" ? input : input.toString();
|
|
131
|
-
const iterable = await session.sse(url, injectPayerHeader(init));
|
|
132
|
-
if (session.channelId && session.channelId !== persistedChannelId) {
|
|
133
|
-
persistedChannelId = session.channelId;
|
|
134
|
-
if (debug) process.stderr.write(`[x402-proxy] channelId: ${persistedChannelId}\n`);
|
|
135
|
-
try {
|
|
136
|
-
saveSession({
|
|
137
|
-
channelId: session.channelId,
|
|
138
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
139
|
-
});
|
|
140
|
-
} catch {}
|
|
141
|
-
}
|
|
142
|
-
paymentQueue.push({
|
|
143
|
-
protocol: "mpp",
|
|
144
|
-
network: TEMPO_NETWORK,
|
|
145
|
-
intent: "session"
|
|
146
|
-
});
|
|
147
|
-
return iterable;
|
|
148
|
-
},
|
|
149
|
-
shiftPayment: () => paymentQueue.shift(),
|
|
150
|
-
async close() {
|
|
151
|
-
if (session?.opened) {
|
|
152
|
-
const receipt = await session.close();
|
|
153
|
-
try {
|
|
154
|
-
clearSession();
|
|
155
|
-
} catch {}
|
|
156
|
-
if (receipt) {
|
|
157
|
-
const spentUsdc = receipt.spent ? (Number(receipt.spent) / 1e6).toString() : void 0;
|
|
158
|
-
paymentQueue.push({
|
|
159
|
-
protocol: "mpp",
|
|
160
|
-
network: TEMPO_NETWORK,
|
|
161
|
-
intent: "session",
|
|
162
|
-
amount: spentUsdc,
|
|
163
|
-
channelId: session.channelId ?? void 0,
|
|
164
|
-
receipt: {
|
|
165
|
-
method: receipt.method,
|
|
166
|
-
reference: receipt.reference,
|
|
167
|
-
status: receipt.status,
|
|
168
|
-
timestamp: receipt.timestamp,
|
|
169
|
-
acceptedCumulative: receipt.acceptedCumulative,
|
|
170
|
-
txHash: receipt.txHash
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
//#endregion
|
|
179
|
-
//#region src/lib/optimized-svm-scheme.ts
|
|
180
|
-
const MEMO_PROGRAM_ADDRESS = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr";
|
|
181
|
-
const COMPUTE_UNIT_LIMIT = 2e4;
|
|
182
|
-
const COMPUTE_UNIT_PRICE_MICROLAMPORTS = 1n;
|
|
183
|
-
const USDC_MINT$1 = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
184
|
-
const USDC_DECIMALS$1 = 6;
|
|
185
|
-
const MAINNET_RPC_URLS = ["https://api.mainnet.solana.com", "https://public.rpc.solanavibestation.com"];
|
|
186
|
-
/**
|
|
187
|
-
* Create a failover transport that tries each RPC in order.
|
|
188
|
-
* On 429 from one endpoint, immediately tries the next instead of waiting.
|
|
189
|
-
* Each transport gets its own coalescing via createDefaultRpcTransport.
|
|
190
|
-
*/
|
|
191
|
-
function createFailoverTransport(urls) {
|
|
192
|
-
const transports = urls.map((url) => createDefaultRpcTransport({ url }));
|
|
193
|
-
const failover = (async (config) => {
|
|
194
|
-
let lastError;
|
|
195
|
-
for (const transport of transports) try {
|
|
196
|
-
return await transport(config);
|
|
197
|
-
} catch (e) {
|
|
198
|
-
lastError = e;
|
|
199
|
-
if (isSolanaError(e, SOLANA_ERROR__RPC__TRANSPORT_HTTP_ERROR) && e.context.statusCode === 429) continue;
|
|
200
|
-
throw e;
|
|
201
|
-
}
|
|
202
|
-
throw lastError;
|
|
203
|
-
});
|
|
204
|
-
return failover;
|
|
205
|
-
}
|
|
206
|
-
function createRpcClient(customRpcUrl) {
|
|
207
|
-
return createSolanaRpcFromTransport(createFailoverTransport((customRpcUrl ? [customRpcUrl, ...MAINNET_RPC_URLS] : MAINNET_RPC_URLS).map((u) => mainnet(u))));
|
|
208
|
-
}
|
|
209
|
-
/**
|
|
210
|
-
* Optimized ExactSvmScheme that replaces upstream @x402/svm to prevent
|
|
211
|
-
* RPC rate-limit failures on parallel payments.
|
|
212
|
-
*
|
|
213
|
-
* Two optimizations over upstream:
|
|
214
|
-
* 1. Shared RPC client - @solana/kit's built-in request coalescing
|
|
215
|
-
* merges identical getLatestBlockhash calls in the same tick into 1.
|
|
216
|
-
* 2. Hardcoded USDC - skips fetchMint RPC call for USDC (immutable data).
|
|
217
|
-
*/
|
|
218
|
-
var OptimizedSvmScheme = class {
|
|
219
|
-
scheme = "exact";
|
|
220
|
-
rpc;
|
|
221
|
-
constructor(signer, config) {
|
|
222
|
-
this.signer = signer;
|
|
223
|
-
this.rpc = createRpcClient(config?.rpcUrl);
|
|
224
|
-
}
|
|
225
|
-
async createPaymentPayload(x402Version, paymentRequirements) {
|
|
226
|
-
const rpc = this.rpc;
|
|
227
|
-
const asset = paymentRequirements.asset;
|
|
228
|
-
if (asset !== USDC_MINT$1) throw new Error(`Unsupported asset: ${asset}. Only USDC is supported.`);
|
|
229
|
-
const [[sourceATA], [destinationATA]] = await Promise.all([findAssociatedTokenPda({
|
|
230
|
-
mint: asset,
|
|
231
|
-
owner: this.signer.address,
|
|
232
|
-
tokenProgram: TOKEN_PROGRAM_ADDRESS
|
|
233
|
-
}), findAssociatedTokenPda({
|
|
234
|
-
mint: asset,
|
|
235
|
-
owner: paymentRequirements.payTo,
|
|
236
|
-
tokenProgram: TOKEN_PROGRAM_ADDRESS
|
|
237
|
-
})]);
|
|
238
|
-
const transferIx = getTransferCheckedInstruction({
|
|
239
|
-
source: sourceATA,
|
|
240
|
-
mint: asset,
|
|
241
|
-
destination: destinationATA,
|
|
242
|
-
authority: this.signer,
|
|
243
|
-
amount: BigInt(paymentRequirements.amount),
|
|
244
|
-
decimals: USDC_DECIMALS$1
|
|
245
|
-
});
|
|
246
|
-
const feePayer = paymentRequirements.extra?.feePayer;
|
|
247
|
-
if (!feePayer) throw new Error("feePayer is required in paymentRequirements.extra for SVM transactions");
|
|
248
|
-
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
|
|
249
|
-
const nonce = crypto.getRandomValues(new Uint8Array(16));
|
|
250
|
-
const memoIx = {
|
|
251
|
-
programAddress: MEMO_PROGRAM_ADDRESS,
|
|
252
|
-
accounts: [],
|
|
253
|
-
data: new TextEncoder().encode(Array.from(nonce).map((b) => b.toString(16).padStart(2, "0")).join(""))
|
|
254
|
-
};
|
|
255
|
-
return {
|
|
256
|
-
x402Version,
|
|
257
|
-
payload: { transaction: getBase64EncodedWireTransaction(await partiallySignTransactionMessageWithSigners(pipe(createTransactionMessage({ version: 0 }), (tx) => setTransactionMessageComputeUnitPrice(COMPUTE_UNIT_PRICE_MICROLAMPORTS, tx), (tx) => setTransactionMessageComputeUnitLimit(COMPUTE_UNIT_LIMIT, tx), (tx) => setTransactionMessageFeePayer(feePayer, tx), (tx) => appendTransactionMessageInstructions([transferIx, memoIx], tx), (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx)))) }
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
};
|
|
261
|
-
function isMeaningfulInferenceRecord(record) {
|
|
262
|
-
if (record.kind !== "x402_inference") return true;
|
|
263
|
-
if (!record.ok) return true;
|
|
264
|
-
return record.amount != null || record.model != null || record.inputTokens != null || record.outputTokens != null || record.tx != null;
|
|
265
|
-
}
|
|
266
|
-
function appendHistory(historyPath, record) {
|
|
267
|
-
try {
|
|
268
|
-
mkdirSync(dirname(historyPath), { recursive: true });
|
|
269
|
-
appendFileSync(historyPath, `${JSON.stringify(record)}\n`);
|
|
270
|
-
if (existsSync(historyPath)) {
|
|
271
|
-
if (statSync(historyPath).size > 1e3 * 200) {
|
|
272
|
-
const lines = readFileSync(historyPath, "utf-8").trimEnd().split("\n");
|
|
273
|
-
if (lines.length > 1e3) writeFileSync(historyPath, `${lines.slice(-500).join("\n")}\n`);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
} catch {}
|
|
277
|
-
}
|
|
278
|
-
function readHistory(historyPath) {
|
|
279
|
-
try {
|
|
280
|
-
if (!existsSync(historyPath)) return [];
|
|
281
|
-
const content = readFileSync(historyPath, "utf-8").trimEnd();
|
|
282
|
-
if (!content) return [];
|
|
283
|
-
return content.split("\n").flatMap((line) => {
|
|
284
|
-
try {
|
|
285
|
-
const parsed = JSON.parse(line);
|
|
286
|
-
if (typeof parsed.t !== "number" || typeof parsed.kind !== "string") return [];
|
|
287
|
-
const record = parsed;
|
|
288
|
-
return isMeaningfulInferenceRecord(record) ? [record] : [];
|
|
289
|
-
} catch {
|
|
290
|
-
return [];
|
|
291
|
-
}
|
|
292
|
-
});
|
|
293
|
-
} catch {
|
|
294
|
-
return [];
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
function calcSpend(records) {
|
|
298
|
-
const todayStart = /* @__PURE__ */ new Date();
|
|
299
|
-
todayStart.setUTCHours(0, 0, 0, 0);
|
|
300
|
-
const todayMs = todayStart.getTime();
|
|
301
|
-
let today = 0;
|
|
302
|
-
let total = 0;
|
|
303
|
-
let count = 0;
|
|
304
|
-
for (const r of records) {
|
|
305
|
-
if (!r.ok || r.amount == null) continue;
|
|
306
|
-
if (r.token !== "USDC") continue;
|
|
307
|
-
total += r.amount;
|
|
308
|
-
count++;
|
|
309
|
-
if (r.t >= todayMs) today += r.amount;
|
|
310
|
-
}
|
|
311
|
-
return {
|
|
312
|
-
today,
|
|
313
|
-
total,
|
|
314
|
-
count
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
function formatUsdcValue(amount) {
|
|
318
|
-
return new Intl.NumberFormat("en-US", {
|
|
319
|
-
useGrouping: false,
|
|
320
|
-
minimumFractionDigits: 0,
|
|
321
|
-
maximumFractionDigits: 12
|
|
322
|
-
}).format(amount);
|
|
323
|
-
}
|
|
324
|
-
function formatAmount(amount, token) {
|
|
325
|
-
if (token === "USDC") return `${formatUsdcValue(amount)} USDC`;
|
|
326
|
-
if (token === "SOL") return `${amount} SOL`;
|
|
327
|
-
return `${amount} ${token}`;
|
|
328
|
-
}
|
|
329
|
-
const KIND_LABELS = {
|
|
330
|
-
x402_inference: "inference",
|
|
331
|
-
x402_payment: "payment",
|
|
332
|
-
mpp_payment: "mpp payment",
|
|
333
|
-
transfer: "transfer",
|
|
334
|
-
buy: "buy",
|
|
335
|
-
sell: "sell",
|
|
336
|
-
mint: "mint",
|
|
337
|
-
swap: "swap"
|
|
338
|
-
};
|
|
339
|
-
function explorerUrl(net, tx) {
|
|
340
|
-
if (net.startsWith("eip155:")) {
|
|
341
|
-
const chainId = net.split(":")[1];
|
|
342
|
-
if (chainId === "4217") return `https://explore.mainnet.tempo.xyz/tx/${tx}`;
|
|
343
|
-
if (chainId === "8453") return `https://basescan.org/tx/${tx}`;
|
|
344
|
-
return `https://basescan.org/tx/${tx}`;
|
|
345
|
-
}
|
|
346
|
-
return `https://solscan.io/tx/${tx}`;
|
|
347
|
-
}
|
|
348
|
-
/** Strip provider prefix and OpenRouter date suffixes from model IDs.
|
|
349
|
-
* e.g. "minimax/minimax-m2.5-20260211" -> "minimax-m2.5"
|
|
350
|
-
* "moonshotai/kimi-k2.5-0127" -> "kimi-k2.5" */
|
|
351
|
-
function shortModel(model) {
|
|
352
|
-
const parts = model.split("/");
|
|
353
|
-
return parts[parts.length - 1].replace(/-\d{6,8}$/, "").replace(/-\d{4}$/, "");
|
|
354
|
-
}
|
|
355
|
-
function shortNetwork(net) {
|
|
356
|
-
if (net === "eip155:8453") return "base";
|
|
357
|
-
if (net === "eip155:4217") return "tempo";
|
|
358
|
-
if (net.startsWith("eip155:")) return `evm:${net.split(":")[1]}`;
|
|
359
|
-
if (net.startsWith("solana:")) return "sol";
|
|
360
|
-
return net;
|
|
361
|
-
}
|
|
362
|
-
function formatTxLine(r, opts) {
|
|
363
|
-
const time = new Date(r.t).toLocaleTimeString("en-US", {
|
|
364
|
-
hour: "2-digit",
|
|
365
|
-
minute: "2-digit",
|
|
366
|
-
hour12: false,
|
|
367
|
-
timeZone: "UTC"
|
|
368
|
-
});
|
|
369
|
-
const timeStr = r.tx ? `[${time}](${explorerUrl(r.net, r.tx)})` : time;
|
|
370
|
-
const parts = [r.kind === "x402_inference" && r.model ? shortModel(r.model) : KIND_LABELS[r.kind] ?? r.kind];
|
|
371
|
-
if (r.label) parts.push(r.label);
|
|
372
|
-
if (r.ok && r.amount != null && r.token) parts.push(formatAmount(r.amount, r.token));
|
|
373
|
-
else if (r.ok && r.kind === "sell" && r.meta?.pct != null) parts.push(`${r.meta.pct}%`);
|
|
374
|
-
parts.push(shortNetwork(r.net));
|
|
375
|
-
if (opts?.verbose && r.tx) {
|
|
376
|
-
const short = r.tx.length > 20 ? `${r.tx.slice(0, 10)}...${r.tx.slice(-6)}` : r.tx;
|
|
377
|
-
parts.push(short);
|
|
378
|
-
}
|
|
379
|
-
return ` ${timeStr} ${r.ok ? "" : "✗ "}${parts.join(" · ")}`;
|
|
380
|
-
}
|
|
381
|
-
//#endregion
|
|
382
|
-
//#region src/lib/derive.ts
|
|
383
|
-
/**
|
|
384
|
-
* Wallet derivation from BIP-39 mnemonic.
|
|
385
|
-
*
|
|
386
|
-
* A single 24-word mnemonic is the root secret. Both Solana and EVM keypairs
|
|
387
|
-
* are deterministically derived from it.
|
|
388
|
-
*
|
|
389
|
-
* Solana: SLIP-10 Ed25519 at m/44'/501'/0'/0'
|
|
390
|
-
* EVM: BIP-32 secp256k1 at m/44'/60'/0'/0/0
|
|
391
|
-
*
|
|
392
|
-
* Ported from agentbox/packages/openclaw-x402/src/wallet.ts
|
|
393
|
-
*/
|
|
394
|
-
/**
|
|
395
|
-
* SLIP-10 Ed25519 derivation at m/44'/501'/0'/0' (Phantom/Backpack compatible).
|
|
396
|
-
*/
|
|
397
|
-
const enc = new TextEncoder();
|
|
398
|
-
function deriveSolanaKeypair(mnemonic) {
|
|
399
|
-
const seed = mnemonicToSeedSync(mnemonic);
|
|
400
|
-
let I = hmac(sha512, enc.encode("ed25519 seed"), seed);
|
|
401
|
-
let key = I.slice(0, 32);
|
|
402
|
-
let chainCode = I.slice(32);
|
|
403
|
-
for (const index of [
|
|
404
|
-
2147483692,
|
|
405
|
-
2147484149,
|
|
406
|
-
2147483648,
|
|
407
|
-
2147483648
|
|
408
|
-
]) {
|
|
409
|
-
const data = new Uint8Array(37);
|
|
410
|
-
data[0] = 0;
|
|
411
|
-
data.set(key, 1);
|
|
412
|
-
data[33] = index >>> 24 & 255;
|
|
413
|
-
data[34] = index >>> 16 & 255;
|
|
414
|
-
data[35] = index >>> 8 & 255;
|
|
415
|
-
data[36] = index & 255;
|
|
416
|
-
I = hmac(sha512, chainCode, data);
|
|
417
|
-
key = I.slice(0, 32);
|
|
418
|
-
chainCode = I.slice(32);
|
|
419
|
-
}
|
|
420
|
-
const secretKey = new Uint8Array(key);
|
|
421
|
-
const publicKey = ed25519.getPublicKey(secretKey);
|
|
422
|
-
return {
|
|
423
|
-
secretKey,
|
|
424
|
-
publicKey,
|
|
425
|
-
address: base58.encode(publicKey)
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
/**
|
|
429
|
-
* BIP-32 secp256k1 derivation at m/44'/60'/0'/0/0.
|
|
430
|
-
*/
|
|
431
|
-
function deriveEvmKeypair(mnemonic) {
|
|
432
|
-
const seed = mnemonicToSeedSync(mnemonic);
|
|
433
|
-
const derived = HDKey.fromMasterSeed(seed).derive("m/44'/60'/0'/0/0");
|
|
434
|
-
if (!derived.privateKey) throw new Error("Failed to derive EVM private key");
|
|
435
|
-
const privateKey = `0x${Buffer.from(derived.privateKey).toString("hex")}`;
|
|
436
|
-
const hash = keccak_256(secp256k1.getPublicKey(derived.privateKey, false).slice(1));
|
|
437
|
-
return {
|
|
438
|
-
privateKey,
|
|
439
|
-
address: checksumAddress(Buffer.from(hash.slice(-20)).toString("hex"))
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
function checksumAddress(addr) {
|
|
443
|
-
const hash = Buffer.from(keccak_256(enc.encode(addr))).toString("hex");
|
|
444
|
-
let out = "0x";
|
|
445
|
-
for (let i = 0; i < 40; i++) out += Number.parseInt(hash[i], 16) >= 8 ? addr[i].toUpperCase() : addr[i];
|
|
446
|
-
return out;
|
|
447
|
-
}
|
|
448
|
-
//#endregion
|
|
449
|
-
//#region src/lib/resolve-wallet.ts
|
|
450
|
-
/**
|
|
451
|
-
* Resolve wallet keys following the priority cascade:
|
|
452
|
-
* 1. Flags (--evm-key / --solana-key as raw key strings)
|
|
453
|
-
* 2. X402_PROXY_WALLET_EVM_KEY / X402_PROXY_WALLET_SOLANA_KEY env vars
|
|
454
|
-
* 3. X402_PROXY_WALLET_MNEMONIC env var (derives both)
|
|
455
|
-
* 4. ~/.config/x402-proxy/wallet.json (mnemonic file)
|
|
456
|
-
*/
|
|
457
|
-
function resolveWallet(opts) {
|
|
458
|
-
if (opts?.evmKey || opts?.solanaKey) {
|
|
459
|
-
const result = { source: "flag" };
|
|
460
|
-
if (opts.evmKey) {
|
|
461
|
-
const hex = opts.evmKey.startsWith("0x") ? opts.evmKey : `0x${opts.evmKey}`;
|
|
462
|
-
result.evmKey = hex;
|
|
463
|
-
result.evmAddress = privateKeyToAccount(hex).address;
|
|
464
|
-
}
|
|
465
|
-
if (opts.solanaKey) {
|
|
466
|
-
result.solanaKey = parsesolanaKey(opts.solanaKey);
|
|
467
|
-
result.solanaAddress = solanaAddressFromKey(result.solanaKey);
|
|
468
|
-
}
|
|
469
|
-
return result;
|
|
470
|
-
}
|
|
471
|
-
const envEvm = process.env.X402_PROXY_WALLET_EVM_KEY;
|
|
472
|
-
const envSol = process.env.X402_PROXY_WALLET_SOLANA_KEY;
|
|
473
|
-
if (envEvm || envSol) {
|
|
474
|
-
const result = { source: "env" };
|
|
475
|
-
if (envEvm) {
|
|
476
|
-
const hex = envEvm.startsWith("0x") ? envEvm : `0x${envEvm}`;
|
|
477
|
-
result.evmKey = hex;
|
|
478
|
-
result.evmAddress = privateKeyToAccount(hex).address;
|
|
479
|
-
}
|
|
480
|
-
if (envSol) {
|
|
481
|
-
result.solanaKey = parsesolanaKey(envSol);
|
|
482
|
-
result.solanaAddress = solanaAddressFromKey(result.solanaKey);
|
|
483
|
-
}
|
|
484
|
-
return result;
|
|
485
|
-
}
|
|
486
|
-
const envMnemonic = process.env.X402_PROXY_WALLET_MNEMONIC;
|
|
487
|
-
if (envMnemonic) return resolveFromMnemonic(envMnemonic, "mnemonic-env");
|
|
488
|
-
const walletFile = loadWalletFile();
|
|
489
|
-
if (walletFile) return resolveFromMnemonic(walletFile.mnemonic, "wallet-file");
|
|
490
|
-
return { source: "none" };
|
|
491
|
-
}
|
|
492
|
-
function resolveFromMnemonic(mnemonic, source) {
|
|
493
|
-
const evm = deriveEvmKeypair(mnemonic);
|
|
494
|
-
const sol = deriveSolanaKeypair(mnemonic);
|
|
495
|
-
const solanaKey = new Uint8Array(64);
|
|
496
|
-
solanaKey.set(sol.secretKey, 0);
|
|
497
|
-
solanaKey.set(sol.publicKey, 32);
|
|
498
|
-
return {
|
|
499
|
-
evmKey: evm.privateKey,
|
|
500
|
-
evmAddress: evm.address,
|
|
501
|
-
solanaKey,
|
|
502
|
-
solanaAddress: sol.address,
|
|
503
|
-
source
|
|
504
|
-
};
|
|
505
|
-
}
|
|
506
|
-
function parsesolanaKey(input) {
|
|
507
|
-
const trimmed = input.trim();
|
|
508
|
-
if (trimmed.startsWith("[")) {
|
|
509
|
-
const arr = JSON.parse(trimmed);
|
|
510
|
-
return new Uint8Array(arr);
|
|
511
|
-
}
|
|
512
|
-
return base58.decode(trimmed);
|
|
513
|
-
}
|
|
514
|
-
function solanaAddressFromKey(keyBytes) {
|
|
515
|
-
if (keyBytes.length >= 64) return base58.encode(keyBytes.slice(32));
|
|
516
|
-
return base58.encode(ed25519.getPublicKey(keyBytes));
|
|
517
|
-
}
|
|
518
|
-
//#endregion
|
|
519
|
-
//#region src/wallet.ts
|
|
520
|
-
/**
|
|
521
|
-
* Load a Solana keypair from a solana-keygen JSON file.
|
|
522
|
-
* Format: JSON array of 64 numbers [32 secret bytes + 32 public bytes].
|
|
523
|
-
*/
|
|
524
|
-
async function loadSvmWallet(keypairPath) {
|
|
525
|
-
const data = JSON.parse(readFileSync(keypairPath, "utf-8"));
|
|
526
|
-
return createKeyPairSignerFromBytes(new Uint8Array(data));
|
|
527
|
-
}
|
|
528
|
-
//#endregion
|
|
529
|
-
//#region src/openclaw/solana.ts
|
|
530
|
-
const USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
531
|
-
const TOKEN_PROGRAM$1 = address("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
|
|
532
|
-
const ASSOCIATED_TOKEN_PROGRAM = address("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL");
|
|
533
|
-
async function findAta(mint, owner) {
|
|
534
|
-
const encoder = getAddressEncoder();
|
|
535
|
-
const [pda] = await getProgramDerivedAddress({
|
|
536
|
-
programAddress: ASSOCIATED_TOKEN_PROGRAM,
|
|
537
|
-
seeds: [
|
|
538
|
-
encoder.encode(owner),
|
|
539
|
-
encoder.encode(TOKEN_PROGRAM$1),
|
|
540
|
-
encoder.encode(mint)
|
|
541
|
-
]
|
|
542
|
-
});
|
|
543
|
-
return pda;
|
|
544
|
-
}
|
|
545
|
-
function transferCheckedIx(source, mint, destination, authority, amount, decimals) {
|
|
546
|
-
const data = new Uint8Array(10);
|
|
547
|
-
data[0] = 12;
|
|
548
|
-
new DataView(data.buffer).setBigUint64(1, amount, true);
|
|
549
|
-
data[9] = decimals;
|
|
550
|
-
return {
|
|
551
|
-
programAddress: TOKEN_PROGRAM$1,
|
|
552
|
-
accounts: [
|
|
553
|
-
{
|
|
554
|
-
address: source,
|
|
555
|
-
role: 2
|
|
556
|
-
},
|
|
557
|
-
{
|
|
558
|
-
address: mint,
|
|
559
|
-
role: 0
|
|
560
|
-
},
|
|
561
|
-
{
|
|
562
|
-
address: destination,
|
|
563
|
-
role: 2
|
|
564
|
-
},
|
|
565
|
-
{
|
|
566
|
-
address: authority.address,
|
|
567
|
-
role: 1
|
|
568
|
-
}
|
|
569
|
-
],
|
|
570
|
-
data
|
|
571
|
-
};
|
|
572
|
-
}
|
|
573
|
-
async function getTokenAccounts(rpcUrl, owner) {
|
|
574
|
-
const { value } = await createSolanaRpc(rpcUrl).getTokenAccountsByOwner(address(owner), { programId: TOKEN_PROGRAM$1 }, { encoding: "jsonParsed" }).send();
|
|
575
|
-
return value.map((v) => {
|
|
576
|
-
const info = v.account.data.parsed.info;
|
|
577
|
-
return {
|
|
578
|
-
mint: info.mint,
|
|
579
|
-
amount: info.tokenAmount.uiAmountString,
|
|
580
|
-
decimals: info.tokenAmount.decimals
|
|
581
|
-
};
|
|
582
|
-
}).filter((t) => t.mint !== USDC_MINT && t.amount !== "0");
|
|
583
|
-
}
|
|
584
|
-
async function getUsdcBalance(rpcUrl, owner) {
|
|
585
|
-
const { value } = await createSolanaRpc(rpcUrl).getTokenAccountsByOwner(address(owner), { mint: address(USDC_MINT) }, { encoding: "jsonParsed" }).send();
|
|
586
|
-
if (value.length > 0) {
|
|
587
|
-
const ta = value[0].account.data.parsed.info.tokenAmount;
|
|
588
|
-
return {
|
|
589
|
-
raw: BigInt(ta.amount),
|
|
590
|
-
ui: ta.uiAmount !== null ? ta.uiAmount.toFixed(2) : "0.00"
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
return {
|
|
594
|
-
raw: 0n,
|
|
595
|
-
ui: "0.00"
|
|
596
|
-
};
|
|
597
|
-
}
|
|
598
|
-
async function getSolBalanceLamports(rpcUrl, owner) {
|
|
599
|
-
const { value } = await createSolanaRpc(rpcUrl).getBalance(address(owner)).send();
|
|
600
|
-
return value;
|
|
601
|
-
}
|
|
602
|
-
async function getSolBalance(rpcUrl, owner) {
|
|
603
|
-
const lamports = await getSolBalanceLamports(rpcUrl, owner);
|
|
604
|
-
return (Number(lamports) / 1e9).toFixed(4);
|
|
605
|
-
}
|
|
606
|
-
async function checkAtaExists(rpcUrl, owner) {
|
|
607
|
-
const ata = await findAta(address(USDC_MINT), address(owner));
|
|
608
|
-
const { value } = await createSolanaRpc(rpcUrl).getAccountInfo(ata, { encoding: "base64" }).send();
|
|
609
|
-
return value !== null;
|
|
610
|
-
}
|
|
611
|
-
async function transferUsdc(signer, rpcUrl, dest, amountRaw) {
|
|
612
|
-
const rpc = createSolanaRpc(rpcUrl);
|
|
613
|
-
const usdcMint = address(USDC_MINT);
|
|
614
|
-
const transferIx = transferCheckedIx(await findAta(usdcMint, signer.address), usdcMint, await findAta(usdcMint, address(dest)), signer, amountRaw, 6);
|
|
615
|
-
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
|
|
616
|
-
const encoded = getBase64EncodedWireTransaction(await partiallySignTransactionMessageWithSigners(pipe(createTransactionMessage({ version: 0 }), (m) => setTransactionMessageFeePayer(signer.address, m), (m) => appendTransactionMessageInstructions([transferIx], m), (m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m))));
|
|
617
|
-
return await rpc.sendTransaction(encoded, { encoding: "base64" }).send();
|
|
618
|
-
}
|
|
619
|
-
//#endregion
|
|
620
|
-
//#region src/lib/output.ts
|
|
621
|
-
function isTTY() {
|
|
622
|
-
return !!process.stderr.isTTY;
|
|
623
|
-
}
|
|
624
|
-
function info(msg) {
|
|
625
|
-
process.stderr.write(`${isTTY() ? pc.cyan(msg) : msg}\n`);
|
|
626
|
-
}
|
|
627
|
-
function dim(msg) {
|
|
628
|
-
process.stderr.write(`${isTTY() ? pc.dim(msg) : msg}\n`);
|
|
629
|
-
}
|
|
630
|
-
//#endregion
|
|
631
|
-
//#region src/commands/wallet.ts
|
|
632
|
-
const BASE_RPC = "https://mainnet.base.org";
|
|
633
|
-
const SOLANA_RPC = "https://api.mainnet-beta.solana.com";
|
|
634
|
-
const TEMPO_RPC = "https://rpc.presto.tempo.xyz";
|
|
635
|
-
const USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
636
|
-
const USDC_TEMPO = "0x20C000000000000000000000b9537d11c60E8b50";
|
|
637
|
-
const USDC_SOLANA_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
638
|
-
const TOKEN_PROGRAM = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
|
|
639
|
-
const ATA_PROGRAM = "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
|
|
640
|
-
async function rpcCall(url, method, params) {
|
|
641
|
-
const res = await fetch(url, {
|
|
642
|
-
method: "POST",
|
|
643
|
-
headers: { "Content-Type": "application/json" },
|
|
644
|
-
body: JSON.stringify({
|
|
645
|
-
jsonrpc: "2.0",
|
|
646
|
-
method,
|
|
647
|
-
params,
|
|
648
|
-
id: 1
|
|
649
|
-
})
|
|
650
|
-
});
|
|
651
|
-
if (!res.ok) throw new Error(`RPC ${method} failed: ${res.status} ${res.statusText}`);
|
|
652
|
-
return res.json();
|
|
653
|
-
}
|
|
654
|
-
async function fetchEvmBalances(address) {
|
|
655
|
-
const usdcData = `0x70a08231${address.slice(2).padStart(64, "0")}`;
|
|
656
|
-
const [ethRes, usdcRes] = await Promise.all([rpcCall(BASE_RPC, "eth_getBalance", [address, "latest"]), rpcCall(BASE_RPC, "eth_call", [{
|
|
657
|
-
to: USDC_BASE,
|
|
658
|
-
data: usdcData
|
|
659
|
-
}, "latest"])]);
|
|
660
|
-
return {
|
|
661
|
-
eth: ethRes.result ? (Number(BigInt(ethRes.result)) / 0xde0b6b3a7640000).toFixed(6) : "?",
|
|
662
|
-
usdc: usdcRes.result ? formatUsdcValue(Number(BigInt(usdcRes.result)) / 1e6) : "?"
|
|
663
|
-
};
|
|
664
|
-
}
|
|
665
|
-
async function fetchTempoBalances(address) {
|
|
666
|
-
const res = await rpcCall(TEMPO_RPC, "eth_call", [{
|
|
667
|
-
to: USDC_TEMPO,
|
|
668
|
-
data: `0x70a08231${address.slice(2).padStart(64, "0")}`
|
|
669
|
-
}, "latest"]);
|
|
670
|
-
return { usdc: res.result ? formatUsdcValue(Number(BigInt(res.result)) / 1e6) : "?" };
|
|
671
|
-
}
|
|
672
|
-
async function getUsdcAta(owner) {
|
|
673
|
-
const encoder = getAddressEncoder();
|
|
674
|
-
const [ata] = await getProgramDerivedAddress({
|
|
675
|
-
programAddress: address(ATA_PROGRAM),
|
|
676
|
-
seeds: [
|
|
677
|
-
encoder.encode(address(owner)),
|
|
678
|
-
encoder.encode(address(TOKEN_PROGRAM)),
|
|
679
|
-
encoder.encode(address(USDC_SOLANA_MINT))
|
|
680
|
-
]
|
|
681
|
-
});
|
|
682
|
-
return ata;
|
|
683
|
-
}
|
|
684
|
-
async function fetchSolanaBalances(ownerAddress) {
|
|
685
|
-
const ata = await getUsdcAta(ownerAddress);
|
|
686
|
-
const [solRes, usdcRes] = await Promise.all([rpcCall(SOLANA_RPC, "getBalance", [ownerAddress]), rpcCall(SOLANA_RPC, "getTokenAccountBalance", [ata])]);
|
|
687
|
-
const sol = solRes.result?.value != null ? (solRes.result.value / 1e9).toFixed(6) : "?";
|
|
688
|
-
const usdcVal = usdcRes.result?.value;
|
|
689
|
-
return {
|
|
690
|
-
sol,
|
|
691
|
-
usdc: usdcVal ? formatUsdcValue(Number(usdcVal.uiAmountString)) : "0"
|
|
692
|
-
};
|
|
693
|
-
}
|
|
694
|
-
function balanceLine(usdc, native, nativeSymbol) {
|
|
695
|
-
return pc.dim(` (${usdc} USDC, ${native} ${nativeSymbol})`);
|
|
696
|
-
}
|
|
697
|
-
async function fetchAllBalances(evmAddress, solanaAddress) {
|
|
698
|
-
const [evmResult, solResult, tempoResult] = await Promise.allSettled([
|
|
699
|
-
evmAddress ? fetchEvmBalances(evmAddress) : Promise.resolve(null),
|
|
700
|
-
solanaAddress ? fetchSolanaBalances(solanaAddress) : Promise.resolve(null),
|
|
701
|
-
evmAddress ? fetchTempoBalances(evmAddress) : Promise.resolve(null)
|
|
702
|
-
]);
|
|
703
|
-
return {
|
|
704
|
-
evm: evmResult.status === "fulfilled" ? evmResult.value : null,
|
|
705
|
-
sol: solResult.status === "fulfilled" ? solResult.value : null,
|
|
706
|
-
tempo: tempoResult.status === "fulfilled" ? tempoResult.value : null
|
|
707
|
-
};
|
|
708
|
-
}
|
|
709
|
-
buildCommand({
|
|
710
|
-
docs: { brief: "Show wallet addresses and balances" },
|
|
711
|
-
parameters: {
|
|
712
|
-
flags: { verbose: {
|
|
713
|
-
kind: "boolean",
|
|
714
|
-
brief: "Show transaction IDs",
|
|
715
|
-
default: false
|
|
716
|
-
} },
|
|
717
|
-
positional: {
|
|
718
|
-
kind: "tuple",
|
|
719
|
-
parameters: []
|
|
720
|
-
}
|
|
721
|
-
},
|
|
722
|
-
async func(flags) {
|
|
723
|
-
const wallet = resolveWallet();
|
|
724
|
-
if (wallet.source === "none") {
|
|
725
|
-
console.log(pc.yellow("No wallet configured."));
|
|
726
|
-
console.log(pc.dim(`\nRun:\n ${pc.cyan("$ npx x402-proxy setup")}\n\nOr set ${pc.cyan("X402_PROXY_WALLET_MNEMONIC")} environment variable.`));
|
|
727
|
-
process.exit(1);
|
|
728
|
-
}
|
|
729
|
-
console.log();
|
|
730
|
-
info("Wallet");
|
|
731
|
-
console.log();
|
|
732
|
-
console.log(pc.dim(` Source: ${wallet.source}`));
|
|
733
|
-
const { evm, sol, tempo } = await fetchAllBalances(wallet.evmAddress, wallet.solanaAddress);
|
|
734
|
-
if (wallet.evmAddress) {
|
|
735
|
-
const bal = evm ? balanceLine(evm.usdc, evm.eth, "ETH") : pc.dim(" (network error)");
|
|
736
|
-
console.log(` Base: ${pc.green(wallet.evmAddress)}${bal}`);
|
|
737
|
-
}
|
|
738
|
-
if (wallet.evmAddress) {
|
|
739
|
-
const bal = tempo ? pc.dim(` (${tempo.usdc} USDC)`) : pc.dim(" (network error)");
|
|
740
|
-
console.log(` Tempo: ${pc.green(wallet.evmAddress)}${bal}`);
|
|
741
|
-
}
|
|
742
|
-
if (wallet.solanaAddress) {
|
|
743
|
-
const bal = sol ? balanceLine(sol.usdc, sol.sol, "SOL") : pc.dim(" (network error)");
|
|
744
|
-
console.log(` Solana: ${pc.green(wallet.solanaAddress)}${bal}`);
|
|
745
|
-
}
|
|
746
|
-
const evmEmpty = !evm || Number(evm.usdc) === 0;
|
|
747
|
-
const solEmpty = !sol || Number(sol.usdc) === 0;
|
|
748
|
-
const tempoEmpty = !tempo || Number(tempo.usdc) === 0;
|
|
749
|
-
if (evmEmpty && solEmpty && tempoEmpty) {
|
|
750
|
-
console.log();
|
|
751
|
-
dim(" Send USDC to any address above to start using paid APIs.");
|
|
752
|
-
}
|
|
753
|
-
console.log();
|
|
754
|
-
const records = readHistory(getHistoryPath());
|
|
755
|
-
if (records.length > 0) {
|
|
756
|
-
const spend = calcSpend(records);
|
|
757
|
-
const recent = records.slice(-10);
|
|
758
|
-
dim(" Recent transactions:");
|
|
759
|
-
for (const r of recent) {
|
|
760
|
-
const line = formatTxLine(r, { verbose: flags.verbose }).replace(/\[([^\]]+)\]\([^)]+\)/g, "$1");
|
|
761
|
-
console.log(line);
|
|
762
|
-
}
|
|
763
|
-
console.log();
|
|
764
|
-
console.log(pc.dim(` Today: ${formatAmount(spend.today, "USDC")} | Total: ${formatAmount(spend.total, "USDC")} | ${spend.count} tx`));
|
|
765
|
-
} else dim(" No transactions yet.");
|
|
766
|
-
console.log();
|
|
767
|
-
console.log(pc.dim(" See also: wallet history, wallet export-key"));
|
|
768
|
-
console.log();
|
|
769
|
-
}
|
|
770
|
-
});
|
|
771
|
-
//#endregion
|
|
772
|
-
//#region src/openclaw/tools.ts
|
|
773
|
-
const SOL_MAINNET = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
774
|
-
const USDC_DECIMALS = 6;
|
|
775
|
-
const INFERENCE_RESERVE = .3;
|
|
776
|
-
const MAX_RESPONSE_CHARS = 5e4;
|
|
777
|
-
function paymentAmount(payment) {
|
|
778
|
-
if (!payment?.amount) return void 0;
|
|
779
|
-
const parsed = Number.parseFloat(payment.amount);
|
|
780
|
-
return Number.isNaN(parsed) ? void 0 : parsed / 10 ** USDC_DECIMALS;
|
|
781
|
-
}
|
|
782
|
-
function parseMppAmount(value) {
|
|
783
|
-
if (!value) return void 0;
|
|
784
|
-
const parsed = Number(value);
|
|
785
|
-
return Number.isFinite(parsed) ? parsed : void 0;
|
|
786
|
-
}
|
|
787
|
-
function toolResult(text) {
|
|
788
|
-
return {
|
|
789
|
-
content: [{
|
|
790
|
-
type: "text",
|
|
791
|
-
text
|
|
792
|
-
}],
|
|
793
|
-
details: {}
|
|
794
|
-
};
|
|
795
|
-
}
|
|
796
|
-
function addressForNetwork(evmAddress, solanaAddress, network) {
|
|
797
|
-
if (network?.startsWith("eip155:")) return evmAddress ?? solanaAddress;
|
|
798
|
-
if (network?.startsWith("solana:")) return solanaAddress ?? evmAddress;
|
|
799
|
-
return solanaAddress ?? evmAddress;
|
|
800
|
-
}
|
|
801
|
-
function walletAddressForNetwork(ctx, network) {
|
|
802
|
-
return addressForNetwork(ctx.getEvmWalletAddress(), ctx.getSolanaWalletAddress(), network) ?? "unknown";
|
|
803
|
-
}
|
|
804
|
-
async function getWalletSnapshot(rpcUrl, solanaWallet, evmWallet, historyPath) {
|
|
805
|
-
const [{ ui, raw }, sol, tokens, balances] = await Promise.all([
|
|
806
|
-
solanaWallet ? getUsdcBalance(rpcUrl, solanaWallet) : Promise.resolve({
|
|
807
|
-
ui: "0",
|
|
808
|
-
raw: 0n
|
|
809
|
-
}),
|
|
810
|
-
solanaWallet ? getSolBalance(rpcUrl, solanaWallet) : Promise.resolve("0"),
|
|
811
|
-
solanaWallet ? getTokenAccounts(rpcUrl, solanaWallet).catch(() => []) : Promise.resolve([]),
|
|
812
|
-
fetchAllBalances(evmWallet ?? void 0, solanaWallet ?? void 0)
|
|
813
|
-
]);
|
|
814
|
-
const records = readHistory(historyPath);
|
|
815
|
-
return {
|
|
816
|
-
ui,
|
|
817
|
-
raw,
|
|
818
|
-
sol,
|
|
819
|
-
tokens,
|
|
820
|
-
balances,
|
|
821
|
-
records,
|
|
822
|
-
spend: calcSpend(records)
|
|
823
|
-
};
|
|
824
|
-
}
|
|
825
|
-
function createWalletTool(ctx) {
|
|
826
|
-
return {
|
|
827
|
-
name: "x_wallet",
|
|
828
|
-
label: "Wallet Status",
|
|
829
|
-
description: "Check wallet readiness, balances, and spend history for x402 and MPP payments before making paid requests.",
|
|
830
|
-
parameters: Type.Object({}),
|
|
831
|
-
async execute() {
|
|
832
|
-
await ctx.ensureReady();
|
|
833
|
-
const solanaWallet = ctx.getSolanaWalletAddress();
|
|
834
|
-
const evmWallet = ctx.getEvmWalletAddress();
|
|
835
|
-
if (!solanaWallet && !evmWallet) return toolResult("Wallet not configured yet. Run `x402-proxy setup` or set X402_PROXY_WALLET_MNEMONIC.");
|
|
836
|
-
try {
|
|
837
|
-
const snap = await getWalletSnapshot(ctx.rpcUrl, solanaWallet, evmWallet, ctx.historyPath);
|
|
838
|
-
const total = Number.parseFloat(snap.ui);
|
|
839
|
-
const available = Math.max(0, total - INFERENCE_RESERVE);
|
|
840
|
-
const tokenLines = snap.tokens.slice(0, 5).map((t) => {
|
|
841
|
-
const short = `${t.mint.slice(0, 4)}...${t.mint.slice(-4)}`;
|
|
842
|
-
return `${Number.parseFloat(t.amount).toLocaleString("en-US", { maximumFractionDigits: 0 })} (${short})`;
|
|
843
|
-
});
|
|
844
|
-
const lines = [
|
|
845
|
-
`Default protocol: ${ctx.getDefaultRequestProtocol()}`,
|
|
846
|
-
`MPP budget: ${ctx.getDefaultMppSessionBudget()} USDC`,
|
|
847
|
-
`MPP ready: ${evmWallet ? "yes" : "no"}`,
|
|
848
|
-
`x402 ready: ${solanaWallet ? "yes" : "no"}`
|
|
849
|
-
];
|
|
850
|
-
if (evmWallet) {
|
|
851
|
-
lines.push(`Base wallet: ${evmWallet}`);
|
|
852
|
-
lines.push(`Base balance: ${snap.balances.evm?.usdc ?? "?"} USDC`);
|
|
853
|
-
lines.push(`Tempo balance: ${snap.balances.tempo?.usdc ?? "?"} USDC`);
|
|
854
|
-
}
|
|
855
|
-
if (solanaWallet) {
|
|
856
|
-
lines.push(`Solana wallet: ${solanaWallet}`);
|
|
857
|
-
lines.push(`Solana: ${snap.sol} SOL`);
|
|
858
|
-
lines.push(`Solana USDC: ${snap.ui} USDC`);
|
|
859
|
-
lines.push(`Available for x402 tools: ${available.toFixed(2)} USDC`);
|
|
860
|
-
lines.push(`Reserved for inference: ${INFERENCE_RESERVE.toFixed(2)} USDC`);
|
|
861
|
-
}
|
|
862
|
-
lines.push(`Spent today: ${formatAmount(snap.spend.today, "USDC")}`);
|
|
863
|
-
lines.push(`Total spent: ${formatAmount(snap.spend.total, "USDC")} (${snap.spend.count} txs)`);
|
|
864
|
-
if (tokenLines.length > 0) lines.push(`Tokens held: ${tokenLines.join(", ")}`);
|
|
865
|
-
if (!evmWallet) lines.push("MPP setup hint: set X402_PROXY_WALLET_MNEMONIC or X402_PROXY_WALLET_EVM_KEY, or run x402-proxy setup.");
|
|
866
|
-
if (!solanaWallet) lines.push("x402 setup hint: add a Solana wallet or mnemonic if you want to pay Solana x402 endpoints.");
|
|
867
|
-
return toolResult(lines.join("\n"));
|
|
868
|
-
} catch (err) {
|
|
869
|
-
return toolResult(`Failed to check wallet status: ${String(err)}`);
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
};
|
|
873
|
-
}
|
|
874
|
-
function createRequestTool(ctx) {
|
|
875
|
-
return {
|
|
876
|
-
name: "x_request",
|
|
877
|
-
label: "Paid Request",
|
|
878
|
-
description: "Call a paid HTTP endpoint with automatic x402 or MPP settlement. Defaults to the plugin protocol and supports explicit protocol override.",
|
|
879
|
-
parameters: Type.Object({
|
|
880
|
-
url: Type.String({ description: "The paid endpoint URL" }),
|
|
881
|
-
method: Type.Optional(Type.String({ description: "HTTP method (default: GET)" })),
|
|
882
|
-
params: Type.Optional(Type.String({ description: "For GET: query params as JSON object. For POST/PUT/PATCH: JSON request body." })),
|
|
883
|
-
headers: Type.Optional(Type.String({ description: "Custom HTTP headers as JSON object" })),
|
|
884
|
-
protocol: Type.Optional(Type.Union([
|
|
885
|
-
Type.Literal("x402"),
|
|
886
|
-
Type.Literal("mpp"),
|
|
887
|
-
Type.Literal("auto")
|
|
888
|
-
], { description: "Override the default payment protocol for this request." }))
|
|
889
|
-
}),
|
|
890
|
-
async execute(_id, params) {
|
|
891
|
-
await ctx.ensureReady();
|
|
892
|
-
const solanaWallet = ctx.getSolanaWalletAddress();
|
|
893
|
-
const evmWallet = ctx.getEvmWalletAddress();
|
|
894
|
-
const method = (params.method || "GET").toUpperCase();
|
|
895
|
-
let url = params.url;
|
|
896
|
-
const reqInit = { method };
|
|
897
|
-
if (params.headers) try {
|
|
898
|
-
reqInit.headers = JSON.parse(params.headers);
|
|
899
|
-
} catch {
|
|
900
|
-
return toolResult("Invalid headers JSON.");
|
|
901
|
-
}
|
|
902
|
-
if (params.params) if (method === "GET" || method === "HEAD") try {
|
|
903
|
-
const qp = JSON.parse(params.params);
|
|
904
|
-
const qs = new URLSearchParams(qp).toString();
|
|
905
|
-
url = qs ? `${url}${url.includes("?") ? "&" : "?"}${qs}` : url;
|
|
906
|
-
} catch {
|
|
907
|
-
return toolResult("Invalid params JSON for GET request.");
|
|
908
|
-
}
|
|
909
|
-
else {
|
|
910
|
-
reqInit.body = params.params;
|
|
911
|
-
reqInit.headers = {
|
|
912
|
-
"Content-Type": "application/json",
|
|
913
|
-
...reqInit.headers
|
|
914
|
-
};
|
|
915
|
-
}
|
|
916
|
-
const protocol = params.protocol && [
|
|
917
|
-
"x402",
|
|
918
|
-
"mpp",
|
|
919
|
-
"auto"
|
|
920
|
-
].includes(params.protocol) ? params.protocol : ctx.getDefaultRequestProtocol();
|
|
921
|
-
const toolStartMs = Date.now();
|
|
922
|
-
if ((protocol === "mpp" || protocol === "auto") && !ctx.getEvmKey()) return toolResult("MPP request failed: no EVM wallet configured. Run x402-proxy setup or set X402_PROXY_WALLET_EVM_KEY.");
|
|
923
|
-
if (protocol === "x402") {
|
|
924
|
-
if (!solanaWallet) return toolResult("x402 request failed: no Solana wallet configured. Add a mnemonic or Solana key first.");
|
|
925
|
-
try {
|
|
926
|
-
const { ui } = await getUsdcBalance(ctx.rpcUrl, solanaWallet);
|
|
927
|
-
if (Number.parseFloat(ui) <= INFERENCE_RESERVE) return toolResult(`Insufficient funds. Balance: ${ui} USDC, reserved for inference: ${INFERENCE_RESERVE.toFixed(2)} USDC. Top up wallet: ${solanaWallet}`);
|
|
928
|
-
} catch {}
|
|
929
|
-
const proxy = ctx.getX402Proxy();
|
|
930
|
-
if (!proxy) return toolResult("x402 wallet is not ready yet. Try again in a moment.");
|
|
931
|
-
try {
|
|
932
|
-
const response = await proxy.x402Fetch(url, reqInit);
|
|
933
|
-
const body = await response.text();
|
|
934
|
-
if (response.status === 402) {
|
|
935
|
-
const payment = proxy.shiftPayment();
|
|
936
|
-
appendHistory(ctx.historyPath, {
|
|
937
|
-
t: Date.now(),
|
|
938
|
-
ok: false,
|
|
939
|
-
kind: "x402_payment",
|
|
940
|
-
net: payment?.network ?? "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
|
|
941
|
-
from: walletAddressForNetwork(ctx, payment?.network),
|
|
942
|
-
label: url,
|
|
943
|
-
ms: Date.now() - toolStartMs,
|
|
944
|
-
error: "payment_required"
|
|
945
|
-
});
|
|
946
|
-
return toolResult(`Payment failed (402): ${body.substring(0, 500)}. Wallet: ${walletAddressForNetwork(ctx, payment?.network)}`);
|
|
947
|
-
}
|
|
948
|
-
const payment = proxy.shiftPayment();
|
|
949
|
-
const amount = paymentAmount(payment);
|
|
950
|
-
appendHistory(ctx.historyPath, {
|
|
951
|
-
t: Date.now(),
|
|
952
|
-
ok: true,
|
|
953
|
-
kind: "x402_payment",
|
|
954
|
-
net: payment?.network ?? "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
|
|
955
|
-
from: walletAddressForNetwork(ctx, payment?.network),
|
|
956
|
-
to: payment?.payTo,
|
|
957
|
-
tx: extractTxSignature(response),
|
|
958
|
-
amount,
|
|
959
|
-
token: "USDC",
|
|
960
|
-
label: url,
|
|
961
|
-
ms: Date.now() - toolStartMs
|
|
962
|
-
});
|
|
963
|
-
const truncated = body.length > MAX_RESPONSE_CHARS ? `${body.substring(0, MAX_RESPONSE_CHARS)}\n\n[Truncated - response was ${body.length} chars]` : body;
|
|
964
|
-
return toolResult(`HTTP ${response.status}\n\n${truncated}`);
|
|
965
|
-
} catch (err) {
|
|
966
|
-
ctx.getX402Proxy()?.shiftPayment();
|
|
967
|
-
appendHistory(ctx.historyPath, {
|
|
968
|
-
t: Date.now(),
|
|
969
|
-
ok: false,
|
|
970
|
-
kind: "x402_payment",
|
|
971
|
-
net: SOL_MAINNET,
|
|
972
|
-
from: solanaWallet,
|
|
973
|
-
label: params.url,
|
|
974
|
-
error: String(err).substring(0, 200)
|
|
975
|
-
});
|
|
976
|
-
const msg = String(err);
|
|
977
|
-
return toolResult(msg.includes("Simulation failed") || msg.includes("insufficient") ? `Payment failed - insufficient funds. Wallet: ${solanaWallet}. Error: ${msg}` : `Request failed: ${msg}`);
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
const evmKey = ctx.getEvmKey();
|
|
981
|
-
if (!evmKey) return toolResult("MPP request failed: no EVM wallet configured. Run x402-proxy setup or set X402_PROXY_WALLET_EVM_KEY.");
|
|
982
|
-
const mpp = await createMppProxyHandler({
|
|
983
|
-
evmKey,
|
|
984
|
-
maxDeposit: ctx.getDefaultMppSessionBudget()
|
|
985
|
-
});
|
|
986
|
-
try {
|
|
987
|
-
const response = await mpp.fetch(url, reqInit);
|
|
988
|
-
const body = await response.text();
|
|
989
|
-
const payment = mpp.shiftPayment();
|
|
990
|
-
if (response.status === 402) {
|
|
991
|
-
appendHistory(ctx.historyPath, {
|
|
992
|
-
t: Date.now(),
|
|
993
|
-
ok: false,
|
|
994
|
-
kind: "mpp_payment",
|
|
995
|
-
net: payment?.network ?? "eip155:4217",
|
|
996
|
-
from: evmWallet ?? "unknown",
|
|
997
|
-
label: url,
|
|
998
|
-
ms: Date.now() - toolStartMs,
|
|
999
|
-
error: "payment_required"
|
|
1000
|
-
});
|
|
1001
|
-
return toolResult(`MPP payment failed (402): ${body.substring(0, 500)}`);
|
|
1002
|
-
}
|
|
1003
|
-
appendHistory(ctx.historyPath, {
|
|
1004
|
-
t: Date.now(),
|
|
1005
|
-
ok: true,
|
|
1006
|
-
kind: "mpp_payment",
|
|
1007
|
-
net: payment?.network ?? "eip155:4217",
|
|
1008
|
-
from: evmWallet ?? "unknown",
|
|
1009
|
-
tx: extractTxSignature(response) ?? payment?.receipt?.reference,
|
|
1010
|
-
amount: parseMppAmount(payment?.amount),
|
|
1011
|
-
token: "USDC",
|
|
1012
|
-
label: url,
|
|
1013
|
-
ms: Date.now() - toolStartMs
|
|
1014
|
-
});
|
|
1015
|
-
const truncated = body.length > MAX_RESPONSE_CHARS ? `${body.substring(0, MAX_RESPONSE_CHARS)}\n\n[Truncated - response was ${body.length} chars]` : body;
|
|
1016
|
-
return toolResult(`HTTP ${response.status}\n\n${truncated}`);
|
|
1017
|
-
} catch (err) {
|
|
1018
|
-
appendHistory(ctx.historyPath, {
|
|
1019
|
-
t: Date.now(),
|
|
1020
|
-
ok: false,
|
|
1021
|
-
kind: "mpp_payment",
|
|
1022
|
-
net: TEMPO_NETWORK,
|
|
1023
|
-
from: evmWallet ?? "unknown",
|
|
1024
|
-
label: params.url,
|
|
1025
|
-
error: String(err).substring(0, 200)
|
|
1026
|
-
});
|
|
1027
|
-
return toolResult(`MPP request failed: ${String(err)}`);
|
|
1028
|
-
} finally {
|
|
1029
|
-
await mpp.close().catch(() => {});
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
};
|
|
1033
|
-
}
|
|
1034
|
-
//#endregion
|
|
1035
|
-
//#region src/openclaw/commands.ts
|
|
1036
|
-
const HISTORY_PAGE_SIZE = 5;
|
|
1037
|
-
const STATUS_HISTORY_COUNT = 3;
|
|
1038
|
-
const INLINE_HISTORY_TOKEN_THRESHOLD = 3;
|
|
1039
|
-
const SEND_CONFIRM_TTL_MS = 300 * 1e3;
|
|
1040
|
-
const TOKEN_SYMBOL_CACHE_MAX = 200;
|
|
1041
|
-
const tokenSymbolCache = /* @__PURE__ */ new Map();
|
|
1042
|
-
const pendingSends = /* @__PURE__ */ new Map();
|
|
1043
|
-
async function resolveTokenSymbols(mints) {
|
|
1044
|
-
const result = /* @__PURE__ */ new Map();
|
|
1045
|
-
const toResolve = [];
|
|
1046
|
-
for (const m of mints) {
|
|
1047
|
-
const cached = tokenSymbolCache.get(m);
|
|
1048
|
-
if (cached) result.set(m, cached);
|
|
1049
|
-
else toResolve.push(m);
|
|
1050
|
-
}
|
|
1051
|
-
if (toResolve.length === 0) return result;
|
|
1052
|
-
try {
|
|
1053
|
-
const res = await globalThis.fetch(`https://api.dexscreener.com/tokens/v1/solana/${toResolve.join(",")}`, { signal: AbortSignal.timeout(5e3) });
|
|
1054
|
-
if (!res.ok) return result;
|
|
1055
|
-
const pairs = await res.json();
|
|
1056
|
-
for (const pair of pairs) {
|
|
1057
|
-
const addr = pair.baseToken?.address;
|
|
1058
|
-
const sym = pair.baseToken?.symbol;
|
|
1059
|
-
if (addr && sym && toResolve.includes(addr)) {
|
|
1060
|
-
if (tokenSymbolCache.size >= TOKEN_SYMBOL_CACHE_MAX) {
|
|
1061
|
-
const oldest = tokenSymbolCache.keys().next();
|
|
1062
|
-
if (!oldest.done) tokenSymbolCache.delete(oldest.value);
|
|
1063
|
-
}
|
|
1064
|
-
tokenSymbolCache.set(addr, sym);
|
|
1065
|
-
result.set(addr, sym);
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
} catch {}
|
|
1069
|
-
return result;
|
|
1070
|
-
}
|
|
1071
|
-
function senderKey(cmdCtx) {
|
|
1072
|
-
return [
|
|
1073
|
-
cmdCtx.channel,
|
|
1074
|
-
cmdCtx.accountId ?? "",
|
|
1075
|
-
cmdCtx.senderId ?? cmdCtx.from ?? "",
|
|
1076
|
-
String(cmdCtx.messageThreadId ?? "")
|
|
1077
|
-
].join(":");
|
|
1078
|
-
}
|
|
1079
|
-
async function executeSend(amountStr, destination, wallet, signer, rpc, histPath) {
|
|
1080
|
-
if (!signer) return { text: "Wallet not loaded yet. Please wait for the gateway to finish starting." };
|
|
1081
|
-
try {
|
|
1082
|
-
if (!await checkAtaExists(rpc, destination)) return { text: "Recipient does not have a USDC token account.\nThey need to receive USDC at least once to create one." };
|
|
1083
|
-
let amountRaw;
|
|
1084
|
-
let amountUi;
|
|
1085
|
-
if (amountStr.toLowerCase() === "all") {
|
|
1086
|
-
const balance = await getUsdcBalance(rpc, wallet);
|
|
1087
|
-
if (balance.raw === 0n) return { text: "Wallet has no USDC to send." };
|
|
1088
|
-
amountRaw = balance.raw;
|
|
1089
|
-
amountUi = balance.ui;
|
|
1090
|
-
} else {
|
|
1091
|
-
const amount = Number.parseFloat(amountStr);
|
|
1092
|
-
if (Number.isNaN(amount) || amount <= 0) return { text: `Invalid amount: ${amountStr}` };
|
|
1093
|
-
amountRaw = BigInt(Math.round(amount * 1e6));
|
|
1094
|
-
amountUi = amount.toString();
|
|
1095
|
-
}
|
|
1096
|
-
const sig = await transferUsdc(signer, rpc, destination, amountRaw);
|
|
1097
|
-
appendHistory(histPath, {
|
|
1098
|
-
t: Date.now(),
|
|
1099
|
-
ok: true,
|
|
1100
|
-
kind: "transfer",
|
|
1101
|
-
net: SOL_MAINNET,
|
|
1102
|
-
from: wallet,
|
|
1103
|
-
to: destination,
|
|
1104
|
-
tx: sig,
|
|
1105
|
-
amount: Number.parseFloat(amountUi),
|
|
1106
|
-
token: "USDC",
|
|
1107
|
-
label: `${destination.slice(0, 4)}...${destination.slice(-4)}`
|
|
1108
|
-
});
|
|
1109
|
-
return { text: `Sent ${amountUi} USDC to \`${destination}\`\n[View transaction](https://solscan.io/tx/${sig})` };
|
|
1110
|
-
} catch (err) {
|
|
1111
|
-
appendHistory(histPath, {
|
|
1112
|
-
t: Date.now(),
|
|
1113
|
-
ok: false,
|
|
1114
|
-
kind: "transfer",
|
|
1115
|
-
net: SOL_MAINNET,
|
|
1116
|
-
from: wallet,
|
|
1117
|
-
to: destination,
|
|
1118
|
-
token: "USDC",
|
|
1119
|
-
error: String(err).substring(0, 200)
|
|
1120
|
-
});
|
|
1121
|
-
const msg = String(err);
|
|
1122
|
-
const detail = err.cause?.message || msg;
|
|
1123
|
-
if (detail.includes("insufficient") || detail.includes("lamports")) return { text: "Send failed - insufficient SOL for fees\nBalance too low for transaction fees. Fund wallet with SOL." };
|
|
1124
|
-
return { text: `Send failed: ${detail}` };
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
function handleHistory(histPath, page) {
|
|
1128
|
-
const records = readHistory(histPath);
|
|
1129
|
-
const totalTxs = records.length;
|
|
1130
|
-
const start = (page - 1) * HISTORY_PAGE_SIZE;
|
|
1131
|
-
const fromEnd = totalTxs - start;
|
|
1132
|
-
const pageRecords = records.slice(Math.max(0, fromEnd - HISTORY_PAGE_SIZE), fromEnd).reverse();
|
|
1133
|
-
if (pageRecords.length === 0) return { text: page === 1 ? "No transactions yet." : "No more transactions." };
|
|
1134
|
-
const lines = [`**History** (${start + 1}-${start + pageRecords.length})`, ""];
|
|
1135
|
-
for (const r of pageRecords) lines.push(formatTxLine(r));
|
|
1136
|
-
const nav = [];
|
|
1137
|
-
if (page > 1) nav.push(`Newer: \`/x_wallet history${page === 2 ? "" : ` ${page - 1}`}\``);
|
|
1138
|
-
if (start + HISTORY_PAGE_SIZE < totalTxs) nav.push(`Older: \`/x_wallet history ${page + 1}\``);
|
|
1139
|
-
if (nav.length > 0) lines.push("", nav.join(" · "));
|
|
1140
|
-
return { text: lines.join("\n") };
|
|
1141
|
-
}
|
|
1142
|
-
function createWalletCommand(ctx) {
|
|
1143
|
-
return {
|
|
1144
|
-
name: "x_wallet",
|
|
1145
|
-
description: "Wallet status, balances, payment readiness, and transaction history",
|
|
1146
|
-
acceptsArgs: true,
|
|
1147
|
-
requireAuth: true,
|
|
1148
|
-
handler: async (cmdCtx) => {
|
|
1149
|
-
await ctx.ensureReady();
|
|
1150
|
-
const solanaWallet = ctx.getSolanaWalletAddress();
|
|
1151
|
-
const evmWallet = ctx.getEvmWalletAddress();
|
|
1152
|
-
if (!solanaWallet && !evmWallet) return { text: "Wallet not configured yet.\nRun `x402-proxy setup` or set `X402_PROXY_WALLET_MNEMONIC` on the gateway host." };
|
|
1153
|
-
const parts = (cmdCtx.args?.trim() ?? "").split(/\s+/).filter(Boolean);
|
|
1154
|
-
if (parts[0]?.toLowerCase() === "history") {
|
|
1155
|
-
const pageArg = parts[1];
|
|
1156
|
-
const page = pageArg ? Math.max(1, Number.parseInt(pageArg, 10) || 1) : 1;
|
|
1157
|
-
return handleHistory(ctx.historyPath, page);
|
|
1158
|
-
}
|
|
1159
|
-
if (parts[0]?.toLowerCase() === "send") return { text: "Use `/x_send <amount|all> <address>` for transfers." };
|
|
1160
|
-
try {
|
|
1161
|
-
const snap = await getWalletSnapshot(ctx.rpcUrl, solanaWallet, evmWallet, ctx.historyPath);
|
|
1162
|
-
const lines = [`x402-proxy v0.10.7`];
|
|
1163
|
-
const defaultModel = ctx.allModels[0];
|
|
1164
|
-
if (defaultModel) lines.push("", `**Model** - ${defaultModel.name} (${defaultModel.provider})`);
|
|
1165
|
-
lines.push("", `**Protocol** - ${ctx.getDefaultRequestProtocol()}`);
|
|
1166
|
-
lines.push(`MPP session budget: ${ctx.getDefaultMppSessionBudget()} USDC`);
|
|
1167
|
-
lines.push(`MPP ready: ${evmWallet ? "yes" : "no"}`);
|
|
1168
|
-
lines.push(`x402 ready: ${solanaWallet ? "yes" : "no"}`);
|
|
1169
|
-
if (evmWallet) {
|
|
1170
|
-
lines.push("", "**EVM / Tempo**");
|
|
1171
|
-
lines.push(`\`${evmWallet}\``);
|
|
1172
|
-
lines.push(` Base: ${snap.balances.evm?.usdc ?? "?"} USDC`);
|
|
1173
|
-
lines.push(` Tempo: ${snap.balances.tempo?.usdc ?? "?"} USDC`);
|
|
1174
|
-
}
|
|
1175
|
-
if (solanaWallet) {
|
|
1176
|
-
const solscanUrl = `https://solscan.io/account/${solanaWallet}`;
|
|
1177
|
-
lines.push("", `**[Solana Wallet](${solscanUrl})**`, `\`${solanaWallet}\``);
|
|
1178
|
-
lines.push(` ${snap.sol} SOL`, ` ${snap.ui} USDC`);
|
|
1179
|
-
if (snap.spend.today > 0) lines.push(` -${snap.spend.today.toFixed(2)} USDC today`);
|
|
1180
|
-
}
|
|
1181
|
-
if (snap.tokens.length > 0) {
|
|
1182
|
-
const displayTokens = snap.tokens.slice(0, 10);
|
|
1183
|
-
const symbols = await resolveTokenSymbols(displayTokens.map((t) => t.mint));
|
|
1184
|
-
lines.push("", "**Solana Tokens**");
|
|
1185
|
-
for (const t of displayTokens) {
|
|
1186
|
-
const label = symbols.get(t.mint) ?? `${t.mint.slice(0, 4)}...${t.mint.slice(-4)}`;
|
|
1187
|
-
const amt = Number.parseFloat(t.amount).toLocaleString("en-US", { maximumFractionDigits: 0 });
|
|
1188
|
-
lines.push(` ${amt} ${label}`);
|
|
1189
|
-
}
|
|
1190
|
-
if (snap.tokens.length > 10) lines.push(` ...and ${snap.tokens.length - 10} more`);
|
|
1191
|
-
}
|
|
1192
|
-
if (snap.tokens.length <= INLINE_HISTORY_TOKEN_THRESHOLD) {
|
|
1193
|
-
const recentRecords = snap.records.slice(-STATUS_HISTORY_COUNT).reverse();
|
|
1194
|
-
if (recentRecords.length > 0) {
|
|
1195
|
-
lines.push("", "**Recent**");
|
|
1196
|
-
for (const r of recentRecords) lines.push(formatTxLine(r));
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
if (!evmWallet) lines.push("", "MPP setup: add an EVM wallet via `x402-proxy setup` or `X402_PROXY_WALLET_EVM_KEY`.");
|
|
1200
|
-
if (!solanaWallet) lines.push("", "x402 setup: add a Solana wallet or mnemonic if you need Solana x402.");
|
|
1201
|
-
lines.push("", "History: `/x_wallet history`", "Send: `/x_send <amount|all> <address>`");
|
|
1202
|
-
if (ctx.dashboardUrl) lines.push(`[Dashboard](${ctx.dashboardUrl})`);
|
|
1203
|
-
return { text: lines.join("\n") };
|
|
1204
|
-
} catch (err) {
|
|
1205
|
-
return { text: `Failed to check wallet status: ${String(err)}` };
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
};
|
|
1209
|
-
}
|
|
1210
|
-
function createSendCommand(ctx) {
|
|
1211
|
-
return {
|
|
1212
|
-
name: "x_send",
|
|
1213
|
-
description: "Send USDC from the plugin wallet with an explicit confirmation step",
|
|
1214
|
-
acceptsArgs: true,
|
|
1215
|
-
requireAuth: true,
|
|
1216
|
-
handler: async (cmdCtx) => {
|
|
1217
|
-
await ctx.ensureReady();
|
|
1218
|
-
const wallet = ctx.getSolanaWalletAddress();
|
|
1219
|
-
if (!wallet) return { text: "No Solana wallet configured. `/x_send` only works with the Solana USDC wallet." };
|
|
1220
|
-
const key = senderKey(cmdCtx);
|
|
1221
|
-
const now = Date.now();
|
|
1222
|
-
const pending = pendingSends.get(key);
|
|
1223
|
-
if (pending && now - pending.createdAt > SEND_CONFIRM_TTL_MS) pendingSends.delete(key);
|
|
1224
|
-
const parts = (cmdCtx.args?.trim() ?? "").split(/\s+/).filter(Boolean);
|
|
1225
|
-
if (parts.length === 0) return { text: "Usage: `/x_send <amount|all> <address>`\nConfirm with `/x_send confirm`\nCancel with `/x_send cancel`" };
|
|
1226
|
-
if (parts[0]?.toLowerCase() === "confirm") {
|
|
1227
|
-
const next = pendingSends.get(key);
|
|
1228
|
-
if (!next) return { text: "No pending transfer to confirm." };
|
|
1229
|
-
pendingSends.delete(key);
|
|
1230
|
-
return executeSend(next.amount, next.destination, wallet, ctx.getSigner(), ctx.rpcUrl, ctx.historyPath);
|
|
1231
|
-
}
|
|
1232
|
-
if (parts[0]?.toLowerCase() === "cancel") {
|
|
1233
|
-
pendingSends.delete(key);
|
|
1234
|
-
return { text: "Pending transfer cleared." };
|
|
1235
|
-
}
|
|
1236
|
-
if (parts.length !== 2) return { text: "Usage: `/x_send <amount|all> <address>`\nExample: `/x_send 0.5 7xKXtg...`" };
|
|
1237
|
-
const [amount, destination] = parts;
|
|
1238
|
-
if (destination.length < 32 || destination.length > 44) return { text: `Invalid Solana address: ${destination}` };
|
|
1239
|
-
pendingSends.set(key, {
|
|
1240
|
-
amount,
|
|
1241
|
-
destination,
|
|
1242
|
-
createdAt: now
|
|
1243
|
-
});
|
|
1244
|
-
return { text: `Pending transfer: send ${amount} USDC to \`${destination}\`\nConfirm within 5 minutes with \`/x_send confirm\`\nCancel with \`/x_send cancel\`` };
|
|
1245
|
-
}
|
|
1246
|
-
};
|
|
1247
|
-
}
|
|
1248
|
-
//#endregion
|
|
1249
|
-
//#region src/openclaw/defaults.ts
|
|
1250
|
-
const DEFAULT_SURF_PROVIDER_ID = "surf";
|
|
1251
|
-
const DEFAULT_SURF_BASE_URL = "/x402-proxy/v1";
|
|
1252
|
-
const DEFAULT_SURF_UPSTREAM_URL = "https://surf.cascade.fyi/api/v1/inference";
|
|
1253
|
-
const DEFAULT_SURF_MODELS = [
|
|
1254
|
-
{
|
|
1255
|
-
id: "anthropic/claude-opus-4.6",
|
|
1256
|
-
name: "Claude Opus 4.6",
|
|
1257
|
-
maxTokens: 2e5,
|
|
1258
|
-
reasoning: true,
|
|
1259
|
-
input: ["text", "image"],
|
|
1260
|
-
cost: {
|
|
1261
|
-
input: .015,
|
|
1262
|
-
output: .075,
|
|
1263
|
-
cacheRead: .0015,
|
|
1264
|
-
cacheWrite: .01875
|
|
1265
|
-
},
|
|
1266
|
-
contextWindow: 2e5
|
|
1267
|
-
},
|
|
1268
|
-
{
|
|
1269
|
-
id: "anthropic/claude-sonnet-4.6",
|
|
1270
|
-
name: "Claude Sonnet 4.6",
|
|
1271
|
-
maxTokens: 2e5,
|
|
1272
|
-
reasoning: true,
|
|
1273
|
-
input: ["text", "image"],
|
|
1274
|
-
cost: {
|
|
1275
|
-
input: .003,
|
|
1276
|
-
output: .015,
|
|
1277
|
-
cacheRead: 3e-4,
|
|
1278
|
-
cacheWrite: .00375
|
|
1279
|
-
},
|
|
1280
|
-
contextWindow: 2e5
|
|
1281
|
-
},
|
|
1282
|
-
{
|
|
1283
|
-
id: "x-ai/grok-4.20-beta",
|
|
1284
|
-
name: "Grok 4.20 Beta",
|
|
1285
|
-
maxTokens: 131072,
|
|
1286
|
-
reasoning: true,
|
|
1287
|
-
input: ["text"],
|
|
1288
|
-
cost: {
|
|
1289
|
-
input: .003,
|
|
1290
|
-
output: .015,
|
|
1291
|
-
cacheRead: 0,
|
|
1292
|
-
cacheWrite: 0
|
|
1293
|
-
},
|
|
1294
|
-
contextWindow: 131072
|
|
1295
|
-
},
|
|
1296
|
-
{
|
|
1297
|
-
id: "minimax/minimax-m2.5",
|
|
1298
|
-
name: "MiniMax M2.5",
|
|
1299
|
-
maxTokens: 1e6,
|
|
1300
|
-
reasoning: false,
|
|
1301
|
-
input: ["text"],
|
|
1302
|
-
cost: {
|
|
1303
|
-
input: .001,
|
|
1304
|
-
output: .005,
|
|
1305
|
-
cacheRead: 0,
|
|
1306
|
-
cacheWrite: 0
|
|
1307
|
-
},
|
|
1308
|
-
contextWindow: 1e6
|
|
1309
|
-
},
|
|
1310
|
-
{
|
|
1311
|
-
id: "moonshotai/kimi-k2.5",
|
|
1312
|
-
name: "Kimi K2.5",
|
|
1313
|
-
maxTokens: 131072,
|
|
1314
|
-
reasoning: true,
|
|
1315
|
-
input: ["text"],
|
|
1316
|
-
cost: {
|
|
1317
|
-
input: .002,
|
|
1318
|
-
output: .008,
|
|
1319
|
-
cacheRead: 0,
|
|
1320
|
-
cacheWrite: 0
|
|
1321
|
-
},
|
|
1322
|
-
contextWindow: 131072
|
|
1323
|
-
},
|
|
1324
|
-
{
|
|
1325
|
-
id: "z-ai/glm-5",
|
|
1326
|
-
name: "GLM-5",
|
|
1327
|
-
maxTokens: 128e3,
|
|
1328
|
-
reasoning: false,
|
|
1329
|
-
input: ["text"],
|
|
1330
|
-
cost: {
|
|
1331
|
-
input: .001,
|
|
1332
|
-
output: .005,
|
|
1333
|
-
cacheRead: 0,
|
|
1334
|
-
cacheWrite: 0
|
|
1335
|
-
},
|
|
1336
|
-
contextWindow: 128e3
|
|
1337
|
-
}
|
|
1338
|
-
];
|
|
1339
|
-
function resolveProviders(config) {
|
|
1340
|
-
const defaultProtocol = resolveProtocol(config.protocol);
|
|
1341
|
-
const defaultMppSessionBudget = resolveMppSessionBudget(config.mppSessionBudget);
|
|
1342
|
-
const raw = config.providers ?? {};
|
|
1343
|
-
const entries = Object.entries(raw).length > 0 ? Object.entries(raw).map(([id, provider]) => ({
|
|
1344
|
-
id,
|
|
1345
|
-
baseUrl: provider.baseUrl || "/x402-proxy/v1",
|
|
1346
|
-
upstreamUrl: provider.upstreamUrl || "https://surf.cascade.fyi/api/v1/inference",
|
|
1347
|
-
protocol: resolveProtocol(provider.protocol, defaultProtocol),
|
|
1348
|
-
mppSessionBudget: resolveMppSessionBudget(provider.mppSessionBudget, defaultMppSessionBudget),
|
|
1349
|
-
models: provider.models && provider.models.length > 0 ? provider.models : DEFAULT_SURF_MODELS
|
|
1350
|
-
})) : [{
|
|
1351
|
-
id: DEFAULT_SURF_PROVIDER_ID,
|
|
1352
|
-
baseUrl: DEFAULT_SURF_BASE_URL,
|
|
1353
|
-
upstreamUrl: DEFAULT_SURF_UPSTREAM_URL,
|
|
1354
|
-
protocol: defaultProtocol,
|
|
1355
|
-
mppSessionBudget: defaultMppSessionBudget,
|
|
1356
|
-
models: DEFAULT_SURF_MODELS
|
|
1357
|
-
}];
|
|
1358
|
-
return {
|
|
1359
|
-
providers: entries,
|
|
1360
|
-
models: entries.flatMap((provider) => provider.models.map((model) => ({
|
|
1361
|
-
...model,
|
|
1362
|
-
provider: provider.id
|
|
1363
|
-
})))
|
|
1364
|
-
};
|
|
1365
|
-
}
|
|
1366
|
-
function routePrefixForBaseUrl(baseUrl) {
|
|
1367
|
-
const segments = baseUrl.split("/").filter(Boolean);
|
|
1368
|
-
return segments.length > 0 ? `/${segments[0]}` : "/";
|
|
1369
|
-
}
|
|
1370
|
-
function resolveProtocol(value, fallback = "mpp") {
|
|
1371
|
-
return value === "x402" || value === "mpp" || value === "auto" ? value : fallback;
|
|
1372
|
-
}
|
|
1373
|
-
function resolveMppSessionBudget(value, fallback = "0.5") {
|
|
1374
|
-
return typeof value === "string" && value.trim().length > 0 ? value : fallback;
|
|
1375
|
-
}
|
|
1376
|
-
//#endregion
|
|
1377
|
-
//#region src/openclaw/route.ts
|
|
1378
|
-
function dbg(msg) {
|
|
1379
|
-
if (process.env.X402_PROXY_DEBUG === "1") process.stderr.write(`[x402-proxy] ${msg}\n`);
|
|
1380
|
-
}
|
|
1381
|
-
function createDownstreamAbort(req, res) {
|
|
1382
|
-
const controller = new AbortController();
|
|
1383
|
-
const abort = () => {
|
|
1384
|
-
if (!controller.signal.aborted) controller.abort();
|
|
1385
|
-
};
|
|
1386
|
-
const onReqAborted = () => abort();
|
|
1387
|
-
const onResClose = () => abort();
|
|
1388
|
-
req.once("aborted", onReqAborted);
|
|
1389
|
-
res.once("close", onResClose);
|
|
1390
|
-
return {
|
|
1391
|
-
signal: controller.signal,
|
|
1392
|
-
cleanup() {
|
|
1393
|
-
req.off("aborted", onReqAborted);
|
|
1394
|
-
res.off("close", onResClose);
|
|
1395
|
-
}
|
|
1396
|
-
};
|
|
1397
|
-
}
|
|
1398
|
-
function createInferenceProxyRouteHandler(opts) {
|
|
1399
|
-
const { providers, getX402Proxy, getMppHandler, getWalletAddress, getWalletAddressForNetwork, historyPath, allModels, logger } = opts;
|
|
1400
|
-
const sortedProviders = providers.slice().sort((left, right) => right.baseUrl.length - left.baseUrl.length);
|
|
1401
|
-
return async (req, res) => {
|
|
1402
|
-
const downstream = createDownstreamAbort(req, res);
|
|
1403
|
-
const url = new URL(req.url ?? "/", "http://localhost");
|
|
1404
|
-
const provider = sortedProviders.find((entry) => entry.baseUrl === "/" || url.pathname.startsWith(entry.baseUrl));
|
|
1405
|
-
if (!provider) {
|
|
1406
|
-
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1407
|
-
res.end(JSON.stringify({ error: {
|
|
1408
|
-
message: "Unknown inference route",
|
|
1409
|
-
code: "not_found"
|
|
1410
|
-
} }));
|
|
1411
|
-
return true;
|
|
1412
|
-
}
|
|
1413
|
-
const walletAddress = getWalletAddress();
|
|
1414
|
-
if (!walletAddress) {
|
|
1415
|
-
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1416
|
-
res.end(JSON.stringify({ error: {
|
|
1417
|
-
message: "Wallet not loaded yet",
|
|
1418
|
-
code: "not_ready"
|
|
1419
|
-
} }));
|
|
1420
|
-
return true;
|
|
1421
|
-
}
|
|
1422
|
-
const pathSuffix = provider.baseUrl === "/" ? url.pathname : url.pathname.slice(provider.baseUrl.length);
|
|
1423
|
-
const upstreamUrl = `${provider.upstreamUrl.replace(/\/+$/, "")}${pathSuffix.startsWith("/") ? pathSuffix : `/${pathSuffix}`}${url.search}`;
|
|
1424
|
-
dbg(`${req.method} ${url.pathname} -> ${upstreamUrl}`);
|
|
1425
|
-
logger.info(`proxy: intercepting ${upstreamUrl.substring(0, 80)}`);
|
|
1426
|
-
const chunks = [];
|
|
1427
|
-
for await (const chunk of req) chunks.push(Buffer.from(chunk));
|
|
1428
|
-
let body = Buffer.concat(chunks).toString("utf-8");
|
|
1429
|
-
const HOP_BY_HOP = new Set([
|
|
1430
|
-
"authorization",
|
|
1431
|
-
"host",
|
|
1432
|
-
"connection",
|
|
1433
|
-
"content-length",
|
|
1434
|
-
"transfer-encoding",
|
|
1435
|
-
"keep-alive",
|
|
1436
|
-
"te",
|
|
1437
|
-
"upgrade"
|
|
1438
|
-
]);
|
|
1439
|
-
const headers = {};
|
|
1440
|
-
for (const [key, val] of Object.entries(req.headers)) {
|
|
1441
|
-
if (HOP_BY_HOP.has(key)) continue;
|
|
1442
|
-
if (typeof val === "string") headers[key] = val;
|
|
1443
|
-
}
|
|
1444
|
-
const isChatCompletion = pathSuffix.includes("/chat/completions");
|
|
1445
|
-
const isMessagesApi = pathSuffix.includes("/messages");
|
|
1446
|
-
const isLlmEndpoint = isChatCompletion || isMessagesApi;
|
|
1447
|
-
let thinkingMode;
|
|
1448
|
-
if (isChatCompletion && body) try {
|
|
1449
|
-
const parsed = JSON.parse(body);
|
|
1450
|
-
if (parsed.reasoning_effort) thinkingMode = String(parsed.reasoning_effort);
|
|
1451
|
-
if (!parsed.stream_options) {
|
|
1452
|
-
parsed.stream_options = { include_usage: true };
|
|
1453
|
-
body = JSON.stringify(parsed);
|
|
1454
|
-
}
|
|
1455
|
-
} catch {}
|
|
1456
|
-
if (isMessagesApi && body) try {
|
|
1457
|
-
const thinking = JSON.parse(body).thinking;
|
|
1458
|
-
if (thinking?.type === "enabled" && thinking.budget_tokens) thinkingMode = `budget_${thinking.budget_tokens}`;
|
|
1459
|
-
} catch {}
|
|
1460
|
-
const method = req.method ?? "GET";
|
|
1461
|
-
const startMs = Date.now();
|
|
1462
|
-
try {
|
|
1463
|
-
const requestInit = {
|
|
1464
|
-
method,
|
|
1465
|
-
headers,
|
|
1466
|
-
body: ["GET", "HEAD"].includes(method) ? void 0 : body,
|
|
1467
|
-
signal: downstream.signal
|
|
1468
|
-
};
|
|
1469
|
-
const useMpp = provider.protocol === "mpp" || provider.protocol === "auto";
|
|
1470
|
-
const wantsStreaming = isLlmEndpoint && /"stream"\s*:\s*true/.test(body);
|
|
1471
|
-
if (useMpp) {
|
|
1472
|
-
const mpp = getMppHandler();
|
|
1473
|
-
if (!mpp) {
|
|
1474
|
-
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1475
|
-
res.end(JSON.stringify({ error: {
|
|
1476
|
-
message: "MPP inference requires an EVM wallet. Configure X402_PROXY_WALLET_MNEMONIC or X402_PROXY_WALLET_EVM_KEY.",
|
|
1477
|
-
code: "mpp_wallet_missing"
|
|
1478
|
-
} }));
|
|
1479
|
-
return true;
|
|
1480
|
-
}
|
|
1481
|
-
const mppWalletAddress = getWalletAddressForNetwork?.("eip155:4217") ?? walletAddress;
|
|
1482
|
-
return await handleMppRequest({
|
|
1483
|
-
res,
|
|
1484
|
-
upstreamUrl,
|
|
1485
|
-
requestInit,
|
|
1486
|
-
abortSignal: downstream.signal,
|
|
1487
|
-
isLlmEndpoint,
|
|
1488
|
-
walletAddress: mppWalletAddress,
|
|
1489
|
-
historyPath,
|
|
1490
|
-
logger,
|
|
1491
|
-
allModels,
|
|
1492
|
-
thinkingMode,
|
|
1493
|
-
wantsStreaming,
|
|
1494
|
-
isMessagesApi,
|
|
1495
|
-
startMs,
|
|
1496
|
-
mpp
|
|
1497
|
-
});
|
|
1498
|
-
}
|
|
1499
|
-
const proxy = getX402Proxy();
|
|
1500
|
-
if (!proxy) {
|
|
1501
|
-
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1502
|
-
res.end(JSON.stringify({ error: {
|
|
1503
|
-
message: "x402 wallet not loaded yet. Configure a Solana wallet or switch the provider to mpp.",
|
|
1504
|
-
code: "x402_wallet_missing"
|
|
1505
|
-
} }));
|
|
1506
|
-
return true;
|
|
1507
|
-
}
|
|
1508
|
-
const response = await proxy.x402Fetch(upstreamUrl, requestInit);
|
|
1509
|
-
if (response.status === 402) {
|
|
1510
|
-
const responseBody = await response.text();
|
|
1511
|
-
logger.error(`x402: payment failed, raw response: ${responseBody}`);
|
|
1512
|
-
const payment = proxy.shiftPayment();
|
|
1513
|
-
const amount = paymentAmount(payment);
|
|
1514
|
-
const paymentFrom = (payment?.network && getWalletAddressForNetwork?.(payment.network)) ?? walletAddress;
|
|
1515
|
-
const paymentNetwork = payment?.network ?? "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
1516
|
-
if (isLlmEndpoint) appendHistory(historyPath, {
|
|
1517
|
-
t: Date.now(),
|
|
1518
|
-
ok: false,
|
|
1519
|
-
kind: "x402_inference",
|
|
1520
|
-
net: paymentNetwork,
|
|
1521
|
-
from: paymentFrom,
|
|
1522
|
-
to: payment?.payTo,
|
|
1523
|
-
amount,
|
|
1524
|
-
token: amount != null ? "USDC" : void 0,
|
|
1525
|
-
ms: Date.now() - startMs,
|
|
1526
|
-
error: "payment_required"
|
|
1527
|
-
});
|
|
1528
|
-
let userMessage;
|
|
1529
|
-
if (responseBody.includes("simulation") || responseBody.includes("Simulation")) userMessage = `Insufficient USDC or SOL in wallet ${walletAddress}. Fund it with USDC (SPL token) to pay for inference.`;
|
|
1530
|
-
else if (responseBody.includes("insufficient") || responseBody.includes("balance")) userMessage = `Insufficient funds in wallet ${walletAddress}. Top up with USDC on Solana mainnet.`;
|
|
1531
|
-
else userMessage = `x402 payment failed: ${responseBody.substring(0, 200) || "unknown error"}. Wallet: ${walletAddress}`;
|
|
1532
|
-
writeErrorResponse(res, 402, userMessage, "x402_payment_error", "payment_failed", isMessagesApi);
|
|
1533
|
-
return true;
|
|
1534
|
-
}
|
|
1535
|
-
if (!response.ok && isLlmEndpoint) {
|
|
1536
|
-
const responseBody = await response.text();
|
|
1537
|
-
logger.error(`x402: upstream error ${response.status}: ${responseBody.substring(0, 300)}`);
|
|
1538
|
-
const payment = proxy.shiftPayment();
|
|
1539
|
-
const amount = paymentAmount(payment);
|
|
1540
|
-
const paymentFrom = (payment?.network && getWalletAddressForNetwork?.(payment.network)) ?? walletAddress;
|
|
1541
|
-
const paymentNetwork = payment?.network ?? "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
1542
|
-
appendHistory(historyPath, {
|
|
1543
|
-
t: Date.now(),
|
|
1544
|
-
ok: false,
|
|
1545
|
-
kind: "x402_inference",
|
|
1546
|
-
net: paymentNetwork,
|
|
1547
|
-
from: paymentFrom,
|
|
1548
|
-
to: payment?.payTo,
|
|
1549
|
-
amount,
|
|
1550
|
-
token: amount != null ? "USDC" : void 0,
|
|
1551
|
-
ms: Date.now() - startMs,
|
|
1552
|
-
error: `upstream_${response.status}`
|
|
1553
|
-
});
|
|
1554
|
-
writeErrorResponse(res, 502, `LLM provider temporarily unavailable (HTTP ${response.status}). Try again shortly.`, "x402_upstream_error", "upstream_failed", isMessagesApi);
|
|
1555
|
-
return true;
|
|
1556
|
-
}
|
|
1557
|
-
logger.info(`x402: response ${response.status}`);
|
|
1558
|
-
const txSig = extractTxSignature(response);
|
|
1559
|
-
const payment = proxy.shiftPayment();
|
|
1560
|
-
const amount = paymentAmount(payment);
|
|
1561
|
-
const paymentFrom = (payment?.network && getWalletAddressForNetwork?.(payment.network)) ?? walletAddress;
|
|
1562
|
-
const paymentNetwork = payment?.network ?? "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp";
|
|
1563
|
-
const resHeaders = {};
|
|
1564
|
-
for (const [key, val] of response.headers.entries()) resHeaders[key] = val;
|
|
1565
|
-
res.writeHead(response.status, resHeaders);
|
|
1566
|
-
if (!response.body) {
|
|
1567
|
-
res.end();
|
|
1568
|
-
if (shouldAppendInferenceHistory({
|
|
1569
|
-
isLlmEndpoint,
|
|
1570
|
-
amount
|
|
1571
|
-
})) appendHistory(historyPath, {
|
|
1572
|
-
t: Date.now(),
|
|
1573
|
-
ok: true,
|
|
1574
|
-
kind: "x402_inference",
|
|
1575
|
-
net: paymentNetwork,
|
|
1576
|
-
from: paymentFrom,
|
|
1577
|
-
to: payment?.payTo,
|
|
1578
|
-
tx: txSig,
|
|
1579
|
-
amount,
|
|
1580
|
-
token: "USDC",
|
|
1581
|
-
ms: Date.now() - startMs
|
|
1582
|
-
});
|
|
1583
|
-
return true;
|
|
1584
|
-
}
|
|
1585
|
-
const ct = response.headers.get("content-type") || "";
|
|
1586
|
-
const sse = isLlmEndpoint && ct.includes("text/event-stream") ? createSseTracker() : null;
|
|
1587
|
-
const reader = response.body.getReader();
|
|
1588
|
-
const decoder = new TextDecoder();
|
|
1589
|
-
try {
|
|
1590
|
-
while (true) {
|
|
1591
|
-
if (downstream.signal.aborted) {
|
|
1592
|
-
await reader.cancel().catch(() => {});
|
|
1593
|
-
break;
|
|
1594
|
-
}
|
|
1595
|
-
const { done, value } = await reader.read();
|
|
1596
|
-
if (done) break;
|
|
1597
|
-
res.write(value);
|
|
1598
|
-
sse?.push(decoder.decode(value, { stream: true }));
|
|
1599
|
-
if (isMessagesApi && sse?.sawAnthropicMessageStop) {
|
|
1600
|
-
await reader.cancel().catch(() => {});
|
|
1601
|
-
break;
|
|
1602
|
-
}
|
|
1603
|
-
}
|
|
1604
|
-
} finally {
|
|
1605
|
-
reader.releaseLock();
|
|
1606
|
-
}
|
|
1607
|
-
if (!res.writableEnded) res.end();
|
|
1608
|
-
if (shouldAppendInferenceHistory({
|
|
1609
|
-
isLlmEndpoint,
|
|
1610
|
-
amount,
|
|
1611
|
-
usage: sse?.result
|
|
1612
|
-
})) appendInferenceHistory({
|
|
1613
|
-
historyPath,
|
|
1614
|
-
allModels,
|
|
1615
|
-
walletAddress: paymentFrom,
|
|
1616
|
-
paymentNetwork,
|
|
1617
|
-
paymentTo: payment?.payTo,
|
|
1618
|
-
tx: txSig,
|
|
1619
|
-
amount,
|
|
1620
|
-
thinkingMode,
|
|
1621
|
-
usage: sse?.result,
|
|
1622
|
-
durationMs: Date.now() - startMs
|
|
1623
|
-
});
|
|
1624
|
-
return true;
|
|
1625
|
-
} catch (err) {
|
|
1626
|
-
const msg = String(err);
|
|
1627
|
-
logger.error(`x402: fetch threw: ${msg}`);
|
|
1628
|
-
getX402Proxy()?.shiftPayment();
|
|
1629
|
-
if (isLlmEndpoint) appendHistory(historyPath, {
|
|
1630
|
-
t: Date.now(),
|
|
1631
|
-
ok: false,
|
|
1632
|
-
kind: "x402_inference",
|
|
1633
|
-
net: SOL_MAINNET,
|
|
1634
|
-
from: walletAddress,
|
|
1635
|
-
ms: Date.now() - startMs,
|
|
1636
|
-
error: msg.substring(0, 200)
|
|
1637
|
-
});
|
|
1638
|
-
let userMessage;
|
|
1639
|
-
if (msg.includes("Simulation failed") || msg.includes("simulation")) userMessage = `Insufficient USDC or SOL in wallet ${walletAddress}. Fund it with USDC and SOL to pay for inference.`;
|
|
1640
|
-
else if (msg.includes("Failed to create payment")) userMessage = `x402 payment creation failed: ${msg}. Wallet: ${walletAddress}`;
|
|
1641
|
-
else userMessage = `x402 request failed: ${msg}`;
|
|
1642
|
-
if (!res.headersSent) writeErrorResponse(res, 402, userMessage, "x402_payment_error", "payment_failed", isMessagesApi);
|
|
1643
|
-
return true;
|
|
1644
|
-
} finally {
|
|
1645
|
-
downstream.cleanup();
|
|
1646
|
-
}
|
|
1647
|
-
};
|
|
1648
|
-
}
|
|
1649
|
-
function writeErrorResponse(res, status, message, type, code, isAnthropicFormat) {
|
|
1650
|
-
res.writeHead(status, { "Content-Type": "application/json" });
|
|
1651
|
-
if (isAnthropicFormat) res.end(JSON.stringify({
|
|
1652
|
-
type: "error",
|
|
1653
|
-
error: {
|
|
1654
|
-
type,
|
|
1655
|
-
message
|
|
1656
|
-
}
|
|
1657
|
-
}));
|
|
1658
|
-
else res.end(JSON.stringify({ error: {
|
|
1659
|
-
message,
|
|
1660
|
-
type,
|
|
1661
|
-
code
|
|
1662
|
-
} }));
|
|
1663
|
-
}
|
|
1664
|
-
function shouldAppendInferenceHistory(opts) {
|
|
1665
|
-
if (!opts.isLlmEndpoint) return false;
|
|
1666
|
-
return opts.amount != null || opts.usage != null;
|
|
1667
|
-
}
|
|
1668
|
-
function createSseTracker() {
|
|
1669
|
-
let residual = "";
|
|
1670
|
-
let anthropicModel = "";
|
|
1671
|
-
let anthropicInputTokens = 0;
|
|
1672
|
-
let anthropicOutputTokens = 0;
|
|
1673
|
-
let anthropicCacheRead;
|
|
1674
|
-
let anthropicCacheWrite;
|
|
1675
|
-
let anthropicMessageStop = false;
|
|
1676
|
-
let lastOpenAiData = "";
|
|
1677
|
-
let isAnthropic = false;
|
|
1678
|
-
function processJson(json) {
|
|
1679
|
-
let parsed;
|
|
1680
|
-
try {
|
|
1681
|
-
parsed = JSON.parse(json);
|
|
1682
|
-
} catch {
|
|
1683
|
-
return;
|
|
1684
|
-
}
|
|
1685
|
-
const type = parsed.type;
|
|
1686
|
-
if (type === "message_start") {
|
|
1687
|
-
isAnthropic = true;
|
|
1688
|
-
const msg = parsed.message;
|
|
1689
|
-
anthropicModel = msg?.model ?? "";
|
|
1690
|
-
const u = msg?.usage;
|
|
1691
|
-
if (u) {
|
|
1692
|
-
anthropicInputTokens = u.input_tokens ?? 0;
|
|
1693
|
-
anthropicCacheWrite = u.cache_creation_input_tokens ?? void 0;
|
|
1694
|
-
anthropicCacheRead = u.cache_read_input_tokens ?? void 0;
|
|
1695
|
-
}
|
|
1696
|
-
return;
|
|
1697
|
-
}
|
|
1698
|
-
if (type === "message_delta") {
|
|
1699
|
-
isAnthropic = true;
|
|
1700
|
-
const u = parsed.usage;
|
|
1701
|
-
if (u?.output_tokens != null) anthropicOutputTokens = u.output_tokens;
|
|
1702
|
-
return;
|
|
1703
|
-
}
|
|
1704
|
-
if (type === "message_stop") {
|
|
1705
|
-
isAnthropic = true;
|
|
1706
|
-
anthropicMessageStop = true;
|
|
1707
|
-
return;
|
|
1708
|
-
}
|
|
1709
|
-
if (type === "message") {
|
|
1710
|
-
isAnthropic = true;
|
|
1711
|
-
anthropicModel = parsed.model ?? "";
|
|
1712
|
-
const u = parsed.usage;
|
|
1713
|
-
if (u) {
|
|
1714
|
-
anthropicInputTokens = u.input_tokens ?? 0;
|
|
1715
|
-
anthropicOutputTokens = u.output_tokens ?? 0;
|
|
1716
|
-
anthropicCacheWrite = u.cache_creation_input_tokens ?? void 0;
|
|
1717
|
-
anthropicCacheRead = u.cache_read_input_tokens ?? void 0;
|
|
1718
|
-
}
|
|
1719
|
-
return;
|
|
1720
|
-
}
|
|
1721
|
-
if (parsed.usage || parsed.model) lastOpenAiData = json;
|
|
1722
|
-
}
|
|
1723
|
-
return {
|
|
1724
|
-
push(text) {
|
|
1725
|
-
const lines = (residual + text).split("\n");
|
|
1726
|
-
residual = lines.pop() ?? "";
|
|
1727
|
-
for (const line of lines) if (line.startsWith("data: ") && line !== "data: [DONE]") processJson(line.slice(6));
|
|
1728
|
-
},
|
|
1729
|
-
pushJson(text) {
|
|
1730
|
-
processJson(text);
|
|
1731
|
-
},
|
|
1732
|
-
get sawAnthropicMessageStop() {
|
|
1733
|
-
return anthropicMessageStop;
|
|
1734
|
-
},
|
|
1735
|
-
get result() {
|
|
1736
|
-
if (isAnthropic) {
|
|
1737
|
-
if (!anthropicModel && !anthropicInputTokens && !anthropicOutputTokens) return void 0;
|
|
1738
|
-
return {
|
|
1739
|
-
model: anthropicModel,
|
|
1740
|
-
inputTokens: anthropicInputTokens,
|
|
1741
|
-
outputTokens: anthropicOutputTokens,
|
|
1742
|
-
cacheRead: anthropicCacheRead,
|
|
1743
|
-
cacheWrite: anthropicCacheWrite
|
|
1744
|
-
};
|
|
1745
|
-
}
|
|
1746
|
-
if (!lastOpenAiData) return void 0;
|
|
1747
|
-
try {
|
|
1748
|
-
const parsed = JSON.parse(lastOpenAiData);
|
|
1749
|
-
return {
|
|
1750
|
-
model: parsed.model ?? "",
|
|
1751
|
-
inputTokens: parsed.usage?.prompt_tokens ?? 0,
|
|
1752
|
-
outputTokens: parsed.usage?.completion_tokens ?? 0,
|
|
1753
|
-
reasoningTokens: parsed.usage?.completion_tokens_details?.reasoning_tokens,
|
|
1754
|
-
cacheRead: parsed.usage?.prompt_tokens_details?.cached_tokens,
|
|
1755
|
-
cacheWrite: parsed.usage?.prompt_tokens_details?.cache_creation_input_tokens
|
|
1756
|
-
};
|
|
1757
|
-
} catch {
|
|
1758
|
-
return;
|
|
1759
|
-
}
|
|
1760
|
-
}
|
|
1761
|
-
};
|
|
1762
|
-
}
|
|
1763
|
-
async function handleMppRequest(opts) {
|
|
1764
|
-
const { res, upstreamUrl, requestInit, abortSignal, isLlmEndpoint, walletAddress, historyPath, logger, allModels, thinkingMode, wantsStreaming, isMessagesApi, startMs, mpp } = opts;
|
|
1765
|
-
try {
|
|
1766
|
-
if (wantsStreaming) {
|
|
1767
|
-
res.writeHead(200, {
|
|
1768
|
-
"Content-Type": "text/event-stream; charset=utf-8",
|
|
1769
|
-
"Cache-Control": "no-cache, no-transform",
|
|
1770
|
-
Connection: "keep-alive"
|
|
1771
|
-
});
|
|
1772
|
-
const sse = createSseTracker();
|
|
1773
|
-
dbg(`mpp.sse() calling ${upstreamUrl}`);
|
|
1774
|
-
const stream = await mpp.sse(upstreamUrl, requestInit);
|
|
1775
|
-
dbg("mpp.sse() resolved, iterating stream");
|
|
1776
|
-
const iterator = stream[Symbol.asyncIterator]();
|
|
1777
|
-
let semanticComplete = false;
|
|
1778
|
-
try {
|
|
1779
|
-
if (isMessagesApi) while (true) {
|
|
1780
|
-
if (abortSignal.aborted) break;
|
|
1781
|
-
const { done, value } = await iterator.next();
|
|
1782
|
-
if (done) break;
|
|
1783
|
-
const text = String(value);
|
|
1784
|
-
let eventType = "unknown";
|
|
1785
|
-
try {
|
|
1786
|
-
eventType = JSON.parse(text).type ?? "unknown";
|
|
1787
|
-
} catch {}
|
|
1788
|
-
res.write(`event: ${eventType}\ndata: ${text}\n\n`);
|
|
1789
|
-
sse.pushJson(text);
|
|
1790
|
-
if (sse.sawAnthropicMessageStop) {
|
|
1791
|
-
semanticComplete = true;
|
|
1792
|
-
break;
|
|
1793
|
-
}
|
|
1794
|
-
}
|
|
1795
|
-
else {
|
|
1796
|
-
while (true) {
|
|
1797
|
-
if (abortSignal.aborted) break;
|
|
1798
|
-
const { done, value } = await iterator.next();
|
|
1799
|
-
if (done) break;
|
|
1800
|
-
const text = String(value);
|
|
1801
|
-
res.write(`data: ${text}\n\n`);
|
|
1802
|
-
sse.pushJson(text);
|
|
1803
|
-
}
|
|
1804
|
-
if (!abortSignal.aborted) res.write("data: [DONE]\n\n");
|
|
1805
|
-
}
|
|
1806
|
-
} catch (err) {
|
|
1807
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
1808
|
-
if (!(abortSignal.aborted || semanticComplete) || !msg.includes("terminated")) throw err;
|
|
1809
|
-
} finally {
|
|
1810
|
-
await iterator.return?.().catch(() => {});
|
|
1811
|
-
}
|
|
1812
|
-
dbg(`stream done, ${sse.result ? `${sse.result.model} ${sse.result.inputTokens}+${sse.result.outputTokens}t` : "no usage"}`);
|
|
1813
|
-
if (!res.writableEnded) res.end();
|
|
1814
|
-
mpp.shiftPayment();
|
|
1815
|
-
if (shouldAppendInferenceHistory({
|
|
1816
|
-
isLlmEndpoint,
|
|
1817
|
-
usage: sse.result
|
|
1818
|
-
})) appendInferenceHistory({
|
|
1819
|
-
historyPath,
|
|
1820
|
-
allModels,
|
|
1821
|
-
walletAddress,
|
|
1822
|
-
paymentNetwork: TEMPO_NETWORK,
|
|
1823
|
-
paymentTo: void 0,
|
|
1824
|
-
thinkingMode,
|
|
1825
|
-
usage: sse.result,
|
|
1826
|
-
durationMs: Date.now() - startMs
|
|
1827
|
-
});
|
|
1828
|
-
return true;
|
|
1829
|
-
}
|
|
1830
|
-
const response = await mpp.fetch(upstreamUrl, requestInit);
|
|
1831
|
-
const tx = extractTxSignature(response);
|
|
1832
|
-
if (response.status === 402) {
|
|
1833
|
-
const responseBody = await response.text();
|
|
1834
|
-
logger.error(`mpp: payment failed, raw response: ${responseBody}`);
|
|
1835
|
-
if (isLlmEndpoint) appendHistory(historyPath, {
|
|
1836
|
-
t: Date.now(),
|
|
1837
|
-
ok: false,
|
|
1838
|
-
kind: "x402_inference",
|
|
1839
|
-
net: TEMPO_NETWORK,
|
|
1840
|
-
from: walletAddress,
|
|
1841
|
-
ms: Date.now() - startMs,
|
|
1842
|
-
error: "payment_required"
|
|
1843
|
-
});
|
|
1844
|
-
writeErrorResponse(res, 402, `MPP payment failed: ${responseBody.substring(0, 200) || "unknown error"}. Wallet: ${walletAddress}`, "mpp_payment_error", "payment_failed", isMessagesApi);
|
|
1845
|
-
return true;
|
|
1846
|
-
}
|
|
1847
|
-
const resHeaders = {};
|
|
1848
|
-
for (const [key, value] of response.headers.entries()) resHeaders[key] = value;
|
|
1849
|
-
res.writeHead(response.status, resHeaders);
|
|
1850
|
-
const responseBody = await response.text();
|
|
1851
|
-
res.end(responseBody);
|
|
1852
|
-
const usageTracker = createSseTracker();
|
|
1853
|
-
usageTracker.pushJson(responseBody);
|
|
1854
|
-
const payment = mpp.shiftPayment();
|
|
1855
|
-
const amount = parseMppAmount(payment?.amount);
|
|
1856
|
-
if (shouldAppendInferenceHistory({
|
|
1857
|
-
isLlmEndpoint,
|
|
1858
|
-
amount,
|
|
1859
|
-
usage: usageTracker.result
|
|
1860
|
-
})) appendInferenceHistory({
|
|
1861
|
-
historyPath,
|
|
1862
|
-
allModels,
|
|
1863
|
-
walletAddress,
|
|
1864
|
-
paymentNetwork: payment?.network ?? "eip155:4217",
|
|
1865
|
-
paymentTo: void 0,
|
|
1866
|
-
tx: tx ?? payment?.receipt?.reference,
|
|
1867
|
-
amount,
|
|
1868
|
-
thinkingMode,
|
|
1869
|
-
usage: usageTracker.result,
|
|
1870
|
-
durationMs: Date.now() - startMs
|
|
1871
|
-
});
|
|
1872
|
-
return true;
|
|
1873
|
-
} catch (err) {
|
|
1874
|
-
dbg(`mpp error: ${String(err)}`);
|
|
1875
|
-
logger.error(`mpp: fetch threw: ${String(err)}`);
|
|
1876
|
-
if (isLlmEndpoint) appendHistory(historyPath, {
|
|
1877
|
-
t: Date.now(),
|
|
1878
|
-
ok: false,
|
|
1879
|
-
kind: "x402_inference",
|
|
1880
|
-
net: TEMPO_NETWORK,
|
|
1881
|
-
from: walletAddress,
|
|
1882
|
-
ms: Date.now() - startMs,
|
|
1883
|
-
error: String(err).substring(0, 200)
|
|
1884
|
-
});
|
|
1885
|
-
if (!res.headersSent) writeErrorResponse(res, 402, `MPP request failed: ${String(err)}`, "mpp_payment_error", "payment_failed", isMessagesApi);
|
|
1886
|
-
else if (!res.writableEnded) res.end();
|
|
1887
|
-
return true;
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1890
|
-
function appendInferenceHistory(opts) {
|
|
1891
|
-
const { historyPath, allModels, walletAddress, paymentNetwork, paymentTo, tx, amount, thinkingMode, usage, durationMs } = opts;
|
|
1892
|
-
if (usage) appendHistory(historyPath, {
|
|
1893
|
-
t: Date.now(),
|
|
1894
|
-
ok: true,
|
|
1895
|
-
kind: "x402_inference",
|
|
1896
|
-
net: paymentNetwork,
|
|
1897
|
-
from: walletAddress,
|
|
1898
|
-
to: paymentTo,
|
|
1899
|
-
tx,
|
|
1900
|
-
amount,
|
|
1901
|
-
token: "USDC",
|
|
1902
|
-
provider: allModels.find((entry) => entry.id === usage.model || `${entry.provider}/${entry.id}` === usage.model)?.provider,
|
|
1903
|
-
model: usage.model,
|
|
1904
|
-
inputTokens: usage.inputTokens,
|
|
1905
|
-
outputTokens: usage.outputTokens,
|
|
1906
|
-
reasoningTokens: usage.reasoningTokens,
|
|
1907
|
-
cacheRead: usage.cacheRead,
|
|
1908
|
-
cacheWrite: usage.cacheWrite,
|
|
1909
|
-
thinking: thinkingMode,
|
|
1910
|
-
ms: durationMs
|
|
1911
|
-
});
|
|
1912
|
-
else appendHistory(historyPath, {
|
|
1913
|
-
t: Date.now(),
|
|
1914
|
-
ok: true,
|
|
1915
|
-
kind: "x402_inference",
|
|
1916
|
-
net: paymentNetwork,
|
|
1917
|
-
from: walletAddress,
|
|
1918
|
-
to: paymentTo,
|
|
1919
|
-
tx,
|
|
1920
|
-
amount,
|
|
1921
|
-
token: "USDC",
|
|
1922
|
-
ms: durationMs
|
|
1923
|
-
});
|
|
1924
|
-
}
|
|
1925
|
-
//#endregion
|
|
1926
|
-
//#region src/openclaw/plugin.ts
|
|
1927
|
-
function register(api) {
|
|
1928
|
-
const config = api.pluginConfig ?? {};
|
|
1929
|
-
const explicitKeypairPath = config.keypairPath;
|
|
1930
|
-
const rpcUrl = config.rpcUrl || "https://api.mainnet-beta.solana.com";
|
|
1931
|
-
const dashboardUrl = config.dashboardUrl || "";
|
|
1932
|
-
const { providers, models: allModels } = resolveProviders(config);
|
|
1933
|
-
const defaultProvider = providers[0];
|
|
1934
|
-
for (const provider of providers) api.registerProvider({
|
|
1935
|
-
id: provider.id,
|
|
1936
|
-
label: provider.id,
|
|
1937
|
-
auth: [],
|
|
1938
|
-
catalog: {
|
|
1939
|
-
order: "simple",
|
|
1940
|
-
run: async () => ({ provider: {
|
|
1941
|
-
baseUrl: provider.baseUrl,
|
|
1942
|
-
api: "openai-completions",
|
|
1943
|
-
authHeader: false,
|
|
1944
|
-
models: provider.models
|
|
1945
|
-
} })
|
|
1946
|
-
}
|
|
1947
|
-
});
|
|
1948
|
-
api.logger.info(`x402-proxy: ${providers.map((provider) => `${provider.id}:${provider.protocol}`).join(", ")} - ${allModels.length} models`);
|
|
1949
|
-
let solanaWalletAddress = null;
|
|
1950
|
-
let evmWalletAddress = null;
|
|
1951
|
-
let signerRef = null;
|
|
1952
|
-
let proxyRef = null;
|
|
1953
|
-
let mppHandlerRef = null;
|
|
1954
|
-
let evmKeyRef = null;
|
|
1955
|
-
let walletLoadPromise = null;
|
|
1956
|
-
const historyPath = getHistoryPath();
|
|
1957
|
-
const routePrefixes = [...new Set(providers.map((provider) => routePrefixForBaseUrl(provider.baseUrl)))];
|
|
1958
|
-
const handler = createInferenceProxyRouteHandler({
|
|
1959
|
-
providers,
|
|
1960
|
-
getX402Proxy: () => proxyRef,
|
|
1961
|
-
getMppHandler: () => mppHandlerRef,
|
|
1962
|
-
getWalletAddress: () => solanaWalletAddress ?? evmWalletAddress,
|
|
1963
|
-
getWalletAddressForNetwork: (network) => addressForNetwork(evmWalletAddress, solanaWalletAddress, network),
|
|
1964
|
-
historyPath,
|
|
1965
|
-
allModels,
|
|
1966
|
-
logger: api.logger
|
|
1967
|
-
});
|
|
1968
|
-
for (const path of routePrefixes) api.registerHttpRoute({
|
|
1969
|
-
path,
|
|
1970
|
-
match: "prefix",
|
|
1971
|
-
auth: "plugin",
|
|
1972
|
-
handler
|
|
1973
|
-
});
|
|
1974
|
-
api.logger.info(`proxy: HTTP routes ${routePrefixes.join(", ")} registered for ${providers.map((provider) => provider.upstreamUrl).join(", ")}`);
|
|
1975
|
-
async function ensureWalletLoaded() {
|
|
1976
|
-
if (walletLoadPromise) {
|
|
1977
|
-
await walletLoadPromise;
|
|
1978
|
-
return;
|
|
1979
|
-
}
|
|
1980
|
-
walletLoadPromise = (async () => {
|
|
1981
|
-
try {
|
|
1982
|
-
const resolution = resolveWallet();
|
|
1983
|
-
evmKeyRef = resolution.evmKey ?? null;
|
|
1984
|
-
evmWalletAddress = resolution.evmAddress ?? null;
|
|
1985
|
-
if (explicitKeypairPath) {
|
|
1986
|
-
signerRef = await loadSvmWallet(explicitKeypairPath.startsWith("~/") ? join(homedir(), explicitKeypairPath.slice(2)) : explicitKeypairPath);
|
|
1987
|
-
solanaWalletAddress = signerRef.address;
|
|
1988
|
-
} else if (resolution.solanaKey && resolution.solanaAddress) {
|
|
1989
|
-
signerRef = await createKeyPairSignerFromBytes(resolution.solanaKey);
|
|
1990
|
-
solanaWalletAddress = resolution.solanaAddress;
|
|
1991
|
-
} else {
|
|
1992
|
-
signerRef = null;
|
|
1993
|
-
solanaWalletAddress = null;
|
|
1994
|
-
}
|
|
1995
|
-
if (!solanaWalletAddress && !evmWalletAddress) {
|
|
1996
|
-
api.logger.error("x402-proxy: no wallet found. Run `x402-proxy setup` to create one, or set X402_PROXY_WALLET_MNEMONIC / X402_PROXY_WALLET_EVM_KEY.");
|
|
1997
|
-
return;
|
|
1998
|
-
}
|
|
1999
|
-
if (signerRef) {
|
|
2000
|
-
const client = new x402Client();
|
|
2001
|
-
client.register(SOL_MAINNET, new OptimizedSvmScheme(signerRef, { rpcUrl }));
|
|
2002
|
-
proxyRef = createX402ProxyHandler({ client });
|
|
2003
|
-
} else proxyRef = null;
|
|
2004
|
-
if (evmKeyRef) {
|
|
2005
|
-
const maxBudget = Math.max(...providers.map((p) => Number(p.mppSessionBudget) || .5)).toString();
|
|
2006
|
-
mppHandlerRef = await createMppProxyHandler({
|
|
2007
|
-
evmKey: evmKeyRef,
|
|
2008
|
-
maxDeposit: maxBudget
|
|
2009
|
-
});
|
|
2010
|
-
} else mppHandlerRef = null;
|
|
2011
|
-
api.logger.info(`wallets: solana=${solanaWalletAddress ?? "missing"} evm=${evmWalletAddress ?? "missing"}`);
|
|
2012
|
-
} catch (err) {
|
|
2013
|
-
api.logger.error(`wallet load failed: ${err}`);
|
|
2014
|
-
}
|
|
2015
|
-
})();
|
|
2016
|
-
await walletLoadPromise;
|
|
2017
|
-
}
|
|
2018
|
-
ensureWalletLoaded();
|
|
2019
|
-
api.registerService({
|
|
2020
|
-
id: "x402-wallet",
|
|
2021
|
-
async start() {
|
|
2022
|
-
await ensureWalletLoaded();
|
|
2023
|
-
},
|
|
2024
|
-
async stop() {
|
|
2025
|
-
if (mppHandlerRef) try {
|
|
2026
|
-
await mppHandlerRef.close();
|
|
2027
|
-
} catch {}
|
|
2028
|
-
}
|
|
2029
|
-
});
|
|
2030
|
-
const toolCtx = {
|
|
2031
|
-
ensureReady: ensureWalletLoaded,
|
|
2032
|
-
getSolanaWalletAddress: () => solanaWalletAddress,
|
|
2033
|
-
getEvmWalletAddress: () => evmWalletAddress,
|
|
2034
|
-
getSigner: () => signerRef,
|
|
2035
|
-
getX402Proxy: () => proxyRef,
|
|
2036
|
-
getEvmKey: () => evmKeyRef,
|
|
2037
|
-
getDefaultRequestProtocol: () => defaultProvider?.protocol ?? "mpp",
|
|
2038
|
-
getDefaultMppSessionBudget: () => defaultProvider?.mppSessionBudget ?? "0.5",
|
|
2039
|
-
rpcUrl,
|
|
2040
|
-
historyPath,
|
|
2041
|
-
allModels
|
|
2042
|
-
};
|
|
2043
|
-
api.registerTool(createWalletTool(toolCtx), { names: ["x_balance"] });
|
|
2044
|
-
api.registerTool(createRequestTool(toolCtx), { names: ["x_payment"] });
|
|
2045
|
-
const cmdCtx = {
|
|
2046
|
-
ensureReady: ensureWalletLoaded,
|
|
2047
|
-
getSolanaWalletAddress: () => solanaWalletAddress,
|
|
2048
|
-
getEvmWalletAddress: () => evmWalletAddress,
|
|
2049
|
-
getSigner: () => signerRef,
|
|
2050
|
-
getDefaultRequestProtocol: () => defaultProvider?.protocol ?? "mpp",
|
|
2051
|
-
getDefaultMppSessionBudget: () => defaultProvider?.mppSessionBudget ?? "0.5",
|
|
2052
|
-
rpcUrl,
|
|
2053
|
-
dashboardUrl,
|
|
2054
|
-
historyPath,
|
|
2055
|
-
allModels
|
|
2056
|
-
};
|
|
2057
|
-
api.registerCommand(createWalletCommand(cmdCtx));
|
|
2058
|
-
api.registerCommand(createSendCommand(cmdCtx));
|
|
2059
|
-
}
|
|
2060
|
-
var plugin_default = definePluginEntry({
|
|
2061
|
-
id: "x402-proxy",
|
|
2062
|
-
name: "mpp/x402 Payments Proxy",
|
|
2063
|
-
description: "x402 and MPP payments, wallet tools, and paid inference proxying",
|
|
2064
|
-
register
|
|
2065
|
-
});
|
|
2066
|
-
//#endregion
|
|
2067
|
-
export { plugin_default as default, register };
|