diff --git a/src/cli/context.ts b/src/cli/context.ts index a863674..43e5451 100644 --- a/src/cli/context.ts +++ b/src/cli/context.ts @@ -7,6 +7,7 @@ import { privateKeyToAccount } from "viem/accounts"; import type { Config } from "../lib/config.js"; import type { Address, Hex } from "viem"; import { ServerClient, tryConnectToServer } from "../client/index.js"; +import { createCwpWallet } from "../lib/cwp.js"; export interface CLIContext { config: Config; @@ -15,7 +16,6 @@ export interface CLIContext { getWalletAddress(): Address; getServerClient(): Promise; hasAccount(): boolean; - requiresAccountSetup(): boolean; } export function createContext(config: Config): CLIContext { @@ -39,19 +39,22 @@ export function createContext(config: Config): CLIContext { getWalletClient(): ExchangeClient { if (!walletClient) { - if (!config.privateKey) { - if (config.account?.type === "readonly") { - throw new Error( - `Account "${config.account.alias}" is read-only and cannot perform trading operations.\n` + - "Run 'hl account add' to set up an API wallet for trading." - ); - } + if (config.account?.type === "walletconnect" && config.cwpProvider && config.walletAddress) { + const wallet = createCwpWallet(config.cwpProvider, config.walletAddress); + walletClient = new ExchangeClient({ transport, wallet }); + } else if (config.privateKey) { + const account = privateKeyToAccount(config.privateKey as Hex); + walletClient = new ExchangeClient({ transport, wallet: account }); + } else if (config.account?.type === "readonly") { + throw new Error( + `Account "${config.account.alias}" is read-only and cannot perform trading operations.\n` + + "Run 'hl account add' to set up an API wallet for trading." + ); + } else { throw new Error( "No account configured. Run 'hl account add' to set up your account." ); } - const account = privateKeyToAccount(config.privateKey as Hex); - walletClient = new ExchangeClient({ transport, wallet: account }); } return walletClient; }, @@ -82,9 +85,5 @@ export function createContext(config: Config): CLIContext { hasAccount(): boolean { return !!(config.walletAddress || config.privateKey); }, - - requiresAccountSetup(): boolean { - return !config.walletAddress && !config.privateKey; - }, }; } diff --git a/src/commands/account/add.ts b/src/commands/account/add.ts index 8da926f..57943a8 100644 --- a/src/commands/account/add.ts +++ b/src/commands/account/add.ts @@ -6,11 +6,13 @@ import { validateAddress } from "../../lib/validation.js" import { prompt, select, confirm, pressEnterOrEsc } from "../../lib/prompts.js" import { createAccount, getAccountCount, isAliasTaken } from "../../lib/db/index.js" import { validateApiKey } from "../../lib/api-wallet.js" +import { isCwpAvailable, connectCwp } from "../../lib/cwp.js" import type { Hex } from "viem" const REFERRAL_LINK = "https://app.hyperliquid.xyz/join/CHRISLING" +const CWP_BINARY = "walletconnect" -type SetupMethod = "existing" | "new" | "readonly" +type SetupMethod = "existing" | "new" | "readonly" | "walletconnect" export function registerAddCommand(account: Command): void { account @@ -36,6 +38,11 @@ export function registerAddCommand(account: Command): void { label: "Create new wallet", description: "Generate a new wallet with encrypted keystore (Coming Soon)", }, + { + value: "walletconnect", + label: "Connect via WalletConnect", + description: "Scan QR code with mobile wallet — no private key needed", + }, { value: "readonly", label: "Add read-only account", @@ -51,6 +58,8 @@ export function registerAddCommand(account: Command): void { if (setupMethod === "existing") { await handleExistingWallet(isTestnet, outputOpts) + } else if (setupMethod === "walletconnect") { + await handleWalletConnect(outputOpts) } else { await handleReadOnly(outputOpts) } @@ -193,6 +202,60 @@ async function handleReadOnly(outputOpts: { json: boolean }): Promise { } } +async function handleWalletConnect(outputOpts: { json: boolean }): Promise { + // Step 1: Check if walletconnect binary is available + const available = await isCwpAvailable(CWP_BINARY) + if (!available) { + throw new Error( + `"${CWP_BINARY}" CLI not found on PATH.\n` + + "Install it with: npm install -g @anthropic/walletconnect-cli", + ) + } + + // Step 2: Connect via WalletConnect (QR code displayed in terminal) + console.log("\nStarting WalletConnect session...\n") + const userAddress = await connectCwp(CWP_BINARY) + console.log(`\nConnected wallet: ${userAddress.slice(0, 6)}...${userAddress.slice(-4)}`) + + // Step 3: Get alias + const alias = await promptForAlias() + + // Step 4: Check if user wants to set as default + let setAsDefault = false + const existingCount = getAccountCount() + + if (existingCount > 0) { + setAsDefault = await confirm("\nSet this as your default account?", true) + } + + // Step 5: Save account + const newAccount = createAccount({ + alias, + userAddress, + type: "walletconnect", + source: "cli_import", + cwpProvider: CWP_BINARY, + setAsDefault, + }) + + if (outputOpts.json) { + output(newAccount, outputOpts) + } else { + console.log("") + outputSuccess(`Account "${alias}" added successfully!`) + console.log("") + console.log("Account details:") + console.log(` Alias: ${newAccount.alias}`) + console.log(` Address: ${newAccount.userAddress}`) + console.log(` Type: ${newAccount.type}`) + console.log(` Provider: ${newAccount.cwpProvider}`) + console.log(` Default: ${newAccount.isDefault ? "Yes" : "No"}`) + console.log("") + console.log("Trading orders will require approval on your mobile wallet.") + console.log("") + } +} + async function promptForAlias(): Promise { while (true) { const alias = await prompt("Enter an alias for this account (e.g., 'main', 'trading'): ") diff --git a/src/commands/account/ls.tsx b/src/commands/account/ls.tsx index 4cfbac6..6008638 100644 --- a/src/commands/account/ls.tsx +++ b/src/commands/account/ls.tsx @@ -12,6 +12,7 @@ interface AccountRow { address: string type: string apiWallet: string + provider: string default: string } @@ -36,15 +37,19 @@ function AccountsList({ accounts }: { accounts: Account[] }): React.ReactElement apiWallet: acc.apiWalletPublicKey ? formatAddress(acc.apiWalletPublicKey) : "-", + provider: acc.cwpProvider || "-", default: acc.isDefault ? "*" : "", })) + const hasWalletConnect = accounts.some((acc) => acc.type === "walletconnect") + const columns: Column[] = [ { key: "default", header: "", width: 2 }, { key: "alias", header: "Alias" }, { key: "address", header: "Address" }, { key: "type", header: "Type" }, { key: "apiWallet", header: "API Wallet" }, + ...(hasWalletConnect ? [{ key: "provider" as const, header: "Provider" }] : []), ] return ( @@ -75,6 +80,7 @@ export function registerLsCommand(account: Command): void { type: acc.type, source: acc.source, apiWalletPublicKey: acc.apiWalletPublicKey, + cwpProvider: acc.cwpProvider, isDefault: acc.isDefault, createdAt: acc.createdAt, updatedAt: acc.updatedAt, diff --git a/src/commands/fund.ts b/src/commands/fund.ts new file mode 100644 index 0000000..51c2776 --- /dev/null +++ b/src/commands/fund.ts @@ -0,0 +1,189 @@ +import { Command } from "commander" +import { getContext, getOutputOptions } from "../cli/program.js" +import { output, outputError, outputSuccess } from "../cli/output.js" +import { cwpExec } from "../lib/cwp.js" + +// Arbitrum USDC (native) +const ARB_USDC = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831" +const ARB_USDC_TESTNET = "0x1baAbB04529D43a73232B713C0FE471f7c7334d5" + +// Hyperliquid Bridge2 contract on Arbitrum +const BRIDGE_MAINNET = "0x2df1c51e09aecf9cacb7bc98cb1742757f163df7" +const BRIDGE_TESTNET = "0x08cfc1B6b2dCF36A1480b99353A354AA8AC56f89" + +const ARB_CHAIN = "eip155:42161" +const ARB_CHAIN_TESTNET = "eip155:421614" +const ARB_RPC = "https://arb1.arbitrum.io/rpc" +const ARB_RPC_TESTNET = "https://sepolia-rollup.arbitrum.io/rpc" + +interface TxReceipt { status: string; gasUsed: string; transactionHash: string } + +/** Pad an address to 32-byte ABI-encoded form */ +function padAddress(addr: string): string { + return addr.slice(2).toLowerCase().padStart(64, "0") +} + +async function rpcCall(rpcUrl: string, method: string, params: unknown[]): Promise { + const res = await fetch(rpcUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }), + }) + if (!res.ok) { + throw new Error(`RPC request failed: ${res.status} ${res.statusText}`) + } + const json = (await res.json()) as { result?: unknown; error?: { code: number; message: string } } + if (json.error) { + throw new Error(`RPC error: ${json.error.message} (code ${json.error.code})`) + } + return json.result +} + +async function getUsdcBalance(rpc: string, usdcAddress: string, walletAddress: string): Promise { + // balanceOf(address) = 0x70a08231 + const data = "0x70a08231" + padAddress(walletAddress) + const result = await rpcCall(rpc, "eth_call", [{ to: usdcAddress, data }, "latest"]) as string + return parseInt(result, 16) / 1e6 +} + +async function pollReceipt(rpc: string, txHash: string, timeoutMs = 60_000): Promise { + const deadline = Date.now() + timeoutMs + while (Date.now() < deadline) { + try { + const receipt = await rpcCall(rpc, "eth_getTransactionReceipt", [txHash]) as TxReceipt | null + if (receipt) return receipt + } catch { /* retry */ } + await new Promise((r) => setTimeout(r, 2_000)) + } + return null +} + +/** + * Send a transaction via CWP binary, poll for receipt, and verify success. + * Returns the confirmed transaction hash. + */ +async function sendAndConfirm( + binary: string, + rpc: string, + tx: { to: string; data: string; value: string; gas: string; chainId: string }, + label: string, +): Promise { + const result = await cwpExec(binary, ["send-transaction", JSON.stringify(tx)], 120_000) as + { transactionHash?: string } + + const txHash = result?.transactionHash + if (!txHash) { + throw new Error(`${label} failed — no transaction hash returned.`) + } + console.log(` Tx: ${txHash}`) + console.log(" Waiting for confirmation...") + + const receipt = await pollReceipt(rpc, txHash) + if (!receipt) { + throw new Error(`${label} timed out waiting for receipt.`) + } + if (receipt.status !== "0x1") { + throw new Error(`${label} reverted on-chain.\n Tx: ${txHash}`) + } + console.log(" Confirmed!") + return txHash +} + +export function registerFundCommand(program: Command): void { + program + .command("fund ") + .description("Deposit USDC into Hyperliquid (requires WalletConnect account)") + .action(async function (this: Command, amountStr: string) { + const ctx = getContext(this) + const outputOpts = getOutputOptions(this) + + try { + if (ctx.config.account?.type !== "walletconnect") { + throw new Error( + "The 'fund' command requires a WalletConnect account.\n" + + "Run 'hl account add' and select 'Connect via WalletConnect'.", + ) + } + + if (!ctx.config.cwpProvider) { + throw new Error("No WalletConnect provider configured for this account.") + } + + const amount = parseFloat(amountStr) + if (isNaN(amount) || amount < 5) { + throw new Error("Minimum deposit is 5 USDC. Amounts below 5 USDC will be lost.") + } + + const isTestnet = ctx.config.testnet + const usdcAddress = isTestnet ? ARB_USDC_TESTNET : ARB_USDC + const bridgeAddress = isTestnet ? BRIDGE_TESTNET : BRIDGE_MAINNET + const rpc = isTestnet ? ARB_RPC_TESTNET : ARB_RPC + const chainId = isTestnet ? ARB_CHAIN_TESTNET : ARB_CHAIN + const binary = ctx.config.cwpProvider + const walletAddress = ctx.config.walletAddress + if (!walletAddress) { + throw new Error("No wallet address configured.") + } + + // Pre-flight: check USDC balance on Arbitrum + console.log("\nChecking USDC balance on Arbitrum...") + const balance = await getUsdcBalance(rpc, usdcAddress, walletAddress) + console.log(` Balance: ${balance.toFixed(2)} USDC`) + + if (balance < amount) { + throw new Error( + balance < 5 + ? `Insufficient USDC on Arbitrum (${balance.toFixed(2)} USDC).\n` + + "You need at least 5 USDC on Arbitrum to deposit.\n" + + "Use 'walletconnect swidge' to bridge USDC to Arbitrum first." + : `Insufficient USDC on Arbitrum: ${balance.toFixed(2)} USDC available, ${amount} USDC requested.\n` + + "Use 'walletconnect swidge' to bridge more USDC to Arbitrum.", + ) + } + + // USDC has 6 decimals + const amountWei = Math.round(amount * 1e6) + const amountHex = amountWei.toString(16).padStart(64, "0") + const bridgePadded = padAddress(bridgeAddress) + + console.log(`\nDepositing ${amount} USDC into Hyperliquid...\n`) + + const baseTx = { to: usdcAddress, value: "0x0", gas: "0x186a0", chainId } + + // Step 1: Approve USDC to bridge contract + // approve(address spender, uint256 amount) = 0x095ea7b3 + console.log("Step 1/2: Approving USDC transfer...") + const approveHash = await sendAndConfirm(binary, rpc, { + ...baseTx, + data: "0x095ea7b3" + bridgePadded + amountHex, + }, "Approval transaction") + + // Step 2: Transfer USDC to bridge contract + // transfer(address to, uint256 amount) = 0xa9059cbb + console.log("\nStep 2/2: Sending USDC to Hyperliquid bridge...") + const txHash = await sendAndConfirm(binary, rpc, { + ...baseTx, + data: "0xa9059cbb" + bridgePadded + amountHex, + }, "Transfer transaction") + + if (outputOpts.json) { + output({ + status: "deposited", + amount, + approveTxHash: approveHash, + transferTxHash: txHash, + bridge: bridgeAddress, + }, outputOpts) + } else { + console.log("") + outputSuccess(`Deposited ${amount} USDC to Hyperliquid!`) + console.log(` Tx: ${txHash}`) + console.log(" Funds will appear in your account within ~1 minute.") + console.log("") + } + } catch (err: unknown) { + outputError(err instanceof Error ? err.message : String(err)) + process.exit(1) + } + }) +} diff --git a/src/commands/index.ts b/src/commands/index.ts index d8f3556..0b386e9 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -5,12 +5,16 @@ import { registerAssetCommands } from "./asset/index.js" import { registerOrderCommands } from "./order/index.js" import { registerServerCommands } from "./server.js" import { registerUpgradeCommand } from "./upgrade.js" +import { registerFundCommand } from "./fund.js" +import { registerWithdrawCommand } from "./withdraw.js" export function registerCommands(program: Command): void { registerAccountCommands(program) registerMarketsCommands(program) registerAssetCommands(program) registerOrderCommands(program) + registerFundCommand(program) + registerWithdrawCommand(program) registerServerCommands(program) registerUpgradeCommand(program) } diff --git a/src/commands/withdraw.ts b/src/commands/withdraw.ts new file mode 100644 index 0000000..effc620 --- /dev/null +++ b/src/commands/withdraw.ts @@ -0,0 +1,56 @@ +import { Command } from "commander" +import { getContext, getOutputOptions } from "../cli/program.js" +import { output, outputError, outputSuccess } from "../cli/output.js" +import { validateAddress } from "../lib/validation.js" + +export function registerWithdrawCommand(program: Command): void { + program + .command("withdraw ") + .description("Withdraw USDC from Hyperliquid to your wallet on Arbitrum") + .option("-d, --destination
", "Destination address (defaults to your wallet address)") + .action(async function (this: Command, amountStr: string) { + const ctx = getContext(this) + const outputOpts = getOutputOptions(this) + + try { + const amount = parseFloat(amountStr) + if (isNaN(amount) || amount <= 0) { + throw new Error("Amount must be a positive number.") + } + + const rawDestination = this.opts().destination || ctx.config.walletAddress + if (!rawDestination) { + throw new Error( + "No destination address. Specify one with --destination or set up an account first.", + ) + } + const destination = validateAddress(rawDestination) + + const client = ctx.getWalletClient() + + console.log(`\nWithdrawing ${amount} USDC to ${destination}...\n`) + + const result = await client.withdraw3({ + destination, + amount: amount.toString(), + }) + + if (outputOpts.json) { + output({ + status: "withdrawn", + amount, + destination, + response: result, + }, outputOpts) + } else { + outputSuccess(`Withdrew ${amount} USDC from Hyperliquid!`) + console.log(` Destination: ${destination}`) + console.log(" Funds will arrive on Arbitrum within ~1 minute.") + console.log("") + } + } catch (err: unknown) { + outputError(err instanceof Error ? err.message : String(err)) + process.exit(1) + } + }) +} diff --git a/src/lib/config.ts b/src/lib/config.ts index 4354675..51d1b69 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -1,15 +1,16 @@ import { privateKeyToAccount } from "viem/accounts"; import type { Hex, Address } from "viem"; -import { getDefaultAccount, type Account } from "./db/index.js"; +import { getDefaultAccount, type Account, type AccountType } from "./db/index.js"; export interface Config { privateKey?: Hex; walletAddress?: Address; testnet: boolean; + cwpProvider?: string; // Account info if loaded from database account?: { alias: string; - type: "readonly" | "api_wallet"; + type: AccountType; }; } @@ -27,6 +28,7 @@ export function loadConfig(testnet: boolean): Config { privateKey: defaultAccount.apiWalletPrivateKey || undefined, walletAddress: defaultAccount.userAddress, testnet, + cwpProvider: defaultAccount.cwpProvider || undefined, account: { alias: defaultAccount.alias, type: defaultAccount.type, diff --git a/src/lib/cwp.ts b/src/lib/cwp.ts new file mode 100644 index 0000000..27b716c --- /dev/null +++ b/src/lib/cwp.ts @@ -0,0 +1,141 @@ +import { execFile, spawn } from "node:child_process" +import { promisify } from "node:util" +import type { Address } from "viem" +import type { AbstractViemJsonRpcAccount } from "@nktkas/hyperliquid/signing" + +const execFileAsync = promisify(execFile) + +/** + * Execute a CWP binary command and return parsed JSON output + */ +export async function cwpExec( + binary: string, + args: string[], + timeout = 30_000, +): Promise { + try { + const { stdout } = await execFileAsync(binary, args, { + timeout, + encoding: "utf-8", + }) + return JSON.parse(stdout.trim()) + } catch (err: unknown) { + if (err && typeof err === "object" && "code" in err && err.code === "ENOENT") { + throw new Error( + `"${binary}" not found. Install it with: npm install -g @anthropic/walletconnect-cli\n` + + "See: https://github.com/anthropic/walletconnect-cli", + ) + } + if (err && typeof err === "object" && "killed" in err && err.killed) { + throw new Error( + `WalletConnect request timed out after ${Math.round(timeout / 1000)}s. ` + + "Check your phone and try again.", + ) + } + throw err + } +} + +/** + * Check if the CWP binary is available on PATH + */ +export async function isCwpAvailable(binary: string): Promise { + try { + await execFileAsync(binary, ["--version"], { timeout: 5_000 }) + return true + } catch { + return false + } +} + +/** + * Connect to a wallet via CWP (shows QR code in terminal) + * Spawns the connect command with inherited stdio so the QR code is visible, + * then reads the connected address via whoami. + */ +export async function connectCwp(binary: string): Promise
{ + // Spawn connect with inherited stdio so QR code renders in terminal + // 5 minute timeout — user needs time to scan QR and approve on phone + await new Promise((resolve, reject) => { + const child = spawn(binary, ["connect"], { stdio: "inherit", timeout: 300_000 }) + child.on("close", (code) => { + if (code === 0) { + resolve() + } else { + reject(new Error(`"${binary} connect" exited with code ${code}`)) + } + }) + child.on("error", (err) => { + if ("code" in err && err.code === "ENOENT") { + reject(new Error( + `"${binary}" not found. Install it with: npm install -g @anthropic/walletconnect-cli`, + )) + } else { + reject(err) + } + }) + }) + + // Read connected address — whoami returns { accounts: [{ chain, address }] } + const result = await cwpExec(binary, ["whoami", "--json"]) as { + accounts: { chain: string; address: string }[] + } + const address = result.accounts?.[0]?.address + if (!address) { + throw new Error("Failed to read wallet address after WalletConnect connection") + } + return address as Address +} + +/** + * Create a CWP wallet adapter that satisfies AbstractViemJsonRpcAccount. + * + * The adapter proxies signTypedData calls to the CWP binary subprocess. + * The signTypedData function must have .length === 1 to pass the SDK's + * valibot schema check for viem JSON-RPC accounts. + */ +export function createCwpWallet( + binary: string, + address: Address, +): AbstractViemJsonRpcAccount { + return { + // Single-param function — .length === 1 satisfies valibot check + async signTypedData(params: { + domain: { + name: string + version: string + chainId: number + verifyingContract: `0x${string}` + } + types: { + [key: string]: { name: string; type: string }[] + } + primaryType: string + message: Record + }): Promise<`0x${string}`> { + const payload = JSON.stringify({ + domain: params.domain, + types: params.types, + primaryType: params.primaryType, + message: params.message, + }) + + // 120s timeout — user needs to approve on phone + const result = await cwpExec(binary, ["sign-typed-data", payload], 120_000) + const sig = result as { signature: string } + if (!sig.signature) { + throw new Error("CWP sign-typed-data returned no signature") + } + return sig.signature as `0x${string}` + }, + + async getAddresses(): Promise<`0x${string}`[]> { + return [address as `0x${string}`] + }, + + async getChainId(): Promise { + // Arbitrum One — the SDK overrides chainId for L1 actions anyway + return 42161 + }, + } +} diff --git a/src/lib/db/accounts.ts b/src/lib/db/accounts.ts index 907c476..6d0e042 100644 --- a/src/lib/db/accounts.ts +++ b/src/lib/db/accounts.ts @@ -4,7 +4,7 @@ import { getDb } from "./index.js" /** * Account types */ -export type AccountType = "readonly" | "api_wallet" +export type AccountType = "readonly" | "api_wallet" | "walletconnect" export type AccountSource = "cli_import" // Future: "web", etc. /** @@ -18,6 +18,7 @@ export interface Account { source: AccountSource apiWalletPrivateKey: Hex | null apiWalletPublicKey: Address | null + cwpProvider: string | null isDefault: boolean createdAt: number updatedAt: number @@ -34,6 +35,7 @@ interface AccountRow { source: string api_wallet_private_key: string | null api_wallet_public_key: string | null + cwp_provider: string | null is_default: number created_at: number updated_at: number @@ -51,6 +53,7 @@ function rowToAccount(row: AccountRow): Account { source: row.source as AccountSource, apiWalletPrivateKey: row.api_wallet_private_key as Hex | null, apiWalletPublicKey: row.api_wallet_public_key as Address | null, + cwpProvider: row.cwp_provider, isDefault: row.is_default === 1, createdAt: row.created_at, updatedAt: row.updated_at, @@ -67,6 +70,7 @@ export interface CreateAccountInput { source?: AccountSource apiWalletPrivateKey?: Hex apiWalletPublicKey?: Address + cwpProvider?: string setAsDefault?: boolean } @@ -93,8 +97,9 @@ export function createAccount(input: CreateAccountInput): Account { source, api_wallet_private_key, api_wallet_public_key, + cwp_provider, is_default - ) VALUES (?, ?, ?, ?, ?, ?, ?) + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `).run( input.alias, input.userAddress, @@ -102,6 +107,7 @@ export function createAccount(input: CreateAccountInput): Account { input.source || "cli_import", input.apiWalletPrivateKey || null, input.apiWalletPublicKey || null, + input.cwpProvider || null, shouldBeDefault ? 1 : 0 ) diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index 28e7774..6af858a 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -73,6 +73,42 @@ function runMigrations(db: Database.Database): void { CREATE INDEX IF NOT EXISTS idx_accounts_user_address ON accounts(user_address); `, }, + { + name: "002_add_walletconnect_type", + sql: ` + -- SQLite doesn't support ALTER CONSTRAINT, so recreate the table + CREATE TABLE accounts_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + alias TEXT NOT NULL UNIQUE, + user_address TEXT NOT NULL, + type TEXT NOT NULL CHECK (type IN ('readonly', 'api_wallet', 'walletconnect')), + source TEXT NOT NULL DEFAULT 'cli_import', + api_wallet_private_key TEXT, + api_wallet_public_key TEXT, + cwp_provider TEXT, + is_default INTEGER NOT NULL DEFAULT 0, + created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')), + updated_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now')) + ); + + INSERT INTO accounts_new ( + id, alias, user_address, type, source, + api_wallet_private_key, api_wallet_public_key, + is_default, created_at, updated_at + ) + SELECT + id, alias, user_address, type, source, + api_wallet_private_key, api_wallet_public_key, + is_default, created_at, updated_at + FROM accounts; + + DROP TABLE accounts; + ALTER TABLE accounts_new RENAME TO accounts; + + CREATE INDEX IF NOT EXISTS idx_accounts_is_default ON accounts(is_default); + CREATE INDEX IF NOT EXISTS idx_accounts_user_address ON accounts(user_address); + `, + }, ] const appliedMigrations = db