Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@blockrun/clawrouter",
"version": "0.11.14",
"version": "0.12.0",
"description": "Smart LLM router — save 92% on inference costs. 41+ models, one wallet, x402 micropayments.",
"type": "module",
"main": "dist/index.js",
Expand Down
58 changes: 56 additions & 2 deletions src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
generateWalletMnemonic,
isValidMnemonic,
deriveSolanaKeyBytes,
deriveSolanaKeyBytesLegacy,
deriveAllKeys,
} from "./wallet.js";

Expand Down Expand Up @@ -193,6 +194,39 @@ async function generateAndSaveWallet(): Promise<{
};
}

/**
* Log migration warning when legacy and new Solana addresses differ.
* Checks old wallet USDC balance and prints instructions.
*/
async function logMigrationWarning(
legacyKeyBytes: Uint8Array,
newKeyBytes: Uint8Array,
): Promise<void> {
try {
const { createKeyPairSignerFromPrivateKeyBytes } = await import("@solana/kit");
const [oldSigner, newSigner] = await Promise.all([
createKeyPairSignerFromPrivateKeyBytes(legacyKeyBytes),
createKeyPairSignerFromPrivateKeyBytes(newKeyBytes),
]);

console.log(`[ClawRouter]`);
console.log(`[ClawRouter] ⚠ SOLANA WALLET MIGRATION DETECTED`);
console.log(`[ClawRouter] ════════════════════════════════════════════════`);
console.log(`[ClawRouter] Old address (secp256k1): ${oldSigner.address}`);
console.log(`[ClawRouter] New address (SLIP-10): ${newSigner.address}`);
console.log(`[ClawRouter]`);
console.log(`[ClawRouter] Your Solana wallet derivation has been fixed to use`);
console.log(`[ClawRouter] SLIP-10 Ed25519 (Phantom/Solflare compatible).`);
console.log(`[ClawRouter]`);
console.log(`[ClawRouter] If you had funds in the old wallet, run:`);
console.log(`[ClawRouter] /wallet migrate-solana`);
console.log(`[ClawRouter] ════════════════════════════════════════════════`);
console.log(`[ClawRouter]`);
} catch {
// Non-fatal — don't block startup if signer creation fails
}
}

/**
* Resolve wallet key: load saved → env var → auto-generate.
* Also loads mnemonic if available for Solana key derivation.
Expand All @@ -204,6 +238,8 @@ export type WalletResolution = {
source: "saved" | "env" | "generated";
mnemonic?: string;
solanaPrivateKeyBytes?: Uint8Array;
/** Legacy (secp256k1) Solana key bytes, present when migration is needed. */
legacySolanaKeyBytes?: Uint8Array;
};

export async function resolveOrGenerateWalletKey(): Promise<WalletResolution> {
Expand All @@ -216,13 +252,22 @@ export async function resolveOrGenerateWalletKey(): Promise<WalletResolution> {
const mnemonic = await loadMnemonic();
if (mnemonic) {
const solanaKeyBytes = deriveSolanaKeyBytes(mnemonic);
return {
const result: WalletResolution = {
key: saved,
address: account.address,
source: "saved",
mnemonic,
solanaPrivateKeyBytes: solanaKeyBytes,
};

// Migration detection: compare legacy (secp256k1) vs new (SLIP-10) Solana keys
const legacyKeyBytes = deriveSolanaKeyBytesLegacy(mnemonic);
if (Buffer.from(legacyKeyBytes).toString("hex") !== Buffer.from(solanaKeyBytes).toString("hex")) {
result.legacySolanaKeyBytes = legacyKeyBytes;
await logMigrationWarning(legacyKeyBytes, solanaKeyBytes);
}

return result;
}

return { key: saved, address: account.address, source: "saved" };
Expand All @@ -237,13 +282,22 @@ export async function resolveOrGenerateWalletKey(): Promise<WalletResolution> {
const mnemonic = await loadMnemonic();
if (mnemonic) {
const solanaKeyBytes = deriveSolanaKeyBytes(mnemonic);
return {
const result: WalletResolution = {
key: envKey,
address: account.address,
source: "env",
mnemonic,
solanaPrivateKeyBytes: solanaKeyBytes,
};

// Migration detection
const legacyKeyBytes = deriveSolanaKeyBytesLegacy(mnemonic);
if (Buffer.from(legacyKeyBytes).toString("hex") !== Buffer.from(solanaKeyBytes).toString("hex")) {
result.legacySolanaKeyBytes = legacyKeyBytes;
await logMigrationWarning(legacyKeyBytes, solanaKeyBytes);
}

return result;
}

return { key: envKey, address: account.address, source: "env" };
Expand Down
15 changes: 8 additions & 7 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@
*/

import { startProxy, getProxyPort } from "./proxy.js";
import { resolveOrGenerateWalletKey } from "./auth.js";
import { BalanceMonitor } from "./balance.js";
import { resolveOrGenerateWalletKey, resolvePaymentChain } from "./auth.js";
import { generateReport } from "./report.js";
import { VERSION } from "./version.js";
import { runDoctor } from "./doctor.js";
Expand Down Expand Up @@ -246,20 +245,22 @@ async function main(): Promise<void> {
},
});

// Check balance
const monitor = new BalanceMonitor(wallet.address);
// Check balance on the active payment chain
const paymentChain = await resolvePaymentChain();
const displayAddress =
paymentChain === "solana" && proxy.solanaAddress ? proxy.solanaAddress : wallet.address;
try {
const balance = await monitor.checkBalance();
const balance = await proxy.balanceMonitor.checkBalance();
if (balance.isEmpty) {
console.log(`[ClawRouter] Wallet balance: $0.00 (using FREE model)`);
console.log(`[ClawRouter] Fund wallet for premium models: ${wallet.address}`);
console.log(`[ClawRouter] Fund wallet for premium models: ${displayAddress}`);
} else if (balance.isLow) {
console.log(`[ClawRouter] Wallet balance: ${balance.balanceUSD} (low)`);
} else {
console.log(`[ClawRouter] Wallet balance: ${balance.balanceUSD}`);
}
} catch {
console.log(`[ClawRouter] Wallet: ${wallet.address} (balance check pending)`);
console.log(`[ClawRouter] Wallet: ${displayAddress} (balance check pending)`);
}

console.log(`[ClawRouter] Ready - Ctrl+C to stop`);
Expand Down
107 changes: 105 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -476,8 +476,10 @@ async function startProxyInBackground(api: OpenClawPluginApi): Promise<void> {
api.logger.info(`Pricing: Simple ~$0.001 | Code ~$0.01 | Complex ~$0.05 | Free: $0`);

// Non-blocking balance check AFTER proxy is ready (won't hang startup)
// Uses the proxy's chain-aware balance monitor (Solana or EVM)
const displayAddress = proxy.solanaAddress ?? wallet.address;
// Uses the proxy's chain-aware balance monitor and matching active-chain address.
const currentChain = await resolvePaymentChain();
const displayAddress =
currentChain === "solana" && proxy.solanaAddress ? proxy.solanaAddress : wallet.address;
proxy.balanceMonitor
.checkBalance()
.then((balance) => {
Expand Down Expand Up @@ -679,6 +681,103 @@ async function createWalletCommand(): Promise<OpenClawPluginCommandDefinition> {
}
}

if (subcommand === "migrate-solana") {
// Sweep USDC from legacy (secp256k1) wallet to new (SLIP-10) wallet
try {
if (!existsSync(MNEMONIC_FILE)) {
return {
text: "No mnemonic file found. Solana wallet not set up — nothing to migrate.",
isError: true,
};
}

const mnemonic = readTextFileSync(MNEMONIC_FILE).trim();
if (!mnemonic) {
return { text: "Mnemonic file is empty.", isError: true };
}

const { deriveSolanaKeyBytes, deriveSolanaKeyBytesLegacy } = await import("./wallet.js");
const { createKeyPairSignerFromPrivateKeyBytes } = await import("@solana/kit");

const legacyKeyBytes = deriveSolanaKeyBytesLegacy(mnemonic);
const newKeyBytes = deriveSolanaKeyBytes(mnemonic);

const [oldSigner, newSigner] = await Promise.all([
createKeyPairSignerFromPrivateKeyBytes(legacyKeyBytes),
createKeyPairSignerFromPrivateKeyBytes(newKeyBytes),
]);

if (oldSigner.address === newSigner.address) {
return { text: "Legacy and new Solana addresses are the same. No migration needed." };
}

// Check old wallet balance before attempting sweep
let oldUsdcText = "unknown";
try {
const { SolanaBalanceMonitor } = await import("./solana-balance.js");
const monitor = new SolanaBalanceMonitor(oldSigner.address);
const balance = await monitor.checkBalance();
oldUsdcText = balance.balanceUSD;

if (balance.isEmpty) {
return {
text: [
"**Solana Migration Status**",
"",
`Old wallet (secp256k1): \`${oldSigner.address}\``,
` USDC: $0.00`,
"",
`New wallet (SLIP-10): \`${newSigner.address}\``,
"",
"No USDC in old wallet. Nothing to sweep.",
"Your new SLIP-10 address is Phantom/Solflare compatible.",
].join("\n"),
};
}
} catch {
// Continue — sweep function will also check balance
}

// Attempt sweep
const { sweepSolanaWallet } = await import("./solana-sweep.js");
const result = await sweepSolanaWallet(legacyKeyBytes, newSigner.address);

if ("error" in result) {
return {
text: [
"**Solana Migration Failed**",
"",
`Old wallet: \`${result.oldAddress}\` (USDC: ${oldUsdcText})`,
`New wallet: \`${result.newAddress || newSigner.address}\``,
"",
`Error: ${result.error}`,
].join("\n"),
isError: true,
};
}

return {
text: [
"**Solana Migration Complete**",
"",
`Swept **${result.transferred}** USDC from old to new wallet.`,
"",
`Old wallet: \`${result.oldAddress}\``,
`New wallet: \`${result.newAddress}\``,
`TX: https://solscan.io/tx/${result.txSignature}`,
"",
"Your new SLIP-10 address is Phantom/Solflare compatible.",
"You can recover it from your 24-word mnemonic in any standard wallet.",
].join("\n"),
};
} catch (err) {
return {
text: `Migration failed: ${err instanceof Error ? err.message : String(err)}`,
isError: true,
};
}
}

if (subcommand === "base") {
// Switch back to Base (EVM) payment chain
try {
Expand Down Expand Up @@ -762,6 +861,7 @@ async function createWalletCommand(): Promise<OpenClawPluginCommandDefinition> {
!solanaSection ? "• `/wallet solana` - Enable Solana payments" : "",
solanaSection ? "• `/wallet base` - Switch to Base (EVM)" : "",
solanaSection ? "• `/wallet solana` - Switch to Solana" : "",
solanaSection ? "• `/wallet migrate-solana` - Sweep funds from old Solana wallet" : "",
]
.filter(Boolean)
.join("\n"),
Expand Down Expand Up @@ -1026,9 +1126,12 @@ export {
isValidMnemonic,
deriveEvmKey,
deriveSolanaKeyBytes,
deriveSolanaKeyBytesLegacy,
deriveAllKeys,
} from "./wallet.js";
export type { DerivedKeys } from "./wallet.js";
export { sweepSolanaWallet } from "./solana-sweep.js";
export type { SweepResult, SweepError } from "./solana-sweep.js";
export { setupSolana, savePaymentChain, loadPaymentChain, resolvePaymentChain } from "./auth.js";
export {
InsufficientFundsError,
Expand Down
Loading
Loading