x402-proxy-openclaw 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 +453 -0
- package/LICENSE +201 -0
- package/README.md +35 -0
- package/dist/commands/wallet.js +149 -0
- package/dist/commands.js +218 -0
- package/dist/defaults.js +129 -0
- package/dist/handler.js +319 -0
- package/dist/history.js +124 -0
- package/dist/lib/config.js +47 -0
- package/dist/lib/debug-log.js +29 -0
- package/dist/lib/derive.js +77 -0
- package/dist/lib/env.js +9 -0
- package/dist/lib/optimized-svm-scheme.js +86 -0
- package/dist/lib/output.js +13 -0
- package/dist/lib/wallet-resolution.js +76 -0
- package/dist/openclaw/plugin.d.ts +15 -0
- package/dist/openclaw/plugin.js +156 -0
- package/dist/openclaw.plugin.json +112 -0
- package/dist/route.js +671 -0
- package/dist/solana.js +93 -0
- package/dist/tools.js +269 -0
- package/dist/wallet.js +15 -0
- package/openclaw.plugin.json +112 -0
- package/package.json +93 -0
- package/skills/SKILL.md +183 -0
- package/skills/references/library.md +85 -0
- package/skills/references/openclaw-plugin.md +145 -0
package/dist/handler.js
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { getMppVoucherHeadroomUsdc, isDebugEnabled } from "./lib/env.js";
|
|
2
|
+
import { decodePaymentResponseHeader, wrapFetchWithPayment } from "@x402/fetch";
|
|
3
|
+
import { parseUnits } from "viem";
|
|
4
|
+
//#region packages/x402-proxy/src/handler.ts
|
|
5
|
+
/**
|
|
6
|
+
* Extract the on-chain transaction signature from an x402 payment response header.
|
|
7
|
+
*/
|
|
8
|
+
function extractTxSignature(response) {
|
|
9
|
+
const x402Header = response.headers.get("PAYMENT-RESPONSE") ?? response.headers.get("X-PAYMENT-RESPONSE");
|
|
10
|
+
if (x402Header) try {
|
|
11
|
+
return decodePaymentResponseHeader(x402Header).transaction ?? void 0;
|
|
12
|
+
} catch {}
|
|
13
|
+
const mppHeader = response.headers.get("Payment-Receipt");
|
|
14
|
+
if (mppHeader) try {
|
|
15
|
+
return JSON.parse(Buffer.from(mppHeader, "base64url").toString()).reference ?? void 0;
|
|
16
|
+
} catch {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Create an x402 proxy handler that wraps fetch with automatic payment.
|
|
22
|
+
*/
|
|
23
|
+
function createX402ProxyHandler(opts) {
|
|
24
|
+
const { client } = opts;
|
|
25
|
+
const paymentQueue = [];
|
|
26
|
+
client.onAfterPaymentCreation(async (hookCtx) => {
|
|
27
|
+
const raw = hookCtx.selectedRequirements.amount;
|
|
28
|
+
paymentQueue.push({
|
|
29
|
+
protocol: "x402",
|
|
30
|
+
network: hookCtx.selectedRequirements.network,
|
|
31
|
+
payTo: hookCtx.selectedRequirements.payTo,
|
|
32
|
+
amount: raw,
|
|
33
|
+
asset: hookCtx.selectedRequirements.asset
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
return {
|
|
37
|
+
x402Fetch: wrapFetchWithPayment(globalThis.fetch, client),
|
|
38
|
+
shiftPayment: () => paymentQueue.shift()
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const TEMPO_NETWORK = "eip155:4217";
|
|
42
|
+
const MPP_VOUCHER_HEADROOM_USDC_DEFAULT = "0.005";
|
|
43
|
+
function computeMppVoucherTarget(params) {
|
|
44
|
+
const { requiredCumulative, deposit, headroom } = params;
|
|
45
|
+
if (deposit <= requiredCumulative) return requiredCumulative;
|
|
46
|
+
if (headroom <= 0n) return requiredCumulative;
|
|
47
|
+
const target = requiredCumulative + headroom;
|
|
48
|
+
return target > deposit ? deposit : target;
|
|
49
|
+
}
|
|
50
|
+
function parseVoucherHeadroom(value) {
|
|
51
|
+
const configured = value?.trim() || MPP_VOUCHER_HEADROOM_USDC_DEFAULT;
|
|
52
|
+
try {
|
|
53
|
+
const parsed = parseUnits(configured, 6);
|
|
54
|
+
return parsed > 0n ? parsed : 0n;
|
|
55
|
+
} catch {
|
|
56
|
+
return parseUnits(MPP_VOUCHER_HEADROOM_USDC_DEFAULT, 6);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Create an MPP proxy handler using mppx client.
|
|
61
|
+
* Dynamically imports mppx/client to keep startup fast.
|
|
62
|
+
*/
|
|
63
|
+
async function createMppProxyHandler(opts) {
|
|
64
|
+
const { Mppx, tempo } = await import("mppx/client");
|
|
65
|
+
const { Session } = await import("mppx/tempo");
|
|
66
|
+
const { privateKeyToAccount } = await import("viem/accounts");
|
|
67
|
+
const { saveSession, clearSession } = await import("./lib/config.js");
|
|
68
|
+
const account = privateKeyToAccount(opts.evmKey);
|
|
69
|
+
const maxDeposit = opts.maxDeposit ?? "1";
|
|
70
|
+
const voucherHeadroomRaw = parseVoucherHeadroom(getMppVoucherHeadroomUsdc());
|
|
71
|
+
const paymentQueue = [];
|
|
72
|
+
let lastChallengeAmount;
|
|
73
|
+
const debug = isDebugEnabled();
|
|
74
|
+
const mppx = Mppx.create({
|
|
75
|
+
methods: [tempo({
|
|
76
|
+
account,
|
|
77
|
+
maxDeposit
|
|
78
|
+
})],
|
|
79
|
+
polyfill: false,
|
|
80
|
+
onChallenge: async (challenge) => {
|
|
81
|
+
const req = challenge.request;
|
|
82
|
+
if (req.amount) lastChallengeAmount = (Number(req.amount) / 10 ** (req.decimals ?? 6)).toString();
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
const payerAddress = account.address;
|
|
86
|
+
function injectPayerHeader(init) {
|
|
87
|
+
const existing = init?.headers instanceof Headers ? Object.fromEntries(init.headers.entries()) : init?.headers ?? {};
|
|
88
|
+
return {
|
|
89
|
+
...init,
|
|
90
|
+
headers: {
|
|
91
|
+
...existing,
|
|
92
|
+
"X-Payer-Address": payerAddress
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const activeSessions = /* @__PURE__ */ new Set();
|
|
97
|
+
let persistedChannelId;
|
|
98
|
+
function rememberSession(session) {
|
|
99
|
+
activeSessions.add(session);
|
|
100
|
+
if (session.channelId && session.channelId !== persistedChannelId) {
|
|
101
|
+
persistedChannelId = session.channelId;
|
|
102
|
+
if (debug) process.stderr.write(`[x402-proxy] channelId: ${persistedChannelId}\n`);
|
|
103
|
+
try {
|
|
104
|
+
saveSession({
|
|
105
|
+
channelId: session.channelId,
|
|
106
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
107
|
+
});
|
|
108
|
+
} catch {}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function pushCloseReceipt(session, receipt) {
|
|
112
|
+
if (!receipt) return;
|
|
113
|
+
const spentUsdc = receipt.spent ? (Number(receipt.spent) / 1e6).toString() : void 0;
|
|
114
|
+
paymentQueue.push({
|
|
115
|
+
protocol: "mpp",
|
|
116
|
+
network: TEMPO_NETWORK,
|
|
117
|
+
intent: "session",
|
|
118
|
+
amount: spentUsdc,
|
|
119
|
+
channelId: session.channelId ?? void 0,
|
|
120
|
+
receipt: {
|
|
121
|
+
method: receipt.method,
|
|
122
|
+
reference: receipt.reference,
|
|
123
|
+
status: receipt.status,
|
|
124
|
+
timestamp: receipt.timestamp,
|
|
125
|
+
acceptedCumulative: receipt.acceptedCumulative,
|
|
126
|
+
txHash: receipt.txHash
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
async fetch(input, init) {
|
|
132
|
+
const response = await mppx.fetch(typeof input === "string" ? input : input.toString(), injectPayerHeader(init));
|
|
133
|
+
const receiptHeader = response.headers.get("Payment-Receipt");
|
|
134
|
+
if (receiptHeader) {
|
|
135
|
+
try {
|
|
136
|
+
const receipt = JSON.parse(Buffer.from(receiptHeader, "base64url").toString());
|
|
137
|
+
paymentQueue.push({
|
|
138
|
+
protocol: "mpp",
|
|
139
|
+
network: TEMPO_NETWORK,
|
|
140
|
+
amount: lastChallengeAmount,
|
|
141
|
+
receipt
|
|
142
|
+
});
|
|
143
|
+
} catch {
|
|
144
|
+
paymentQueue.push({
|
|
145
|
+
protocol: "mpp",
|
|
146
|
+
network: TEMPO_NETWORK,
|
|
147
|
+
amount: lastChallengeAmount
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
lastChallengeAmount = void 0;
|
|
151
|
+
}
|
|
152
|
+
return response;
|
|
153
|
+
},
|
|
154
|
+
async sse(input, init) {
|
|
155
|
+
const url = typeof input === "string" ? input : input.toString();
|
|
156
|
+
let createCredential;
|
|
157
|
+
let spent = 0n;
|
|
158
|
+
const session = {
|
|
159
|
+
channelId: void 0,
|
|
160
|
+
cumulative: 0n,
|
|
161
|
+
opened: false,
|
|
162
|
+
async close() {
|
|
163
|
+
if (!session.opened || !session.channelId || !createCredential) return void 0;
|
|
164
|
+
const credential = await createCredential({
|
|
165
|
+
action: "close",
|
|
166
|
+
channelId: session.channelId,
|
|
167
|
+
cumulativeAmountRaw: spent.toString()
|
|
168
|
+
});
|
|
169
|
+
const receiptHeader = (await mppx.rawFetch(url, {
|
|
170
|
+
method: "POST",
|
|
171
|
+
headers: {
|
|
172
|
+
Authorization: credential,
|
|
173
|
+
"X-Payer-Address": payerAddress
|
|
174
|
+
}
|
|
175
|
+
})).headers.get("Payment-Receipt");
|
|
176
|
+
if (!receiptHeader) return void 0;
|
|
177
|
+
try {
|
|
178
|
+
const receipt = Session.Receipt.deserializeSessionReceipt(receiptHeader);
|
|
179
|
+
spent = spent > BigInt(receipt.spent) ? spent : BigInt(receipt.spent);
|
|
180
|
+
return receipt;
|
|
181
|
+
} catch {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
const sessionMppx = Mppx.create({
|
|
187
|
+
methods: [tempo({
|
|
188
|
+
account,
|
|
189
|
+
maxDeposit,
|
|
190
|
+
onChannelUpdate(entry) {
|
|
191
|
+
session.channelId = entry.channelId;
|
|
192
|
+
session.cumulative = entry.cumulativeAmount;
|
|
193
|
+
session.opened = entry.opened;
|
|
194
|
+
if (entry.channelId && entry.channelId !== persistedChannelId) {
|
|
195
|
+
persistedChannelId = entry.channelId;
|
|
196
|
+
if (debug) process.stderr.write(`[x402-proxy] channelId: ${persistedChannelId}\n`);
|
|
197
|
+
try {
|
|
198
|
+
saveSession({
|
|
199
|
+
channelId: entry.channelId,
|
|
200
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
201
|
+
});
|
|
202
|
+
} catch {}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
})],
|
|
206
|
+
polyfill: false,
|
|
207
|
+
onChallenge: async (challenge, helpers) => {
|
|
208
|
+
const req = challenge.request;
|
|
209
|
+
if (req.amount) lastChallengeAmount = (Number(req.amount) / 10 ** (req.decimals ?? 6)).toString();
|
|
210
|
+
createCredential = helpers.createCredential;
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
const response = await sessionMppx.fetch(url, injectPayerHeader({
|
|
214
|
+
...init,
|
|
215
|
+
headers: {
|
|
216
|
+
...init?.headers instanceof Headers ? Object.fromEntries(init.headers.entries()) : init?.headers ?? {},
|
|
217
|
+
Accept: "text/event-stream"
|
|
218
|
+
}
|
|
219
|
+
}));
|
|
220
|
+
if (!response.body) throw new Error("MPP SSE response has no body");
|
|
221
|
+
rememberSession(session);
|
|
222
|
+
paymentQueue.push({
|
|
223
|
+
protocol: "mpp",
|
|
224
|
+
network: TEMPO_NETWORK,
|
|
225
|
+
intent: "session"
|
|
226
|
+
});
|
|
227
|
+
const reader = response.body.getReader();
|
|
228
|
+
const decoder = new TextDecoder();
|
|
229
|
+
async function* iterate() {
|
|
230
|
+
let buffer = "";
|
|
231
|
+
try {
|
|
232
|
+
while (true) {
|
|
233
|
+
const { done, value } = await reader.read();
|
|
234
|
+
if (done) break;
|
|
235
|
+
buffer += decoder.decode(value, { stream: true });
|
|
236
|
+
const parts = buffer.split("\n\n");
|
|
237
|
+
buffer = parts.pop() ?? "";
|
|
238
|
+
for (const part of parts) {
|
|
239
|
+
if (!part.trim()) continue;
|
|
240
|
+
const event = Session.Sse.parseEvent(part);
|
|
241
|
+
if (!event) continue;
|
|
242
|
+
switch (event.type) {
|
|
243
|
+
case "message":
|
|
244
|
+
yield event.data;
|
|
245
|
+
break;
|
|
246
|
+
case "payment-receipt":
|
|
247
|
+
spent = spent > BigInt(event.data.spent) ? spent : BigInt(event.data.spent);
|
|
248
|
+
break;
|
|
249
|
+
case "payment-need-voucher": {
|
|
250
|
+
if (!createCredential) throw new Error("MPP voucher requested before challenge helper was captured");
|
|
251
|
+
const target = computeMppVoucherTarget({
|
|
252
|
+
requiredCumulative: BigInt(event.data.requiredCumulative),
|
|
253
|
+
deposit: BigInt(event.data.deposit),
|
|
254
|
+
headroom: voucherHeadroomRaw
|
|
255
|
+
});
|
|
256
|
+
const credential = await createCredential({
|
|
257
|
+
action: "voucher",
|
|
258
|
+
channelId: event.data.channelId,
|
|
259
|
+
cumulativeAmountRaw: target.toString()
|
|
260
|
+
});
|
|
261
|
+
const voucherResponse = await sessionMppx.rawFetch(url, {
|
|
262
|
+
method: "POST",
|
|
263
|
+
headers: {
|
|
264
|
+
Authorization: credential,
|
|
265
|
+
"X-Payer-Address": payerAddress
|
|
266
|
+
},
|
|
267
|
+
signal: init?.signal
|
|
268
|
+
});
|
|
269
|
+
if (!voucherResponse.ok) {
|
|
270
|
+
const body = await voucherResponse.text().catch(() => "");
|
|
271
|
+
throw new Error(`Voucher POST failed with status ${voucherResponse.status}${body ? `: ${body.slice(0, 200)}` : ""}`);
|
|
272
|
+
}
|
|
273
|
+
const receiptHeader = voucherResponse.headers.get("Payment-Receipt");
|
|
274
|
+
if (receiptHeader) try {
|
|
275
|
+
const receipt = Session.Receipt.deserializeSessionReceipt(receiptHeader);
|
|
276
|
+
spent = spent > BigInt(receipt.spent) ? spent : BigInt(receipt.spent);
|
|
277
|
+
} catch {}
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
} finally {
|
|
284
|
+
reader.releaseLock();
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return iterate();
|
|
288
|
+
},
|
|
289
|
+
shiftPayment: () => paymentQueue.shift(),
|
|
290
|
+
async close() {
|
|
291
|
+
const sessions = Array.from(activeSessions);
|
|
292
|
+
if (sessions.length === 0) return;
|
|
293
|
+
const byChannelId = /* @__PURE__ */ new Map();
|
|
294
|
+
const sessionsWithoutChannel = [];
|
|
295
|
+
for (const session of sessions) {
|
|
296
|
+
if (!session.opened) continue;
|
|
297
|
+
if (!session.channelId) {
|
|
298
|
+
sessionsWithoutChannel.push(session);
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
const existing = byChannelId.get(session.channelId);
|
|
302
|
+
if (!existing || session.cumulative > existing.cumulative) byChannelId.set(session.channelId, session);
|
|
303
|
+
}
|
|
304
|
+
for (const session of sessions) activeSessions.delete(session);
|
|
305
|
+
const sessionsToClose = [...byChannelId.values(), ...sessionsWithoutChannel];
|
|
306
|
+
let shouldClearPersistedSession = false;
|
|
307
|
+
for (const session of sessionsToClose) {
|
|
308
|
+
const receipt = await session.close();
|
|
309
|
+
if (session.channelId && session.channelId === persistedChannelId) shouldClearPersistedSession = true;
|
|
310
|
+
pushCloseReceipt(session, receipt);
|
|
311
|
+
}
|
|
312
|
+
if (shouldClearPersistedSession) try {
|
|
313
|
+
clearSession();
|
|
314
|
+
} catch {}
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
//#endregion
|
|
319
|
+
export { TEMPO_NETWORK, createMppProxyHandler, createX402ProxyHandler, extractTxSignature };
|
package/dist/history.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { dirname } from "node:path";
|
|
2
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
function isMeaningfulInferenceRecord(record) {
|
|
4
|
+
if (record.kind !== "x402_inference") return true;
|
|
5
|
+
if (!record.ok) return true;
|
|
6
|
+
return record.amount != null || record.model != null || record.inputTokens != null || record.outputTokens != null || record.tx != null;
|
|
7
|
+
}
|
|
8
|
+
function appendHistory(historyPath, record) {
|
|
9
|
+
try {
|
|
10
|
+
mkdirSync(dirname(historyPath), { recursive: true });
|
|
11
|
+
appendFileSync(historyPath, `${JSON.stringify(record)}\n`);
|
|
12
|
+
if (existsSync(historyPath)) {
|
|
13
|
+
if (statSync(historyPath).size > 1e3 * 200) {
|
|
14
|
+
const lines = readFileSync(historyPath, "utf-8").trimEnd().split("\n");
|
|
15
|
+
if (lines.length > 1e3) writeFileSync(historyPath, `${lines.slice(-500).join("\n")}\n`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
} catch {}
|
|
19
|
+
}
|
|
20
|
+
function readHistory(historyPath) {
|
|
21
|
+
try {
|
|
22
|
+
if (!existsSync(historyPath)) return [];
|
|
23
|
+
const content = readFileSync(historyPath, "utf-8").trimEnd();
|
|
24
|
+
if (!content) return [];
|
|
25
|
+
return content.split("\n").flatMap((line) => {
|
|
26
|
+
try {
|
|
27
|
+
const parsed = JSON.parse(line);
|
|
28
|
+
if (typeof parsed.t !== "number" || typeof parsed.kind !== "string") return [];
|
|
29
|
+
const record = parsed;
|
|
30
|
+
return isMeaningfulInferenceRecord(record) ? [record] : [];
|
|
31
|
+
} catch {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
} catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function calcSpend(records) {
|
|
40
|
+
const todayStart = /* @__PURE__ */ new Date();
|
|
41
|
+
todayStart.setUTCHours(0, 0, 0, 0);
|
|
42
|
+
const todayMs = todayStart.getTime();
|
|
43
|
+
let today = 0;
|
|
44
|
+
let total = 0;
|
|
45
|
+
let count = 0;
|
|
46
|
+
for (const r of records) {
|
|
47
|
+
if (!r.ok || r.amount == null) continue;
|
|
48
|
+
if (r.token !== "USDC") continue;
|
|
49
|
+
total += r.amount;
|
|
50
|
+
count++;
|
|
51
|
+
if (r.t >= todayMs) today += r.amount;
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
today,
|
|
55
|
+
total,
|
|
56
|
+
count
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function formatUsdcValue(amount) {
|
|
60
|
+
return new Intl.NumberFormat("en-US", {
|
|
61
|
+
useGrouping: false,
|
|
62
|
+
minimumFractionDigits: 0,
|
|
63
|
+
maximumFractionDigits: 12
|
|
64
|
+
}).format(amount);
|
|
65
|
+
}
|
|
66
|
+
function formatAmount(amount, token) {
|
|
67
|
+
if (token === "USDC") return `${formatUsdcValue(amount)} USDC`;
|
|
68
|
+
if (token === "SOL") return `${amount} SOL`;
|
|
69
|
+
return `${amount} ${token}`;
|
|
70
|
+
}
|
|
71
|
+
const KIND_LABELS = {
|
|
72
|
+
x402_inference: "inference",
|
|
73
|
+
x402_payment: "payment",
|
|
74
|
+
mpp_payment: "mpp payment",
|
|
75
|
+
transfer: "transfer",
|
|
76
|
+
buy: "buy",
|
|
77
|
+
sell: "sell",
|
|
78
|
+
mint: "mint",
|
|
79
|
+
swap: "swap"
|
|
80
|
+
};
|
|
81
|
+
function explorerUrl(net, tx) {
|
|
82
|
+
if (net.startsWith("eip155:")) {
|
|
83
|
+
const chainId = net.split(":")[1];
|
|
84
|
+
if (chainId === "4217") return `https://explore.mainnet.tempo.xyz/tx/${tx}`;
|
|
85
|
+
if (chainId === "8453") return `https://basescan.org/tx/${tx}`;
|
|
86
|
+
return `https://basescan.org/tx/${tx}`;
|
|
87
|
+
}
|
|
88
|
+
return `https://solscan.io/tx/${tx}`;
|
|
89
|
+
}
|
|
90
|
+
/** Strip provider prefix and OpenRouter date suffixes from model IDs.
|
|
91
|
+
* e.g. "minimax/minimax-m2.5-20260211" -> "minimax-m2.5"
|
|
92
|
+
* "moonshotai/kimi-k2.5-0127" -> "kimi-k2.5" */
|
|
93
|
+
function shortModel(model) {
|
|
94
|
+
const parts = model.split("/");
|
|
95
|
+
return parts[parts.length - 1].replace(/-\d{6,8}$/, "").replace(/-\d{4}$/, "");
|
|
96
|
+
}
|
|
97
|
+
function shortNetwork(net) {
|
|
98
|
+
if (net === "eip155:8453") return "base";
|
|
99
|
+
if (net === "eip155:4217") return "tempo";
|
|
100
|
+
if (net.startsWith("eip155:")) return `evm:${net.split(":")[1]}`;
|
|
101
|
+
if (net.startsWith("solana:")) return "sol";
|
|
102
|
+
return net;
|
|
103
|
+
}
|
|
104
|
+
function formatTxLine(r, opts) {
|
|
105
|
+
const time = new Date(r.t).toLocaleTimeString("en-US", {
|
|
106
|
+
hour: "2-digit",
|
|
107
|
+
minute: "2-digit",
|
|
108
|
+
hour12: false,
|
|
109
|
+
timeZone: "UTC"
|
|
110
|
+
});
|
|
111
|
+
const timeStr = r.tx ? `[${time}](${explorerUrl(r.net, r.tx)})` : time;
|
|
112
|
+
const parts = [r.kind === "x402_inference" && r.model ? shortModel(r.model) : KIND_LABELS[r.kind] ?? r.kind];
|
|
113
|
+
if (r.label) parts.push(r.label);
|
|
114
|
+
if (r.ok && r.amount != null && r.token) parts.push(formatAmount(r.amount, r.token));
|
|
115
|
+
else if (r.ok && r.kind === "sell" && r.meta?.pct != null) parts.push(`${r.meta.pct}%`);
|
|
116
|
+
parts.push(shortNetwork(r.net));
|
|
117
|
+
if (opts?.verbose && r.tx) {
|
|
118
|
+
const short = r.tx.length > 20 ? `${r.tx.slice(0, 10)}...${r.tx.slice(-6)}` : r.tx;
|
|
119
|
+
parts.push(short);
|
|
120
|
+
}
|
|
121
|
+
return ` ${timeStr} ${r.ok ? "" : "✗ "}${parts.join(" · ")}`;
|
|
122
|
+
}
|
|
123
|
+
//#endregion
|
|
124
|
+
export { appendHistory, calcSpend, formatAmount, formatTxLine, formatUsdcValue, readHistory };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import "yaml";
|
|
5
|
+
//#region packages/x402-proxy/src/lib/config.ts
|
|
6
|
+
const APP_NAME = "x402-proxy";
|
|
7
|
+
function getConfigDir() {
|
|
8
|
+
const xdg = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
|
|
9
|
+
return path.join(xdg, APP_NAME);
|
|
10
|
+
}
|
|
11
|
+
function getWalletPath() {
|
|
12
|
+
return path.join(getConfigDir(), "wallet.json");
|
|
13
|
+
}
|
|
14
|
+
function getHistoryPath() {
|
|
15
|
+
return path.join(getConfigDir(), "history.jsonl");
|
|
16
|
+
}
|
|
17
|
+
function getDebugLogPath() {
|
|
18
|
+
return path.join(getConfigDir(), "debug.log");
|
|
19
|
+
}
|
|
20
|
+
function ensureConfigDir() {
|
|
21
|
+
fs.mkdirSync(getConfigDir(), { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
function loadWalletFile() {
|
|
24
|
+
const p = getWalletPath();
|
|
25
|
+
if (!fs.existsSync(p)) return null;
|
|
26
|
+
try {
|
|
27
|
+
const data = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
28
|
+
if (data.version === 1 && typeof data.mnemonic === "string") return data;
|
|
29
|
+
return null;
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function getSessionPath() {
|
|
35
|
+
return path.join(getConfigDir(), "session.json");
|
|
36
|
+
}
|
|
37
|
+
function saveSession(session) {
|
|
38
|
+
ensureConfigDir();
|
|
39
|
+
fs.writeFileSync(getSessionPath(), JSON.stringify(session, null, 2), "utf-8");
|
|
40
|
+
}
|
|
41
|
+
function clearSession() {
|
|
42
|
+
try {
|
|
43
|
+
fs.unlinkSync(getSessionPath());
|
|
44
|
+
} catch {}
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
export { clearSession, ensureConfigDir, getDebugLogPath, getHistoryPath, loadWalletFile, saveSession };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { ensureConfigDir, getDebugLogPath } from "./config.js";
|
|
2
|
+
import { appendFileSync, existsSync, renameSync, rmSync, statSync } from "node:fs";
|
|
3
|
+
//#region packages/x402-proxy/src/lib/debug-log.ts
|
|
4
|
+
const MAX_DEBUG_LOG_BYTES = 10 * 1024 * 1024;
|
|
5
|
+
const ROTATED_DEBUG_LOG_SUFFIX = ".1";
|
|
6
|
+
function rotateIfNeeded(path) {
|
|
7
|
+
if (!existsSync(path)) return;
|
|
8
|
+
const { size } = statSync(path);
|
|
9
|
+
if (size <= MAX_DEBUG_LOG_BYTES) return;
|
|
10
|
+
const rotated = `${path}${ROTATED_DEBUG_LOG_SUFFIX}`;
|
|
11
|
+
try {
|
|
12
|
+
rmSync(rotated, { force: true });
|
|
13
|
+
} catch {}
|
|
14
|
+
renameSync(path, rotated);
|
|
15
|
+
}
|
|
16
|
+
function writeDebugLog(event, fields = {}) {
|
|
17
|
+
try {
|
|
18
|
+
ensureConfigDir();
|
|
19
|
+
const path = getDebugLogPath();
|
|
20
|
+
rotateIfNeeded(path);
|
|
21
|
+
appendFileSync(path, `${JSON.stringify({
|
|
22
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
23
|
+
event,
|
|
24
|
+
...fields
|
|
25
|
+
})}\n`, "utf-8");
|
|
26
|
+
} catch {}
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
export { writeDebugLog };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { ed25519 } from "@noble/curves/ed25519.js";
|
|
2
|
+
import { base58 } from "@scure/base";
|
|
3
|
+
import { secp256k1 } from "@noble/curves/secp256k1.js";
|
|
4
|
+
import { hmac } from "@noble/hashes/hmac.js";
|
|
5
|
+
import { sha512 } from "@noble/hashes/sha2.js";
|
|
6
|
+
import { keccak_256 } from "@noble/hashes/sha3.js";
|
|
7
|
+
import { HDKey } from "@scure/bip32";
|
|
8
|
+
import { mnemonicToSeedSync } from "@scure/bip39";
|
|
9
|
+
import "@scure/bip39/wordlists/english.js";
|
|
10
|
+
//#region packages/x402-proxy/src/lib/derive.ts
|
|
11
|
+
/**
|
|
12
|
+
* Wallet derivation from BIP-39 mnemonic.
|
|
13
|
+
*
|
|
14
|
+
* A single 24-word mnemonic is the root secret. Both Solana and EVM keypairs
|
|
15
|
+
* are deterministically derived from it.
|
|
16
|
+
*
|
|
17
|
+
* Solana: SLIP-10 Ed25519 at m/44'/501'/0'/0'
|
|
18
|
+
* EVM: BIP-32 secp256k1 at m/44'/60'/0'/0/0
|
|
19
|
+
*
|
|
20
|
+
* Ported from agentbox/packages/openclaw-x402/src/wallet.ts
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* SLIP-10 Ed25519 derivation at m/44'/501'/0'/0' (Phantom/Backpack compatible).
|
|
24
|
+
*/
|
|
25
|
+
const enc = new TextEncoder();
|
|
26
|
+
function deriveSolanaKeypair(mnemonic) {
|
|
27
|
+
const seed = mnemonicToSeedSync(mnemonic);
|
|
28
|
+
let I = hmac(sha512, enc.encode("ed25519 seed"), seed);
|
|
29
|
+
let key = I.slice(0, 32);
|
|
30
|
+
let chainCode = I.slice(32);
|
|
31
|
+
for (const index of [
|
|
32
|
+
2147483692,
|
|
33
|
+
2147484149,
|
|
34
|
+
2147483648,
|
|
35
|
+
2147483648
|
|
36
|
+
]) {
|
|
37
|
+
const data = new Uint8Array(37);
|
|
38
|
+
data[0] = 0;
|
|
39
|
+
data.set(key, 1);
|
|
40
|
+
data[33] = index >>> 24 & 255;
|
|
41
|
+
data[34] = index >>> 16 & 255;
|
|
42
|
+
data[35] = index >>> 8 & 255;
|
|
43
|
+
data[36] = index & 255;
|
|
44
|
+
I = hmac(sha512, chainCode, data);
|
|
45
|
+
key = I.slice(0, 32);
|
|
46
|
+
chainCode = I.slice(32);
|
|
47
|
+
}
|
|
48
|
+
const secretKey = new Uint8Array(key);
|
|
49
|
+
const publicKey = ed25519.getPublicKey(secretKey);
|
|
50
|
+
return {
|
|
51
|
+
secretKey,
|
|
52
|
+
publicKey,
|
|
53
|
+
address: base58.encode(publicKey)
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* BIP-32 secp256k1 derivation at m/44'/60'/0'/0/0.
|
|
58
|
+
*/
|
|
59
|
+
function deriveEvmKeypair(mnemonic) {
|
|
60
|
+
const seed = mnemonicToSeedSync(mnemonic);
|
|
61
|
+
const derived = HDKey.fromMasterSeed(seed).derive("m/44'/60'/0'/0/0");
|
|
62
|
+
if (!derived.privateKey) throw new Error("Failed to derive EVM private key");
|
|
63
|
+
const privateKey = `0x${Buffer.from(derived.privateKey).toString("hex")}`;
|
|
64
|
+
const hash = keccak_256(secp256k1.getPublicKey(derived.privateKey, false).slice(1));
|
|
65
|
+
return {
|
|
66
|
+
privateKey,
|
|
67
|
+
address: checksumAddress(Buffer.from(hash.slice(-20)).toString("hex"))
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function checksumAddress(addr) {
|
|
71
|
+
const hash = Buffer.from(keccak_256(enc.encode(addr))).toString("hex");
|
|
72
|
+
let out = "0x";
|
|
73
|
+
for (let i = 0; i < 40; i++) out += Number.parseInt(hash[i], 16) >= 8 ? addr[i].toUpperCase() : addr[i];
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
77
|
+
export { deriveEvmKeypair, deriveSolanaKeypair };
|
package/dist/lib/env.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region packages/x402-proxy/src/lib/env.ts
|
|
2
|
+
function isDebugEnabled() {
|
|
3
|
+
return process.env.X402_PROXY_DEBUG === "1";
|
|
4
|
+
}
|
|
5
|
+
function getMppVoucherHeadroomUsdc() {
|
|
6
|
+
return process.env.X402_PROXY_MPP_VOUCHER_HEADROOM_USDC;
|
|
7
|
+
}
|
|
8
|
+
//#endregion
|
|
9
|
+
export { getMppVoucherHeadroomUsdc, isDebugEnabled };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { SOLANA_ERROR__RPC__TRANSPORT_HTTP_ERROR, appendTransactionMessageInstructions, createDefaultRpcTransport, createSolanaRpcFromTransport, createTransactionMessage, getBase64EncodedWireTransaction, isSolanaError, mainnet, partiallySignTransactionMessageWithSigners, pipe, setTransactionMessageComputeUnitLimit, setTransactionMessageComputeUnitPrice, setTransactionMessageFeePayer, setTransactionMessageLifetimeUsingBlockhash } from "@solana/kit";
|
|
2
|
+
import { TOKEN_PROGRAM_ADDRESS, findAssociatedTokenPda, getTransferCheckedInstruction } from "@solana-program/token";
|
|
3
|
+
//#region packages/x402-proxy/src/lib/optimized-svm-scheme.ts
|
|
4
|
+
const MEMO_PROGRAM_ADDRESS = "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr";
|
|
5
|
+
const COMPUTE_UNIT_LIMIT = 2e4;
|
|
6
|
+
const COMPUTE_UNIT_PRICE_MICROLAMPORTS = 1n;
|
|
7
|
+
const USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
8
|
+
const USDC_DECIMALS = 6;
|
|
9
|
+
const MAINNET_RPC_URLS = ["https://api.mainnet.solana.com", "https://public.rpc.solanavibestation.com"];
|
|
10
|
+
/**
|
|
11
|
+
* Create a failover transport that tries each RPC in order.
|
|
12
|
+
* On 429 from one endpoint, immediately tries the next instead of waiting.
|
|
13
|
+
* Each transport gets its own coalescing via createDefaultRpcTransport.
|
|
14
|
+
*/
|
|
15
|
+
function createFailoverTransport(urls) {
|
|
16
|
+
const transports = urls.map((url) => createDefaultRpcTransport({ url }));
|
|
17
|
+
const failover = (async (config) => {
|
|
18
|
+
let lastError;
|
|
19
|
+
for (const transport of transports) try {
|
|
20
|
+
return await transport(config);
|
|
21
|
+
} catch (e) {
|
|
22
|
+
lastError = e;
|
|
23
|
+
if (isSolanaError(e, SOLANA_ERROR__RPC__TRANSPORT_HTTP_ERROR) && e.context.statusCode === 429) continue;
|
|
24
|
+
throw e;
|
|
25
|
+
}
|
|
26
|
+
throw lastError;
|
|
27
|
+
});
|
|
28
|
+
return failover;
|
|
29
|
+
}
|
|
30
|
+
function createRpcClient(customRpcUrl) {
|
|
31
|
+
return createSolanaRpcFromTransport(createFailoverTransport((customRpcUrl ? [customRpcUrl, ...MAINNET_RPC_URLS] : MAINNET_RPC_URLS).map((u) => mainnet(u))));
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Optimized ExactSvmScheme that replaces upstream @x402/svm to prevent
|
|
35
|
+
* RPC rate-limit failures on parallel payments.
|
|
36
|
+
*
|
|
37
|
+
* Two optimizations over upstream:
|
|
38
|
+
* 1. Shared RPC client - @solana/kit's built-in request coalescing
|
|
39
|
+
* merges identical getLatestBlockhash calls in the same tick into 1.
|
|
40
|
+
* 2. Hardcoded USDC - skips fetchMint RPC call for USDC (immutable data).
|
|
41
|
+
*/
|
|
42
|
+
var OptimizedSvmScheme = class {
|
|
43
|
+
scheme = "exact";
|
|
44
|
+
rpc;
|
|
45
|
+
constructor(signer, config) {
|
|
46
|
+
this.signer = signer;
|
|
47
|
+
this.rpc = createRpcClient(config?.rpcUrl);
|
|
48
|
+
}
|
|
49
|
+
async createPaymentPayload(x402Version, paymentRequirements) {
|
|
50
|
+
const rpc = this.rpc;
|
|
51
|
+
const asset = paymentRequirements.asset;
|
|
52
|
+
if (asset !== USDC_MINT) throw new Error(`Unsupported asset: ${asset}. Only USDC is supported.`);
|
|
53
|
+
const [[sourceATA], [destinationATA]] = await Promise.all([findAssociatedTokenPda({
|
|
54
|
+
mint: asset,
|
|
55
|
+
owner: this.signer.address,
|
|
56
|
+
tokenProgram: TOKEN_PROGRAM_ADDRESS
|
|
57
|
+
}), findAssociatedTokenPda({
|
|
58
|
+
mint: asset,
|
|
59
|
+
owner: paymentRequirements.payTo,
|
|
60
|
+
tokenProgram: TOKEN_PROGRAM_ADDRESS
|
|
61
|
+
})]);
|
|
62
|
+
const transferIx = getTransferCheckedInstruction({
|
|
63
|
+
source: sourceATA,
|
|
64
|
+
mint: asset,
|
|
65
|
+
destination: destinationATA,
|
|
66
|
+
authority: this.signer,
|
|
67
|
+
amount: BigInt(paymentRequirements.amount),
|
|
68
|
+
decimals: USDC_DECIMALS
|
|
69
|
+
});
|
|
70
|
+
const feePayer = paymentRequirements.extra?.feePayer;
|
|
71
|
+
if (!feePayer) throw new Error("feePayer is required in paymentRequirements.extra for SVM transactions");
|
|
72
|
+
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
|
|
73
|
+
const nonce = crypto.getRandomValues(new Uint8Array(16));
|
|
74
|
+
const memoIx = {
|
|
75
|
+
programAddress: MEMO_PROGRAM_ADDRESS,
|
|
76
|
+
accounts: [],
|
|
77
|
+
data: new TextEncoder().encode(Array.from(nonce).map((b) => b.toString(16).padStart(2, "0")).join(""))
|
|
78
|
+
};
|
|
79
|
+
return {
|
|
80
|
+
x402Version,
|
|
81
|
+
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)))) }
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
//#endregion
|
|
86
|
+
export { OptimizedSvmScheme };
|