@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/index.js
CHANGED
|
@@ -1,3 +1,168 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/solana-balance.ts
|
|
12
|
+
var solana_balance_exports = {};
|
|
13
|
+
__export(solana_balance_exports, {
|
|
14
|
+
SolanaBalanceMonitor: () => SolanaBalanceMonitor
|
|
15
|
+
});
|
|
16
|
+
import { address as solAddress, createSolanaRpc } from "@solana/kit";
|
|
17
|
+
var SOLANA_USDC_MINT, SOLANA_DEFAULT_RPC, BALANCE_TIMEOUT_MS, CACHE_TTL_MS2, SolanaBalanceMonitor;
|
|
18
|
+
var init_solana_balance = __esm({
|
|
19
|
+
"src/solana-balance.ts"() {
|
|
20
|
+
"use strict";
|
|
21
|
+
SOLANA_USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
|
|
22
|
+
SOLANA_DEFAULT_RPC = "https://api.mainnet-beta.solana.com";
|
|
23
|
+
BALANCE_TIMEOUT_MS = 1e4;
|
|
24
|
+
CACHE_TTL_MS2 = 3e4;
|
|
25
|
+
SolanaBalanceMonitor = class {
|
|
26
|
+
rpc;
|
|
27
|
+
walletAddress;
|
|
28
|
+
cachedBalance = null;
|
|
29
|
+
cachedAt = 0;
|
|
30
|
+
constructor(walletAddress, rpcUrl) {
|
|
31
|
+
this.walletAddress = walletAddress;
|
|
32
|
+
const url = rpcUrl || process["env"].CLAWROUTER_SOLANA_RPC_URL || SOLANA_DEFAULT_RPC;
|
|
33
|
+
this.rpc = createSolanaRpc(url);
|
|
34
|
+
}
|
|
35
|
+
async checkBalance() {
|
|
36
|
+
const now = Date.now();
|
|
37
|
+
if (this.cachedBalance !== null && now - this.cachedAt < CACHE_TTL_MS2) {
|
|
38
|
+
return this.buildInfo(this.cachedBalance);
|
|
39
|
+
}
|
|
40
|
+
const balance = await this.fetchBalance();
|
|
41
|
+
this.cachedBalance = balance;
|
|
42
|
+
this.cachedAt = now;
|
|
43
|
+
return this.buildInfo(balance);
|
|
44
|
+
}
|
|
45
|
+
deductEstimated(amountMicros) {
|
|
46
|
+
if (this.cachedBalance !== null && this.cachedBalance >= amountMicros) {
|
|
47
|
+
this.cachedBalance -= amountMicros;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
invalidate() {
|
|
51
|
+
this.cachedBalance = null;
|
|
52
|
+
this.cachedAt = 0;
|
|
53
|
+
}
|
|
54
|
+
async refresh() {
|
|
55
|
+
this.invalidate();
|
|
56
|
+
return this.checkBalance();
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Check if balance is sufficient for an estimated cost.
|
|
60
|
+
*/
|
|
61
|
+
async checkSufficient(estimatedCostMicros) {
|
|
62
|
+
const info = await this.checkBalance();
|
|
63
|
+
if (info.balance >= estimatedCostMicros) {
|
|
64
|
+
return { sufficient: true, info };
|
|
65
|
+
}
|
|
66
|
+
const shortfall = estimatedCostMicros - info.balance;
|
|
67
|
+
return {
|
|
68
|
+
sufficient: false,
|
|
69
|
+
info,
|
|
70
|
+
shortfall: this.formatUSDC(shortfall)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Format USDC amount (in micros) as "$X.XX".
|
|
75
|
+
*/
|
|
76
|
+
formatUSDC(amountMicros) {
|
|
77
|
+
const dollars = Number(amountMicros) / 1e6;
|
|
78
|
+
return `$${dollars.toFixed(2)}`;
|
|
79
|
+
}
|
|
80
|
+
getWalletAddress() {
|
|
81
|
+
return this.walletAddress;
|
|
82
|
+
}
|
|
83
|
+
async fetchBalance() {
|
|
84
|
+
const owner = solAddress(this.walletAddress);
|
|
85
|
+
const mint = solAddress(SOLANA_USDC_MINT);
|
|
86
|
+
const controller = new AbortController();
|
|
87
|
+
const timer = setTimeout(() => controller.abort(), BALANCE_TIMEOUT_MS);
|
|
88
|
+
try {
|
|
89
|
+
const response = await this.rpc.getTokenAccountsByOwner(owner, { mint }, { encoding: "jsonParsed" }).send({ abortSignal: controller.signal });
|
|
90
|
+
if (response.value.length === 0) return 0n;
|
|
91
|
+
let total = 0n;
|
|
92
|
+
for (const account of response.value) {
|
|
93
|
+
const parsed = account.account.data;
|
|
94
|
+
total += BigInt(parsed.parsed.info.tokenAmount.amount);
|
|
95
|
+
}
|
|
96
|
+
return total;
|
|
97
|
+
} catch (err) {
|
|
98
|
+
throw new Error(`Failed to fetch Solana USDC balance: ${err instanceof Error ? err.message : String(err)}`);
|
|
99
|
+
} finally {
|
|
100
|
+
clearTimeout(timer);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
buildInfo(balance) {
|
|
104
|
+
const dollars = Number(balance) / 1e6;
|
|
105
|
+
return {
|
|
106
|
+
balance,
|
|
107
|
+
balanceUSD: `$${dollars.toFixed(2)}`,
|
|
108
|
+
isLow: balance < 1000000n,
|
|
109
|
+
isEmpty: balance < 100n,
|
|
110
|
+
walletAddress: this.walletAddress
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// src/wallet.ts
|
|
118
|
+
var wallet_exports = {};
|
|
119
|
+
__export(wallet_exports, {
|
|
120
|
+
deriveAllKeys: () => deriveAllKeys,
|
|
121
|
+
deriveEvmKey: () => deriveEvmKey,
|
|
122
|
+
deriveSolanaKeyBytes: () => deriveSolanaKeyBytes,
|
|
123
|
+
generateWalletMnemonic: () => generateWalletMnemonic,
|
|
124
|
+
isValidMnemonic: () => isValidMnemonic
|
|
125
|
+
});
|
|
126
|
+
import { HDKey } from "@scure/bip32";
|
|
127
|
+
import { generateMnemonic, mnemonicToSeedSync, validateMnemonic } from "@scure/bip39";
|
|
128
|
+
import { wordlist as english } from "@scure/bip39/wordlists/english";
|
|
129
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
130
|
+
function generateWalletMnemonic() {
|
|
131
|
+
return generateMnemonic(english, 256);
|
|
132
|
+
}
|
|
133
|
+
function isValidMnemonic(mnemonic) {
|
|
134
|
+
return validateMnemonic(mnemonic, english);
|
|
135
|
+
}
|
|
136
|
+
function deriveEvmKey(mnemonic) {
|
|
137
|
+
const seed = mnemonicToSeedSync(mnemonic);
|
|
138
|
+
const hdKey = HDKey.fromMasterSeed(seed);
|
|
139
|
+
const derived = hdKey.derive(ETH_DERIVATION_PATH);
|
|
140
|
+
if (!derived.privateKey) throw new Error("Failed to derive EVM private key");
|
|
141
|
+
const hex = `0x${Buffer.from(derived.privateKey).toString("hex")}`;
|
|
142
|
+
const account = privateKeyToAccount(hex);
|
|
143
|
+
return { privateKey: hex, address: account.address };
|
|
144
|
+
}
|
|
145
|
+
function deriveSolanaKeyBytes(mnemonic) {
|
|
146
|
+
const seed = mnemonicToSeedSync(mnemonic);
|
|
147
|
+
const hdKey = HDKey.fromMasterSeed(seed);
|
|
148
|
+
const derived = hdKey.derive(SOLANA_DERIVATION_PATH);
|
|
149
|
+
if (!derived.privateKey) throw new Error("Failed to derive Solana private key");
|
|
150
|
+
return new Uint8Array(derived.privateKey);
|
|
151
|
+
}
|
|
152
|
+
function deriveAllKeys(mnemonic) {
|
|
153
|
+
const { privateKey: evmPrivateKey, address: evmAddress } = deriveEvmKey(mnemonic);
|
|
154
|
+
const solanaPrivateKeyBytes = deriveSolanaKeyBytes(mnemonic);
|
|
155
|
+
return { mnemonic, evmPrivateKey, evmAddress, solanaPrivateKeyBytes };
|
|
156
|
+
}
|
|
157
|
+
var ETH_DERIVATION_PATH, SOLANA_DERIVATION_PATH;
|
|
158
|
+
var init_wallet = __esm({
|
|
159
|
+
"src/wallet.ts"() {
|
|
160
|
+
"use strict";
|
|
161
|
+
ETH_DERIVATION_PATH = "m/44'/60'/0'/0/0";
|
|
162
|
+
SOLANA_DERIVATION_PATH = "m/44'/501'/0'/0'";
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
1
166
|
// src/models.ts
|
|
2
167
|
var MODEL_ALIASES = {
|
|
3
168
|
// Claude - use newest versions (4.6)
|
|
@@ -644,271 +809,72 @@ var blockrunProvider = {
|
|
|
644
809
|
// src/proxy.ts
|
|
645
810
|
import { createServer } from "http";
|
|
646
811
|
import { finished } from "stream";
|
|
647
|
-
import {
|
|
812
|
+
import { createPublicClient as createPublicClient2, http as http2 } from "viem";
|
|
813
|
+
import { base as base2 } from "viem/chains";
|
|
814
|
+
import { privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
|
|
815
|
+
import { x402Client } from "@x402/fetch";
|
|
648
816
|
|
|
649
|
-
// src/
|
|
650
|
-
import {
|
|
651
|
-
|
|
652
|
-
// src/payment-cache.ts
|
|
817
|
+
// src/payment-preauth.ts
|
|
818
|
+
import { x402HTTPClient } from "@x402/fetch";
|
|
653
819
|
var DEFAULT_TTL_MS = 36e5;
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
return entry;
|
|
669
|
-
}
|
|
670
|
-
/** Cache payment params from a 402 response. */
|
|
671
|
-
set(endpointPath, params) {
|
|
672
|
-
this.cache.set(endpointPath, { ...params, cachedAt: Date.now() });
|
|
673
|
-
}
|
|
674
|
-
/** Invalidate cache for an endpoint (e.g., if payTo changed). */
|
|
675
|
-
invalidate(endpointPath) {
|
|
676
|
-
this.cache.delete(endpointPath);
|
|
677
|
-
}
|
|
678
|
-
};
|
|
679
|
-
|
|
680
|
-
// src/x402.ts
|
|
681
|
-
var BASE_CHAIN_ID = 8453;
|
|
682
|
-
var BASE_SEPOLIA_CHAIN_ID = 84532;
|
|
683
|
-
var DEFAULT_TOKEN_NAME = "USD Coin";
|
|
684
|
-
var DEFAULT_TOKEN_VERSION = "2";
|
|
685
|
-
var DEFAULT_NETWORK = "eip155:8453";
|
|
686
|
-
var DEFAULT_MAX_TIMEOUT_SECONDS = 300;
|
|
687
|
-
var TRANSFER_TYPES = {
|
|
688
|
-
TransferWithAuthorization: [
|
|
689
|
-
{ name: "from", type: "address" },
|
|
690
|
-
{ name: "to", type: "address" },
|
|
691
|
-
{ name: "value", type: "uint256" },
|
|
692
|
-
{ name: "validAfter", type: "uint256" },
|
|
693
|
-
{ name: "validBefore", type: "uint256" },
|
|
694
|
-
{ name: "nonce", type: "bytes32" }
|
|
695
|
-
]
|
|
696
|
-
};
|
|
697
|
-
function createNonce() {
|
|
698
|
-
const bytes = new Uint8Array(32);
|
|
699
|
-
crypto.getRandomValues(bytes);
|
|
700
|
-
return `0x${Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
701
|
-
}
|
|
702
|
-
function decodeBase64Json(value) {
|
|
703
|
-
const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
704
|
-
const padding = (4 - normalized.length % 4) % 4;
|
|
705
|
-
const padded = normalized + "=".repeat(padding);
|
|
706
|
-
const decoded = Buffer.from(padded, "base64").toString("utf8");
|
|
707
|
-
return JSON.parse(decoded);
|
|
708
|
-
}
|
|
709
|
-
function encodeBase64Json(value) {
|
|
710
|
-
return Buffer.from(JSON.stringify(value), "utf8").toString("base64");
|
|
711
|
-
}
|
|
712
|
-
function parsePaymentRequired(headerValue) {
|
|
713
|
-
return decodeBase64Json(headerValue);
|
|
714
|
-
}
|
|
715
|
-
function normalizeNetwork(network) {
|
|
716
|
-
if (!network || network.trim().length === 0) {
|
|
717
|
-
return DEFAULT_NETWORK;
|
|
718
|
-
}
|
|
719
|
-
return network.trim().toLowerCase();
|
|
720
|
-
}
|
|
721
|
-
function resolveChainId(network) {
|
|
722
|
-
const eip155Match = network.match(/^eip155:(\d+)$/i);
|
|
723
|
-
if (eip155Match) {
|
|
724
|
-
const parsed = Number.parseInt(eip155Match[1], 10);
|
|
725
|
-
if (Number.isFinite(parsed) && parsed > 0) {
|
|
726
|
-
return parsed;
|
|
727
|
-
}
|
|
728
|
-
}
|
|
729
|
-
if (network === "base") return BASE_CHAIN_ID;
|
|
730
|
-
if (network === "base-sepolia") return BASE_SEPOLIA_CHAIN_ID;
|
|
731
|
-
return BASE_CHAIN_ID;
|
|
732
|
-
}
|
|
733
|
-
function parseHexAddress(value) {
|
|
734
|
-
if (!value) return void 0;
|
|
735
|
-
const direct = value.match(/^0x[a-fA-F0-9]{40}$/);
|
|
736
|
-
if (direct) {
|
|
737
|
-
return direct[0];
|
|
738
|
-
}
|
|
739
|
-
const caipSuffix = value.match(/0x[a-fA-F0-9]{40}$/);
|
|
740
|
-
if (caipSuffix) {
|
|
741
|
-
return caipSuffix[0];
|
|
742
|
-
}
|
|
743
|
-
return void 0;
|
|
744
|
-
}
|
|
745
|
-
function requireHexAddress(value, field) {
|
|
746
|
-
const parsed = parseHexAddress(value);
|
|
747
|
-
if (!parsed) {
|
|
748
|
-
throw new Error(`Invalid ${field} in payment requirements: ${String(value)}`);
|
|
749
|
-
}
|
|
750
|
-
return parsed;
|
|
751
|
-
}
|
|
752
|
-
function setPaymentHeaders(headers, payload) {
|
|
753
|
-
headers.set("payment-signature", payload);
|
|
754
|
-
headers.set("x-payment", payload);
|
|
755
|
-
}
|
|
756
|
-
async function createPaymentPayload(privateKey, fromAddress, option, amount, requestUrl, resource) {
|
|
757
|
-
const network = normalizeNetwork(option.network);
|
|
758
|
-
const chainId = resolveChainId(network);
|
|
759
|
-
const recipient = requireHexAddress(option.payTo, "payTo");
|
|
760
|
-
const verifyingContract = requireHexAddress(option.asset, "asset");
|
|
761
|
-
const maxTimeoutSeconds = typeof option.maxTimeoutSeconds === "number" && option.maxTimeoutSeconds > 0 ? Math.floor(option.maxTimeoutSeconds) : DEFAULT_MAX_TIMEOUT_SECONDS;
|
|
762
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
763
|
-
const validAfter = now - 600;
|
|
764
|
-
const validBefore = now + maxTimeoutSeconds;
|
|
765
|
-
const nonce = createNonce();
|
|
766
|
-
const signature = await signTypedData({
|
|
767
|
-
privateKey,
|
|
768
|
-
domain: {
|
|
769
|
-
name: option.extra?.name || DEFAULT_TOKEN_NAME,
|
|
770
|
-
version: option.extra?.version || DEFAULT_TOKEN_VERSION,
|
|
771
|
-
chainId,
|
|
772
|
-
verifyingContract
|
|
773
|
-
},
|
|
774
|
-
types: TRANSFER_TYPES,
|
|
775
|
-
primaryType: "TransferWithAuthorization",
|
|
776
|
-
message: {
|
|
777
|
-
from: fromAddress,
|
|
778
|
-
to: recipient,
|
|
779
|
-
value: BigInt(amount),
|
|
780
|
-
validAfter: BigInt(validAfter),
|
|
781
|
-
validBefore: BigInt(validBefore),
|
|
782
|
-
nonce
|
|
783
|
-
}
|
|
784
|
-
});
|
|
785
|
-
const paymentData = {
|
|
786
|
-
x402Version: 2,
|
|
787
|
-
resource: {
|
|
788
|
-
url: resource?.url || requestUrl,
|
|
789
|
-
description: resource?.description || "BlockRun AI API call",
|
|
790
|
-
mimeType: "application/json"
|
|
791
|
-
},
|
|
792
|
-
accepted: {
|
|
793
|
-
scheme: option.scheme,
|
|
794
|
-
network,
|
|
795
|
-
amount,
|
|
796
|
-
asset: option.asset,
|
|
797
|
-
payTo: option.payTo,
|
|
798
|
-
maxTimeoutSeconds: option.maxTimeoutSeconds,
|
|
799
|
-
extra: option.extra
|
|
800
|
-
},
|
|
801
|
-
payload: {
|
|
802
|
-
signature,
|
|
803
|
-
authorization: {
|
|
804
|
-
from: fromAddress,
|
|
805
|
-
to: recipient,
|
|
806
|
-
value: amount,
|
|
807
|
-
validAfter: validAfter.toString(),
|
|
808
|
-
validBefore: validBefore.toString(),
|
|
809
|
-
nonce
|
|
810
|
-
}
|
|
811
|
-
},
|
|
812
|
-
extensions: {}
|
|
813
|
-
};
|
|
814
|
-
return encodeBase64Json(paymentData);
|
|
815
|
-
}
|
|
816
|
-
function createPaymentFetch(privateKey) {
|
|
817
|
-
const account = privateKeyToAccount(privateKey);
|
|
818
|
-
const walletAddress = account.address;
|
|
819
|
-
const paymentCache = new PaymentCache();
|
|
820
|
-
const payFetch = async (input, init, preAuth) => {
|
|
821
|
-
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
|
|
822
|
-
const endpointPath = new URL(url).pathname;
|
|
823
|
-
const cached = paymentCache.get(endpointPath);
|
|
824
|
-
if (cached && preAuth?.estimatedAmount) {
|
|
825
|
-
const paymentPayload = await createPaymentPayload(
|
|
826
|
-
privateKey,
|
|
827
|
-
walletAddress,
|
|
828
|
-
{
|
|
829
|
-
scheme: cached.scheme,
|
|
830
|
-
network: cached.network,
|
|
831
|
-
asset: cached.asset,
|
|
832
|
-
payTo: cached.payTo,
|
|
833
|
-
maxTimeoutSeconds: cached.maxTimeoutSeconds,
|
|
834
|
-
extra: cached.extra
|
|
835
|
-
},
|
|
836
|
-
preAuth.estimatedAmount,
|
|
837
|
-
url,
|
|
838
|
-
{
|
|
839
|
-
url: cached.resourceUrl,
|
|
840
|
-
description: cached.resourceDescription
|
|
820
|
+
function createPayFetchWithPreAuth(baseFetch, client, ttlMs = DEFAULT_TTL_MS, options) {
|
|
821
|
+
const httpClient = new x402HTTPClient(client);
|
|
822
|
+
const cache = /* @__PURE__ */ new Map();
|
|
823
|
+
return async (input, init) => {
|
|
824
|
+
const request = new Request(input, init);
|
|
825
|
+
const urlPath = new URL(request.url).pathname;
|
|
826
|
+
const cached = !options?.skipPreAuth ? cache.get(urlPath) : void 0;
|
|
827
|
+
if (cached && Date.now() - cached.cachedAt < ttlMs) {
|
|
828
|
+
try {
|
|
829
|
+
const payload2 = await client.createPaymentPayload(cached.paymentRequired);
|
|
830
|
+
const headers = httpClient.encodePaymentSignatureHeader(payload2);
|
|
831
|
+
const preAuthRequest = request.clone();
|
|
832
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
833
|
+
preAuthRequest.headers.set(key, value);
|
|
841
834
|
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
const paymentHeader2 = response2.headers.get("x-payment-required");
|
|
850
|
-
if (paymentHeader2) {
|
|
851
|
-
return handle402(input, init, url, endpointPath, paymentHeader2);
|
|
852
|
-
}
|
|
853
|
-
paymentCache.invalidate(endpointPath);
|
|
854
|
-
const cleanResponse = await fetch(input, init);
|
|
855
|
-
if (cleanResponse.status !== 402) {
|
|
856
|
-
return cleanResponse;
|
|
857
|
-
}
|
|
858
|
-
const cleanHeader = cleanResponse.headers.get("x-payment-required");
|
|
859
|
-
if (!cleanHeader) {
|
|
860
|
-
throw new Error("402 response missing x-payment-required header");
|
|
835
|
+
const response2 = await baseFetch(preAuthRequest);
|
|
836
|
+
if (response2.status !== 402) {
|
|
837
|
+
return response2;
|
|
838
|
+
}
|
|
839
|
+
cache.delete(urlPath);
|
|
840
|
+
} catch {
|
|
841
|
+
cache.delete(urlPath);
|
|
861
842
|
}
|
|
862
|
-
return handle402(input, init, url, endpointPath, cleanHeader);
|
|
863
843
|
}
|
|
864
|
-
const
|
|
844
|
+
const clonedRequest = request.clone();
|
|
845
|
+
const response = await baseFetch(request);
|
|
865
846
|
if (response.status !== 402) {
|
|
866
847
|
return response;
|
|
867
848
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
849
|
+
let paymentRequired;
|
|
850
|
+
try {
|
|
851
|
+
const getHeader = (name) => response.headers.get(name);
|
|
852
|
+
let body;
|
|
853
|
+
try {
|
|
854
|
+
const responseText = await response.text();
|
|
855
|
+
if (responseText) body = JSON.parse(responseText);
|
|
856
|
+
} catch {
|
|
857
|
+
}
|
|
858
|
+
paymentRequired = httpClient.getPaymentRequiredResponse(getHeader, body);
|
|
859
|
+
cache.set(urlPath, { paymentRequired, cachedAt: Date.now() });
|
|
860
|
+
} catch (error) {
|
|
861
|
+
throw new Error(
|
|
862
|
+
`Failed to parse payment requirements: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
863
|
+
);
|
|
864
|
+
}
|
|
865
|
+
const payload = await client.createPaymentPayload(paymentRequired);
|
|
866
|
+
const paymentHeaders = httpClient.encodePaymentSignatureHeader(payload);
|
|
867
|
+
for (const [key, value] of Object.entries(paymentHeaders)) {
|
|
868
|
+
clonedRequest.headers.set(key, value);
|
|
871
869
|
}
|
|
872
|
-
return
|
|
870
|
+
return baseFetch(clonedRequest);
|
|
873
871
|
};
|
|
874
|
-
async function handle402(input, init, url, endpointPath, paymentHeader) {
|
|
875
|
-
const paymentRequired = parsePaymentRequired(paymentHeader);
|
|
876
|
-
const option = paymentRequired.accepts?.[0];
|
|
877
|
-
if (!option) {
|
|
878
|
-
throw new Error("No payment options in 402 response");
|
|
879
|
-
}
|
|
880
|
-
const amount = option.amount || option.maxAmountRequired;
|
|
881
|
-
if (!amount) {
|
|
882
|
-
throw new Error("No amount in payment requirements");
|
|
883
|
-
}
|
|
884
|
-
paymentCache.set(endpointPath, {
|
|
885
|
-
payTo: option.payTo,
|
|
886
|
-
asset: option.asset,
|
|
887
|
-
scheme: option.scheme,
|
|
888
|
-
network: option.network,
|
|
889
|
-
extra: option.extra,
|
|
890
|
-
maxTimeoutSeconds: option.maxTimeoutSeconds,
|
|
891
|
-
resourceUrl: paymentRequired.resource?.url,
|
|
892
|
-
resourceDescription: paymentRequired.resource?.description
|
|
893
|
-
});
|
|
894
|
-
const paymentPayload = await createPaymentPayload(
|
|
895
|
-
privateKey,
|
|
896
|
-
walletAddress,
|
|
897
|
-
option,
|
|
898
|
-
amount,
|
|
899
|
-
url,
|
|
900
|
-
paymentRequired.resource
|
|
901
|
-
);
|
|
902
|
-
const retryHeaders = new Headers(init?.headers);
|
|
903
|
-
setPaymentHeaders(retryHeaders, paymentPayload);
|
|
904
|
-
return fetch(input, {
|
|
905
|
-
...init,
|
|
906
|
-
headers: retryHeaders
|
|
907
|
-
});
|
|
908
|
-
}
|
|
909
|
-
return { fetch: payFetch, cache: paymentCache };
|
|
910
872
|
}
|
|
911
873
|
|
|
874
|
+
// src/proxy.ts
|
|
875
|
+
import { registerExactEvmScheme } from "@x402/evm/exact/client";
|
|
876
|
+
import { toClientEvmSigner } from "@x402/evm";
|
|
877
|
+
|
|
912
878
|
// src/router/rules.ts
|
|
913
879
|
function scoreTokenCount(estimatedTokens, thresholds) {
|
|
914
880
|
if (estimatedTokens < thresholds.simple) {
|
|
@@ -2435,7 +2401,7 @@ function route(prompt, systemPrompt, maxOutputTokens, options) {
|
|
|
2435
2401
|
const ruleResult = classifyByRules(prompt, systemPrompt, estimatedTokens, config.scoring);
|
|
2436
2402
|
const { routingProfile } = options;
|
|
2437
2403
|
let tierConfigs;
|
|
2438
|
-
let profileSuffix
|
|
2404
|
+
let profileSuffix;
|
|
2439
2405
|
if (routingProfile === "eco" && config.ecoTiers) {
|
|
2440
2406
|
tierConfigs = config.ecoTiers;
|
|
2441
2407
|
profileSuffix = " | eco";
|
|
@@ -3252,6 +3218,199 @@ var BalanceMonitor = class {
|
|
|
3252
3218
|
}
|
|
3253
3219
|
};
|
|
3254
3220
|
|
|
3221
|
+
// src/proxy.ts
|
|
3222
|
+
init_solana_balance();
|
|
3223
|
+
|
|
3224
|
+
// src/auth.ts
|
|
3225
|
+
import { writeFile, mkdir as mkdir2 } from "fs/promises";
|
|
3226
|
+
init_wallet();
|
|
3227
|
+
import { join as join4 } from "path";
|
|
3228
|
+
import { homedir as homedir3 } from "os";
|
|
3229
|
+
import { privateKeyToAccount as privateKeyToAccount2 } from "viem/accounts";
|
|
3230
|
+
var WALLET_DIR = join4(homedir3(), ".openclaw", "blockrun");
|
|
3231
|
+
var WALLET_FILE = join4(WALLET_DIR, "wallet.key");
|
|
3232
|
+
var MNEMONIC_FILE = join4(WALLET_DIR, "mnemonic");
|
|
3233
|
+
var CHAIN_FILE = join4(WALLET_DIR, "payment-chain");
|
|
3234
|
+
async function loadSavedWallet() {
|
|
3235
|
+
try {
|
|
3236
|
+
const key = (await readTextFile(WALLET_FILE)).trim();
|
|
3237
|
+
if (key.startsWith("0x") && key.length === 66) {
|
|
3238
|
+
console.log(`[ClawRouter] \u2713 Loaded existing wallet from ${WALLET_FILE}`);
|
|
3239
|
+
return key;
|
|
3240
|
+
}
|
|
3241
|
+
console.error(`[ClawRouter] \u2717 CRITICAL: Wallet file exists but has invalid format!`);
|
|
3242
|
+
console.error(`[ClawRouter] File: ${WALLET_FILE}`);
|
|
3243
|
+
console.error(`[ClawRouter] Expected: 0x followed by 64 hex characters (66 chars total)`);
|
|
3244
|
+
console.error(
|
|
3245
|
+
`[ClawRouter] To fix: restore your backup key or set BLOCKRUN_WALLET_KEY env var`
|
|
3246
|
+
);
|
|
3247
|
+
throw new Error(
|
|
3248
|
+
`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.`
|
|
3249
|
+
);
|
|
3250
|
+
} catch (err) {
|
|
3251
|
+
if (err.code !== "ENOENT") {
|
|
3252
|
+
if (err instanceof Error && err.message.includes("Refusing to auto-generate")) {
|
|
3253
|
+
throw err;
|
|
3254
|
+
}
|
|
3255
|
+
console.error(
|
|
3256
|
+
`[ClawRouter] \u2717 Failed to read wallet file: ${err instanceof Error ? err.message : String(err)}`
|
|
3257
|
+
);
|
|
3258
|
+
throw new Error(
|
|
3259
|
+
`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.`,
|
|
3260
|
+
{ cause: err }
|
|
3261
|
+
);
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
return void 0;
|
|
3265
|
+
}
|
|
3266
|
+
async function loadMnemonic() {
|
|
3267
|
+
try {
|
|
3268
|
+
const mnemonic = (await readTextFile(MNEMONIC_FILE)).trim();
|
|
3269
|
+
if (mnemonic && isValidMnemonic(mnemonic)) {
|
|
3270
|
+
return mnemonic;
|
|
3271
|
+
}
|
|
3272
|
+
console.warn(`[ClawRouter] \u26A0 Mnemonic file exists but has invalid format \u2014 ignoring`);
|
|
3273
|
+
return void 0;
|
|
3274
|
+
} catch (err) {
|
|
3275
|
+
if (err.code !== "ENOENT") {
|
|
3276
|
+
console.warn(`[ClawRouter] \u26A0 Cannot read mnemonic file \u2014 ignoring`);
|
|
3277
|
+
}
|
|
3278
|
+
}
|
|
3279
|
+
return void 0;
|
|
3280
|
+
}
|
|
3281
|
+
async function saveMnemonic(mnemonic) {
|
|
3282
|
+
await mkdir2(WALLET_DIR, { recursive: true });
|
|
3283
|
+
await writeFile(MNEMONIC_FILE, mnemonic + "\n", { mode: 384 });
|
|
3284
|
+
}
|
|
3285
|
+
async function generateAndSaveWallet() {
|
|
3286
|
+
const existingMnemonic = await loadMnemonic();
|
|
3287
|
+
if (existingMnemonic) {
|
|
3288
|
+
throw new Error(
|
|
3289
|
+
`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>`
|
|
3290
|
+
);
|
|
3291
|
+
}
|
|
3292
|
+
const mnemonic = generateWalletMnemonic();
|
|
3293
|
+
const derived = deriveAllKeys(mnemonic);
|
|
3294
|
+
await mkdir2(WALLET_DIR, { recursive: true });
|
|
3295
|
+
await writeFile(WALLET_FILE, derived.evmPrivateKey + "\n", { mode: 384 });
|
|
3296
|
+
await writeFile(MNEMONIC_FILE, mnemonic + "\n", { mode: 384 });
|
|
3297
|
+
try {
|
|
3298
|
+
const verification = (await readTextFile(WALLET_FILE)).trim();
|
|
3299
|
+
if (verification !== derived.evmPrivateKey) {
|
|
3300
|
+
throw new Error("Wallet file verification failed - content mismatch");
|
|
3301
|
+
}
|
|
3302
|
+
console.log(`[ClawRouter] Wallet saved and verified at ${WALLET_FILE}`);
|
|
3303
|
+
} catch (err) {
|
|
3304
|
+
throw new Error(
|
|
3305
|
+
`Failed to verify wallet file after creation: ${err instanceof Error ? err.message : String(err)}`,
|
|
3306
|
+
{ cause: err }
|
|
3307
|
+
);
|
|
3308
|
+
}
|
|
3309
|
+
console.log(`[ClawRouter]`);
|
|
3310
|
+
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`);
|
|
3311
|
+
console.log(`[ClawRouter] NEW WALLET GENERATED \u2014 BACK UP YOUR KEY NOW`);
|
|
3312
|
+
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`);
|
|
3313
|
+
console.log(`[ClawRouter] EVM Address : ${derived.evmAddress}`);
|
|
3314
|
+
console.log(`[ClawRouter] Key file : ${WALLET_FILE}`);
|
|
3315
|
+
console.log(`[ClawRouter] Mnemonic : ${MNEMONIC_FILE}`);
|
|
3316
|
+
console.log(`[ClawRouter]`);
|
|
3317
|
+
console.log(`[ClawRouter] Both EVM (Base) and Solana wallets are ready.`);
|
|
3318
|
+
console.log(`[ClawRouter] To back up, run in OpenClaw:`);
|
|
3319
|
+
console.log(`[ClawRouter] /wallet export`);
|
|
3320
|
+
console.log(`[ClawRouter]`);
|
|
3321
|
+
console.log(`[ClawRouter] To restore on another machine:`);
|
|
3322
|
+
console.log(`[ClawRouter] export BLOCKRUN_WALLET_KEY=<your_key>`);
|
|
3323
|
+
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`);
|
|
3324
|
+
console.log(`[ClawRouter]`);
|
|
3325
|
+
return {
|
|
3326
|
+
key: derived.evmPrivateKey,
|
|
3327
|
+
address: derived.evmAddress,
|
|
3328
|
+
mnemonic,
|
|
3329
|
+
solanaPrivateKeyBytes: derived.solanaPrivateKeyBytes
|
|
3330
|
+
};
|
|
3331
|
+
}
|
|
3332
|
+
async function resolveOrGenerateWalletKey() {
|
|
3333
|
+
const saved = await loadSavedWallet();
|
|
3334
|
+
if (saved) {
|
|
3335
|
+
const account = privateKeyToAccount2(saved);
|
|
3336
|
+
const mnemonic = await loadMnemonic();
|
|
3337
|
+
if (mnemonic) {
|
|
3338
|
+
const solanaKeyBytes = deriveSolanaKeyBytes(mnemonic);
|
|
3339
|
+
return {
|
|
3340
|
+
key: saved,
|
|
3341
|
+
address: account.address,
|
|
3342
|
+
source: "saved",
|
|
3343
|
+
mnemonic,
|
|
3344
|
+
solanaPrivateKeyBytes: solanaKeyBytes
|
|
3345
|
+
};
|
|
3346
|
+
}
|
|
3347
|
+
return { key: saved, address: account.address, source: "saved" };
|
|
3348
|
+
}
|
|
3349
|
+
const envKey = process["env"].BLOCKRUN_WALLET_KEY;
|
|
3350
|
+
if (typeof envKey === "string" && envKey.startsWith("0x") && envKey.length === 66) {
|
|
3351
|
+
const account = privateKeyToAccount2(envKey);
|
|
3352
|
+
const mnemonic = await loadMnemonic();
|
|
3353
|
+
if (mnemonic) {
|
|
3354
|
+
const solanaKeyBytes = deriveSolanaKeyBytes(mnemonic);
|
|
3355
|
+
return {
|
|
3356
|
+
key: envKey,
|
|
3357
|
+
address: account.address,
|
|
3358
|
+
source: "env",
|
|
3359
|
+
mnemonic,
|
|
3360
|
+
solanaPrivateKeyBytes: solanaKeyBytes
|
|
3361
|
+
};
|
|
3362
|
+
}
|
|
3363
|
+
return { key: envKey, address: account.address, source: "env" };
|
|
3364
|
+
}
|
|
3365
|
+
const result = await generateAndSaveWallet();
|
|
3366
|
+
return {
|
|
3367
|
+
key: result.key,
|
|
3368
|
+
address: result.address,
|
|
3369
|
+
source: "generated",
|
|
3370
|
+
mnemonic: result.mnemonic,
|
|
3371
|
+
solanaPrivateKeyBytes: result.solanaPrivateKeyBytes
|
|
3372
|
+
};
|
|
3373
|
+
}
|
|
3374
|
+
async function setupSolana() {
|
|
3375
|
+
const existing = await loadMnemonic();
|
|
3376
|
+
if (existing) {
|
|
3377
|
+
throw new Error(
|
|
3378
|
+
"Solana wallet already set up. Mnemonic file exists at " + MNEMONIC_FILE
|
|
3379
|
+
);
|
|
3380
|
+
}
|
|
3381
|
+
const savedKey = await loadSavedWallet();
|
|
3382
|
+
if (!savedKey) {
|
|
3383
|
+
throw new Error(
|
|
3384
|
+
"No EVM wallet found. Run ClawRouter first to generate a wallet before setting up Solana."
|
|
3385
|
+
);
|
|
3386
|
+
}
|
|
3387
|
+
const mnemonic = generateWalletMnemonic();
|
|
3388
|
+
const solanaKeyBytes = deriveSolanaKeyBytes(mnemonic);
|
|
3389
|
+
await saveMnemonic(mnemonic);
|
|
3390
|
+
console.log(`[ClawRouter] Solana wallet set up successfully.`);
|
|
3391
|
+
console.log(`[ClawRouter] Mnemonic saved to ${MNEMONIC_FILE}`);
|
|
3392
|
+
console.log(`[ClawRouter] Existing EVM wallet unchanged.`);
|
|
3393
|
+
return { mnemonic, solanaPrivateKeyBytes: solanaKeyBytes };
|
|
3394
|
+
}
|
|
3395
|
+
async function savePaymentChain(chain) {
|
|
3396
|
+
await mkdir2(WALLET_DIR, { recursive: true });
|
|
3397
|
+
await writeFile(CHAIN_FILE, chain + "\n", { mode: 384 });
|
|
3398
|
+
}
|
|
3399
|
+
async function loadPaymentChain() {
|
|
3400
|
+
try {
|
|
3401
|
+
const content = (await readTextFile(CHAIN_FILE)).trim();
|
|
3402
|
+
if (content === "solana") return "solana";
|
|
3403
|
+
return "base";
|
|
3404
|
+
} catch {
|
|
3405
|
+
return "base";
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
async function resolvePaymentChain() {
|
|
3409
|
+
if (process["env"].CLAWROUTER_PAYMENT_CHAIN === "solana") return "solana";
|
|
3410
|
+
if (process["env"].CLAWROUTER_PAYMENT_CHAIN === "base") return "base";
|
|
3411
|
+
return loadPaymentChain();
|
|
3412
|
+
}
|
|
3413
|
+
|
|
3255
3414
|
// src/compression/types.ts
|
|
3256
3415
|
var DEFAULT_COMPRESSION_CONFIG = {
|
|
3257
3416
|
enabled: true,
|
|
@@ -3281,7 +3440,7 @@ var DEFAULT_COMPRESSION_CONFIG = {
|
|
|
3281
3440
|
};
|
|
3282
3441
|
|
|
3283
3442
|
// src/compression/layers/deduplication.ts
|
|
3284
|
-
import
|
|
3443
|
+
import crypto from "crypto";
|
|
3285
3444
|
function hashMessage(message) {
|
|
3286
3445
|
let contentStr = "";
|
|
3287
3446
|
if (typeof message.content === "string") {
|
|
@@ -3301,7 +3460,7 @@ function hashMessage(message) {
|
|
|
3301
3460
|
);
|
|
3302
3461
|
}
|
|
3303
3462
|
const content = parts.join("|");
|
|
3304
|
-
return
|
|
3463
|
+
return crypto.createHash("md5").update(content).digest("hex");
|
|
3305
3464
|
}
|
|
3306
3465
|
function deduplicateMessages(messages) {
|
|
3307
3466
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -3454,7 +3613,7 @@ function generateCodebookHeader(usedCodes, pathMap = {}) {
|
|
|
3454
3613
|
parts.push(`[Dict: ${codeEntries}]`);
|
|
3455
3614
|
}
|
|
3456
3615
|
if (Object.keys(pathMap).length > 0) {
|
|
3457
|
-
const pathEntries = Object.entries(pathMap).map(([code,
|
|
3616
|
+
const pathEntries = Object.entries(pathMap).map(([code, path2]) => `${code}=${path2}`).join(", ");
|
|
3458
3617
|
parts.push(`[Paths: ${pathEntries}]`);
|
|
3459
3618
|
}
|
|
3460
3619
|
return parts.join("\n");
|
|
@@ -3528,8 +3687,8 @@ function extractPaths(messages) {
|
|
|
3528
3687
|
}
|
|
3529
3688
|
function findFrequentPrefixes(paths) {
|
|
3530
3689
|
const prefixCounts = /* @__PURE__ */ new Map();
|
|
3531
|
-
for (const
|
|
3532
|
-
const parts =
|
|
3690
|
+
for (const path2 of paths) {
|
|
3691
|
+
const parts = path2.split("/").filter(Boolean);
|
|
3533
3692
|
for (let i = 2; i < parts.length; i++) {
|
|
3534
3693
|
const prefix = "/" + parts.slice(0, i).join("/") + "/";
|
|
3535
3694
|
prefixCounts.set(prefix, (prefixCounts.get(prefix) || 0) + 1);
|
|
@@ -4398,6 +4557,7 @@ ${lines.join("\n")}`;
|
|
|
4398
4557
|
|
|
4399
4558
|
// src/proxy.ts
|
|
4400
4559
|
var BLOCKRUN_API = "https://blockrun.ai/api";
|
|
4560
|
+
var BLOCKRUN_SOLANA_API = "https://sol.blockrun.ai/api";
|
|
4401
4561
|
var AUTO_MODEL = "blockrun/auto";
|
|
4402
4562
|
var ROUTING_PROFILES = /* @__PURE__ */ new Set([
|
|
4403
4563
|
"blockrun/free",
|
|
@@ -4526,7 +4686,7 @@ async function checkExistingProxy(port) {
|
|
|
4526
4686
|
if (response.ok) {
|
|
4527
4687
|
const data = await response.json();
|
|
4528
4688
|
if (data.status === "ok" && data.wallet) {
|
|
4529
|
-
return data.wallet;
|
|
4689
|
+
return { wallet: data.wallet, paymentChain: data.paymentChain };
|
|
4530
4690
|
}
|
|
4531
4691
|
}
|
|
4532
4692
|
return void 0;
|
|
@@ -4956,31 +5116,78 @@ async function uploadDataUriToHost(dataUri) {
|
|
|
4956
5116
|
throw new Error(`catbox.moe upload failed: ${result}`);
|
|
4957
5117
|
}
|
|
4958
5118
|
async function startProxy(options) {
|
|
4959
|
-
const
|
|
5119
|
+
const walletKey = typeof options.wallet === "string" ? options.wallet : options.wallet.key;
|
|
5120
|
+
const solanaPrivateKeyBytes = typeof options.wallet === "string" ? void 0 : options.wallet.solanaPrivateKeyBytes;
|
|
5121
|
+
const paymentChain = options.paymentChain ?? await resolvePaymentChain();
|
|
5122
|
+
const apiBase = options.apiBase ?? (paymentChain === "solana" && solanaPrivateKeyBytes ? BLOCKRUN_SOLANA_API : BLOCKRUN_API);
|
|
5123
|
+
if (paymentChain === "solana" && !solanaPrivateKeyBytes) {
|
|
5124
|
+
console.warn(`[ClawRouter] Payment chain is Solana but no Solana keys provided. Using Base (EVM).`);
|
|
5125
|
+
} else if (paymentChain === "solana") {
|
|
5126
|
+
console.log(`[ClawRouter] Payment chain: Solana (${BLOCKRUN_SOLANA_API})`);
|
|
5127
|
+
}
|
|
4960
5128
|
const listenPort = options.port ?? getProxyPort();
|
|
4961
|
-
const
|
|
4962
|
-
if (
|
|
4963
|
-
const account2 =
|
|
4964
|
-
const balanceMonitor2 = new BalanceMonitor(account2.address);
|
|
5129
|
+
const existingProxy = await checkExistingProxy(listenPort);
|
|
5130
|
+
if (existingProxy) {
|
|
5131
|
+
const account2 = privateKeyToAccount3(walletKey);
|
|
4965
5132
|
const baseUrl2 = `http://127.0.0.1:${listenPort}`;
|
|
4966
|
-
if (
|
|
5133
|
+
if (existingProxy.wallet !== account2.address) {
|
|
4967
5134
|
console.warn(
|
|
4968
|
-
`[ClawRouter] Existing proxy on port ${listenPort} uses wallet ${
|
|
5135
|
+
`[ClawRouter] Existing proxy on port ${listenPort} uses wallet ${existingProxy.wallet}, but current config uses ${account2.address}. Reusing existing proxy.`
|
|
5136
|
+
);
|
|
5137
|
+
}
|
|
5138
|
+
if (existingProxy.paymentChain) {
|
|
5139
|
+
if (existingProxy.paymentChain !== paymentChain) {
|
|
5140
|
+
throw new Error(
|
|
5141
|
+
`Existing proxy on port ${listenPort} is using ${existingProxy.paymentChain} but ${paymentChain} was requested. Stop the existing proxy first or use a different port.`
|
|
5142
|
+
);
|
|
5143
|
+
}
|
|
5144
|
+
} else if (paymentChain !== "base") {
|
|
5145
|
+
console.warn(`[ClawRouter] Existing proxy on port ${listenPort} does not report paymentChain (pre-v0.11 instance). Assuming Base.`);
|
|
5146
|
+
throw new Error(
|
|
5147
|
+
`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.`
|
|
4969
5148
|
);
|
|
4970
5149
|
}
|
|
5150
|
+
let reuseSolanaAddress;
|
|
5151
|
+
if (solanaPrivateKeyBytes) {
|
|
5152
|
+
const { createKeyPairSignerFromPrivateKeyBytes } = await import("@solana/kit");
|
|
5153
|
+
const solanaSigner = await createKeyPairSignerFromPrivateKeyBytes(solanaPrivateKeyBytes);
|
|
5154
|
+
reuseSolanaAddress = solanaSigner.address;
|
|
5155
|
+
}
|
|
5156
|
+
const balanceMonitor2 = paymentChain === "solana" && reuseSolanaAddress ? new SolanaBalanceMonitor(reuseSolanaAddress) : new BalanceMonitor(account2.address);
|
|
4971
5157
|
options.onReady?.(listenPort);
|
|
4972
5158
|
return {
|
|
4973
5159
|
port: listenPort,
|
|
4974
5160
|
baseUrl: baseUrl2,
|
|
4975
|
-
walletAddress:
|
|
5161
|
+
walletAddress: existingProxy.wallet,
|
|
5162
|
+
solanaAddress: reuseSolanaAddress,
|
|
4976
5163
|
balanceMonitor: balanceMonitor2,
|
|
4977
5164
|
close: async () => {
|
|
4978
5165
|
}
|
|
4979
5166
|
};
|
|
4980
5167
|
}
|
|
4981
|
-
const account =
|
|
4982
|
-
const {
|
|
4983
|
-
const
|
|
5168
|
+
const account = privateKeyToAccount3(walletKey);
|
|
5169
|
+
const evmPublicClient = createPublicClient2({ chain: base2, transport: http2() });
|
|
5170
|
+
const evmSigner = toClientEvmSigner(account, evmPublicClient);
|
|
5171
|
+
const x402 = new x402Client();
|
|
5172
|
+
registerExactEvmScheme(x402, { signer: evmSigner });
|
|
5173
|
+
let solanaAddress;
|
|
5174
|
+
if (solanaPrivateKeyBytes) {
|
|
5175
|
+
const { registerExactSvmScheme } = await import("@x402/svm/exact/client");
|
|
5176
|
+
const { createKeyPairSignerFromPrivateKeyBytes } = await import("@solana/kit");
|
|
5177
|
+
const solanaSigner = await createKeyPairSignerFromPrivateKeyBytes(solanaPrivateKeyBytes);
|
|
5178
|
+
solanaAddress = solanaSigner.address;
|
|
5179
|
+
registerExactSvmScheme(x402, { signer: solanaSigner });
|
|
5180
|
+
console.log(`[ClawRouter] Solana x402 scheme registered: ${solanaAddress}`);
|
|
5181
|
+
}
|
|
5182
|
+
x402.onAfterPaymentCreation(async (context) => {
|
|
5183
|
+
const network = context.selectedRequirements.network;
|
|
5184
|
+
const chain = network.startsWith("eip155") ? "Base (EVM)" : network.startsWith("solana") ? "Solana" : network;
|
|
5185
|
+
console.log(`[ClawRouter] Payment signed on ${chain} (${network})`);
|
|
5186
|
+
});
|
|
5187
|
+
const payFetch = createPayFetchWithPreAuth(fetch, x402, void 0, {
|
|
5188
|
+
skipPreAuth: paymentChain === "solana"
|
|
5189
|
+
});
|
|
5190
|
+
const balanceMonitor = paymentChain === "solana" && solanaAddress ? new SolanaBalanceMonitor(solanaAddress) : new BalanceMonitor(account.address);
|
|
4984
5191
|
const routingConfig = mergeRoutingConfig(options.routingConfig);
|
|
4985
5192
|
const modelPricing = buildModelPricing();
|
|
4986
5193
|
const routerOpts = {
|
|
@@ -5014,8 +5221,12 @@ async function startProxy(options) {
|
|
|
5014
5221
|
const full = url.searchParams.get("full") === "true";
|
|
5015
5222
|
const response = {
|
|
5016
5223
|
status: "ok",
|
|
5017
|
-
wallet: account.address
|
|
5224
|
+
wallet: account.address,
|
|
5225
|
+
paymentChain
|
|
5018
5226
|
};
|
|
5227
|
+
if (solanaAddress) {
|
|
5228
|
+
response.solana = solanaAddress;
|
|
5229
|
+
}
|
|
5019
5230
|
if (full) {
|
|
5020
5231
|
try {
|
|
5021
5232
|
const balanceInfo = await balanceMonitor.checkBalance();
|
|
@@ -5127,10 +5338,10 @@ async function startProxy(options) {
|
|
|
5127
5338
|
const onError = async (err) => {
|
|
5128
5339
|
server.removeListener("error", onError);
|
|
5129
5340
|
if (err.code === "EADDRINUSE") {
|
|
5130
|
-
const
|
|
5131
|
-
if (
|
|
5341
|
+
const existingProxy2 = await checkExistingProxy(listenPort);
|
|
5342
|
+
if (existingProxy2) {
|
|
5132
5343
|
console.log(`[ClawRouter] Existing proxy detected on port ${listenPort}, reusing`);
|
|
5133
|
-
rejectAttempt({ code: "REUSE_EXISTING", wallet:
|
|
5344
|
+
rejectAttempt({ code: "REUSE_EXISTING", wallet: existingProxy2.wallet, existingChain: existingProxy2.paymentChain });
|
|
5134
5345
|
return;
|
|
5135
5346
|
}
|
|
5136
5347
|
if (attempt < PORT_RETRY_ATTEMPTS) {
|
|
@@ -5163,6 +5374,11 @@ async function startProxy(options) {
|
|
|
5163
5374
|
} catch (err) {
|
|
5164
5375
|
const error = err;
|
|
5165
5376
|
if (error.code === "REUSE_EXISTING" && error.wallet) {
|
|
5377
|
+
if (error.existingChain && error.existingChain !== paymentChain) {
|
|
5378
|
+
throw new Error(
|
|
5379
|
+
`Existing proxy on port ${listenPort} is using ${error.existingChain} but ${paymentChain} was requested. Stop the existing proxy first or use a different port.`
|
|
5380
|
+
);
|
|
5381
|
+
}
|
|
5166
5382
|
const baseUrl2 = `http://127.0.0.1:${listenPort}`;
|
|
5167
5383
|
options.onReady?.(listenPort);
|
|
5168
5384
|
return {
|
|
@@ -5220,6 +5436,7 @@ async function startProxy(options) {
|
|
|
5220
5436
|
port,
|
|
5221
5437
|
baseUrl,
|
|
5222
5438
|
walletAddress: account.address,
|
|
5439
|
+
solanaAddress,
|
|
5223
5440
|
balanceMonitor,
|
|
5224
5441
|
close: () => new Promise((res, rej) => {
|
|
5225
5442
|
const timeout = setTimeout(() => {
|
|
@@ -5266,8 +5483,6 @@ async function tryModelRequest(upstreamUrl, method, headers, body, modelId, maxT
|
|
|
5266
5483
|
requestBody = Buffer.from(JSON.stringify(parsed));
|
|
5267
5484
|
} catch {
|
|
5268
5485
|
}
|
|
5269
|
-
const estimated = estimateAmount(modelId, requestBody.length, maxTokens);
|
|
5270
|
-
const preAuth = estimated ? { estimatedAmount: estimated } : void 0;
|
|
5271
5486
|
try {
|
|
5272
5487
|
const response = await payFetch(
|
|
5273
5488
|
upstreamUrl,
|
|
@@ -5276,8 +5491,7 @@ async function tryModelRequest(upstreamUrl, method, headers, body, modelId, maxT
|
|
|
5276
5491
|
headers,
|
|
5277
5492
|
body: requestBody.length > 0 ? new Uint8Array(requestBody) : void 0,
|
|
5278
5493
|
signal
|
|
5279
|
-
}
|
|
5280
|
-
preAuth
|
|
5494
|
+
}
|
|
5281
5495
|
);
|
|
5282
5496
|
if (response.status !== 200) {
|
|
5283
5497
|
const errorBody = await response.text();
|
|
@@ -5913,6 +6127,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
5913
6127
|
}
|
|
5914
6128
|
deduplicator.markInflight(dedupKey);
|
|
5915
6129
|
let estimatedCostMicros;
|
|
6130
|
+
let balanceFallbackNotice;
|
|
5916
6131
|
const isFreeModel = modelId === FREE_MODEL;
|
|
5917
6132
|
if (modelId && !options.skipBalanceCheck && !isFreeModel) {
|
|
5918
6133
|
const estimated = estimateAmount(modelId, body.length, maxTokens);
|
|
@@ -5923,12 +6138,17 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
5923
6138
|
if (sufficiency.info.isEmpty || !sufficiency.sufficient) {
|
|
5924
6139
|
const originalModel = modelId;
|
|
5925
6140
|
console.log(
|
|
5926
|
-
`[ClawRouter] Wallet ${sufficiency.info.isEmpty ? "empty" : "insufficient"} (
|
|
6141
|
+
`[ClawRouter] Wallet ${sufficiency.info.isEmpty ? "empty" : "insufficient"} (${sufficiency.info.balanceUSD}), falling back to free model: ${FREE_MODEL} (requested: ${originalModel})`
|
|
5927
6142
|
);
|
|
5928
6143
|
modelId = FREE_MODEL;
|
|
5929
6144
|
const parsed = JSON.parse(body.toString());
|
|
5930
6145
|
parsed.model = FREE_MODEL;
|
|
5931
6146
|
body = Buffer.from(JSON.stringify(parsed));
|
|
6147
|
+
balanceFallbackNotice = sufficiency.info.isEmpty ? `> **\u26A0\uFE0F Wallet empty** \u2014 using free model. Fund your wallet to use ${originalModel}.
|
|
6148
|
+
|
|
6149
|
+
` : `> **\u26A0\uFE0F Insufficient balance** (${sufficiency.info.balanceUSD}) \u2014 using free model instead of ${originalModel}.
|
|
6150
|
+
|
|
6151
|
+
`;
|
|
5932
6152
|
options.onLowBalance?.({
|
|
5933
6153
|
balanceUSD: sufficiency.info.balanceUSD,
|
|
5934
6154
|
walletAddress: sufficiency.info.walletAddress
|
|
@@ -6220,6 +6440,18 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
6220
6440
|
`;
|
|
6221
6441
|
safeWrite(res, roleData);
|
|
6222
6442
|
responseChunks.push(Buffer.from(roleData));
|
|
6443
|
+
if (balanceFallbackNotice) {
|
|
6444
|
+
const noticeChunk = {
|
|
6445
|
+
...baseChunk,
|
|
6446
|
+
choices: [{ index, delta: { content: balanceFallbackNotice }, logprobs: null, finish_reason: null }]
|
|
6447
|
+
};
|
|
6448
|
+
const noticeData = `data: ${JSON.stringify(noticeChunk)}
|
|
6449
|
+
|
|
6450
|
+
`;
|
|
6451
|
+
safeWrite(res, noticeData);
|
|
6452
|
+
responseChunks.push(Buffer.from(noticeData));
|
|
6453
|
+
balanceFallbackNotice = void 0;
|
|
6454
|
+
}
|
|
6223
6455
|
if (content) {
|
|
6224
6456
|
const contentChunk = {
|
|
6225
6457
|
...baseChunk,
|
|
@@ -6304,23 +6536,36 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
6304
6536
|
responseHeaders["x-clawrouter-agentic-score"] = routingDecision.agenticScore.toFixed(2);
|
|
6305
6537
|
}
|
|
6306
6538
|
}
|
|
6307
|
-
|
|
6539
|
+
const bodyParts = [];
|
|
6308
6540
|
if (upstream.body) {
|
|
6309
6541
|
const reader = upstream.body.getReader();
|
|
6310
6542
|
try {
|
|
6311
6543
|
while (true) {
|
|
6312
6544
|
const { done, value } = await reader.read();
|
|
6313
6545
|
if (done) break;
|
|
6314
|
-
|
|
6315
|
-
safeWrite(res, chunk);
|
|
6316
|
-
responseChunks.push(chunk);
|
|
6546
|
+
bodyParts.push(Buffer.from(value));
|
|
6317
6547
|
}
|
|
6318
6548
|
} finally {
|
|
6319
6549
|
reader.releaseLock();
|
|
6320
6550
|
}
|
|
6321
6551
|
}
|
|
6552
|
+
let responseBody = Buffer.concat(bodyParts);
|
|
6553
|
+
if (balanceFallbackNotice && responseBody.length > 0) {
|
|
6554
|
+
try {
|
|
6555
|
+
const parsed = JSON.parse(responseBody.toString());
|
|
6556
|
+
if (parsed.choices?.[0]?.message?.content !== void 0) {
|
|
6557
|
+
parsed.choices[0].message.content = balanceFallbackNotice + parsed.choices[0].message.content;
|
|
6558
|
+
responseBody = Buffer.from(JSON.stringify(parsed));
|
|
6559
|
+
}
|
|
6560
|
+
} catch {
|
|
6561
|
+
}
|
|
6562
|
+
balanceFallbackNotice = void 0;
|
|
6563
|
+
}
|
|
6564
|
+
responseHeaders["content-length"] = String(responseBody.length);
|
|
6565
|
+
res.writeHead(upstream.status, responseHeaders);
|
|
6566
|
+
safeWrite(res, responseBody);
|
|
6567
|
+
responseChunks.push(responseBody);
|
|
6322
6568
|
res.end();
|
|
6323
|
-
const responseBody = Buffer.concat(responseChunks);
|
|
6324
6569
|
deduplicator.complete(dedupKey, {
|
|
6325
6570
|
status: upstream.status,
|
|
6326
6571
|
headers: responseHeaders,
|
|
@@ -6372,7 +6617,7 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
6372
6617
|
deduplicator.removeInflight(dedupKey);
|
|
6373
6618
|
balanceMonitor.invalidate();
|
|
6374
6619
|
if (err instanceof Error && err.name === "AbortError") {
|
|
6375
|
-
throw new Error(`Request timed out after ${timeoutMs}ms
|
|
6620
|
+
throw new Error(`Request timed out after ${timeoutMs}ms`, { cause: err });
|
|
6376
6621
|
}
|
|
6377
6622
|
throw err;
|
|
6378
6623
|
}
|
|
@@ -6403,102 +6648,17 @@ async function proxyRequest(req, res, apiBase, payFetch, options, routerOpts, de
|
|
|
6403
6648
|
}
|
|
6404
6649
|
}
|
|
6405
6650
|
|
|
6406
|
-
// src/auth.ts
|
|
6407
|
-
import { writeFile, mkdir as mkdir2 } from "fs/promises";
|
|
6408
|
-
import { join as join4 } from "path";
|
|
6409
|
-
import { homedir as homedir3 } from "os";
|
|
6410
|
-
import { generatePrivateKey, privateKeyToAccount as privateKeyToAccount3 } from "viem/accounts";
|
|
6411
|
-
var WALLET_DIR = join4(homedir3(), ".openclaw", "blockrun");
|
|
6412
|
-
var WALLET_FILE = join4(WALLET_DIR, "wallet.key");
|
|
6413
|
-
async function loadSavedWallet() {
|
|
6414
|
-
try {
|
|
6415
|
-
const key = (await readTextFile(WALLET_FILE)).trim();
|
|
6416
|
-
if (key.startsWith("0x") && key.length === 66) {
|
|
6417
|
-
console.log(`[ClawRouter] \u2713 Loaded existing wallet from ${WALLET_FILE}`);
|
|
6418
|
-
return key;
|
|
6419
|
-
}
|
|
6420
|
-
console.error(`[ClawRouter] \u2717 CRITICAL: Wallet file exists but has invalid format!`);
|
|
6421
|
-
console.error(`[ClawRouter] File: ${WALLET_FILE}`);
|
|
6422
|
-
console.error(`[ClawRouter] Expected: 0x followed by 64 hex characters (66 chars total)`);
|
|
6423
|
-
console.error(
|
|
6424
|
-
`[ClawRouter] To fix: restore your backup key or set BLOCKRUN_WALLET_KEY env var`
|
|
6425
|
-
);
|
|
6426
|
-
throw new Error(
|
|
6427
|
-
`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.`
|
|
6428
|
-
);
|
|
6429
|
-
} catch (err) {
|
|
6430
|
-
if (err.code !== "ENOENT") {
|
|
6431
|
-
if (err instanceof Error && err.message.includes("Refusing to auto-generate")) {
|
|
6432
|
-
throw err;
|
|
6433
|
-
}
|
|
6434
|
-
console.error(
|
|
6435
|
-
`[ClawRouter] \u2717 Failed to read wallet file: ${err instanceof Error ? err.message : String(err)}`
|
|
6436
|
-
);
|
|
6437
|
-
throw new Error(
|
|
6438
|
-
`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.`
|
|
6439
|
-
);
|
|
6440
|
-
}
|
|
6441
|
-
}
|
|
6442
|
-
return void 0;
|
|
6443
|
-
}
|
|
6444
|
-
async function generateAndSaveWallet() {
|
|
6445
|
-
const key = generatePrivateKey();
|
|
6446
|
-
const account = privateKeyToAccount3(key);
|
|
6447
|
-
await mkdir2(WALLET_DIR, { recursive: true });
|
|
6448
|
-
await writeFile(WALLET_FILE, key + "\n", { mode: 384 });
|
|
6449
|
-
try {
|
|
6450
|
-
const verification = (await readTextFile(WALLET_FILE)).trim();
|
|
6451
|
-
if (verification !== key) {
|
|
6452
|
-
throw new Error("Wallet file verification failed - content mismatch");
|
|
6453
|
-
}
|
|
6454
|
-
console.log(`[ClawRouter] \u2713 Wallet saved and verified at ${WALLET_FILE}`);
|
|
6455
|
-
} catch (err) {
|
|
6456
|
-
throw new Error(
|
|
6457
|
-
`Failed to verify wallet file after creation: ${err instanceof Error ? err.message : String(err)}`
|
|
6458
|
-
);
|
|
6459
|
-
}
|
|
6460
|
-
console.log(`[ClawRouter]`);
|
|
6461
|
-
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`);
|
|
6462
|
-
console.log(`[ClawRouter] NEW WALLET GENERATED \u2014 BACK UP YOUR KEY NOW`);
|
|
6463
|
-
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`);
|
|
6464
|
-
console.log(`[ClawRouter] Address : ${account.address}`);
|
|
6465
|
-
console.log(`[ClawRouter] Key file: ${WALLET_FILE}`);
|
|
6466
|
-
console.log(`[ClawRouter]`);
|
|
6467
|
-
console.log(`[ClawRouter] To back up, run in OpenClaw:`);
|
|
6468
|
-
console.log(`[ClawRouter] /wallet export`);
|
|
6469
|
-
console.log(`[ClawRouter]`);
|
|
6470
|
-
console.log(`[ClawRouter] To restore on another machine:`);
|
|
6471
|
-
console.log(`[ClawRouter] export BLOCKRUN_WALLET_KEY=<your_key>`);
|
|
6472
|
-
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`);
|
|
6473
|
-
console.log(`[ClawRouter]`);
|
|
6474
|
-
return { key, address: account.address };
|
|
6475
|
-
}
|
|
6476
|
-
async function resolveOrGenerateWalletKey() {
|
|
6477
|
-
const saved = await loadSavedWallet();
|
|
6478
|
-
if (saved) {
|
|
6479
|
-
const account = privateKeyToAccount3(saved);
|
|
6480
|
-
return { key: saved, address: account.address, source: "saved" };
|
|
6481
|
-
}
|
|
6482
|
-
const envKey = process["env"].BLOCKRUN_WALLET_KEY;
|
|
6483
|
-
if (typeof envKey === "string" && envKey.startsWith("0x") && envKey.length === 66) {
|
|
6484
|
-
const account = privateKeyToAccount3(envKey);
|
|
6485
|
-
return { key: envKey, address: account.address, source: "env" };
|
|
6486
|
-
}
|
|
6487
|
-
const { key, address } = await generateAndSaveWallet();
|
|
6488
|
-
return { key, address, source: "generated" };
|
|
6489
|
-
}
|
|
6490
|
-
|
|
6491
6651
|
// src/index.ts
|
|
6492
6652
|
import {
|
|
6493
|
-
writeFileSync,
|
|
6494
|
-
existsSync,
|
|
6653
|
+
writeFileSync as writeFileSync2,
|
|
6654
|
+
existsSync as existsSync2,
|
|
6495
6655
|
readdirSync,
|
|
6496
|
-
mkdirSync,
|
|
6656
|
+
mkdirSync as mkdirSync2,
|
|
6497
6657
|
copyFileSync,
|
|
6498
6658
|
renameSync
|
|
6499
6659
|
} from "fs";
|
|
6500
|
-
import { homedir as
|
|
6501
|
-
import { join as
|
|
6660
|
+
import { homedir as homedir5 } from "os";
|
|
6661
|
+
import { join as join6 } from "path";
|
|
6502
6662
|
import { privateKeyToAccount as privateKeyToAccount4 } from "viem/accounts";
|
|
6503
6663
|
|
|
6504
6664
|
// src/partners/registry.ts
|
|
@@ -6596,6 +6756,260 @@ function buildPartnerTools(proxyBaseUrl) {
|
|
|
6596
6756
|
return PARTNER_SERVICES.map((service) => buildTool(service, proxyBaseUrl));
|
|
6597
6757
|
}
|
|
6598
6758
|
|
|
6759
|
+
// src/index.ts
|
|
6760
|
+
init_solana_balance();
|
|
6761
|
+
|
|
6762
|
+
// src/spend-control.ts
|
|
6763
|
+
import * as fs from "fs";
|
|
6764
|
+
import * as path from "path";
|
|
6765
|
+
import { homedir as homedir4 } from "os";
|
|
6766
|
+
var WALLET_DIR2 = path.join(homedir4(), ".openclaw", "blockrun");
|
|
6767
|
+
var HOUR_MS = 60 * 60 * 1e3;
|
|
6768
|
+
var DAY_MS = 24 * HOUR_MS;
|
|
6769
|
+
var FileSpendControlStorage = class {
|
|
6770
|
+
spendingFile;
|
|
6771
|
+
constructor() {
|
|
6772
|
+
this.spendingFile = path.join(WALLET_DIR2, "spending.json");
|
|
6773
|
+
}
|
|
6774
|
+
load() {
|
|
6775
|
+
try {
|
|
6776
|
+
if (fs.existsSync(this.spendingFile)) {
|
|
6777
|
+
const data = JSON.parse(readTextFileSync(this.spendingFile));
|
|
6778
|
+
const rawLimits = data.limits ?? {};
|
|
6779
|
+
const rawHistory = data.history ?? [];
|
|
6780
|
+
const limits = {};
|
|
6781
|
+
for (const key of ["perRequest", "hourly", "daily", "session"]) {
|
|
6782
|
+
const val = rawLimits[key];
|
|
6783
|
+
if (typeof val === "number" && val > 0 && Number.isFinite(val)) {
|
|
6784
|
+
limits[key] = val;
|
|
6785
|
+
}
|
|
6786
|
+
}
|
|
6787
|
+
const history = [];
|
|
6788
|
+
if (Array.isArray(rawHistory)) {
|
|
6789
|
+
for (const r of rawHistory) {
|
|
6790
|
+
if (typeof r?.timestamp === "number" && typeof r?.amount === "number" && Number.isFinite(r.timestamp) && Number.isFinite(r.amount) && r.amount >= 0) {
|
|
6791
|
+
history.push({
|
|
6792
|
+
timestamp: r.timestamp,
|
|
6793
|
+
amount: r.amount,
|
|
6794
|
+
model: typeof r.model === "string" ? r.model : void 0,
|
|
6795
|
+
action: typeof r.action === "string" ? r.action : void 0
|
|
6796
|
+
});
|
|
6797
|
+
}
|
|
6798
|
+
}
|
|
6799
|
+
}
|
|
6800
|
+
return { limits, history };
|
|
6801
|
+
}
|
|
6802
|
+
} catch (err) {
|
|
6803
|
+
console.error(`[ClawRouter] Failed to load spending data, starting fresh: ${err}`);
|
|
6804
|
+
}
|
|
6805
|
+
return null;
|
|
6806
|
+
}
|
|
6807
|
+
save(data) {
|
|
6808
|
+
try {
|
|
6809
|
+
if (!fs.existsSync(WALLET_DIR2)) {
|
|
6810
|
+
fs.mkdirSync(WALLET_DIR2, { recursive: true, mode: 448 });
|
|
6811
|
+
}
|
|
6812
|
+
fs.writeFileSync(this.spendingFile, JSON.stringify(data, null, 2), {
|
|
6813
|
+
mode: 384
|
|
6814
|
+
});
|
|
6815
|
+
} catch (err) {
|
|
6816
|
+
console.error(`[ClawRouter] Failed to save spending data: ${err}`);
|
|
6817
|
+
}
|
|
6818
|
+
}
|
|
6819
|
+
};
|
|
6820
|
+
var InMemorySpendControlStorage = class {
|
|
6821
|
+
data = null;
|
|
6822
|
+
load() {
|
|
6823
|
+
return this.data ? {
|
|
6824
|
+
limits: { ...this.data.limits },
|
|
6825
|
+
history: this.data.history.map((r) => ({ ...r }))
|
|
6826
|
+
} : null;
|
|
6827
|
+
}
|
|
6828
|
+
save(data) {
|
|
6829
|
+
this.data = {
|
|
6830
|
+
limits: { ...data.limits },
|
|
6831
|
+
history: data.history.map((r) => ({ ...r }))
|
|
6832
|
+
};
|
|
6833
|
+
}
|
|
6834
|
+
};
|
|
6835
|
+
var SpendControl = class {
|
|
6836
|
+
limits = {};
|
|
6837
|
+
history = [];
|
|
6838
|
+
sessionSpent = 0;
|
|
6839
|
+
sessionCalls = 0;
|
|
6840
|
+
storage;
|
|
6841
|
+
now;
|
|
6842
|
+
constructor(options) {
|
|
6843
|
+
this.storage = options?.storage ?? new FileSpendControlStorage();
|
|
6844
|
+
this.now = options?.now ?? (() => Date.now());
|
|
6845
|
+
this.load();
|
|
6846
|
+
}
|
|
6847
|
+
setLimit(window, amount) {
|
|
6848
|
+
if (!Number.isFinite(amount) || amount <= 0) {
|
|
6849
|
+
throw new Error("Limit must be a finite positive number");
|
|
6850
|
+
}
|
|
6851
|
+
this.limits[window] = amount;
|
|
6852
|
+
this.save();
|
|
6853
|
+
}
|
|
6854
|
+
clearLimit(window) {
|
|
6855
|
+
delete this.limits[window];
|
|
6856
|
+
this.save();
|
|
6857
|
+
}
|
|
6858
|
+
getLimits() {
|
|
6859
|
+
return { ...this.limits };
|
|
6860
|
+
}
|
|
6861
|
+
check(estimatedCost) {
|
|
6862
|
+
const now = this.now();
|
|
6863
|
+
if (this.limits.perRequest !== void 0) {
|
|
6864
|
+
if (estimatedCost > this.limits.perRequest) {
|
|
6865
|
+
return {
|
|
6866
|
+
allowed: false,
|
|
6867
|
+
blockedBy: "perRequest",
|
|
6868
|
+
remaining: this.limits.perRequest,
|
|
6869
|
+
reason: `Per-request limit exceeded: $${estimatedCost.toFixed(4)} > $${this.limits.perRequest.toFixed(2)} max`
|
|
6870
|
+
};
|
|
6871
|
+
}
|
|
6872
|
+
}
|
|
6873
|
+
if (this.limits.hourly !== void 0) {
|
|
6874
|
+
const hourlySpent = this.getSpendingInWindow(now - HOUR_MS, now);
|
|
6875
|
+
const remaining = this.limits.hourly - hourlySpent;
|
|
6876
|
+
if (estimatedCost > remaining) {
|
|
6877
|
+
const oldestInWindow = this.history.find((r) => r.timestamp >= now - HOUR_MS);
|
|
6878
|
+
const resetIn = oldestInWindow ? Math.ceil((oldestInWindow.timestamp + HOUR_MS - now) / 1e3) : 0;
|
|
6879
|
+
return {
|
|
6880
|
+
allowed: false,
|
|
6881
|
+
blockedBy: "hourly",
|
|
6882
|
+
remaining,
|
|
6883
|
+
reason: `Hourly limit exceeded: $${(hourlySpent + estimatedCost).toFixed(2)} > $${this.limits.hourly.toFixed(2)} max`,
|
|
6884
|
+
resetIn
|
|
6885
|
+
};
|
|
6886
|
+
}
|
|
6887
|
+
}
|
|
6888
|
+
if (this.limits.daily !== void 0) {
|
|
6889
|
+
const dailySpent = this.getSpendingInWindow(now - DAY_MS, now);
|
|
6890
|
+
const remaining = this.limits.daily - dailySpent;
|
|
6891
|
+
if (estimatedCost > remaining) {
|
|
6892
|
+
const oldestInWindow = this.history.find((r) => r.timestamp >= now - DAY_MS);
|
|
6893
|
+
const resetIn = oldestInWindow ? Math.ceil((oldestInWindow.timestamp + DAY_MS - now) / 1e3) : 0;
|
|
6894
|
+
return {
|
|
6895
|
+
allowed: false,
|
|
6896
|
+
blockedBy: "daily",
|
|
6897
|
+
remaining,
|
|
6898
|
+
reason: `Daily limit exceeded: $${(dailySpent + estimatedCost).toFixed(2)} > $${this.limits.daily.toFixed(2)} max`,
|
|
6899
|
+
resetIn
|
|
6900
|
+
};
|
|
6901
|
+
}
|
|
6902
|
+
}
|
|
6903
|
+
if (this.limits.session !== void 0) {
|
|
6904
|
+
const remaining = this.limits.session - this.sessionSpent;
|
|
6905
|
+
if (estimatedCost > remaining) {
|
|
6906
|
+
return {
|
|
6907
|
+
allowed: false,
|
|
6908
|
+
blockedBy: "session",
|
|
6909
|
+
remaining,
|
|
6910
|
+
reason: `Session limit exceeded: $${(this.sessionSpent + estimatedCost).toFixed(2)} > $${this.limits.session.toFixed(2)} max`
|
|
6911
|
+
};
|
|
6912
|
+
}
|
|
6913
|
+
}
|
|
6914
|
+
return { allowed: true };
|
|
6915
|
+
}
|
|
6916
|
+
record(amount, metadata) {
|
|
6917
|
+
if (!Number.isFinite(amount) || amount < 0) {
|
|
6918
|
+
throw new Error("Record amount must be a non-negative finite number");
|
|
6919
|
+
}
|
|
6920
|
+
const record = {
|
|
6921
|
+
timestamp: this.now(),
|
|
6922
|
+
amount,
|
|
6923
|
+
model: metadata?.model,
|
|
6924
|
+
action: metadata?.action
|
|
6925
|
+
};
|
|
6926
|
+
this.history.push(record);
|
|
6927
|
+
this.sessionSpent += amount;
|
|
6928
|
+
this.sessionCalls += 1;
|
|
6929
|
+
this.cleanup();
|
|
6930
|
+
this.save();
|
|
6931
|
+
}
|
|
6932
|
+
getSpendingInWindow(from, to) {
|
|
6933
|
+
return this.history.filter((r) => r.timestamp >= from && r.timestamp <= to).reduce((sum, r) => sum + r.amount, 0);
|
|
6934
|
+
}
|
|
6935
|
+
getSpending(window) {
|
|
6936
|
+
const now = this.now();
|
|
6937
|
+
switch (window) {
|
|
6938
|
+
case "hourly":
|
|
6939
|
+
return this.getSpendingInWindow(now - HOUR_MS, now);
|
|
6940
|
+
case "daily":
|
|
6941
|
+
return this.getSpendingInWindow(now - DAY_MS, now);
|
|
6942
|
+
case "session":
|
|
6943
|
+
return this.sessionSpent;
|
|
6944
|
+
}
|
|
6945
|
+
}
|
|
6946
|
+
getRemaining(window) {
|
|
6947
|
+
const limit = this.limits[window];
|
|
6948
|
+
if (limit === void 0) return null;
|
|
6949
|
+
return Math.max(0, limit - this.getSpending(window));
|
|
6950
|
+
}
|
|
6951
|
+
getStatus() {
|
|
6952
|
+
const now = this.now();
|
|
6953
|
+
const hourlySpent = this.getSpendingInWindow(now - HOUR_MS, now);
|
|
6954
|
+
const dailySpent = this.getSpendingInWindow(now - DAY_MS, now);
|
|
6955
|
+
return {
|
|
6956
|
+
limits: { ...this.limits },
|
|
6957
|
+
spending: {
|
|
6958
|
+
hourly: hourlySpent,
|
|
6959
|
+
daily: dailySpent,
|
|
6960
|
+
session: this.sessionSpent
|
|
6961
|
+
},
|
|
6962
|
+
remaining: {
|
|
6963
|
+
hourly: this.limits.hourly !== void 0 ? this.limits.hourly - hourlySpent : null,
|
|
6964
|
+
daily: this.limits.daily !== void 0 ? this.limits.daily - dailySpent : null,
|
|
6965
|
+
session: this.limits.session !== void 0 ? this.limits.session - this.sessionSpent : null
|
|
6966
|
+
},
|
|
6967
|
+
calls: this.sessionCalls
|
|
6968
|
+
};
|
|
6969
|
+
}
|
|
6970
|
+
getHistory(limit) {
|
|
6971
|
+
const records = [...this.history].reverse();
|
|
6972
|
+
return limit ? records.slice(0, limit) : records;
|
|
6973
|
+
}
|
|
6974
|
+
resetSession() {
|
|
6975
|
+
this.sessionSpent = 0;
|
|
6976
|
+
this.sessionCalls = 0;
|
|
6977
|
+
}
|
|
6978
|
+
cleanup() {
|
|
6979
|
+
const cutoff = this.now() - DAY_MS;
|
|
6980
|
+
this.history = this.history.filter((r) => r.timestamp >= cutoff);
|
|
6981
|
+
}
|
|
6982
|
+
save() {
|
|
6983
|
+
this.storage.save({
|
|
6984
|
+
limits: { ...this.limits },
|
|
6985
|
+
history: [...this.history]
|
|
6986
|
+
});
|
|
6987
|
+
}
|
|
6988
|
+
load() {
|
|
6989
|
+
const data = this.storage.load();
|
|
6990
|
+
if (data) {
|
|
6991
|
+
this.limits = data.limits;
|
|
6992
|
+
this.history = data.history;
|
|
6993
|
+
this.cleanup();
|
|
6994
|
+
}
|
|
6995
|
+
}
|
|
6996
|
+
};
|
|
6997
|
+
function formatDuration(seconds) {
|
|
6998
|
+
if (seconds < 60) {
|
|
6999
|
+
return `${seconds}s`;
|
|
7000
|
+
} else if (seconds < 3600) {
|
|
7001
|
+
const mins = Math.ceil(seconds / 60);
|
|
7002
|
+
return `${mins} min`;
|
|
7003
|
+
} else {
|
|
7004
|
+
const hours = Math.floor(seconds / 3600);
|
|
7005
|
+
const mins = Math.ceil(seconds % 3600 / 60);
|
|
7006
|
+
return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
|
|
7007
|
+
}
|
|
7008
|
+
}
|
|
7009
|
+
|
|
7010
|
+
// src/index.ts
|
|
7011
|
+
init_wallet();
|
|
7012
|
+
|
|
6599
7013
|
// src/retry.ts
|
|
6600
7014
|
var DEFAULT_RETRY_CONFIG = {
|
|
6601
7015
|
maxRetries: 2,
|
|
@@ -6674,13 +7088,13 @@ function isGatewayMode() {
|
|
|
6674
7088
|
return args.includes("gateway");
|
|
6675
7089
|
}
|
|
6676
7090
|
function injectModelsConfig(logger) {
|
|
6677
|
-
const configDir =
|
|
6678
|
-
const configPath =
|
|
7091
|
+
const configDir = join6(homedir5(), ".openclaw");
|
|
7092
|
+
const configPath = join6(configDir, "openclaw.json");
|
|
6679
7093
|
let config = {};
|
|
6680
7094
|
let needsWrite = false;
|
|
6681
|
-
if (!
|
|
7095
|
+
if (!existsSync2(configDir)) {
|
|
6682
7096
|
try {
|
|
6683
|
-
|
|
7097
|
+
mkdirSync2(configDir, { recursive: true });
|
|
6684
7098
|
logger.info("Created OpenClaw config directory");
|
|
6685
7099
|
} catch (err) {
|
|
6686
7100
|
logger.info(
|
|
@@ -6689,7 +7103,7 @@ function injectModelsConfig(logger) {
|
|
|
6689
7103
|
return;
|
|
6690
7104
|
}
|
|
6691
7105
|
}
|
|
6692
|
-
if (
|
|
7106
|
+
if (existsSync2(configPath)) {
|
|
6693
7107
|
try {
|
|
6694
7108
|
const content = readTextFileSync(configPath).trim();
|
|
6695
7109
|
if (content) {
|
|
@@ -6834,7 +7248,7 @@ function injectModelsConfig(logger) {
|
|
|
6834
7248
|
if (needsWrite) {
|
|
6835
7249
|
try {
|
|
6836
7250
|
const tmpPath = `${configPath}.tmp.${process.pid}`;
|
|
6837
|
-
|
|
7251
|
+
writeFileSync2(tmpPath, JSON.stringify(config, null, 2));
|
|
6838
7252
|
renameSync(tmpPath, configPath);
|
|
6839
7253
|
logger.info("Smart routing enabled (blockrun/auto)");
|
|
6840
7254
|
} catch (err) {
|
|
@@ -6843,10 +7257,10 @@ function injectModelsConfig(logger) {
|
|
|
6843
7257
|
}
|
|
6844
7258
|
}
|
|
6845
7259
|
function injectAuthProfile(logger) {
|
|
6846
|
-
const agentsDir =
|
|
6847
|
-
if (!
|
|
7260
|
+
const agentsDir = join6(homedir5(), ".openclaw", "agents");
|
|
7261
|
+
if (!existsSync2(agentsDir)) {
|
|
6848
7262
|
try {
|
|
6849
|
-
|
|
7263
|
+
mkdirSync2(agentsDir, { recursive: true });
|
|
6850
7264
|
} catch (err) {
|
|
6851
7265
|
logger.info(
|
|
6852
7266
|
`Could not create agents dir: ${err instanceof Error ? err.message : String(err)}`
|
|
@@ -6860,11 +7274,11 @@ function injectAuthProfile(logger) {
|
|
|
6860
7274
|
agents = ["main", ...agents];
|
|
6861
7275
|
}
|
|
6862
7276
|
for (const agentId of agents) {
|
|
6863
|
-
const authDir =
|
|
6864
|
-
const authPath =
|
|
6865
|
-
if (!
|
|
7277
|
+
const authDir = join6(agentsDir, agentId, "agent");
|
|
7278
|
+
const authPath = join6(authDir, "auth-profiles.json");
|
|
7279
|
+
if (!existsSync2(authDir)) {
|
|
6866
7280
|
try {
|
|
6867
|
-
|
|
7281
|
+
mkdirSync2(authDir, { recursive: true });
|
|
6868
7282
|
} catch {
|
|
6869
7283
|
continue;
|
|
6870
7284
|
}
|
|
@@ -6873,7 +7287,7 @@ function injectAuthProfile(logger) {
|
|
|
6873
7287
|
version: 1,
|
|
6874
7288
|
profiles: {}
|
|
6875
7289
|
};
|
|
6876
|
-
if (
|
|
7290
|
+
if (existsSync2(authPath)) {
|
|
6877
7291
|
try {
|
|
6878
7292
|
const existing = JSON.parse(readTextFileSync(authPath));
|
|
6879
7293
|
if (existing.version && existing.profiles) {
|
|
@@ -6892,7 +7306,7 @@ function injectAuthProfile(logger) {
|
|
|
6892
7306
|
key: "x402-proxy-handles-auth"
|
|
6893
7307
|
};
|
|
6894
7308
|
try {
|
|
6895
|
-
|
|
7309
|
+
writeFileSync2(authPath, JSON.stringify(store, null, 2));
|
|
6896
7310
|
logger.info(`Injected BlockRun auth profile for agent: ${agentId}`);
|
|
6897
7311
|
} catch (err) {
|
|
6898
7312
|
logger.info(
|
|
@@ -6906,22 +7320,22 @@ function injectAuthProfile(logger) {
|
|
|
6906
7320
|
}
|
|
6907
7321
|
var activeProxyHandle = null;
|
|
6908
7322
|
async function startProxyInBackground(api) {
|
|
6909
|
-
const
|
|
6910
|
-
if (source === "generated") {
|
|
7323
|
+
const wallet = await resolveOrGenerateWalletKey();
|
|
7324
|
+
if (wallet.source === "generated") {
|
|
6911
7325
|
api.logger.warn(`\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`);
|
|
6912
7326
|
api.logger.warn(` NEW WALLET GENERATED \u2014 BACK UP YOUR KEY NOW!`);
|
|
6913
|
-
api.logger.warn(` Address : ${address}`);
|
|
7327
|
+
api.logger.warn(` Address : ${wallet.address}`);
|
|
6914
7328
|
api.logger.warn(` Run /wallet export to get your private key`);
|
|
6915
7329
|
api.logger.warn(` Losing this key = losing your USDC funds`);
|
|
6916
7330
|
api.logger.warn(`\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`);
|
|
6917
|
-
} else if (source === "saved") {
|
|
6918
|
-
api.logger.info(`Using saved wallet: ${address}`);
|
|
7331
|
+
} else if (wallet.source === "saved") {
|
|
7332
|
+
api.logger.info(`Using saved wallet: ${wallet.address}`);
|
|
6919
7333
|
} else {
|
|
6920
|
-
api.logger.info(`Using wallet from BLOCKRUN_WALLET_KEY: ${address}`);
|
|
7334
|
+
api.logger.info(`Using wallet from BLOCKRUN_WALLET_KEY: ${wallet.address}`);
|
|
6921
7335
|
}
|
|
6922
7336
|
const routingConfig = api.pluginConfig?.routing;
|
|
6923
7337
|
const proxy = await startProxy({
|
|
6924
|
-
|
|
7338
|
+
wallet,
|
|
6925
7339
|
routingConfig,
|
|
6926
7340
|
onReady: (port) => {
|
|
6927
7341
|
api.logger.info(`BlockRun x402 proxy listening on port ${port}`);
|
|
@@ -6949,18 +7363,18 @@ async function startProxyInBackground(api) {
|
|
|
6949
7363
|
activeProxyHandle = proxy;
|
|
6950
7364
|
api.logger.info(`ClawRouter ready \u2014 smart routing enabled`);
|
|
6951
7365
|
api.logger.info(`Pricing: Simple ~$0.001 | Code ~$0.01 | Complex ~$0.05 | Free: $0`);
|
|
6952
|
-
const
|
|
6953
|
-
|
|
7366
|
+
const displayAddress = proxy.solanaAddress ?? wallet.address;
|
|
7367
|
+
proxy.balanceMonitor.checkBalance().then((balance) => {
|
|
6954
7368
|
if (balance.isEmpty) {
|
|
6955
|
-
api.logger.info(`Wallet: ${
|
|
7369
|
+
api.logger.info(`Wallet: ${displayAddress} | Balance: $0.00`);
|
|
6956
7370
|
api.logger.info(`Using FREE model. Fund wallet for premium models.`);
|
|
6957
7371
|
} else if (balance.isLow) {
|
|
6958
|
-
api.logger.info(`Wallet: ${
|
|
7372
|
+
api.logger.info(`Wallet: ${displayAddress} | Balance: ${balance.balanceUSD} (low)`);
|
|
6959
7373
|
} else {
|
|
6960
|
-
api.logger.info(`Wallet: ${
|
|
7374
|
+
api.logger.info(`Wallet: ${displayAddress} | Balance: ${balance.balanceUSD}`);
|
|
6961
7375
|
}
|
|
6962
7376
|
}).catch(() => {
|
|
6963
|
-
api.logger.info(`Wallet: ${
|
|
7377
|
+
api.logger.info(`Wallet: ${displayAddress} | Balance: (checking...)`);
|
|
6964
7378
|
});
|
|
6965
7379
|
}
|
|
6966
7380
|
async function createStatsCommand() {
|
|
@@ -6998,7 +7412,7 @@ async function createWalletCommand() {
|
|
|
6998
7412
|
let walletKey;
|
|
6999
7413
|
let address;
|
|
7000
7414
|
try {
|
|
7001
|
-
if (
|
|
7415
|
+
if (existsSync2(WALLET_FILE)) {
|
|
7002
7416
|
walletKey = readTextFileSync(WALLET_FILE).trim();
|
|
7003
7417
|
if (walletKey.startsWith("0x") && walletKey.length === 66) {
|
|
7004
7418
|
const account = privateKeyToAccount4(walletKey);
|
|
@@ -7016,48 +7430,176 @@ Run \`openclaw plugins install @blockrun/clawrouter\` to generate a wallet.`,
|
|
|
7016
7430
|
};
|
|
7017
7431
|
}
|
|
7018
7432
|
if (subcommand === "export") {
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7433
|
+
const lines = [
|
|
7434
|
+
"**ClawRouter Wallet Export**",
|
|
7435
|
+
"",
|
|
7436
|
+
"**SECURITY WARNING**: Your private key and mnemonic control your wallet funds.",
|
|
7437
|
+
"Never share these. Anyone with them can spend your USDC.",
|
|
7438
|
+
"",
|
|
7439
|
+
"**EVM (Base):**",
|
|
7440
|
+
` Address: \`${address}\``,
|
|
7441
|
+
` Private Key: \`${walletKey}\``
|
|
7442
|
+
];
|
|
7443
|
+
let hasMnemonic = false;
|
|
7444
|
+
try {
|
|
7445
|
+
if (existsSync2(MNEMONIC_FILE)) {
|
|
7446
|
+
const mnemonic = readTextFileSync(MNEMONIC_FILE).trim();
|
|
7447
|
+
if (mnemonic) {
|
|
7448
|
+
hasMnemonic = true;
|
|
7449
|
+
const { deriveSolanaKeyBytes: deriveSolanaKeyBytes2 } = await Promise.resolve().then(() => (init_wallet(), wallet_exports));
|
|
7450
|
+
const solKeyBytes = deriveSolanaKeyBytes2(mnemonic);
|
|
7451
|
+
const { createKeyPairSignerFromPrivateKeyBytes } = await import("@solana/kit");
|
|
7452
|
+
const signer = await createKeyPairSignerFromPrivateKeyBytes(solKeyBytes);
|
|
7453
|
+
lines.push(
|
|
7454
|
+
"",
|
|
7455
|
+
"**Solana:**",
|
|
7456
|
+
` Address: \`${signer.address}\``,
|
|
7457
|
+
` (Derived from mnemonic below)`,
|
|
7458
|
+
"",
|
|
7459
|
+
"**Mnemonic (24 words):**",
|
|
7460
|
+
`\`${mnemonic}\``,
|
|
7461
|
+
"",
|
|
7462
|
+
"CRITICAL: Back up this mnemonic. It is the ONLY way to recover your Solana wallet."
|
|
7463
|
+
);
|
|
7464
|
+
}
|
|
7465
|
+
}
|
|
7466
|
+
} catch {
|
|
7467
|
+
}
|
|
7468
|
+
lines.push(
|
|
7469
|
+
"",
|
|
7470
|
+
"**To restore on a new machine:**",
|
|
7471
|
+
"1. Set the environment variable before running OpenClaw:",
|
|
7472
|
+
` \`export BLOCKRUN_WALLET_KEY=${walletKey}\``,
|
|
7473
|
+
"2. Or save to file:",
|
|
7474
|
+
` \`mkdir -p ~/.openclaw/blockrun && echo "${walletKey}" > ~/.openclaw/blockrun/wallet.key && chmod 600 ~/.openclaw/blockrun/wallet.key\``
|
|
7475
|
+
);
|
|
7476
|
+
if (hasMnemonic) {
|
|
7477
|
+
lines.push(
|
|
7478
|
+
"3. Restore the mnemonic for Solana:",
|
|
7479
|
+
` \`echo "<your mnemonic>" > ~/.openclaw/blockrun/mnemonic && chmod 600 ~/.openclaw/blockrun/mnemonic\``
|
|
7480
|
+
);
|
|
7481
|
+
}
|
|
7482
|
+
return { text: lines.join("\n") };
|
|
7483
|
+
}
|
|
7484
|
+
if (subcommand === "solana") {
|
|
7485
|
+
try {
|
|
7486
|
+
let solanaAddr;
|
|
7487
|
+
if (existsSync2(MNEMONIC_FILE)) {
|
|
7488
|
+
const existingMnemonic = readTextFileSync(MNEMONIC_FILE).trim();
|
|
7489
|
+
if (existingMnemonic) {
|
|
7490
|
+
await savePaymentChain("solana");
|
|
7491
|
+
const { deriveSolanaKeyBytes: deriveSolanaKeyBytes2 } = await Promise.resolve().then(() => (init_wallet(), wallet_exports));
|
|
7492
|
+
const solKeyBytes = deriveSolanaKeyBytes2(existingMnemonic);
|
|
7493
|
+
const { createKeyPairSignerFromPrivateKeyBytes: createKeyPairSignerFromPrivateKeyBytes2 } = await import("@solana/kit");
|
|
7494
|
+
const signer2 = await createKeyPairSignerFromPrivateKeyBytes2(solKeyBytes);
|
|
7495
|
+
solanaAddr = signer2.address;
|
|
7496
|
+
return {
|
|
7497
|
+
text: [
|
|
7498
|
+
"Payment chain set to Solana. Restart the gateway to apply.",
|
|
7499
|
+
"",
|
|
7500
|
+
`**Solana Address:** \`${solanaAddr}\``,
|
|
7501
|
+
`**Fund with USDC on Solana:** https://solscan.io/account/${solanaAddr}`
|
|
7502
|
+
].join("\n")
|
|
7503
|
+
};
|
|
7504
|
+
}
|
|
7505
|
+
}
|
|
7506
|
+
const { solanaPrivateKeyBytes } = await setupSolana();
|
|
7507
|
+
await savePaymentChain("solana");
|
|
7508
|
+
const { createKeyPairSignerFromPrivateKeyBytes } = await import("@solana/kit");
|
|
7509
|
+
const signer = await createKeyPairSignerFromPrivateKeyBytes(solanaPrivateKeyBytes);
|
|
7510
|
+
return {
|
|
7511
|
+
text: [
|
|
7512
|
+
"**Solana Wallet Set Up**",
|
|
7513
|
+
"",
|
|
7514
|
+
`**Solana Address:** \`${signer.address}\``,
|
|
7515
|
+
`**Mnemonic File:** \`${MNEMONIC_FILE}\``,
|
|
7516
|
+
"",
|
|
7517
|
+
"Your existing EVM wallet is unchanged.",
|
|
7518
|
+
"Payment chain set to Solana. Restart the gateway to apply.",
|
|
7519
|
+
"",
|
|
7520
|
+
`**Fund with USDC on Solana:** https://solscan.io/account/${signer.address}`
|
|
7521
|
+
].join("\n")
|
|
7522
|
+
};
|
|
7523
|
+
} catch (err) {
|
|
7524
|
+
return {
|
|
7525
|
+
text: `Failed to set up Solana: ${err instanceof Error ? err.message : String(err)}`,
|
|
7526
|
+
isError: true
|
|
7527
|
+
};
|
|
7528
|
+
}
|
|
7038
7529
|
}
|
|
7039
|
-
|
|
7530
|
+
if (subcommand === "base") {
|
|
7531
|
+
try {
|
|
7532
|
+
await savePaymentChain("base");
|
|
7533
|
+
return {
|
|
7534
|
+
text: "Payment chain set to Base (EVM). Restart the gateway to apply."
|
|
7535
|
+
};
|
|
7536
|
+
} catch (err) {
|
|
7537
|
+
return {
|
|
7538
|
+
text: `Failed to set payment chain: ${err instanceof Error ? err.message : String(err)}`,
|
|
7539
|
+
isError: true
|
|
7540
|
+
};
|
|
7541
|
+
}
|
|
7542
|
+
}
|
|
7543
|
+
let evmBalanceText = "Balance: (checking...)";
|
|
7040
7544
|
try {
|
|
7041
7545
|
const monitor = new BalanceMonitor(address);
|
|
7042
7546
|
const balance = await monitor.checkBalance();
|
|
7043
|
-
|
|
7547
|
+
evmBalanceText = `Balance: ${balance.balanceUSD}`;
|
|
7548
|
+
} catch {
|
|
7549
|
+
evmBalanceText = "Balance: (could not check)";
|
|
7550
|
+
}
|
|
7551
|
+
let solanaSection = "";
|
|
7552
|
+
try {
|
|
7553
|
+
if (existsSync2(MNEMONIC_FILE)) {
|
|
7554
|
+
const { deriveSolanaKeyBytes: deriveSolanaKeyBytes2 } = await Promise.resolve().then(() => (init_wallet(), wallet_exports));
|
|
7555
|
+
const mnemonic = readTextFileSync(MNEMONIC_FILE).trim();
|
|
7556
|
+
if (mnemonic) {
|
|
7557
|
+
const solKeyBytes = deriveSolanaKeyBytes2(mnemonic);
|
|
7558
|
+
const { createKeyPairSignerFromPrivateKeyBytes } = await import("@solana/kit");
|
|
7559
|
+
const signer = await createKeyPairSignerFromPrivateKeyBytes(solKeyBytes);
|
|
7560
|
+
const solAddr = signer.address;
|
|
7561
|
+
let solBalanceText = "Balance: (checking...)";
|
|
7562
|
+
try {
|
|
7563
|
+
const { SolanaBalanceMonitor: SolanaBalanceMonitor2 } = await Promise.resolve().then(() => (init_solana_balance(), solana_balance_exports));
|
|
7564
|
+
const solMonitor = new SolanaBalanceMonitor2(solAddr);
|
|
7565
|
+
const solBalance = await solMonitor.checkBalance();
|
|
7566
|
+
solBalanceText = `Balance: ${solBalance.balanceUSD}`;
|
|
7567
|
+
} catch {
|
|
7568
|
+
solBalanceText = "Balance: (could not check)";
|
|
7569
|
+
}
|
|
7570
|
+
solanaSection = [
|
|
7571
|
+
"",
|
|
7572
|
+
"**Solana:**",
|
|
7573
|
+
` Address: \`${solAddr}\``,
|
|
7574
|
+
` ${solBalanceText}`,
|
|
7575
|
+
` Fund: https://solscan.io/account/${solAddr}`
|
|
7576
|
+
].join("\n");
|
|
7577
|
+
}
|
|
7578
|
+
}
|
|
7044
7579
|
} catch {
|
|
7045
|
-
balanceText = "Balance: (could not check)";
|
|
7046
7580
|
}
|
|
7581
|
+
const currentChain = await resolvePaymentChain();
|
|
7047
7582
|
return {
|
|
7048
7583
|
text: [
|
|
7049
|
-
"
|
|
7584
|
+
"**ClawRouter Wallet**",
|
|
7585
|
+
"",
|
|
7586
|
+
`**Payment Chain:** ${currentChain === "solana" ? "Solana" : "Base (EVM)"}`,
|
|
7587
|
+
"",
|
|
7588
|
+
"**Base (EVM):**",
|
|
7589
|
+
` Address: \`${address}\``,
|
|
7590
|
+
` ${evmBalanceText}`,
|
|
7591
|
+
` Fund: https://basescan.org/address/${address}`,
|
|
7592
|
+
solanaSection,
|
|
7050
7593
|
"",
|
|
7051
|
-
`**Address:** \`${address}\``,
|
|
7052
|
-
`**${balanceText}**`,
|
|
7053
7594
|
`**Key File:** \`${WALLET_FILE}\``,
|
|
7054
7595
|
"",
|
|
7055
7596
|
"**Commands:**",
|
|
7056
7597
|
"\u2022 `/wallet` - Show this status",
|
|
7057
7598
|
"\u2022 `/wallet export` - Export private key for backup",
|
|
7058
|
-
"",
|
|
7059
|
-
|
|
7060
|
-
|
|
7599
|
+
!solanaSection ? "\u2022 `/wallet solana` - Enable Solana payments" : "",
|
|
7600
|
+
solanaSection ? "\u2022 `/wallet base` - Switch to Base (EVM)" : "",
|
|
7601
|
+
solanaSection ? "\u2022 `/wallet solana` - Switch to Solana" : ""
|
|
7602
|
+
].filter(Boolean).join("\n")
|
|
7061
7603
|
};
|
|
7062
7604
|
}
|
|
7063
7605
|
};
|
|
@@ -7212,23 +7754,30 @@ export {
|
|
|
7212
7754
|
DEFAULT_ROUTING_CONFIG,
|
|
7213
7755
|
DEFAULT_SESSION_CONFIG,
|
|
7214
7756
|
EmptyWalletError,
|
|
7757
|
+
FileSpendControlStorage,
|
|
7758
|
+
InMemorySpendControlStorage,
|
|
7215
7759
|
InsufficientFundsError,
|
|
7216
7760
|
MODEL_ALIASES,
|
|
7217
7761
|
OPENCLAW_MODELS,
|
|
7218
7762
|
PARTNER_SERVICES,
|
|
7219
|
-
PaymentCache,
|
|
7220
7763
|
RequestDeduplicator,
|
|
7221
7764
|
ResponseCache,
|
|
7222
7765
|
RpcError,
|
|
7223
7766
|
SessionStore,
|
|
7767
|
+
SolanaBalanceMonitor,
|
|
7768
|
+
SpendControl,
|
|
7224
7769
|
blockrunProvider,
|
|
7225
7770
|
buildPartnerTools,
|
|
7226
7771
|
buildProviderModels,
|
|
7227
7772
|
calculateModelCost,
|
|
7228
|
-
createPaymentFetch,
|
|
7229
7773
|
index_default as default,
|
|
7774
|
+
deriveAllKeys,
|
|
7775
|
+
deriveEvmKey,
|
|
7776
|
+
deriveSolanaKeyBytes,
|
|
7230
7777
|
fetchWithRetry,
|
|
7778
|
+
formatDuration,
|
|
7231
7779
|
formatStatsAscii,
|
|
7780
|
+
generateWalletMnemonic,
|
|
7232
7781
|
getAgenticModels,
|
|
7233
7782
|
getFallbackChain,
|
|
7234
7783
|
getFallbackChainFiltered,
|
|
@@ -7244,9 +7793,14 @@ export {
|
|
|
7244
7793
|
isInsufficientFundsError,
|
|
7245
7794
|
isRetryable,
|
|
7246
7795
|
isRpcError,
|
|
7796
|
+
isValidMnemonic,
|
|
7797
|
+
loadPaymentChain,
|
|
7247
7798
|
logUsage,
|
|
7248
7799
|
resolveModelAlias,
|
|
7800
|
+
resolvePaymentChain,
|
|
7249
7801
|
route,
|
|
7802
|
+
savePaymentChain,
|
|
7803
|
+
setupSolana,
|
|
7250
7804
|
startProxy
|
|
7251
7805
|
};
|
|
7252
7806
|
//# sourceMappingURL=index.js.map
|