@blockrun/clawrouter 0.11.12 → 0.11.14
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/README.md +19 -12
- package/dist/cli.js +486 -383
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +260 -57
- package/dist/index.js +989 -435
- package/dist/index.js.map +1 -1
- package/package.json +11 -4
package/dist/cli.js
CHANGED
|
@@ -3,271 +3,72 @@
|
|
|
3
3
|
// src/proxy.ts
|
|
4
4
|
import { createServer } from "http";
|
|
5
5
|
import { finished } from "stream";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
import {
|
|
6
|
+
import { createPublicClient as createPublicClient2, http as http2 } from "viem";
|
|
7
|
+
import { base as base2 } from "viem/chains";
|
|
8
|
+
import { privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
|
|
9
|
+
import { x402Client } from "@x402/fetch";
|
|
10
10
|
|
|
11
|
-
// src/payment-
|
|
11
|
+
// src/payment-preauth.ts
|
|
12
|
+
import { x402HTTPClient } from "@x402/fetch";
|
|
12
13
|
var DEFAULT_TTL_MS = 36e5;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return entry;
|
|
28
|
-
}
|
|
29
|
-
/** Cache payment params from a 402 response. */
|
|
30
|
-
set(endpointPath, params) {
|
|
31
|
-
this.cache.set(endpointPath, { ...params, cachedAt: Date.now() });
|
|
32
|
-
}
|
|
33
|
-
/** Invalidate cache for an endpoint (e.g., if payTo changed). */
|
|
34
|
-
invalidate(endpointPath) {
|
|
35
|
-
this.cache.delete(endpointPath);
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
// src/x402.ts
|
|
40
|
-
var BASE_CHAIN_ID = 8453;
|
|
41
|
-
var BASE_SEPOLIA_CHAIN_ID = 84532;
|
|
42
|
-
var DEFAULT_TOKEN_NAME = "USD Coin";
|
|
43
|
-
var DEFAULT_TOKEN_VERSION = "2";
|
|
44
|
-
var DEFAULT_NETWORK = "eip155:8453";
|
|
45
|
-
var DEFAULT_MAX_TIMEOUT_SECONDS = 300;
|
|
46
|
-
var TRANSFER_TYPES = {
|
|
47
|
-
TransferWithAuthorization: [
|
|
48
|
-
{ name: "from", type: "address" },
|
|
49
|
-
{ name: "to", type: "address" },
|
|
50
|
-
{ name: "value", type: "uint256" },
|
|
51
|
-
{ name: "validAfter", type: "uint256" },
|
|
52
|
-
{ name: "validBefore", type: "uint256" },
|
|
53
|
-
{ name: "nonce", type: "bytes32" }
|
|
54
|
-
]
|
|
55
|
-
};
|
|
56
|
-
function createNonce() {
|
|
57
|
-
const bytes = new Uint8Array(32);
|
|
58
|
-
crypto.getRandomValues(bytes);
|
|
59
|
-
return `0x${Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
60
|
-
}
|
|
61
|
-
function decodeBase64Json(value) {
|
|
62
|
-
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
63
|
-
const padding = (4 - normalized.length % 4) % 4;
|
|
64
|
-
const padded = normalized + "=".repeat(padding);
|
|
65
|
-
const decoded = Buffer.from(padded, "base64").toString("utf8");
|
|
66
|
-
return JSON.parse(decoded);
|
|
67
|
-
}
|
|
68
|
-
function encodeBase64Json(value) {
|
|
69
|
-
return Buffer.from(JSON.stringify(value), "utf8").toString("base64");
|
|
70
|
-
}
|
|
71
|
-
function parsePaymentRequired(headerValue) {
|
|
72
|
-
return decodeBase64Json(headerValue);
|
|
73
|
-
}
|
|
74
|
-
function normalizeNetwork(network) {
|
|
75
|
-
if (!network || network.trim().length === 0) {
|
|
76
|
-
return DEFAULT_NETWORK;
|
|
77
|
-
}
|
|
78
|
-
return network.trim().toLowerCase();
|
|
79
|
-
}
|
|
80
|
-
function resolveChainId(network) {
|
|
81
|
-
const eip155Match = network.match(/^eip155:(\d+)$/i);
|
|
82
|
-
if (eip155Match) {
|
|
83
|
-
const parsed = Number.parseInt(eip155Match[1], 10);
|
|
84
|
-
if (Number.isFinite(parsed) && parsed > 0) {
|
|
85
|
-
return parsed;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
if (network === "base") return BASE_CHAIN_ID;
|
|
89
|
-
if (network === "base-sepolia") return BASE_SEPOLIA_CHAIN_ID;
|
|
90
|
-
return BASE_CHAIN_ID;
|
|
91
|
-
}
|
|
92
|
-
function parseHexAddress(value) {
|
|
93
|
-
if (!value) return void 0;
|
|
94
|
-
const direct = value.match(/^0x[a-fA-F0-9]{40}$/);
|
|
95
|
-
if (direct) {
|
|
96
|
-
return direct[0];
|
|
97
|
-
}
|
|
98
|
-
const caipSuffix = value.match(/0x[a-fA-F0-9]{40}$/);
|
|
99
|
-
if (caipSuffix) {
|
|
100
|
-
return caipSuffix[0];
|
|
101
|
-
}
|
|
102
|
-
return void 0;
|
|
103
|
-
}
|
|
104
|
-
function requireHexAddress(value, field) {
|
|
105
|
-
const parsed = parseHexAddress(value);
|
|
106
|
-
if (!parsed) {
|
|
107
|
-
throw new Error(`Invalid ${field} in payment requirements: ${String(value)}`);
|
|
108
|
-
}
|
|
109
|
-
return parsed;
|
|
110
|
-
}
|
|
111
|
-
function setPaymentHeaders(headers, payload) {
|
|
112
|
-
headers.set("payment-signature", payload);
|
|
113
|
-
headers.set("x-payment", payload);
|
|
114
|
-
}
|
|
115
|
-
async function createPaymentPayload(privateKey, fromAddress, option, amount, requestUrl, resource) {
|
|
116
|
-
const network = normalizeNetwork(option.network);
|
|
117
|
-
const chainId = resolveChainId(network);
|
|
118
|
-
const recipient = requireHexAddress(option.payTo, "payTo");
|
|
119
|
-
const verifyingContract = requireHexAddress(option.asset, "asset");
|
|
120
|
-
const maxTimeoutSeconds = typeof option.maxTimeoutSeconds === "number" && option.maxTimeoutSeconds > 0 ? Math.floor(option.maxTimeoutSeconds) : DEFAULT_MAX_TIMEOUT_SECONDS;
|
|
121
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
122
|
-
const validAfter = now - 600;
|
|
123
|
-
const validBefore = now + maxTimeoutSeconds;
|
|
124
|
-
const nonce = createNonce();
|
|
125
|
-
const signature = await signTypedData({
|
|
126
|
-
privateKey,
|
|
127
|
-
domain: {
|
|
128
|
-
name: option.extra?.name || DEFAULT_TOKEN_NAME,
|
|
129
|
-
version: option.extra?.version || DEFAULT_TOKEN_VERSION,
|
|
130
|
-
chainId,
|
|
131
|
-
verifyingContract
|
|
132
|
-
},
|
|
133
|
-
types: TRANSFER_TYPES,
|
|
134
|
-
primaryType: "TransferWithAuthorization",
|
|
135
|
-
message: {
|
|
136
|
-
from: fromAddress,
|
|
137
|
-
to: recipient,
|
|
138
|
-
value: BigInt(amount),
|
|
139
|
-
validAfter: BigInt(validAfter),
|
|
140
|
-
validBefore: BigInt(validBefore),
|
|
141
|
-
nonce
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
const paymentData = {
|
|
145
|
-
x402Version: 2,
|
|
146
|
-
resource: {
|
|
147
|
-
url: resource?.url || requestUrl,
|
|
148
|
-
description: resource?.description || "BlockRun AI API call",
|
|
149
|
-
mimeType: "application/json"
|
|
150
|
-
},
|
|
151
|
-
accepted: {
|
|
152
|
-
scheme: option.scheme,
|
|
153
|
-
network,
|
|
154
|
-
amount,
|
|
155
|
-
asset: option.asset,
|
|
156
|
-
payTo: option.payTo,
|
|
157
|
-
maxTimeoutSeconds: option.maxTimeoutSeconds,
|
|
158
|
-
extra: option.extra
|
|
159
|
-
},
|
|
160
|
-
payload: {
|
|
161
|
-
signature,
|
|
162
|
-
authorization: {
|
|
163
|
-
from: fromAddress,
|
|
164
|
-
to: recipient,
|
|
165
|
-
value: amount,
|
|
166
|
-
validAfter: validAfter.toString(),
|
|
167
|
-
validBefore: validBefore.toString(),
|
|
168
|
-
nonce
|
|
169
|
-
}
|
|
170
|
-
},
|
|
171
|
-
extensions: {}
|
|
172
|
-
};
|
|
173
|
-
return encodeBase64Json(paymentData);
|
|
174
|
-
}
|
|
175
|
-
function createPaymentFetch(privateKey) {
|
|
176
|
-
const account = privateKeyToAccount(privateKey);
|
|
177
|
-
const walletAddress = account.address;
|
|
178
|
-
const paymentCache = new PaymentCache();
|
|
179
|
-
const payFetch = async (input, init, preAuth) => {
|
|
180
|
-
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
181
|
-
const endpointPath = new URL(url).pathname;
|
|
182
|
-
const cached = paymentCache.get(endpointPath);
|
|
183
|
-
if (cached && preAuth?.estimatedAmount) {
|
|
184
|
-
const paymentPayload = await createPaymentPayload(
|
|
185
|
-
privateKey,
|
|
186
|
-
walletAddress,
|
|
187
|
-
{
|
|
188
|
-
scheme: cached.scheme,
|
|
189
|
-
network: cached.network,
|
|
190
|
-
asset: cached.asset,
|
|
191
|
-
payTo: cached.payTo,
|
|
192
|
-
maxTimeoutSeconds: cached.maxTimeoutSeconds,
|
|
193
|
-
extra: cached.extra
|
|
194
|
-
},
|
|
195
|
-
preAuth.estimatedAmount,
|
|
196
|
-
url,
|
|
197
|
-
{
|
|
198
|
-
url: cached.resourceUrl,
|
|
199
|
-
description: cached.resourceDescription
|
|
14
|
+
function createPayFetchWithPreAuth(baseFetch, client, ttlMs = DEFAULT_TTL_MS, options) {
|
|
15
|
+
const httpClient = new x402HTTPClient(client);
|
|
16
|
+
const cache = /* @__PURE__ */ new Map();
|
|
17
|
+
return async (input, init) => {
|
|
18
|
+
const request = new Request(input, init);
|
|
19
|
+
const urlPath = new URL(request.url).pathname;
|
|
20
|
+
const cached = !options?.skipPreAuth ? cache.get(urlPath) : void 0;
|
|
21
|
+
if (cached && Date.now() - cached.cachedAt < ttlMs) {
|
|
22
|
+
try {
|
|
23
|
+
const payload2 = await client.createPaymentPayload(cached.paymentRequired);
|
|
24
|
+
const headers = httpClient.encodePaymentSignatureHeader(payload2);
|
|
25
|
+
const preAuthRequest = request.clone();
|
|
26
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
27
|
+
preAuthRequest.headers.set(key, value);
|
|
200
28
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const paymentHeader2 = response2.headers.get("x-payment-required");
|
|
209
|
-
if (paymentHeader2) {
|
|
210
|
-
return handle402(input, init, url, endpointPath, paymentHeader2);
|
|
211
|
-
}
|
|
212
|
-
paymentCache.invalidate(endpointPath);
|
|
213
|
-
const cleanResponse = await fetch(input, init);
|
|
214
|
-
if (cleanResponse.status !== 402) {
|
|
215
|
-
return cleanResponse;
|
|
216
|
-
}
|
|
217
|
-
const cleanHeader = cleanResponse.headers.get("x-payment-required");
|
|
218
|
-
if (!cleanHeader) {
|
|
219
|
-
throw new Error("402 response missing x-payment-required header");
|
|
29
|
+
const response2 = await baseFetch(preAuthRequest);
|
|
30
|
+
if (response2.status !== 402) {
|
|
31
|
+
return response2;
|
|
32
|
+
}
|
|
33
|
+
cache.delete(urlPath);
|
|
34
|
+
} catch {
|
|
35
|
+
cache.delete(urlPath);
|
|
220
36
|
}
|
|
221
|
-
return handle402(input, init, url, endpointPath, cleanHeader);
|
|
222
37
|
}
|
|
223
|
-
const
|
|
38
|
+
const clonedRequest = request.clone();
|
|
39
|
+
const response = await baseFetch(request);
|
|
224
40
|
if (response.status !== 402) {
|
|
225
41
|
return response;
|
|
226
42
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
43
|
+
let paymentRequired;
|
|
44
|
+
try {
|
|
45
|
+
const getHeader = (name) => response.headers.get(name);
|
|
46
|
+
let body;
|
|
47
|
+
try {
|
|
48
|
+
const responseText = await response.text();
|
|
49
|
+
if (responseText) body = JSON.parse(responseText);
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
paymentRequired = httpClient.getPaymentRequiredResponse(getHeader, body);
|
|
53
|
+
cache.set(urlPath, { paymentRequired, cachedAt: Date.now() });
|
|
54
|
+
} catch (error) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Failed to parse payment requirements: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
const payload = await client.createPaymentPayload(paymentRequired);
|
|
60
|
+
const paymentHeaders = httpClient.encodePaymentSignatureHeader(payload);
|
|
61
|
+
for (const [key, value] of Object.entries(paymentHeaders)) {
|
|
62
|
+
clonedRequest.headers.set(key, value);
|
|
230
63
|
}
|
|
231
|
-
return
|
|
64
|
+
return baseFetch(clonedRequest);
|
|
232
65
|
};
|
|
233
|
-
async function handle402(input, init, url, endpointPath, paymentHeader) {
|
|
234
|
-
const paymentRequired = parsePaymentRequired(paymentHeader);
|
|
235
|
-
const option = paymentRequired.accepts?.[0];
|
|
236
|
-
if (!option) {
|
|
237
|
-
throw new Error("No payment options in 402 response");
|
|
238
|
-
}
|
|
239
|
-
const amount = option.amount || option.maxAmountRequired;
|
|
240
|
-
if (!amount) {
|
|
241
|
-
throw new Error("No amount in payment requirements");
|
|
242
|
-
}
|
|
243
|
-
paymentCache.set(endpointPath, {
|
|
244
|
-
payTo: option.payTo,
|
|
245
|
-
asset: option.asset,
|
|
246
|
-
scheme: option.scheme,
|
|
247
|
-
network: option.network,
|
|
248
|
-
extra: option.extra,
|
|
249
|
-
maxTimeoutSeconds: option.maxTimeoutSeconds,
|
|
250
|
-
resourceUrl: paymentRequired.resource?.url,
|
|
251
|
-
resourceDescription: paymentRequired.resource?.description
|
|
252
|
-
});
|
|
253
|
-
const paymentPayload = await createPaymentPayload(
|
|
254
|
-
privateKey,
|
|
255
|
-
walletAddress,
|
|
256
|
-
option,
|
|
257
|
-
amount,
|
|
258
|
-
url,
|
|
259
|
-
paymentRequired.resource
|
|
260
|
-
);
|
|
261
|
-
const retryHeaders = new Headers(init?.headers);
|
|
262
|
-
setPaymentHeaders(retryHeaders, paymentPayload);
|
|
263
|
-
return fetch(input, {
|
|
264
|
-
...init,
|
|
265
|
-
headers: retryHeaders
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
return { fetch: payFetch, cache: paymentCache };
|
|
269
66
|
}
|
|
270
67
|
|
|
68
|
+
// src/proxy.ts
|
|
69
|
+
import { registerExactEvmScheme } from "@x402/evm/exact/client";
|
|
70
|
+
import { toClientEvmSigner } from "@x402/evm";
|
|
71
|
+
|
|
271
72
|
// src/router/rules.ts
|
|
272
73
|
function scoreTokenCount(estimatedTokens, thresholds) {
|
|
273
74
|
if (estimatedTokens < thresholds.simple) {
|
|
@@ -1794,7 +1595,7 @@ function route(prompt, systemPrompt, maxOutputTokens, options) {
|
|
|
1794
1595
|
const ruleResult = classifyByRules(prompt, systemPrompt, estimatedTokens, config.scoring);
|
|
1795
1596
|
const { routingProfile } = options;
|
|
1796
1597
|
let tierConfigs;
|
|
1797
|
-
let profileSuffix
|
|
1598
|
+
let profileSuffix;
|
|
1798
1599
|
if (routingProfile === "eco" && config.ecoTiers) {
|
|
1799
1600
|
tierConfigs = config.ecoTiers;
|
|
1800
1601
|
profileSuffix = " | eco";
|
|
@@ -3102,6 +2903,299 @@ var BalanceMonitor = class {
|
|
|
3102
2903
|
}
|
|
3103
2904
|
};
|
|
3104
2905
|
|
|
2906
|
+
// src/solana-balance.ts
|
|
2907
|
+
import { address as solAddress, createSolanaRpc } from "@solana/kit";
|
|
2908
|
+
var SOLANA_USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
2909
|
+
var SOLANA_DEFAULT_RPC = "https://api.mainnet-beta.solana.com";
|
|
2910
|
+
var BALANCE_TIMEOUT_MS = 1e4;
|
|
2911
|
+
var CACHE_TTL_MS2 = 3e4;
|
|
2912
|
+
var SolanaBalanceMonitor = class {
|
|
2913
|
+
rpc;
|
|
2914
|
+
walletAddress;
|
|
2915
|
+
cachedBalance = null;
|
|
2916
|
+
cachedAt = 0;
|
|
2917
|
+
constructor(walletAddress, rpcUrl) {
|
|
2918
|
+
this.walletAddress = walletAddress;
|
|
2919
|
+
const url = rpcUrl || process["env"].CLAWROUTER_SOLANA_RPC_URL || SOLANA_DEFAULT_RPC;
|
|
2920
|
+
this.rpc = createSolanaRpc(url);
|
|
2921
|
+
}
|
|
2922
|
+
async checkBalance() {
|
|
2923
|
+
const now = Date.now();
|
|
2924
|
+
if (this.cachedBalance !== null && now - this.cachedAt < CACHE_TTL_MS2) {
|
|
2925
|
+
return this.buildInfo(this.cachedBalance);
|
|
2926
|
+
}
|
|
2927
|
+
const balance = await this.fetchBalance();
|
|
2928
|
+
this.cachedBalance = balance;
|
|
2929
|
+
this.cachedAt = now;
|
|
2930
|
+
return this.buildInfo(balance);
|
|
2931
|
+
}
|
|
2932
|
+
deductEstimated(amountMicros) {
|
|
2933
|
+
if (this.cachedBalance !== null && this.cachedBalance >= amountMicros) {
|
|
2934
|
+
this.cachedBalance -= amountMicros;
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
invalidate() {
|
|
2938
|
+
this.cachedBalance = null;
|
|
2939
|
+
this.cachedAt = 0;
|
|
2940
|
+
}
|
|
2941
|
+
async refresh() {
|
|
2942
|
+
this.invalidate();
|
|
2943
|
+
return this.checkBalance();
|
|
2944
|
+
}
|
|
2945
|
+
/**
|
|
2946
|
+
* Check if balance is sufficient for an estimated cost.
|
|
2947
|
+
*/
|
|
2948
|
+
async checkSufficient(estimatedCostMicros) {
|
|
2949
|
+
const info = await this.checkBalance();
|
|
2950
|
+
if (info.balance >= estimatedCostMicros) {
|
|
2951
|
+
return { sufficient: true, info };
|
|
2952
|
+
}
|
|
2953
|
+
const shortfall = estimatedCostMicros - info.balance;
|
|
2954
|
+
return {
|
|
2955
|
+
sufficient: false,
|
|
2956
|
+
info,
|
|
2957
|
+
shortfall: this.formatUSDC(shortfall)
|
|
2958
|
+
};
|
|
2959
|
+
}
|
|
2960
|
+
/**
|
|
2961
|
+
* Format USDC amount (in micros) as "$X.XX".
|
|
2962
|
+
*/
|
|
2963
|
+
formatUSDC(amountMicros) {
|
|
2964
|
+
const dollars = Number(amountMicros) / 1e6;
|
|
2965
|
+
return `$${dollars.toFixed(2)}`;
|
|
2966
|
+
}
|
|
2967
|
+
getWalletAddress() {
|
|
2968
|
+
return this.walletAddress;
|
|
2969
|
+
}
|
|
2970
|
+
async fetchBalance() {
|
|
2971
|
+
const owner = solAddress(this.walletAddress);
|
|
2972
|
+
const mint = solAddress(SOLANA_USDC_MINT);
|
|
2973
|
+
const controller = new AbortController();
|
|
2974
|
+
const timer = setTimeout(() => controller.abort(), BALANCE_TIMEOUT_MS);
|
|
2975
|
+
try {
|
|
2976
|
+
const response = await this.rpc.getTokenAccountsByOwner(owner, { mint }, { encoding: "jsonParsed" }).send({ abortSignal: controller.signal });
|
|
2977
|
+
if (response.value.length === 0) return 0n;
|
|
2978
|
+
let total = 0n;
|
|
2979
|
+
for (const account of response.value) {
|
|
2980
|
+
const parsed = account.account.data;
|
|
2981
|
+
total += BigInt(parsed.parsed.info.tokenAmount.amount);
|
|
2982
|
+
}
|
|
2983
|
+
return total;
|
|
2984
|
+
} catch (err) {
|
|
2985
|
+
throw new Error(`Failed to fetch Solana USDC balance: ${err instanceof Error ? err.message : String(err)}`);
|
|
2986
|
+
} finally {
|
|
2987
|
+
clearTimeout(timer);
|
|
2988
|
+
}
|
|
2989
|
+
}
|
|
2990
|
+
buildInfo(balance) {
|
|
2991
|
+
const dollars = Number(balance) / 1e6;
|
|
2992
|
+
return {
|
|
2993
|
+
balance,
|
|
2994
|
+
balanceUSD: `$${dollars.toFixed(2)}`,
|
|
2995
|
+
isLow: balance < 1000000n,
|
|
2996
|
+
isEmpty: balance < 100n,
|
|
2997
|
+
walletAddress: this.walletAddress
|
|
2998
|
+
};
|
|
2999
|
+
}
|
|
3000
|
+
};
|
|
3001
|
+
|
|
3002
|
+
// src/auth.ts
|
|
3003
|
+
import { writeFile, mkdir as mkdir2 } from "fs/promises";
|
|
3004
|
+
import { join as join4 } from "path";
|
|
3005
|
+
import { homedir as homedir3 } from "os";
|
|
3006
|
+
import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
|
|
3007
|
+
|
|
3008
|
+
// src/wallet.ts
|
|
3009
|
+
import { HDKey } from "@scure/bip32";
|
|
3010
|
+
import { generateMnemonic, mnemonicToSeedSync, validateMnemonic } from "@scure/bip39";
|
|
3011
|
+
import { wordlist as english } from "@scure/bip39/wordlists/english";
|
|
3012
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
3013
|
+
var ETH_DERIVATION_PATH = "m/44'/60'/0'/0/0";
|
|
3014
|
+
var SOLANA_DERIVATION_PATH = "m/44'/501'/0'/0'";
|
|
3015
|
+
function generateWalletMnemonic() {
|
|
3016
|
+
return generateMnemonic(english, 256);
|
|
3017
|
+
}
|
|
3018
|
+
function isValidMnemonic(mnemonic) {
|
|
3019
|
+
return validateMnemonic(mnemonic, english);
|
|
3020
|
+
}
|
|
3021
|
+
function deriveEvmKey(mnemonic) {
|
|
3022
|
+
const seed = mnemonicToSeedSync(mnemonic);
|
|
3023
|
+
const hdKey = HDKey.fromMasterSeed(seed);
|
|
3024
|
+
const derived = hdKey.derive(ETH_DERIVATION_PATH);
|
|
3025
|
+
if (!derived.privateKey) throw new Error("Failed to derive EVM private key");
|
|
3026
|
+
const hex = `0x${Buffer.from(derived.privateKey).toString("hex")}`;
|
|
3027
|
+
const account = privateKeyToAccount(hex);
|
|
3028
|
+
return { privateKey: hex, address: account.address };
|
|
3029
|
+
}
|
|
3030
|
+
function deriveSolanaKeyBytes(mnemonic) {
|
|
3031
|
+
const seed = mnemonicToSeedSync(mnemonic);
|
|
3032
|
+
const hdKey = HDKey.fromMasterSeed(seed);
|
|
3033
|
+
const derived = hdKey.derive(SOLANA_DERIVATION_PATH);
|
|
3034
|
+
if (!derived.privateKey) throw new Error("Failed to derive Solana private key");
|
|
3035
|
+
return new Uint8Array(derived.privateKey);
|
|
3036
|
+
}
|
|
3037
|
+
function deriveAllKeys(mnemonic) {
|
|
3038
|
+
const { privateKey: evmPrivateKey, address: evmAddress } = deriveEvmKey(mnemonic);
|
|
3039
|
+
const solanaPrivateKeyBytes = deriveSolanaKeyBytes(mnemonic);
|
|
3040
|
+
return { mnemonic, evmPrivateKey, evmAddress, solanaPrivateKeyBytes };
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
// src/auth.ts
|
|
3044
|
+
var WALLET_DIR = join4(homedir3(), ".openclaw", "blockrun");
|
|
3045
|
+
var WALLET_FILE = join4(WALLET_DIR, "wallet.key");
|
|
3046
|
+
var MNEMONIC_FILE = join4(WALLET_DIR, "mnemonic");
|
|
3047
|
+
var CHAIN_FILE = join4(WALLET_DIR, "payment-chain");
|
|
3048
|
+
async function loadSavedWallet() {
|
|
3049
|
+
try {
|
|
3050
|
+
const key = (await readTextFile(WALLET_FILE)).trim();
|
|
3051
|
+
if (key.startsWith("0x") && key.length === 66) {
|
|
3052
|
+
console.log(`[ClawRouter] \u2713 Loaded existing wallet from ${WALLET_FILE}`);
|
|
3053
|
+
return key;
|
|
3054
|
+
}
|
|
3055
|
+
console.error(`[ClawRouter] \u2717 CRITICAL: Wallet file exists but has invalid format!`);
|
|
3056
|
+
console.error(`[ClawRouter] File: ${WALLET_FILE}`);
|
|
3057
|
+
console.error(`[ClawRouter] Expected: 0x followed by 64 hex characters (66 chars total)`);
|
|
3058
|
+
console.error(
|
|
3059
|
+
`[ClawRouter] To fix: restore your backup key or set BLOCKRUN_WALLET_KEY env var`
|
|
3060
|
+
);
|
|
3061
|
+
throw new Error(
|
|
3062
|
+
`Wallet file at ${WALLET_FILE} is corrupted or has wrong format. Refusing to auto-generate new wallet to protect existing funds. Restore your backup key or set BLOCKRUN_WALLET_KEY environment variable.`
|
|
3063
|
+
);
|
|
3064
|
+
} catch (err) {
|
|
3065
|
+
if (err.code !== "ENOENT") {
|
|
3066
|
+
if (err instanceof Error && err.message.includes("Refusing to auto-generate")) {
|
|
3067
|
+
throw err;
|
|
3068
|
+
}
|
|
3069
|
+
console.error(
|
|
3070
|
+
`[ClawRouter] \u2717 Failed to read wallet file: ${err instanceof Error ? err.message : String(err)}`
|
|
3071
|
+
);
|
|
3072
|
+
throw new Error(
|
|
3073
|
+
`Cannot read wallet file at ${WALLET_FILE}: ${err instanceof Error ? err.message : String(err)}. Refusing to auto-generate new wallet to protect existing funds. Fix file permissions or set BLOCKRUN_WALLET_KEY environment variable.`,
|
|
3074
|
+
{ cause: err }
|
|
3075
|
+
);
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
return void 0;
|
|
3079
|
+
}
|
|
3080
|
+
async function loadMnemonic() {
|
|
3081
|
+
try {
|
|
3082
|
+
const mnemonic = (await readTextFile(MNEMONIC_FILE)).trim();
|
|
3083
|
+
if (mnemonic && isValidMnemonic(mnemonic)) {
|
|
3084
|
+
return mnemonic;
|
|
3085
|
+
}
|
|
3086
|
+
console.warn(`[ClawRouter] \u26A0 Mnemonic file exists but has invalid format \u2014 ignoring`);
|
|
3087
|
+
return void 0;
|
|
3088
|
+
} catch (err) {
|
|
3089
|
+
if (err.code !== "ENOENT") {
|
|
3090
|
+
console.warn(`[ClawRouter] \u26A0 Cannot read mnemonic file \u2014 ignoring`);
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
return void 0;
|
|
3094
|
+
}
|
|
3095
|
+
async function generateAndSaveWallet() {
|
|
3096
|
+
const existingMnemonic = await loadMnemonic();
|
|
3097
|
+
if (existingMnemonic) {
|
|
3098
|
+
throw new Error(
|
|
3099
|
+
`Mnemonic file exists at ${MNEMONIC_FILE} but wallet.key is missing. This means a Solana wallet was derived from this mnemonic. Refusing to generate a new wallet to protect Solana funds. Restore your EVM key with: export BLOCKRUN_WALLET_KEY=<your_key>`
|
|
3100
|
+
);
|
|
3101
|
+
}
|
|
3102
|
+
const mnemonic = generateWalletMnemonic();
|
|
3103
|
+
const derived = deriveAllKeys(mnemonic);
|
|
3104
|
+
await mkdir2(WALLET_DIR, { recursive: true });
|
|
3105
|
+
await writeFile(WALLET_FILE, derived.evmPrivateKey + "\n", { mode: 384 });
|
|
3106
|
+
await writeFile(MNEMONIC_FILE, mnemonic + "\n", { mode: 384 });
|
|
3107
|
+
try {
|
|
3108
|
+
const verification = (await readTextFile(WALLET_FILE)).trim();
|
|
3109
|
+
if (verification !== derived.evmPrivateKey) {
|
|
3110
|
+
throw new Error("Wallet file verification failed - content mismatch");
|
|
3111
|
+
}
|
|
3112
|
+
console.log(`[ClawRouter] Wallet saved and verified at ${WALLET_FILE}`);
|
|
3113
|
+
} catch (err) {
|
|
3114
|
+
throw new Error(
|
|
3115
|
+
`Failed to verify wallet file after creation: ${err instanceof Error ? err.message : String(err)}`,
|
|
3116
|
+
{ cause: err }
|
|
3117
|
+
);
|
|
3118
|
+
}
|
|
3119
|
+
console.log(`[ClawRouter]`);
|
|
3120
|
+
console.log(`[ClawRouter] \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`);
|
|
3121
|
+
console.log(`[ClawRouter] NEW WALLET GENERATED \u2014 BACK UP YOUR KEY NOW`);
|
|
3122
|
+
console.log(`[ClawRouter] \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`);
|
|
3123
|
+
console.log(`[ClawRouter] EVM Address : ${derived.evmAddress}`);
|
|
3124
|
+
console.log(`[ClawRouter] Key file : ${WALLET_FILE}`);
|
|
3125
|
+
console.log(`[ClawRouter] Mnemonic : ${MNEMONIC_FILE}`);
|
|
3126
|
+
console.log(`[ClawRouter]`);
|
|
3127
|
+
console.log(`[ClawRouter] Both EVM (Base) and Solana wallets are ready.`);
|
|
3128
|
+
console.log(`[ClawRouter] To back up, run in OpenClaw:`);
|
|
3129
|
+
console.log(`[ClawRouter] /wallet export`);
|
|
3130
|
+
console.log(`[ClawRouter]`);
|
|
3131
|
+
console.log(`[ClawRouter] To restore on another machine:`);
|
|
3132
|
+
console.log(`[ClawRouter] export BLOCKRUN_WALLET_KEY=<your_key>`);
|
|
3133
|
+
console.log(`[ClawRouter] \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`);
|
|
3134
|
+
console.log(`[ClawRouter]`);
|
|
3135
|
+
return {
|
|
3136
|
+
key: derived.evmPrivateKey,
|
|
3137
|
+
address: derived.evmAddress,
|
|
3138
|
+
mnemonic,
|
|
3139
|
+
solanaPrivateKeyBytes: derived.solanaPrivateKeyBytes
|
|
3140
|
+
};
|
|
3141
|
+
}
|
|
3142
|
+
async function resolveOrGenerateWalletKey() {
|
|
3143
|
+
const saved = await loadSavedWallet();
|
|
3144
|
+
if (saved) {
|
|
3145
|
+
const account = privateKeyToAccount2(saved);
|
|
3146
|
+
const mnemonic = await loadMnemonic();
|
|
3147
|
+
if (mnemonic) {
|
|
3148
|
+
const solanaKeyBytes = deriveSolanaKeyBytes(mnemonic);
|
|
3149
|
+
return {
|
|
3150
|
+
key: saved,
|
|
3151
|
+
address: account.address,
|
|
3152
|
+
source: "saved",
|
|
3153
|
+
mnemonic,
|
|
3154
|
+
solanaPrivateKeyBytes: solanaKeyBytes
|
|
3155
|
+
};
|
|
3156
|
+
}
|
|
3157
|
+
return { key: saved, address: account.address, source: "saved" };
|
|
3158
|
+
}
|
|
3159
|
+
const envKey = process["env"].BLOCKRUN_WALLET_KEY;
|
|
3160
|
+
if (typeof envKey === "string" && envKey.startsWith("0x") && envKey.length === 66) {
|
|
3161
|
+
const account = privateKeyToAccount2(envKey);
|
|
3162
|
+
const mnemonic = await loadMnemonic();
|
|
3163
|
+
if (mnemonic) {
|
|
3164
|
+
const solanaKeyBytes = deriveSolanaKeyBytes(mnemonic);
|
|
3165
|
+
return {
|
|
3166
|
+
key: envKey,
|
|
3167
|
+
address: account.address,
|
|
3168
|
+
source: "env",
|
|
3169
|
+
mnemonic,
|
|
3170
|
+
solanaPrivateKeyBytes: solanaKeyBytes
|
|
3171
|
+
};
|
|
3172
|
+
}
|
|
3173
|
+
return { key: envKey, address: account.address, source: "env" };
|
|
3174
|
+
}
|
|
3175
|
+
const result = await generateAndSaveWallet();
|
|
3176
|
+
return {
|
|
3177
|
+
key: result.key,
|
|
3178
|
+
address: result.address,
|
|
3179
|
+
source: "generated",
|
|
3180
|
+
mnemonic: result.mnemonic,
|
|
3181
|
+
solanaPrivateKeyBytes: result.solanaPrivateKeyBytes
|
|
3182
|
+
};
|
|
3183
|
+
}
|
|
3184
|
+
async function loadPaymentChain() {
|
|
3185
|
+
try {
|
|
3186
|
+
const content = (await readTextFile(CHAIN_FILE)).trim();
|
|
3187
|
+
if (content === "solana") return "solana";
|
|
3188
|
+
return "base";
|
|
3189
|
+
} catch {
|
|
3190
|
+
return "base";
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
async function resolvePaymentChain() {
|
|
3194
|
+
if (process["env"].CLAWROUTER_PAYMENT_CHAIN === "solana") return "solana";
|
|
3195
|
+
if (process["env"].CLAWROUTER_PAYMENT_CHAIN === "base") return "base";
|
|
3196
|
+
return loadPaymentChain();
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3105
3199
|
// src/compression/types.ts
|
|
3106
3200
|
var DEFAULT_COMPRESSION_CONFIG = {
|
|
3107
3201
|
enabled: true,
|
|
@@ -3131,7 +3225,7 @@ var DEFAULT_COMPRESSION_CONFIG = {
|
|
|
3131
3225
|
};
|
|
3132
3226
|
|
|
3133
3227
|
// src/compression/layers/deduplication.ts
|
|
3134
|
-
import
|
|
3228
|
+
import crypto from "crypto";
|
|
3135
3229
|
function hashMessage(message) {
|
|
3136
3230
|
let contentStr = "";
|
|
3137
3231
|
if (typeof message.content === "string") {
|
|
@@ -3151,7 +3245,7 @@ function hashMessage(message) {
|
|
|
3151
3245
|
);
|
|
3152
3246
|
}
|
|
3153
3247
|
const content = parts.join("|");
|
|
3154
|
-
return
|
|
3248
|
+
return crypto.createHash("md5").update(content).digest("hex");
|
|
3155
3249
|
}
|
|
3156
3250
|
function deduplicateMessages(messages) {
|
|
3157
3251
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -4248,6 +4342,7 @@ ${lines.join("\n")}`;
|
|
|
4248
4342
|
|
|
4249
4343
|
// src/proxy.ts
|
|
4250
4344
|
var BLOCKRUN_API = "https://blockrun.ai/api";
|
|
4345
|
+
var BLOCKRUN_SOLANA_API = "https://sol.blockrun.ai/api";
|
|
4251
4346
|
var AUTO_MODEL = "blockrun/auto";
|
|
4252
4347
|
var ROUTING_PROFILES = /* @__PURE__ */ new Set([
|
|
4253
4348
|
"blockrun/free",
|
|
@@ -4376,7 +4471,7 @@ async function checkExistingProxy(port) {
|
|
|
4376
4471
|
if (response.ok) {
|
|
4377
4472
|
const data = await response.json();
|
|
4378
4473
|
if (data.status === "ok" && data.wallet) {
|
|
4379
|
-
return data.wallet;
|
|
4474
|
+
return { wallet: data.wallet, paymentChain: data.paymentChain };
|
|
4380
4475
|
}
|
|
4381
4476
|
}
|
|
4382
4477
|
return void 0;
|
|
@@ -4806,31 +4901,78 @@ async function uploadDataUriToHost(dataUri) {
|
|
|
4806
4901
|
throw new Error(`catbox.moe upload failed: ${result}`);
|
|
4807
4902
|
}
|
|
4808
4903
|
async function startProxy(options) {
|
|
4809
|
-
const
|
|
4904
|
+
const walletKey = typeof options.wallet === "string" ? options.wallet : options.wallet.key;
|
|
4905
|
+
const solanaPrivateKeyBytes = typeof options.wallet === "string" ? void 0 : options.wallet.solanaPrivateKeyBytes;
|
|
4906
|
+
const paymentChain = options.paymentChain ?? await resolvePaymentChain();
|
|
4907
|
+
const apiBase = options.apiBase ?? (paymentChain === "solana" && solanaPrivateKeyBytes ? BLOCKRUN_SOLANA_API : BLOCKRUN_API);
|
|
4908
|
+
if (paymentChain === "solana" && !solanaPrivateKeyBytes) {
|
|
4909
|
+
console.warn(`[ClawRouter] Payment chain is Solana but no Solana keys provided. Using Base (EVM).`);
|
|
4910
|
+
} else if (paymentChain === "solana") {
|
|
4911
|
+
console.log(`[ClawRouter] Payment chain: Solana (${BLOCKRUN_SOLANA_API})`);
|
|
4912
|
+
}
|
|
4810
4913
|
const listenPort = options.port ?? getProxyPort();
|
|
4811
|
-
const
|
|
4812
|
-
if (
|
|
4813
|
-
const account2 =
|
|
4814
|
-
const balanceMonitor2 = new BalanceMonitor(account2.address);
|
|
4914
|
+
const existingProxy = await checkExistingProxy(listenPort);
|
|
4915
|
+
if (existingProxy) {
|
|
4916
|
+
const account2 = privateKeyToAccount3(walletKey);
|
|
4815
4917
|
const baseUrl2 = `http://127.0.0.1:${listenPort}`;
|
|
4816
|
-
if (
|
|
4918
|
+
if (existingProxy.wallet !== account2.address) {
|
|
4817
4919
|
console.warn(
|
|
4818
|
-
`[ClawRouter] Existing proxy on port ${listenPort} uses wallet ${
|
|
4920
|
+
`[ClawRouter] Existing proxy on port ${listenPort} uses wallet ${existingProxy.wallet}, but current config uses ${account2.address}. Reusing existing proxy.`
|
|
4819
4921
|
);
|
|
4820
4922
|
}
|
|
4923
|
+
if (existingProxy.paymentChain) {
|
|
4924
|
+
if (existingProxy.paymentChain !== paymentChain) {
|
|
4925
|
+
throw new Error(
|
|
4926
|
+
`Existing proxy on port ${listenPort} is using ${existingProxy.paymentChain} but ${paymentChain} was requested. Stop the existing proxy first or use a different port.`
|
|
4927
|
+
);
|
|
4928
|
+
}
|
|
4929
|
+
} else if (paymentChain !== "base") {
|
|
4930
|
+
console.warn(`[ClawRouter] Existing proxy on port ${listenPort} does not report paymentChain (pre-v0.11 instance). Assuming Base.`);
|
|
4931
|
+
throw new Error(
|
|
4932
|
+
`Existing proxy on port ${listenPort} is a pre-v0.11 instance (assumed Base) but ${paymentChain} was requested. Stop the existing proxy first or use a different port.`
|
|
4933
|
+
);
|
|
4934
|
+
}
|
|
4935
|
+
let reuseSolanaAddress;
|
|
4936
|
+
if (solanaPrivateKeyBytes) {
|
|
4937
|
+
const { createKeyPairSignerFromPrivateKeyBytes } = await import("@solana/kit");
|
|
4938
|
+
const solanaSigner = await createKeyPairSignerFromPrivateKeyBytes(solanaPrivateKeyBytes);
|
|
4939
|
+
reuseSolanaAddress = solanaSigner.address;
|
|
4940
|
+
}
|
|
4941
|
+
const balanceMonitor2 = paymentChain === "solana" && reuseSolanaAddress ? new SolanaBalanceMonitor(reuseSolanaAddress) : new BalanceMonitor(account2.address);
|
|
4821
4942
|
options.onReady?.(listenPort);
|
|
4822
4943
|
return {
|
|
4823
4944
|
port: listenPort,
|
|
4824
4945
|
baseUrl: baseUrl2,
|
|
4825
|
-
walletAddress:
|
|
4946
|
+
walletAddress: existingProxy.wallet,
|
|
4947
|
+
solanaAddress: reuseSolanaAddress,
|
|
4826
4948
|
balanceMonitor: balanceMonitor2,
|
|
4827
4949
|
close: async () => {
|
|
4828
4950
|
}
|
|
4829
4951
|
};
|
|
4830
4952
|
}
|
|
4831
|
-
const account =
|
|
4832
|
-
const {
|
|
4833
|
-
const
|
|
4953
|
+
const account = privateKeyToAccount3(walletKey);
|
|
4954
|
+
const evmPublicClient = createPublicClient2({ chain: base2, transport: http2() });
|
|
4955
|
+
const evmSigner = toClientEvmSigner(account, evmPublicClient);
|
|
4956
|
+
const x402 = new x402Client();
|
|
4957
|
+
registerExactEvmScheme(x402, { signer: evmSigner });
|
|
4958
|
+
let solanaAddress;
|
|
4959
|
+
if (solanaPrivateKeyBytes) {
|
|
4960
|
+
const { registerExactSvmScheme } = await import("@x402/svm/exact/client");
|
|
4961
|
+
const { createKeyPairSignerFromPrivateKeyBytes } = await import("@solana/kit");
|
|
4962
|
+
const solanaSigner = await createKeyPairSignerFromPrivateKeyBytes(solanaPrivateKeyBytes);
|
|
4963
|
+
solanaAddress = solanaSigner.address;
|
|
4964
|
+
registerExactSvmScheme(x402, { signer: solanaSigner });
|
|
4965
|
+
console.log(`[ClawRouter] Solana x402 scheme registered: ${solanaAddress}`);
|
|
4966
|
+
}
|
|
4967
|
+
x402.onAfterPaymentCreation(async (context) => {
|
|
4968
|
+
const network = context.selectedRequirements.network;
|
|
4969
|
+
const chain = network.startsWith("eip155") ? "Base (EVM)" : network.startsWith("solana") ? "Solana" : network;
|
|
4970
|
+
console.log(`[ClawRouter] Payment signed on ${chain} (${network})`);
|
|
4971
|
+
});
|
|
4972
|
+
const payFetch = createPayFetchWithPreAuth(fetch, x402, void 0, {
|
|
4973
|
+
skipPreAuth: paymentChain === "solana"
|
|
4974
|
+
});
|
|
4975
|
+
const balanceMonitor = paymentChain === "solana" && solanaAddress ? new SolanaBalanceMonitor(solanaAddress) : new BalanceMonitor(account.address);
|
|
4834
4976
|
const routingConfig = mergeRoutingConfig(options.routingConfig);
|
|
4835
4977
|
const modelPricing = buildModelPricing();
|
|
4836
4978
|
const routerOpts = {
|
|
@@ -4864,8 +5006,12 @@ async function startProxy(options) {
|
|
|
4864
5006
|
const full = url.searchParams.get("full") === "true";
|
|
4865
5007
|
const response = {
|
|
4866
5008
|
status: "ok",
|
|
4867
|
-
wallet: account.address
|
|
5009
|
+
wallet: account.address,
|
|
5010
|
+
paymentChain
|
|
4868
5011
|
};
|
|
5012
|
+
if (solanaAddress) {
|
|
5013
|
+
response.solana = solanaAddress;
|
|
5014
|
+
}
|
|
4869
5015
|
if (full) {
|
|
4870
5016
|
try {
|
|
4871
5017
|
const balanceInfo = await balanceMonitor.checkBalance();
|
|
@@ -4977,10 +5123,10 @@ async function startProxy(options) {
|
|
|
4977
5123
|
const onError = async (err) => {
|
|
4978
5124
|
server.removeListener("error", onError);
|
|
4979
5125
|
if (err.code === "EADDRINUSE") {
|
|
4980
|
-
const
|
|
4981
|
-
if (
|
|
5126
|
+
const existingProxy2 = await checkExistingProxy(listenPort);
|
|
5127
|
+
if (existingProxy2) {
|
|
4982
5128
|
console.log(`[ClawRouter] Existing proxy detected on port ${listenPort}, reusing`);
|
|
4983
|
-
rejectAttempt({ code: "REUSE_EXISTING", wallet:
|
|
5129
|
+
rejectAttempt({ code: "REUSE_EXISTING", wallet: existingProxy2.wallet, existingChain: existingProxy2.paymentChain });
|
|
4984
5130
|
return;
|
|
4985
5131
|
}
|
|
4986
5132
|
if (attempt < PORT_RETRY_ATTEMPTS) {
|
|
@@ -5013,6 +5159,11 @@ async function startProxy(options) {
|
|
|
5013
5159
|
} catch (err) {
|
|
5014
5160
|
const error = err;
|
|
5015
5161
|
if (error.code === "REUSE_EXISTING" && error.wallet) {
|
|
5162
|
+
if (error.existingChain && error.existingChain !== paymentChain) {
|
|
5163
|
+
throw new Error(
|
|
5164
|
+
`Existing proxy on port ${listenPort} is using ${error.existingChain} but ${paymentChain} was requested. Stop the existing proxy first or use a different port.`
|
|
5165
|
+
);
|
|
5166
|
+
}
|
|
5016
5167
|
const baseUrl2 = `http://127.0.0.1:${listenPort}`;
|
|
5017
5168
|
options.onReady?.(listenPort);
|
|
5018
5169
|
return {
|
|
@@ -5070,6 +5221,7 @@ async function startProxy(options) {
|
|
|
5070
5221
|
port,
|
|
5071
5222
|
baseUrl,
|
|
5072
5223
|
walletAddress: account.address,
|
|
5224
|
+
solanaAddress,
|
|
5073
5225
|
balanceMonitor,
|
|
5074
5226
|
close: () => new Promise((res, rej) => {
|
|
5075
5227
|
const timeout = setTimeout(() => {
|
|
@@ -5116,8 +5268,6 @@ async function tryModelRequest(upstreamUrl, method, headers, body, modelId, maxT
|
|
|
5116
5268
|
requestBody = Buffer.from(JSON.stringify(parsed));
|
|
5117
5269
|
} catch {
|
|
5118
5270
|
}
|
|
5119
|
-
const estimated = estimateAmount(modelId, requestBody.length, maxTokens);
|
|
5120
|
-
const preAuth = estimated ? { estimatedAmount: estimated } : void 0;
|
|
5121
5271
|
try {
|
|
5122
5272
|
const response = await payFetch(
|
|
5123
5273
|
upstreamUrl,
|
|
@@ -5126,8 +5276,7 @@ async function tryModelRequest(upstreamUrl, method, headers, body, modelId, maxT
|
|
|
5126
5276
|
headers,
|
|
5127
5277
|
body: requestBody.length > 0 ? new Uint8Array(requestBody) : void 0,
|
|
5128
5278
|
signal
|
|
5129
|
-
}
|
|
5130
|
-
preAuth
|
|
5279
|
+
}
|
|
5131
5280
|
);
|
|
5132
5281
|
if (response.status !== 200) {
|
|
5133
5282
|
const errorBody = await response.text();
|
|
@@ -5763,6 +5912,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
5763
5912
|
}
|
|
5764
5913
|
deduplicator.markInflight(dedupKey);
|
|
5765
5914
|
let estimatedCostMicros;
|
|
5915
|
+
let balanceFallbackNotice;
|
|
5766
5916
|
const isFreeModel = modelId === FREE_MODEL;
|
|
5767
5917
|
if (modelId && !options.skipBalanceCheck && !isFreeModel) {
|
|
5768
5918
|
const estimated = estimateAmount(modelId, body.length, maxTokens);
|
|
@@ -5773,12 +5923,17 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
5773
5923
|
if (sufficiency.info.isEmpty || !sufficiency.sufficient) {
|
|
5774
5924
|
const originalModel = modelId;
|
|
5775
5925
|
console.log(
|
|
5776
|
-
`[ClawRouter] Wallet ${sufficiency.info.isEmpty ? "empty" : "insufficient"} (
|
|
5926
|
+
`[ClawRouter] Wallet ${sufficiency.info.isEmpty ? "empty" : "insufficient"} (${sufficiency.info.balanceUSD}), falling back to free model: ${FREE_MODEL} (requested: ${originalModel})`
|
|
5777
5927
|
);
|
|
5778
5928
|
modelId = FREE_MODEL;
|
|
5779
5929
|
const parsed = JSON.parse(body.toString());
|
|
5780
5930
|
parsed.model = FREE_MODEL;
|
|
5781
5931
|
body = Buffer.from(JSON.stringify(parsed));
|
|
5932
|
+
balanceFallbackNotice = sufficiency.info.isEmpty ? `> **\u26A0\uFE0F Wallet empty** \u2014 using free model. Fund your wallet to use ${originalModel}.
|
|
5933
|
+
|
|
5934
|
+
` : `> **\u26A0\uFE0F Insufficient balance** (${sufficiency.info.balanceUSD}) \u2014 using free model instead of ${originalModel}.
|
|
5935
|
+
|
|
5936
|
+
`;
|
|
5782
5937
|
options.onLowBalance?.({
|
|
5783
5938
|
balanceUSD: sufficiency.info.balanceUSD,
|
|
5784
5939
|
walletAddress: sufficiency.info.walletAddress
|
|
@@ -6070,6 +6225,18 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
6070
6225
|
`;
|
|
6071
6226
|
safeWrite(res, roleData);
|
|
6072
6227
|
responseChunks.push(Buffer.from(roleData));
|
|
6228
|
+
if (balanceFallbackNotice) {
|
|
6229
|
+
const noticeChunk = {
|
|
6230
|
+
...baseChunk,
|
|
6231
|
+
choices: [{ index, delta: { content: balanceFallbackNotice }, logprobs: null, finish_reason: null }]
|
|
6232
|
+
};
|
|
6233
|
+
const noticeData = `data: ${JSON.stringify(noticeChunk)}
|
|
6234
|
+
|
|
6235
|
+
`;
|
|
6236
|
+
safeWrite(res, noticeData);
|
|
6237
|
+
responseChunks.push(Buffer.from(noticeData));
|
|
6238
|
+
balanceFallbackNotice = void 0;
|
|
6239
|
+
}
|
|
6073
6240
|
if (content) {
|
|
6074
6241
|
const contentChunk = {
|
|
6075
6242
|
...baseChunk,
|
|
@@ -6154,23 +6321,36 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
6154
6321
|
responseHeaders["x-clawrouter-agentic-score"] = routingDecision.agenticScore.toFixed(2);
|
|
6155
6322
|
}
|
|
6156
6323
|
}
|
|
6157
|
-
|
|
6324
|
+
const bodyParts = [];
|
|
6158
6325
|
if (upstream.body) {
|
|
6159
6326
|
const reader = upstream.body.getReader();
|
|
6160
6327
|
try {
|
|
6161
6328
|
while (true) {
|
|
6162
6329
|
const { done, value } = await reader.read();
|
|
6163
6330
|
if (done) break;
|
|
6164
|
-
|
|
6165
|
-
safeWrite(res, chunk);
|
|
6166
|
-
responseChunks.push(chunk);
|
|
6331
|
+
bodyParts.push(Buffer.from(value));
|
|
6167
6332
|
}
|
|
6168
6333
|
} finally {
|
|
6169
6334
|
reader.releaseLock();
|
|
6170
6335
|
}
|
|
6171
6336
|
}
|
|
6337
|
+
let responseBody = Buffer.concat(bodyParts);
|
|
6338
|
+
if (balanceFallbackNotice && responseBody.length > 0) {
|
|
6339
|
+
try {
|
|
6340
|
+
const parsed = JSON.parse(responseBody.toString());
|
|
6341
|
+
if (parsed.choices?.[0]?.message?.content !== void 0) {
|
|
6342
|
+
parsed.choices[0].message.content = balanceFallbackNotice + parsed.choices[0].message.content;
|
|
6343
|
+
responseBody = Buffer.from(JSON.stringify(parsed));
|
|
6344
|
+
}
|
|
6345
|
+
} catch {
|
|
6346
|
+
}
|
|
6347
|
+
balanceFallbackNotice = void 0;
|
|
6348
|
+
}
|
|
6349
|
+
responseHeaders["content-length"] = String(responseBody.length);
|
|
6350
|
+
res.writeHead(upstream.status, responseHeaders);
|
|
6351
|
+
safeWrite(res, responseBody);
|
|
6352
|
+
responseChunks.push(responseBody);
|
|
6172
6353
|
res.end();
|
|
6173
|
-
const responseBody = Buffer.concat(responseChunks);
|
|
6174
6354
|
deduplicator.complete(dedupKey, {
|
|
6175
6355
|
status: upstream.status,
|
|
6176
6356
|
headers: responseHeaders,
|
|
@@ -6222,7 +6402,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
6222
6402
|
deduplicator.removeInflight(dedupKey);
|
|
6223
6403
|
balanceMonitor.invalidate();
|
|
6224
6404
|
if (err instanceof Error && err.name === "AbortError") {
|
|
6225
|
-
throw new Error(`Request timed out after ${timeoutMs}ms
|
|
6405
|
+
throw new Error(`Request timed out after ${timeoutMs}ms`, { cause: err });
|
|
6226
6406
|
}
|
|
6227
6407
|
throw err;
|
|
6228
6408
|
}
|
|
@@ -6253,91 +6433,6 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
6253
6433
|
}
|
|
6254
6434
|
}
|
|
6255
6435
|
|
|
6256
|
-
// src/auth.ts
|
|
6257
|
-
import { writeFile, mkdir as mkdir2 } from "fs/promises";
|
|
6258
|
-
import { join as join4 } from "path";
|
|
6259
|
-
import { homedir as homedir3 } from "os";
|
|
6260
|
-
import { generatePrivateKey, privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
|
|
6261
|
-
var WALLET_DIR = join4(homedir3(), ".openclaw", "blockrun");
|
|
6262
|
-
var WALLET_FILE = join4(WALLET_DIR, "wallet.key");
|
|
6263
|
-
async function loadSavedWallet() {
|
|
6264
|
-
try {
|
|
6265
|
-
const key = (await readTextFile(WALLET_FILE)).trim();
|
|
6266
|
-
if (key.startsWith("0x") && key.length === 66) {
|
|
6267
|
-
console.log(`[ClawRouter] \u2713 Loaded existing wallet from ${WALLET_FILE}`);
|
|
6268
|
-
return key;
|
|
6269
|
-
}
|
|
6270
|
-
console.error(`[ClawRouter] \u2717 CRITICAL: Wallet file exists but has invalid format!`);
|
|
6271
|
-
console.error(`[ClawRouter] File: ${WALLET_FILE}`);
|
|
6272
|
-
console.error(`[ClawRouter] Expected: 0x followed by 64 hex characters (66 chars total)`);
|
|
6273
|
-
console.error(
|
|
6274
|
-
`[ClawRouter] To fix: restore your backup key or set BLOCKRUN_WALLET_KEY env var`
|
|
6275
|
-
);
|
|
6276
|
-
throw new Error(
|
|
6277
|
-
`Wallet file at ${WALLET_FILE} is corrupted or has wrong format. Refusing to auto-generate new wallet to protect existing funds. Restore your backup key or set BLOCKRUN_WALLET_KEY environment variable.`
|
|
6278
|
-
);
|
|
6279
|
-
} catch (err) {
|
|
6280
|
-
if (err.code !== "ENOENT") {
|
|
6281
|
-
if (err instanceof Error && err.message.includes("Refusing to auto-generate")) {
|
|
6282
|
-
throw err;
|
|
6283
|
-
}
|
|
6284
|
-
console.error(
|
|
6285
|
-
`[ClawRouter] \u2717 Failed to read wallet file: ${err instanceof Error ? err.message : String(err)}`
|
|
6286
|
-
);
|
|
6287
|
-
throw new Error(
|
|
6288
|
-
`Cannot read wallet file at ${WALLET_FILE}: ${err instanceof Error ? err.message : String(err)}. Refusing to auto-generate new wallet to protect existing funds. Fix file permissions or set BLOCKRUN_WALLET_KEY environment variable.`
|
|
6289
|
-
);
|
|
6290
|
-
}
|
|
6291
|
-
}
|
|
6292
|
-
return void 0;
|
|
6293
|
-
}
|
|
6294
|
-
async function generateAndSaveWallet() {
|
|
6295
|
-
const key = generatePrivateKey();
|
|
6296
|
-
const account = privateKeyToAccount3(key);
|
|
6297
|
-
await mkdir2(WALLET_DIR, { recursive: true });
|
|
6298
|
-
await writeFile(WALLET_FILE, key + "\n", { mode: 384 });
|
|
6299
|
-
try {
|
|
6300
|
-
const verification = (await readTextFile(WALLET_FILE)).trim();
|
|
6301
|
-
if (verification !== key) {
|
|
6302
|
-
throw new Error("Wallet file verification failed - content mismatch");
|
|
6303
|
-
}
|
|
6304
|
-
console.log(`[ClawRouter] \u2713 Wallet saved and verified at ${WALLET_FILE}`);
|
|
6305
|
-
} catch (err) {
|
|
6306
|
-
throw new Error(
|
|
6307
|
-
`Failed to verify wallet file after creation: ${err instanceof Error ? err.message : String(err)}`
|
|
6308
|
-
);
|
|
6309
|
-
}
|
|
6310
|
-
console.log(`[ClawRouter]`);
|
|
6311
|
-
console.log(`[ClawRouter] \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`);
|
|
6312
|
-
console.log(`[ClawRouter] NEW WALLET GENERATED \u2014 BACK UP YOUR KEY NOW`);
|
|
6313
|
-
console.log(`[ClawRouter] \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`);
|
|
6314
|
-
console.log(`[ClawRouter] Address : ${account.address}`);
|
|
6315
|
-
console.log(`[ClawRouter] Key file: ${WALLET_FILE}`);
|
|
6316
|
-
console.log(`[ClawRouter]`);
|
|
6317
|
-
console.log(`[ClawRouter] To back up, run in OpenClaw:`);
|
|
6318
|
-
console.log(`[ClawRouter] /wallet export`);
|
|
6319
|
-
console.log(`[ClawRouter]`);
|
|
6320
|
-
console.log(`[ClawRouter] To restore on another machine:`);
|
|
6321
|
-
console.log(`[ClawRouter] export BLOCKRUN_WALLET_KEY=<your_key>`);
|
|
6322
|
-
console.log(`[ClawRouter] \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`);
|
|
6323
|
-
console.log(`[ClawRouter]`);
|
|
6324
|
-
return { key, address: account.address };
|
|
6325
|
-
}
|
|
6326
|
-
async function resolveOrGenerateWalletKey() {
|
|
6327
|
-
const saved = await loadSavedWallet();
|
|
6328
|
-
if (saved) {
|
|
6329
|
-
const account = privateKeyToAccount3(saved);
|
|
6330
|
-
return { key: saved, address: account.address, source: "saved" };
|
|
6331
|
-
}
|
|
6332
|
-
const envKey = process["env"].BLOCKRUN_WALLET_KEY;
|
|
6333
|
-
if (typeof envKey === "string" && envKey.startsWith("0x") && envKey.length === 66) {
|
|
6334
|
-
const account = privateKeyToAccount3(envKey);
|
|
6335
|
-
return { key: envKey, address: account.address, source: "env" };
|
|
6336
|
-
}
|
|
6337
|
-
const { key, address } = await generateAndSaveWallet();
|
|
6338
|
-
return { key, address, source: "generated" };
|
|
6339
|
-
}
|
|
6340
|
-
|
|
6341
6436
|
// src/report.ts
|
|
6342
6437
|
async function generateReport(period, json = false) {
|
|
6343
6438
|
const days = period === "daily" ? 1 : period === "weekly" ? 7 : 30;
|
|
@@ -6379,6 +6474,12 @@ function capitalize(str) {
|
|
|
6379
6474
|
|
|
6380
6475
|
// src/doctor.ts
|
|
6381
6476
|
import { platform, arch, freemem, totalmem } from "os";
|
|
6477
|
+
import { createPublicClient as createPublicClient3, http as http3 } from "viem";
|
|
6478
|
+
import { base as base3 } from "viem/chains";
|
|
6479
|
+
import { privateKeyToAccount as privateKeyToAccount4 } from "viem/accounts";
|
|
6480
|
+
import { wrapFetchWithPayment, x402Client as x402Client2 } from "@x402/fetch";
|
|
6481
|
+
import { registerExactEvmScheme as registerExactEvmScheme2 } from "@x402/evm/exact/client";
|
|
6482
|
+
import { toClientEvmSigner as toClientEvmSigner2 } from "@x402/evm";
|
|
6382
6483
|
function formatBytes(bytes) {
|
|
6383
6484
|
const gb = bytes / (1024 * 1024 * 1024);
|
|
6384
6485
|
return `${gb.toFixed(1)}GB`;
|
|
@@ -6463,7 +6564,6 @@ async function collectNetworkInfo() {
|
|
|
6463
6564
|
blockrunLatency = Date.now() - start;
|
|
6464
6565
|
blockrunReachable = response.ok || response.status === 402;
|
|
6465
6566
|
} catch {
|
|
6466
|
-
blockrunReachable = false;
|
|
6467
6567
|
}
|
|
6468
6568
|
let proxyRunning = false;
|
|
6469
6569
|
try {
|
|
@@ -6473,7 +6573,6 @@ async function collectNetworkInfo() {
|
|
|
6473
6573
|
});
|
|
6474
6574
|
proxyRunning = response.ok;
|
|
6475
6575
|
} catch {
|
|
6476
|
-
proxyRunning = false;
|
|
6477
6576
|
}
|
|
6478
6577
|
return {
|
|
6479
6578
|
blockrunApi: { reachable: blockrunReachable, latencyMs: blockrunLatency },
|
|
@@ -6592,7 +6691,12 @@ async function analyzeWithAI(diagnostics, userQuestion, model = "sonnet") {
|
|
|
6592
6691
|
`);
|
|
6593
6692
|
try {
|
|
6594
6693
|
const { key } = await resolveOrGenerateWalletKey();
|
|
6595
|
-
const
|
|
6694
|
+
const account = privateKeyToAccount4(key);
|
|
6695
|
+
const publicClient = createPublicClient3({ chain: base3, transport: http3() });
|
|
6696
|
+
const evmSigner = toClientEvmSigner2(account, publicClient);
|
|
6697
|
+
const x402 = new x402Client2();
|
|
6698
|
+
registerExactEvmScheme2(x402, { signer: evmSigner });
|
|
6699
|
+
const paymentFetch = wrapFetchWithPayment(fetch, x402);
|
|
6596
6700
|
const response = await paymentFetch(
|
|
6597
6701
|
"https://blockrun.ai/api/v1/chat/completions",
|
|
6598
6702
|
{
|
|
@@ -6627,8 +6731,7 @@ Please analyze and help me fix any issues.`
|
|
|
6627
6731
|
],
|
|
6628
6732
|
max_tokens: 1e3
|
|
6629
6733
|
})
|
|
6630
|
-
}
|
|
6631
|
-
void 0
|
|
6734
|
+
}
|
|
6632
6735
|
);
|
|
6633
6736
|
if (!response.ok) {
|
|
6634
6737
|
const text = await response.text();
|
|
@@ -6864,16 +6967,16 @@ ClawRouter Partner APIs (v${VERSION})
|
|
|
6864
6967
|
console.log(report);
|
|
6865
6968
|
process.exit(0);
|
|
6866
6969
|
}
|
|
6867
|
-
const
|
|
6868
|
-
if (source === "generated") {
|
|
6869
|
-
console.log(`[ClawRouter] Generated new wallet: ${address}`);
|
|
6870
|
-
} else if (source === "saved") {
|
|
6871
|
-
console.log(`[ClawRouter] Using saved wallet: ${address}`);
|
|
6970
|
+
const wallet = await resolveOrGenerateWalletKey();
|
|
6971
|
+
if (wallet.source === "generated") {
|
|
6972
|
+
console.log(`[ClawRouter] Generated new wallet: ${wallet.address}`);
|
|
6973
|
+
} else if (wallet.source === "saved") {
|
|
6974
|
+
console.log(`[ClawRouter] Using saved wallet: ${wallet.address}`);
|
|
6872
6975
|
} else {
|
|
6873
|
-
console.log(`[ClawRouter] Using wallet from BLOCKRUN_WALLET_KEY: ${address}`);
|
|
6976
|
+
console.log(`[ClawRouter] Using wallet from BLOCKRUN_WALLET_KEY: ${wallet.address}`);
|
|
6874
6977
|
}
|
|
6875
6978
|
const proxy = await startProxy({
|
|
6876
|
-
|
|
6979
|
+
wallet,
|
|
6877
6980
|
port: args.port,
|
|
6878
6981
|
onReady: (port) => {
|
|
6879
6982
|
console.log(`[ClawRouter] Proxy listening on http://127.0.0.1:${port}`);
|
|
@@ -6897,19 +7000,19 @@ ClawRouter Partner APIs (v${VERSION})
|
|
|
6897
7000
|
console.error(`[ClawRouter] Need help? Run: npx @blockrun/clawrouter doctor`);
|
|
6898
7001
|
}
|
|
6899
7002
|
});
|
|
6900
|
-
const monitor = new BalanceMonitor(address);
|
|
7003
|
+
const monitor = new BalanceMonitor(wallet.address);
|
|
6901
7004
|
try {
|
|
6902
7005
|
const balance = await monitor.checkBalance();
|
|
6903
7006
|
if (balance.isEmpty) {
|
|
6904
7007
|
console.log(`[ClawRouter] Wallet balance: $0.00 (using FREE model)`);
|
|
6905
|
-
console.log(`[ClawRouter] Fund wallet for premium models: ${address}`);
|
|
7008
|
+
console.log(`[ClawRouter] Fund wallet for premium models: ${wallet.address}`);
|
|
6906
7009
|
} else if (balance.isLow) {
|
|
6907
7010
|
console.log(`[ClawRouter] Wallet balance: ${balance.balanceUSD} (low)`);
|
|
6908
7011
|
} else {
|
|
6909
7012
|
console.log(`[ClawRouter] Wallet balance: ${balance.balanceUSD}`);
|
|
6910
7013
|
}
|
|
6911
7014
|
} catch {
|
|
6912
|
-
console.log(`[ClawRouter] Wallet: ${address} (balance check pending)`);
|
|
7015
|
+
console.log(`[ClawRouter] Wallet: ${wallet.address} (balance check pending)`);
|
|
6913
7016
|
}
|
|
6914
7017
|
console.log(`[ClawRouter] Ready - Ctrl+C to stop`);
|
|
6915
7018
|
const shutdown = async (signal) => {
|