@echoclaw/echo-0g 1.0.0
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 +1175 -0
- package/dist/0g-compute/account.d.ts +36 -0
- package/dist/0g-compute/account.d.ts.map +1 -0
- package/dist/0g-compute/account.js +85 -0
- package/dist/0g-compute/account.js.map +1 -0
- package/dist/0g-compute/bridge.d.ts +16 -0
- package/dist/0g-compute/bridge.d.ts.map +1 -0
- package/dist/0g-compute/bridge.js +40 -0
- package/dist/0g-compute/bridge.js.map +1 -0
- package/dist/0g-compute/broker-factory.d.ts +19 -0
- package/dist/0g-compute/broker-factory.d.ts.map +1 -0
- package/dist/0g-compute/broker-factory.js +65 -0
- package/dist/0g-compute/broker-factory.js.map +1 -0
- package/dist/0g-compute/constants.d.ts +10 -0
- package/dist/0g-compute/constants.d.ts.map +1 -0
- package/dist/0g-compute/constants.js +12 -0
- package/dist/0g-compute/constants.js.map +1 -0
- package/dist/0g-compute/monitor.d.ts +43 -0
- package/dist/0g-compute/monitor.d.ts.map +1 -0
- package/dist/0g-compute/monitor.js +302 -0
- package/dist/0g-compute/monitor.js.map +1 -0
- package/dist/0g-compute/pricing.d.ts +43 -0
- package/dist/0g-compute/pricing.d.ts.map +1 -0
- package/dist/0g-compute/pricing.js +53 -0
- package/dist/0g-compute/pricing.js.map +1 -0
- package/dist/0g-compute/sdk-bridge.cjs +17 -0
- package/dist/0g-compute/sdk-bridge.cjs.map +1 -0
- package/dist/0g-compute/sdk-bridge.d.cts +9 -0
- package/dist/0g-compute/sdk-bridge.d.cts.map +1 -0
- package/dist/0g-compute/smoke-test.d.ts +11 -0
- package/dist/0g-compute/smoke-test.d.ts.map +1 -0
- package/dist/0g-compute/smoke-test.js +172 -0
- package/dist/0g-compute/smoke-test.js.map +1 -0
- package/dist/bot/daemon.d.ts +34 -0
- package/dist/bot/daemon.d.ts.map +1 -0
- package/dist/bot/daemon.js +386 -0
- package/dist/bot/daemon.js.map +1 -0
- package/dist/bot/executor.d.ts +14238 -0
- package/dist/bot/executor.d.ts.map +1 -0
- package/dist/bot/executor.js +183 -0
- package/dist/bot/executor.js.map +1 -0
- package/dist/bot/nonce-queue.d.ts +20 -0
- package/dist/bot/nonce-queue.d.ts.map +1 -0
- package/dist/bot/nonce-queue.js +41 -0
- package/dist/bot/nonce-queue.js.map +1 -0
- package/dist/bot/notify.d.ts +15 -0
- package/dist/bot/notify.d.ts.map +1 -0
- package/dist/bot/notify.js +98 -0
- package/dist/bot/notify.js.map +1 -0
- package/dist/bot/orders.d.ts +30 -0
- package/dist/bot/orders.d.ts.map +1 -0
- package/dist/bot/orders.js +172 -0
- package/dist/bot/orders.js.map +1 -0
- package/dist/bot/state.d.ts +14 -0
- package/dist/bot/state.d.ts.map +1 -0
- package/dist/bot/state.js +109 -0
- package/dist/bot/state.js.map +1 -0
- package/dist/bot/stream.d.ts +28 -0
- package/dist/bot/stream.d.ts.map +1 -0
- package/dist/bot/stream.js +96 -0
- package/dist/bot/stream.js.map +1 -0
- package/dist/bot/triggers.d.ts +17 -0
- package/dist/bot/triggers.d.ts.map +1 -0
- package/dist/bot/triggers.js +95 -0
- package/dist/bot/triggers.js.map +1 -0
- package/dist/bot/types.d.ts +199 -0
- package/dist/bot/types.d.ts.map +1 -0
- package/dist/bot/types.js +12 -0
- package/dist/bot/types.js.map +1 -0
- package/dist/chainscan/client.d.ts +28 -0
- package/dist/chainscan/client.d.ts.map +1 -0
- package/dist/chainscan/client.js +361 -0
- package/dist/chainscan/client.js.map +1 -0
- package/dist/chainscan/constants.d.ts +15 -0
- package/dist/chainscan/constants.d.ts.map +1 -0
- package/dist/chainscan/constants.js +15 -0
- package/dist/chainscan/constants.js.map +1 -0
- package/dist/chainscan/types.d.ts +148 -0
- package/dist/chainscan/types.d.ts.map +1 -0
- package/dist/chainscan/types.js +2 -0
- package/dist/chainscan/types.js.map +1 -0
- package/dist/chainscan/validation.d.ts +35 -0
- package/dist/chainscan/validation.d.ts.map +1 -0
- package/dist/chainscan/validation.js +97 -0
- package/dist/chainscan/validation.js.map +1 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +328 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/0g-compute.d.ts +21 -0
- package/dist/commands/0g-compute.d.ts.map +1 -0
- package/dist/commands/0g-compute.js +850 -0
- package/dist/commands/0g-compute.js.map +1 -0
- package/dist/commands/chainscan.d.ts +17 -0
- package/dist/commands/chainscan.d.ts.map +1 -0
- package/dist/commands/chainscan.js +605 -0
- package/dist/commands/chainscan.js.map +1 -0
- package/dist/commands/config.d.ts +3 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +251 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/echobook.d.ts +17 -0
- package/dist/commands/echobook.d.ts.map +1 -0
- package/dist/commands/echobook.js +905 -0
- package/dist/commands/echobook.js.map +1 -0
- package/dist/commands/jaine-subgraph.d.ts +3 -0
- package/dist/commands/jaine-subgraph.d.ts.map +1 -0
- package/dist/commands/jaine-subgraph.js +565 -0
- package/dist/commands/jaine-subgraph.js.map +1 -0
- package/dist/commands/jaine.d.ts +3 -0
- package/dist/commands/jaine.d.ts.map +1 -0
- package/dist/commands/jaine.js +1415 -0
- package/dist/commands/jaine.js.map +1 -0
- package/dist/commands/marketmaker.d.ts +6 -0
- package/dist/commands/marketmaker.d.ts.map +1 -0
- package/dist/commands/marketmaker.js +451 -0
- package/dist/commands/marketmaker.js.map +1 -0
- package/dist/commands/send.d.ts +3 -0
- package/dist/commands/send.d.ts.map +1 -0
- package/dist/commands/send.js +229 -0
- package/dist/commands/send.js.map +1 -0
- package/dist/commands/setup.d.ts +3 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +263 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/slop-app.d.ts +9 -0
- package/dist/commands/slop-app.d.ts.map +1 -0
- package/dist/commands/slop-app.js +708 -0
- package/dist/commands/slop-app.js.map +1 -0
- package/dist/commands/slop-stream.d.ts +9 -0
- package/dist/commands/slop-stream.d.ts.map +1 -0
- package/dist/commands/slop-stream.js +99 -0
- package/dist/commands/slop-stream.js.map +1 -0
- package/dist/commands/slop.d.ts +3 -0
- package/dist/commands/slop.d.ts.map +1 -0
- package/dist/commands/slop.js +1053 -0
- package/dist/commands/slop.js.map +1 -0
- package/dist/commands/wallet.d.ts +13 -0
- package/dist/commands/wallet.d.ts.map +1 -0
- package/dist/commands/wallet.js +748 -0
- package/dist/commands/wallet.js.map +1 -0
- package/dist/config/paths.d.ts +13 -0
- package/dist/config/paths.d.ts.map +1 -0
- package/dist/config/paths.js +33 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/config/store.d.ts +48 -0
- package/dist/config/store.d.ts.map +1 -0
- package/dist/config/store.js +113 -0
- package/dist/config/store.js.map +1 -0
- package/dist/constants/chain.d.ts +57 -0
- package/dist/constants/chain.d.ts.map +1 -0
- package/dist/constants/chain.js +51 -0
- package/dist/constants/chain.js.map +1 -0
- package/dist/echobook/api.d.ts +38 -0
- package/dist/echobook/api.d.ts.map +1 -0
- package/dist/echobook/api.js +86 -0
- package/dist/echobook/api.js.map +1 -0
- package/dist/echobook/auth.d.ts +31 -0
- package/dist/echobook/auth.d.ts.map +1 -0
- package/dist/echobook/auth.js +93 -0
- package/dist/echobook/auth.js.map +1 -0
- package/dist/echobook/comments.d.ts +26 -0
- package/dist/echobook/comments.d.ts.map +1 -0
- package/dist/echobook/comments.js +20 -0
- package/dist/echobook/comments.js.map +1 -0
- package/dist/echobook/follows.d.ts +19 -0
- package/dist/echobook/follows.d.ts.map +1 -0
- package/dist/echobook/follows.js +21 -0
- package/dist/echobook/follows.js.map +1 -0
- package/dist/echobook/jwtCache.d.ts +15 -0
- package/dist/echobook/jwtCache.d.ts.map +1 -0
- package/dist/echobook/jwtCache.js +63 -0
- package/dist/echobook/jwtCache.js.map +1 -0
- package/dist/echobook/notifications.d.ts +30 -0
- package/dist/echobook/notifications.d.ts.map +1 -0
- package/dist/echobook/notifications.js +26 -0
- package/dist/echobook/notifications.js.map +1 -0
- package/dist/echobook/points.d.ts +35 -0
- package/dist/echobook/points.d.ts.map +1 -0
- package/dist/echobook/points.js +20 -0
- package/dist/echobook/points.js.map +1 -0
- package/dist/echobook/posts.d.ts +46 -0
- package/dist/echobook/posts.d.ts.map +1 -0
- package/dist/echobook/posts.js +43 -0
- package/dist/echobook/posts.js.map +1 -0
- package/dist/echobook/profile.d.ts +29 -0
- package/dist/echobook/profile.d.ts.map +1 -0
- package/dist/echobook/profile.js +14 -0
- package/dist/echobook/profile.js.map +1 -0
- package/dist/echobook/submolts.d.ts +22 -0
- package/dist/echobook/submolts.d.ts.map +1 -0
- package/dist/echobook/submolts.js +24 -0
- package/dist/echobook/submolts.js.map +1 -0
- package/dist/echobook/tradeProof.d.ts +21 -0
- package/dist/echobook/tradeProof.d.ts.map +1 -0
- package/dist/echobook/tradeProof.js +14 -0
- package/dist/echobook/tradeProof.js.map +1 -0
- package/dist/echobook/votes.d.ts +17 -0
- package/dist/echobook/votes.d.ts.map +1 -0
- package/dist/echobook/votes.js +20 -0
- package/dist/echobook/votes.js.map +1 -0
- package/dist/errors.d.ts +125 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +147 -0
- package/dist/errors.js.map +1 -0
- package/dist/intents/store.d.ts +22 -0
- package/dist/intents/store.d.ts.map +1 -0
- package/dist/intents/store.js +76 -0
- package/dist/intents/store.js.map +1 -0
- package/dist/intents/types.d.ts +21 -0
- package/dist/intents/types.d.ts.map +1 -0
- package/dist/intents/types.js +2 -0
- package/dist/intents/types.js.map +1 -0
- package/dist/jaine/abi/erc20.d.ts +90 -0
- package/dist/jaine/abi/erc20.d.ts.map +1 -0
- package/dist/jaine/abi/erc20.js +65 -0
- package/dist/jaine/abi/erc20.js.map +1 -0
- package/dist/jaine/abi/factory.d.ts +38 -0
- package/dist/jaine/abi/factory.d.ts.map +1 -0
- package/dist/jaine/abi/factory.js +26 -0
- package/dist/jaine/abi/factory.js.map +1 -0
- package/dist/jaine/abi/index.d.ts +11 -0
- package/dist/jaine/abi/index.d.ts.map +1 -0
- package/dist/jaine/abi/index.js +11 -0
- package/dist/jaine/abi/index.js.map +1 -0
- package/dist/jaine/abi/nftManager.d.ts +282 -0
- package/dist/jaine/abi/nftManager.d.ts.map +1 -0
- package/dist/jaine/abi/nftManager.js +182 -0
- package/dist/jaine/abi/nftManager.js.map +1 -0
- package/dist/jaine/abi/pool.d.ts +77 -0
- package/dist/jaine/abi/pool.d.ts.map +1 -0
- package/dist/jaine/abi/pool.js +56 -0
- package/dist/jaine/abi/pool.js.map +1 -0
- package/dist/jaine/abi/quoter.d.ts +84 -0
- package/dist/jaine/abi/quoter.d.ts.map +1 -0
- package/dist/jaine/abi/quoter.js +53 -0
- package/dist/jaine/abi/quoter.js.map +1 -0
- package/dist/jaine/abi/router.d.ts +135 -0
- package/dist/jaine/abi/router.d.ts.map +1 -0
- package/dist/jaine/abi/router.js +88 -0
- package/dist/jaine/abi/router.js.map +1 -0
- package/dist/jaine/abi/w0g.d.ts +41 -0
- package/dist/jaine/abi/w0g.d.ts.map +1 -0
- package/dist/jaine/abi/w0g.js +34 -0
- package/dist/jaine/abi/w0g.js.map +1 -0
- package/dist/jaine/allowance.d.ts +48 -0
- package/dist/jaine/allowance.d.ts.map +1 -0
- package/dist/jaine/allowance.js +192 -0
- package/dist/jaine/allowance.js.map +1 -0
- package/dist/jaine/coreTokens.d.ts +32 -0
- package/dist/jaine/coreTokens.d.ts.map +1 -0
- package/dist/jaine/coreTokens.js +91 -0
- package/dist/jaine/coreTokens.js.map +1 -0
- package/dist/jaine/pathEncoding.d.ts +39 -0
- package/dist/jaine/pathEncoding.d.ts.map +1 -0
- package/dist/jaine/pathEncoding.js +98 -0
- package/dist/jaine/pathEncoding.js.map +1 -0
- package/dist/jaine/paths.d.ts +11 -0
- package/dist/jaine/paths.d.ts.map +1 -0
- package/dist/jaine/paths.js +20 -0
- package/dist/jaine/paths.js.map +1 -0
- package/dist/jaine/poolCache.d.ts +47 -0
- package/dist/jaine/poolCache.d.ts.map +1 -0
- package/dist/jaine/poolCache.js +195 -0
- package/dist/jaine/poolCache.js.map +1 -0
- package/dist/jaine/routing.d.ts +41 -0
- package/dist/jaine/routing.d.ts.map +1 -0
- package/dist/jaine/routing.js +247 -0
- package/dist/jaine/routing.js.map +1 -0
- package/dist/jaine/subgraph/client.d.ts +26 -0
- package/dist/jaine/subgraph/client.d.ts.map +1 -0
- package/dist/jaine/subgraph/client.js +201 -0
- package/dist/jaine/subgraph/client.js.map +1 -0
- package/dist/jaine/subgraph/constants.d.ts +9 -0
- package/dist/jaine/subgraph/constants.d.ts.map +1 -0
- package/dist/jaine/subgraph/constants.js +9 -0
- package/dist/jaine/subgraph/constants.js.map +1 -0
- package/dist/jaine/subgraph/queries.d.ts +21 -0
- package/dist/jaine/subgraph/queries.d.ts.map +1 -0
- package/dist/jaine/subgraph/queries.js +304 -0
- package/dist/jaine/subgraph/queries.js.map +1 -0
- package/dist/jaine/subgraph/types.d.ts +209 -0
- package/dist/jaine/subgraph/types.d.ts.map +1 -0
- package/dist/jaine/subgraph/types.js +7 -0
- package/dist/jaine/subgraph/types.js.map +1 -0
- package/dist/jaine/userTokens.d.ts +27 -0
- package/dist/jaine/userTokens.d.ts.map +1 -0
- package/dist/jaine/userTokens.js +89 -0
- package/dist/jaine/userTokens.js.map +1 -0
- package/dist/openclaw/config.d.ts +43 -0
- package/dist/openclaw/config.d.ts.map +1 -0
- package/dist/openclaw/config.js +231 -0
- package/dist/openclaw/config.js.map +1 -0
- package/dist/openclaw/hooks-client.d.ts +24 -0
- package/dist/openclaw/hooks-client.d.ts.map +1 -0
- package/dist/openclaw/hooks-client.js +119 -0
- package/dist/openclaw/hooks-client.js.map +1 -0
- package/dist/slop/abi/factory.d.ts +128 -0
- package/dist/slop/abi/factory.d.ts.map +1 -0
- package/dist/slop/abi/factory.js +70 -0
- package/dist/slop/abi/factory.js.map +1 -0
- package/dist/slop/abi/feeCollector.d.ts +95 -0
- package/dist/slop/abi/feeCollector.d.ts.map +1 -0
- package/dist/slop/abi/feeCollector.js +71 -0
- package/dist/slop/abi/feeCollector.js.map +1 -0
- package/dist/slop/abi/index.d.ts +5 -0
- package/dist/slop/abi/index.d.ts.map +1 -0
- package/dist/slop/abi/index.js +5 -0
- package/dist/slop/abi/index.js.map +1 -0
- package/dist/slop/abi/registry.d.ts +135 -0
- package/dist/slop/abi/registry.d.ts.map +1 -0
- package/dist/slop/abi/registry.js +90 -0
- package/dist/slop/abi/registry.js.map +1 -0
- package/dist/slop/abi/token.d.ts +320 -0
- package/dist/slop/abi/token.d.ts.map +1 -0
- package/dist/slop/abi/token.js +251 -0
- package/dist/slop/abi/token.js.map +1 -0
- package/dist/slop/auth.d.ts +19 -0
- package/dist/slop/auth.d.ts.map +1 -0
- package/dist/slop/auth.js +92 -0
- package/dist/slop/auth.js.map +1 -0
- package/dist/slop/jwtCache.d.ts +27 -0
- package/dist/slop/jwtCache.d.ts.map +1 -0
- package/dist/slop/jwtCache.js +91 -0
- package/dist/slop/jwtCache.js.map +1 -0
- package/dist/slop/quote.d.ts +80 -0
- package/dist/slop/quote.d.ts.map +1 -0
- package/dist/slop/quote.js +174 -0
- package/dist/slop/quote.js.map +1 -0
- package/dist/utils/canonicalJson.d.ts +8 -0
- package/dist/utils/canonicalJson.d.ts.map +1 -0
- package/dist/utils/canonicalJson.js +20 -0
- package/dist/utils/canonicalJson.js.map +1 -0
- package/dist/utils/env.d.ts +11 -0
- package/dist/utils/env.d.ts.map +1 -0
- package/dist/utils/env.js +20 -0
- package/dist/utils/env.js.map +1 -0
- package/dist/utils/http.d.ts +19 -0
- package/dist/utils/http.d.ts.map +1 -0
- package/dist/utils/http.js +61 -0
- package/dist/utils/http.js.map +1 -0
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +21 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/output.d.ts +29 -0
- package/dist/utils/output.d.ts.map +1 -0
- package/dist/utils/output.js +51 -0
- package/dist/utils/output.js.map +1 -0
- package/dist/utils/rateLimit.d.ts +22 -0
- package/dist/utils/rateLimit.d.ts.map +1 -0
- package/dist/utils/rateLimit.js +58 -0
- package/dist/utils/rateLimit.js.map +1 -0
- package/dist/utils/respond.d.ts +19 -0
- package/dist/utils/respond.d.ts.map +1 -0
- package/dist/utils/respond.js +25 -0
- package/dist/utils/respond.js.map +1 -0
- package/dist/utils/ui.d.ts +38 -0
- package/dist/utils/ui.d.ts.map +1 -0
- package/dist/utils/ui.js +126 -0
- package/dist/utils/ui.js.map +1 -0
- package/dist/wallet/client.d.ts +4 -0
- package/dist/wallet/client.d.ts.map +1 -0
- package/dist/wallet/client.js +53 -0
- package/dist/wallet/client.js.map +1 -0
- package/dist/wallet/keystore.d.ts +22 -0
- package/dist/wallet/keystore.d.ts.map +1 -0
- package/dist/wallet/keystore.js +111 -0
- package/dist/wallet/keystore.js.map +1 -0
- package/package.json +63 -0
- package/skills/echo/SKILL.md +1121 -0
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, cpSync, rmSync } from "node:fs";
|
|
2
|
+
import { join, dirname } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { platform } from "node:os";
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import { isAddress, getAddress } from "viem";
|
|
7
|
+
import { generatePrivateKey, privateKeyToAddress } from "viem/accounts";
|
|
8
|
+
import { loadConfig, saveConfig } from "../config/store.js";
|
|
9
|
+
import { CONFIG_DIR, BACKUPS_DIR } from "../config/paths.js";
|
|
10
|
+
import { getPublicClient } from "../wallet/client.js";
|
|
11
|
+
import { encryptPrivateKey, decryptPrivateKey, saveKeystore, loadKeystore, keystoreExists, normalizePrivateKey } from "../wallet/keystore.js";
|
|
12
|
+
import { ERC20_ABI } from "../constants/chain.js";
|
|
13
|
+
import { EchoError, ErrorCodes } from "../errors.js";
|
|
14
|
+
import { requireKeystorePassword, getKeystorePassword } from "../utils/env.js";
|
|
15
|
+
import { successBox, infoBox, warnBox, spinner, printTable, colors, formatAddress, formatBalance, } from "../utils/ui.js";
|
|
16
|
+
import { writeStdout, writeStderr, isHeadless, isStdoutTTY, writeJsonSuccess } from "../utils/output.js";
|
|
17
|
+
import logger from "../utils/logger.js";
|
|
18
|
+
const MAX_WATCHLIST_TOKENS = 200;
|
|
19
|
+
const MAX_BACKUPS = 20;
|
|
20
|
+
function getCLIVersion() {
|
|
21
|
+
try {
|
|
22
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const pkgPath = join(__dirname, "..", "..", "package.json");
|
|
24
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
25
|
+
return pkg.version ?? "unknown";
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return "unknown";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Create a backup of keystore.json and/or config.json.
|
|
33
|
+
* Returns backup path, or null if nothing to back up.
|
|
34
|
+
* Throws EchoError(AUTO_BACKUP_FAILED) on write failure.
|
|
35
|
+
*/
|
|
36
|
+
export async function autoBackup() {
|
|
37
|
+
const keystorePath = join(CONFIG_DIR, "keystore.json");
|
|
38
|
+
const configPath = join(CONFIG_DIR, "config.json");
|
|
39
|
+
const hasKeystore = existsSync(keystorePath);
|
|
40
|
+
const hasConfig = existsSync(configPath);
|
|
41
|
+
if (!hasKeystore && !hasConfig) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
mkdirSync(BACKUPS_DIR, { recursive: true });
|
|
46
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "").replace("Z", "Z");
|
|
47
|
+
const backupDir = join(BACKUPS_DIR, timestamp);
|
|
48
|
+
mkdirSync(backupDir, { recursive: true });
|
|
49
|
+
const files = [];
|
|
50
|
+
if (hasKeystore) {
|
|
51
|
+
cpSync(keystorePath, join(backupDir, "keystore.json"));
|
|
52
|
+
files.push("keystore.json");
|
|
53
|
+
}
|
|
54
|
+
if (hasConfig) {
|
|
55
|
+
cpSync(configPath, join(backupDir, "config.json"));
|
|
56
|
+
files.push("config.json");
|
|
57
|
+
}
|
|
58
|
+
const cfg = loadConfig();
|
|
59
|
+
const manifest = {
|
|
60
|
+
version: 1,
|
|
61
|
+
cliVersion: getCLIVersion(),
|
|
62
|
+
createdAt: new Date().toISOString(),
|
|
63
|
+
walletAddress: cfg.wallet.address ?? null,
|
|
64
|
+
chainId: cfg.chain.chainId,
|
|
65
|
+
files,
|
|
66
|
+
};
|
|
67
|
+
writeFileSync(join(backupDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf-8");
|
|
68
|
+
// Enforce retention: remove oldest if over MAX_BACKUPS
|
|
69
|
+
enforceBackupRetention();
|
|
70
|
+
logger.debug(`Auto-backup created at ${backupDir}`);
|
|
71
|
+
return backupDir;
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
throw new EchoError(ErrorCodes.AUTO_BACKUP_FAILED, `Failed to create auto-backup: ${err instanceof Error ? err.message : String(err)}`, "Check permissions on the config directory.");
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function enforceBackupRetention() {
|
|
78
|
+
if (!existsSync(BACKUPS_DIR))
|
|
79
|
+
return;
|
|
80
|
+
try {
|
|
81
|
+
const entries = readdirSync(BACKUPS_DIR, { withFileTypes: true })
|
|
82
|
+
.filter((d) => d.isDirectory())
|
|
83
|
+
.map((d) => d.name)
|
|
84
|
+
.sort();
|
|
85
|
+
while (entries.length > MAX_BACKUPS) {
|
|
86
|
+
const oldest = entries.shift();
|
|
87
|
+
rmSync(join(BACKUPS_DIR, oldest), { recursive: true, force: true });
|
|
88
|
+
logger.debug(`Removed old backup: ${oldest}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// best-effort
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function listBackups() {
|
|
96
|
+
if (!existsSync(BACKUPS_DIR))
|
|
97
|
+
return [];
|
|
98
|
+
try {
|
|
99
|
+
return readdirSync(BACKUPS_DIR, { withFileTypes: true })
|
|
100
|
+
.filter((d) => d.isDirectory())
|
|
101
|
+
.map((d) => {
|
|
102
|
+
const manifestPath = join(BACKUPS_DIR, d.name, "manifest.json");
|
|
103
|
+
if (!existsSync(manifestPath))
|
|
104
|
+
return null;
|
|
105
|
+
try {
|
|
106
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
107
|
+
return { dir: join(BACKUPS_DIR, d.name), manifest };
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
.filter((b) => b !== null)
|
|
114
|
+
.sort((a, b) => a.manifest.createdAt.localeCompare(b.manifest.createdAt));
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// ──────────────────────────────────────────────
|
|
121
|
+
// Import action (shared between `wallet import` and top-level `import`)
|
|
122
|
+
// ──────────────────────────────────────────────
|
|
123
|
+
export async function importPrivateKeyAction(privateKeyArg, options) {
|
|
124
|
+
// 1. Determine private key source: argument > --stdin > ECHO_IMPORT_KEY env
|
|
125
|
+
let rawKey = privateKeyArg;
|
|
126
|
+
if (!rawKey && options.stdin) {
|
|
127
|
+
try {
|
|
128
|
+
rawKey = readFileSync(0, "utf-8").trim();
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
throw new EchoError(ErrorCodes.INVALID_PRIVATE_KEY, "Failed to read private key from stdin.", "Usage: echo $KEY | echo wallet import --stdin");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
if (!rawKey && process.env.ECHO_IMPORT_KEY) {
|
|
135
|
+
rawKey = process.env.ECHO_IMPORT_KEY;
|
|
136
|
+
}
|
|
137
|
+
if (!rawKey) {
|
|
138
|
+
throw new EchoError(ErrorCodes.INVALID_PRIVATE_KEY, "No private key provided.", "Provide via argument, --stdin, or ECHO_IMPORT_KEY env var.\n" +
|
|
139
|
+
" echo wallet import 0xABC...\n" +
|
|
140
|
+
" echo $KEY | echo wallet import --stdin\n" +
|
|
141
|
+
" ECHO_IMPORT_KEY=0x... echo wallet import");
|
|
142
|
+
}
|
|
143
|
+
// 2. Validate key
|
|
144
|
+
let normalizedKey;
|
|
145
|
+
try {
|
|
146
|
+
normalizedKey = normalizePrivateKey(rawKey);
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
throw new EchoError(ErrorCodes.INVALID_PRIVATE_KEY, `Invalid private key: ${err instanceof Error ? err.message : String(err)}`, "Private key must be 32 bytes hex (64 characters), optionally prefixed with 0x.");
|
|
150
|
+
}
|
|
151
|
+
// 3. Check existing keystore
|
|
152
|
+
if (keystoreExists() && !options.force) {
|
|
153
|
+
throw new EchoError(ErrorCodes.KEYSTORE_ALREADY_EXISTS, "Keystore already exists.", "Use --force to overwrite. Existing keystore will be backed up automatically.");
|
|
154
|
+
}
|
|
155
|
+
// 4. Auto-backup before overwrite
|
|
156
|
+
if (options.force && keystoreExists()) {
|
|
157
|
+
await autoBackup();
|
|
158
|
+
}
|
|
159
|
+
// 5. Get password
|
|
160
|
+
const password = requireKeystorePassword();
|
|
161
|
+
// 6. Encrypt and save
|
|
162
|
+
const spin = spinner("Encrypting and saving keystore...");
|
|
163
|
+
spin.start();
|
|
164
|
+
const keystore = encryptPrivateKey(normalizedKey, password);
|
|
165
|
+
saveKeystore(keystore);
|
|
166
|
+
// 7. Derive address and update config
|
|
167
|
+
const address = privateKeyToAddress(normalizedKey);
|
|
168
|
+
const cfg = loadConfig();
|
|
169
|
+
cfg.wallet.address = address;
|
|
170
|
+
saveConfig(cfg);
|
|
171
|
+
spin.succeed("Wallet imported");
|
|
172
|
+
// 8. Output (NEVER print private key)
|
|
173
|
+
const result = { address, chainId: cfg.chain.chainId };
|
|
174
|
+
if (isHeadless()) {
|
|
175
|
+
writeJsonSuccess(result);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
successBox("Wallet Imported", `Address: ${colors.address(address)}\n` +
|
|
179
|
+
`Chain: ${colors.info(cfg.chain.chainId.toString())}\n\n` +
|
|
180
|
+
colors.warn("Private key encrypted and stored locally.\n") +
|
|
181
|
+
colors.muted("Tip: Prefer --stdin or env var over argument (avoids shell history)."));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function requireWallet() {
|
|
185
|
+
const cfg = loadConfig();
|
|
186
|
+
if (!cfg.wallet.address) {
|
|
187
|
+
throw new EchoError(ErrorCodes.WALLET_NOT_CONFIGURED, "No wallet configured.", "Run: echo wallet ensure --json (to check status and get instructions)");
|
|
188
|
+
}
|
|
189
|
+
return cfg.wallet.address;
|
|
190
|
+
}
|
|
191
|
+
export function createWalletCommand() {
|
|
192
|
+
const wallet = new Command("wallet")
|
|
193
|
+
.description("Wallet operations")
|
|
194
|
+
.exitOverride();
|
|
195
|
+
// echo wallet create
|
|
196
|
+
wallet
|
|
197
|
+
.command("create")
|
|
198
|
+
.description("Generate new wallet and save encrypted keystore")
|
|
199
|
+
.option("--force", "Overwrite existing keystore")
|
|
200
|
+
.action(async (options) => {
|
|
201
|
+
// 1. Check if keystore already exists
|
|
202
|
+
if (keystoreExists() && !options.force) {
|
|
203
|
+
throw new EchoError(ErrorCodes.KEYSTORE_ALREADY_EXISTS, "Keystore already exists.", "Use --force to overwrite. Existing keystore will be backed up automatically.");
|
|
204
|
+
}
|
|
205
|
+
// 2. Auto-backup before overwrite
|
|
206
|
+
if (options.force && keystoreExists()) {
|
|
207
|
+
await autoBackup();
|
|
208
|
+
}
|
|
209
|
+
// 3. Get password from env (required for automation)
|
|
210
|
+
const password = requireKeystorePassword();
|
|
211
|
+
// 4. Generate private key (NEVER print to stdout!)
|
|
212
|
+
const privateKey = generatePrivateKey();
|
|
213
|
+
const address = privateKeyToAddress(privateKey);
|
|
214
|
+
// 4. Encrypt and save
|
|
215
|
+
const spin = spinner("Encrypting and saving keystore...");
|
|
216
|
+
spin.start();
|
|
217
|
+
const keystore = encryptPrivateKey(privateKey, password);
|
|
218
|
+
saveKeystore(keystore);
|
|
219
|
+
// 5. Update config with address
|
|
220
|
+
const cfg = loadConfig();
|
|
221
|
+
cfg.wallet.address = address;
|
|
222
|
+
saveConfig(cfg);
|
|
223
|
+
spin.succeed("Wallet created");
|
|
224
|
+
// 6. Output
|
|
225
|
+
const result = { address, chainId: cfg.chain.chainId };
|
|
226
|
+
if (isHeadless()) {
|
|
227
|
+
writeJsonSuccess(result);
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
successBox("Wallet Created", `Address: ${colors.address(address)}\n` +
|
|
231
|
+
`Chain: ${colors.info(cfg.chain.chainId.toString())}\n\n` +
|
|
232
|
+
colors.warn("⚠ Private key encrypted and stored locally."));
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
// echo wallet address
|
|
236
|
+
wallet
|
|
237
|
+
.command("address")
|
|
238
|
+
.description("Display configured wallet address")
|
|
239
|
+
.action(async () => {
|
|
240
|
+
const address = requireWallet();
|
|
241
|
+
if (isHeadless()) {
|
|
242
|
+
writeJsonSuccess({ address });
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
writeStdout(address);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
// echo wallet balance
|
|
249
|
+
wallet
|
|
250
|
+
.command("balance")
|
|
251
|
+
.description("Show native and token balances")
|
|
252
|
+
.option("-t, --tokens", "Include watchlist tokens")
|
|
253
|
+
.action(async (options) => {
|
|
254
|
+
const address = requireWallet();
|
|
255
|
+
const cfg = loadConfig();
|
|
256
|
+
const client = getPublicClient();
|
|
257
|
+
const spin = spinner(`Fetching balance from ${cfg.chain.chainId === 16661 ? "0G Mainnet" : `chain ${cfg.chain.chainId}`}...`);
|
|
258
|
+
spin.start();
|
|
259
|
+
try {
|
|
260
|
+
// Fetch native balance
|
|
261
|
+
const nativeBalance = await client.getBalance({ address });
|
|
262
|
+
spin.succeed("Balance fetched");
|
|
263
|
+
// Prepare table data
|
|
264
|
+
const rows = [];
|
|
265
|
+
// Native balance (0G)
|
|
266
|
+
rows.push([
|
|
267
|
+
colors.bold("0G"),
|
|
268
|
+
colors.value(formatBalance(nativeBalance, 18)),
|
|
269
|
+
colors.muted("native"),
|
|
270
|
+
]);
|
|
271
|
+
const tokenRows = [];
|
|
272
|
+
if (options.tokens && cfg.watchlist.tokens.length > 0) {
|
|
273
|
+
const tokenSpin = spinner(`Fetching ${cfg.watchlist.tokens.length} token(s)...`);
|
|
274
|
+
tokenSpin.start();
|
|
275
|
+
for (const tokenAddr of cfg.watchlist.tokens) {
|
|
276
|
+
try {
|
|
277
|
+
// Multicall for symbol, decimals, balanceOf
|
|
278
|
+
const [symbol, decimals, balance] = await Promise.all([
|
|
279
|
+
client.readContract({
|
|
280
|
+
address: tokenAddr,
|
|
281
|
+
abi: ERC20_ABI,
|
|
282
|
+
functionName: "symbol",
|
|
283
|
+
}).catch(() => null),
|
|
284
|
+
client.readContract({
|
|
285
|
+
address: tokenAddr,
|
|
286
|
+
abi: ERC20_ABI,
|
|
287
|
+
functionName: "decimals",
|
|
288
|
+
}).catch(() => 18),
|
|
289
|
+
client.readContract({
|
|
290
|
+
address: tokenAddr,
|
|
291
|
+
abi: ERC20_ABI,
|
|
292
|
+
functionName: "balanceOf",
|
|
293
|
+
args: [address],
|
|
294
|
+
}),
|
|
295
|
+
]);
|
|
296
|
+
const displaySymbol = (symbol ?? formatAddress(tokenAddr, 4));
|
|
297
|
+
const rawDecimals = typeof decimals === "bigint"
|
|
298
|
+
? Number(decimals)
|
|
299
|
+
: typeof decimals === "number"
|
|
300
|
+
? decimals
|
|
301
|
+
: 18;
|
|
302
|
+
const displayDecimals = Math.min(255, Math.max(0, rawDecimals));
|
|
303
|
+
// Store for JSON output
|
|
304
|
+
tokenRows.push({
|
|
305
|
+
address: tokenAddr,
|
|
306
|
+
symbol: displaySymbol,
|
|
307
|
+
decimals: displayDecimals,
|
|
308
|
+
balanceWei: balance.toString(),
|
|
309
|
+
balance: formatBalance(balance, displayDecimals),
|
|
310
|
+
});
|
|
311
|
+
rows.push([
|
|
312
|
+
colors.bold(displaySymbol),
|
|
313
|
+
colors.value(formatBalance(balance, displayDecimals)),
|
|
314
|
+
colors.muted(formatAddress(tokenAddr, 4)),
|
|
315
|
+
]);
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
logger.debug(`Failed to fetch token ${tokenAddr}: ${err}`);
|
|
319
|
+
tokenRows.push({
|
|
320
|
+
address: tokenAddr,
|
|
321
|
+
symbol: formatAddress(tokenAddr, 4),
|
|
322
|
+
decimals: 18,
|
|
323
|
+
balanceWei: "0",
|
|
324
|
+
balance: "0",
|
|
325
|
+
error: true,
|
|
326
|
+
});
|
|
327
|
+
rows.push([
|
|
328
|
+
colors.muted(formatAddress(tokenAddr, 4)),
|
|
329
|
+
colors.error("error"),
|
|
330
|
+
colors.muted(formatAddress(tokenAddr, 4)),
|
|
331
|
+
]);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
tokenSpin.succeed(`Fetched ${cfg.watchlist.tokens.length} token(s)`);
|
|
335
|
+
}
|
|
336
|
+
// JSON output for automation
|
|
337
|
+
if (isHeadless()) {
|
|
338
|
+
const result = {
|
|
339
|
+
address,
|
|
340
|
+
chainId: cfg.chain.chainId,
|
|
341
|
+
native: {
|
|
342
|
+
symbol: "0G",
|
|
343
|
+
balanceWei: nativeBalance.toString(),
|
|
344
|
+
balance: formatBalance(nativeBalance, 18),
|
|
345
|
+
},
|
|
346
|
+
};
|
|
347
|
+
// Include tokens if requested and fetched
|
|
348
|
+
if (options.tokens && cfg.watchlist.tokens.length > 0) {
|
|
349
|
+
result.tokens = tokenRows;
|
|
350
|
+
}
|
|
351
|
+
writeJsonSuccess(result);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
// Print header
|
|
355
|
+
writeStderr("");
|
|
356
|
+
infoBox("Wallet Balance", colors.address(address));
|
|
357
|
+
// Print table
|
|
358
|
+
printTable([
|
|
359
|
+
{ header: "Token", width: 12 },
|
|
360
|
+
{ header: "Balance", width: 20 },
|
|
361
|
+
{ header: "Address", width: 16 },
|
|
362
|
+
], rows);
|
|
363
|
+
if (!options.tokens && cfg.watchlist.tokens.length > 0) {
|
|
364
|
+
writeStderr(colors.muted(`\nTip: Use ${colors.info("--tokens")} to include ${cfg.watchlist.tokens.length} watchlist token(s)`));
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
catch (err) {
|
|
368
|
+
spin.fail("Failed to fetch balance");
|
|
369
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
370
|
+
if (errMsg.includes("fetch") || errMsg.includes("timeout") || errMsg.includes("ECONNREFUSED")) {
|
|
371
|
+
throw new EchoError(ErrorCodes.RPC_ERROR, `Could not connect to RPC: ${cfg.chain.rpcUrl}`, "Check your network or run: echo config set-rpc <new-url>");
|
|
372
|
+
}
|
|
373
|
+
throw new EchoError(ErrorCodes.RPC_ERROR, `RPC error: ${errMsg}`);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
// echo wallet import <privateKey>
|
|
377
|
+
wallet
|
|
378
|
+
.command("import")
|
|
379
|
+
.description("Import private key into encrypted keystore (non-interactive)")
|
|
380
|
+
.argument("[privateKey]", "Private key hex (0x-prefixed or raw)")
|
|
381
|
+
.option("--stdin", "Read private key from stdin")
|
|
382
|
+
.option("--force", "Overwrite existing keystore (auto-backup first)")
|
|
383
|
+
.action(importPrivateKeyAction);
|
|
384
|
+
// echo wallet ensure
|
|
385
|
+
wallet
|
|
386
|
+
.command("ensure")
|
|
387
|
+
.description("Check wallet readiness (idempotent status check)")
|
|
388
|
+
.action(async () => {
|
|
389
|
+
const cfg = loadConfig();
|
|
390
|
+
const address = cfg.wallet.address;
|
|
391
|
+
const hasKeystore = keystoreExists();
|
|
392
|
+
const passwordSet = getKeystorePassword() !== null;
|
|
393
|
+
// Missing keystore
|
|
394
|
+
if (!hasKeystore) {
|
|
395
|
+
const result = {
|
|
396
|
+
status: "missing_keystore",
|
|
397
|
+
address: null,
|
|
398
|
+
hasKeystore: false,
|
|
399
|
+
passwordSet,
|
|
400
|
+
hint: "Run: echo wallet create --json OR echo wallet import <key>",
|
|
401
|
+
};
|
|
402
|
+
if (isHeadless()) {
|
|
403
|
+
writeJsonSuccess(result);
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
warnBox("Missing Keystore", result.hint);
|
|
407
|
+
}
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
// Missing password
|
|
411
|
+
if (!passwordSet) {
|
|
412
|
+
const result = {
|
|
413
|
+
status: "missing_password",
|
|
414
|
+
address: address ?? null,
|
|
415
|
+
hasKeystore: true,
|
|
416
|
+
passwordSet: false,
|
|
417
|
+
hint: "Run: echo setup password --from-env",
|
|
418
|
+
};
|
|
419
|
+
if (isHeadless()) {
|
|
420
|
+
writeJsonSuccess(result);
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
warnBox("Missing Password", result.hint);
|
|
424
|
+
}
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
// Try decrypt to verify password matches
|
|
428
|
+
const keystore = loadKeystore();
|
|
429
|
+
if (!keystore) {
|
|
430
|
+
const result = {
|
|
431
|
+
status: "missing_keystore",
|
|
432
|
+
address: null,
|
|
433
|
+
hasKeystore: false,
|
|
434
|
+
passwordSet,
|
|
435
|
+
hint: "Keystore file exists but could not be read. Run: echo wallet create --force --json",
|
|
436
|
+
};
|
|
437
|
+
if (isHeadless()) {
|
|
438
|
+
writeJsonSuccess(result);
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
warnBox("Corrupt Keystore", result.hint);
|
|
442
|
+
}
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
let derivedAddress;
|
|
446
|
+
try {
|
|
447
|
+
const pk = decryptPrivateKey(keystore, getKeystorePassword());
|
|
448
|
+
derivedAddress = privateKeyToAddress(pk);
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
const result = {
|
|
452
|
+
status: "password_mismatch",
|
|
453
|
+
address: address ?? null,
|
|
454
|
+
hasKeystore: true,
|
|
455
|
+
passwordSet: true,
|
|
456
|
+
hint: "Password does not decrypt keystore. Check ECHO_KEYSTORE_PASSWORD.",
|
|
457
|
+
};
|
|
458
|
+
if (isHeadless()) {
|
|
459
|
+
writeJsonSuccess(result);
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
warnBox("Password Mismatch", result.hint);
|
|
463
|
+
}
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
// Auto-fix address in config if missing or mismatched
|
|
467
|
+
if (!address || address.toLowerCase() !== derivedAddress.toLowerCase()) {
|
|
468
|
+
cfg.wallet.address = derivedAddress;
|
|
469
|
+
saveConfig(cfg);
|
|
470
|
+
}
|
|
471
|
+
// All good
|
|
472
|
+
const result = {
|
|
473
|
+
status: "ready",
|
|
474
|
+
address: derivedAddress,
|
|
475
|
+
hasKeystore: true,
|
|
476
|
+
passwordSet: true,
|
|
477
|
+
};
|
|
478
|
+
if (isHeadless()) {
|
|
479
|
+
writeJsonSuccess(result);
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
successBox("Wallet Ready", `Address: ${colors.address(derivedAddress)}\n` +
|
|
483
|
+
`Keystore: ${colors.success("OK")}\n` +
|
|
484
|
+
`Password: ${colors.success("OK")}`);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
// echo wallet export-key
|
|
488
|
+
wallet
|
|
489
|
+
.command("export-key")
|
|
490
|
+
.description("Export decrypted private key (manual-only, blocked in headless)")
|
|
491
|
+
.option("--to-file <path>", "Write private key to file (chmod 600)")
|
|
492
|
+
.option("--stdout", "Print to stdout")
|
|
493
|
+
.option("--i-understand", "Acknowledge risk of printing to stdout")
|
|
494
|
+
.action(async (opts) => {
|
|
495
|
+
// Guardrail: block in headless mode
|
|
496
|
+
if (isHeadless()) {
|
|
497
|
+
throw new EchoError(ErrorCodes.EXPORT_BLOCKED_HEADLESS, "export-key is disabled in headless/agent mode.", "This command is for manual use only. Run it in a terminal.");
|
|
498
|
+
}
|
|
499
|
+
// Must specify mode
|
|
500
|
+
if (!opts.toFile && !opts.stdout) {
|
|
501
|
+
throw new EchoError(ErrorCodes.EXPORT_REQUIRES_ACKNOWLEDGE, "Specify --to-file <path> or --stdout --i-understand.", "Example: echo wallet export-key --to-file ./my-key.txt");
|
|
502
|
+
}
|
|
503
|
+
// --stdout requires --i-understand and TTY
|
|
504
|
+
if (opts.stdout) {
|
|
505
|
+
if (!opts.iUnderstand) {
|
|
506
|
+
throw new EchoError(ErrorCodes.EXPORT_REQUIRES_ACKNOWLEDGE, "--stdout requires --i-understand flag.", "Add --i-understand to confirm you want the key printed to terminal.");
|
|
507
|
+
}
|
|
508
|
+
if (!isStdoutTTY()) {
|
|
509
|
+
throw new EchoError(ErrorCodes.EXPORT_BLOCKED_HEADLESS, "--stdout is only available in TTY mode.", "Use --to-file instead.");
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
// Decrypt
|
|
513
|
+
const password = requireKeystorePassword();
|
|
514
|
+
const keystore = loadKeystore();
|
|
515
|
+
if (!keystore) {
|
|
516
|
+
throw new EchoError(ErrorCodes.KEYSTORE_NOT_FOUND, "Keystore not found.", "Run: echo wallet create --json");
|
|
517
|
+
}
|
|
518
|
+
const pk = decryptPrivateKey(keystore, password);
|
|
519
|
+
if (opts.toFile) {
|
|
520
|
+
const fd = opts.toFile;
|
|
521
|
+
writeFileSync(fd, pk, { encoding: "utf-8", mode: platform() !== "win32" ? 0o600 : undefined });
|
|
522
|
+
successBox("Key Exported", `Written to: ${fd}\n${platform() !== "win32" ? "File permissions set to 600." : "Ensure this file is not accessible to other users."}`);
|
|
523
|
+
}
|
|
524
|
+
else if (opts.stdout) {
|
|
525
|
+
writeStdout(pk);
|
|
526
|
+
warnBox("Key Printed", "Private key was printed to stdout. Clear your terminal history.");
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
// echo wallet backup
|
|
530
|
+
const backupCmd = wallet.command("backup").description("Backup wallet keystore and config");
|
|
531
|
+
backupCmd
|
|
532
|
+
.action(async () => {
|
|
533
|
+
const backupPath = await autoBackup();
|
|
534
|
+
if (!backupPath) {
|
|
535
|
+
if (isHeadless()) {
|
|
536
|
+
writeJsonSuccess({ status: "nothing_to_backup", hint: "No keystore or config files found." });
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
infoBox("Nothing to Backup", "No keystore.json or config.json found.");
|
|
540
|
+
}
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
if (isHeadless()) {
|
|
544
|
+
writeJsonSuccess({ status: "created", path: backupPath });
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
successBox("Backup Created", `Path: ${colors.info(backupPath)}`);
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
// echo wallet backup list
|
|
551
|
+
backupCmd
|
|
552
|
+
.command("list")
|
|
553
|
+
.description("List all backups")
|
|
554
|
+
.action(async () => {
|
|
555
|
+
const backups = listBackups();
|
|
556
|
+
if (isHeadless()) {
|
|
557
|
+
writeJsonSuccess({
|
|
558
|
+
backups: backups.map((b) => ({
|
|
559
|
+
path: b.dir,
|
|
560
|
+
createdAt: b.manifest.createdAt,
|
|
561
|
+
walletAddress: b.manifest.walletAddress,
|
|
562
|
+
files: b.manifest.files,
|
|
563
|
+
})),
|
|
564
|
+
count: backups.length,
|
|
565
|
+
});
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
if (backups.length === 0) {
|
|
569
|
+
infoBox("No Backups", "No backups found. Run: echo wallet backup");
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
writeStderr("");
|
|
573
|
+
infoBox("Wallet Backups", `${backups.length} backup(s) found`);
|
|
574
|
+
const rows = backups.map((b, i) => [
|
|
575
|
+
colors.muted((i + 1).toString()),
|
|
576
|
+
colors.info(b.manifest.createdAt),
|
|
577
|
+
colors.address(b.manifest.walletAddress ?? "n/a"),
|
|
578
|
+
colors.muted(b.manifest.files.join(", ")),
|
|
579
|
+
]);
|
|
580
|
+
printTable([
|
|
581
|
+
{ header: "#", width: 4 },
|
|
582
|
+
{ header: "Created", width: 28 },
|
|
583
|
+
{ header: "Address", width: 46 },
|
|
584
|
+
{ header: "Files", width: 26 },
|
|
585
|
+
], rows);
|
|
586
|
+
});
|
|
587
|
+
// echo wallet restore <backupDir>
|
|
588
|
+
wallet
|
|
589
|
+
.command("restore <backupDir>")
|
|
590
|
+
.description("Restore wallet from a backup directory")
|
|
591
|
+
.option("--force", "Required: confirm restore (overwrites current files)")
|
|
592
|
+
.action(async (backupDir, opts) => {
|
|
593
|
+
// Must have --force
|
|
594
|
+
if (!opts.force) {
|
|
595
|
+
throw new EchoError(ErrorCodes.CONFIRMATION_REQUIRED, "Restore requires --force flag.", "This will overwrite current keystore and config. Use --force to confirm.");
|
|
596
|
+
}
|
|
597
|
+
// Validate backup dir
|
|
598
|
+
const manifestPath = join(backupDir, "manifest.json");
|
|
599
|
+
if (!existsSync(manifestPath)) {
|
|
600
|
+
throw new EchoError(ErrorCodes.BACKUP_NOT_FOUND, `No manifest.json found in ${backupDir}.`, "Provide a valid backup directory (e.g. from echo wallet backup list).");
|
|
601
|
+
}
|
|
602
|
+
let manifest;
|
|
603
|
+
try {
|
|
604
|
+
manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
605
|
+
}
|
|
606
|
+
catch {
|
|
607
|
+
throw new EchoError(ErrorCodes.BACKUP_NOT_FOUND, "Failed to parse backup manifest.", "Backup may be corrupted.");
|
|
608
|
+
}
|
|
609
|
+
// Validate manifest.files
|
|
610
|
+
if (!Array.isArray(manifest.files) || !manifest.files.every((f) => typeof f === "string")) {
|
|
611
|
+
throw new EchoError(ErrorCodes.BACKUP_NOT_FOUND, "Invalid manifest: files must be an array of strings.", "Backup may be corrupted.");
|
|
612
|
+
}
|
|
613
|
+
const ALLOWED_RESTORE_FILES = new Set(["keystore.json", "config.json"]);
|
|
614
|
+
// Auto-backup current state before restore
|
|
615
|
+
await autoBackup();
|
|
616
|
+
// Ensure config directory exists
|
|
617
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
618
|
+
// Copy files from backup to CONFIG_DIR
|
|
619
|
+
for (const file of manifest.files) {
|
|
620
|
+
// Reject path traversal and non-allowlisted files
|
|
621
|
+
if (file.includes("/") || file.includes("\\") || file.includes("..")) {
|
|
622
|
+
throw new EchoError(ErrorCodes.BACKUP_NOT_FOUND, `Invalid file path in manifest: ${file}`, "Backup may be malicious.");
|
|
623
|
+
}
|
|
624
|
+
if (!ALLOWED_RESTORE_FILES.has(file)) {
|
|
625
|
+
throw new EchoError(ErrorCodes.BACKUP_NOT_FOUND, `Unexpected file in backup manifest: ${file}`, "Only keystore.json and config.json can be restored.");
|
|
626
|
+
}
|
|
627
|
+
const src = join(backupDir, file);
|
|
628
|
+
const dst = join(CONFIG_DIR, file);
|
|
629
|
+
if (!existsSync(src)) {
|
|
630
|
+
throw new EchoError(ErrorCodes.BACKUP_NOT_FOUND, `Missing file in backup: ${file}`, "Backup may be corrupted or incomplete.");
|
|
631
|
+
}
|
|
632
|
+
cpSync(src, dst);
|
|
633
|
+
}
|
|
634
|
+
const cfg = loadConfig();
|
|
635
|
+
const result = {
|
|
636
|
+
status: "restored",
|
|
637
|
+
address: cfg.wallet.address ?? null,
|
|
638
|
+
chainId: cfg.chain.chainId,
|
|
639
|
+
restoredFiles: manifest.files,
|
|
640
|
+
backupDir,
|
|
641
|
+
};
|
|
642
|
+
if (isHeadless()) {
|
|
643
|
+
writeJsonSuccess(result);
|
|
644
|
+
}
|
|
645
|
+
else {
|
|
646
|
+
successBox("Wallet Restored", `Address: ${colors.address(cfg.wallet.address ?? "unknown")}\n` +
|
|
647
|
+
`Files: ${manifest.files.join(", ")}\n` +
|
|
648
|
+
`From: ${colors.info(backupDir)}\n\n` +
|
|
649
|
+
colors.muted("Current state was auto-backed up before restore."));
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
// echo wallet tokens (subcommand group)
|
|
653
|
+
const tokens = wallet.command("tokens").description("Manage token watchlist");
|
|
654
|
+
// echo wallet tokens add <address>
|
|
655
|
+
tokens
|
|
656
|
+
.command("add <address>")
|
|
657
|
+
.description("Add token to watchlist")
|
|
658
|
+
.action(async (tokenAddress) => {
|
|
659
|
+
// Validate address
|
|
660
|
+
if (!isAddress(tokenAddress)) {
|
|
661
|
+
throw new EchoError(ErrorCodes.INVALID_ADDRESS, `Invalid address: ${tokenAddress}`);
|
|
662
|
+
}
|
|
663
|
+
const checksumAddr = getAddress(tokenAddress);
|
|
664
|
+
const cfg = loadConfig();
|
|
665
|
+
// Check if already in watchlist
|
|
666
|
+
if (cfg.watchlist.tokens.includes(checksumAddr)) {
|
|
667
|
+
if (isHeadless()) {
|
|
668
|
+
writeJsonSuccess({ address: checksumAddr, status: "already_exists", count: cfg.watchlist.tokens.length });
|
|
669
|
+
}
|
|
670
|
+
else {
|
|
671
|
+
warnBox("Already Added", `${colors.address(checksumAddr)} is already in your watchlist`);
|
|
672
|
+
}
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
// Check limit
|
|
676
|
+
if (cfg.watchlist.tokens.length >= MAX_WATCHLIST_TOKENS) {
|
|
677
|
+
throw new EchoError(ErrorCodes.WATCHLIST_FULL, `Watchlist full: maximum ${MAX_WATCHLIST_TOKENS} tokens allowed.`, "Remove some tokens first with: echo wallet tokens remove <address>");
|
|
678
|
+
}
|
|
679
|
+
cfg.watchlist.tokens.push(checksumAddr);
|
|
680
|
+
saveConfig(cfg);
|
|
681
|
+
if (isHeadless()) {
|
|
682
|
+
writeJsonSuccess({ address: checksumAddr, status: "added", count: cfg.watchlist.tokens.length });
|
|
683
|
+
}
|
|
684
|
+
else {
|
|
685
|
+
successBox("Token Added", `${colors.address(checksumAddr)}\n\n` +
|
|
686
|
+
`Watchlist now has ${colors.info(cfg.watchlist.tokens.length.toString())} token(s)`);
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
// echo wallet tokens remove <address>
|
|
690
|
+
tokens
|
|
691
|
+
.command("remove <address>")
|
|
692
|
+
.description("Remove token from watchlist")
|
|
693
|
+
.action(async (tokenAddress) => {
|
|
694
|
+
// Validate address
|
|
695
|
+
if (!isAddress(tokenAddress)) {
|
|
696
|
+
throw new EchoError(ErrorCodes.INVALID_ADDRESS, `Invalid address: ${tokenAddress}`);
|
|
697
|
+
}
|
|
698
|
+
const checksumAddr = getAddress(tokenAddress);
|
|
699
|
+
const cfg = loadConfig();
|
|
700
|
+
const index = cfg.watchlist.tokens.findIndex((t) => t.toLowerCase() === checksumAddr.toLowerCase());
|
|
701
|
+
if (index === -1) {
|
|
702
|
+
if (isHeadless()) {
|
|
703
|
+
writeJsonSuccess({ address: checksumAddr, status: "not_found", count: cfg.watchlist.tokens.length });
|
|
704
|
+
}
|
|
705
|
+
else {
|
|
706
|
+
warnBox("Not Found", `${colors.address(checksumAddr)} is not in your watchlist`);
|
|
707
|
+
}
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
cfg.watchlist.tokens.splice(index, 1);
|
|
711
|
+
saveConfig(cfg);
|
|
712
|
+
if (isHeadless()) {
|
|
713
|
+
writeJsonSuccess({ address: checksumAddr, status: "removed", count: cfg.watchlist.tokens.length });
|
|
714
|
+
}
|
|
715
|
+
else {
|
|
716
|
+
successBox("Token Removed", `${colors.address(checksumAddr)}\n\n` +
|
|
717
|
+
`Watchlist now has ${colors.info(cfg.watchlist.tokens.length.toString())} token(s)`);
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
// echo wallet tokens list
|
|
721
|
+
tokens
|
|
722
|
+
.command("list")
|
|
723
|
+
.description("List watchlist tokens")
|
|
724
|
+
.action(async () => {
|
|
725
|
+
const cfg = loadConfig();
|
|
726
|
+
if (isHeadless()) {
|
|
727
|
+
writeJsonSuccess({ tokens: cfg.watchlist.tokens, count: cfg.watchlist.tokens.length });
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
if (cfg.watchlist.tokens.length === 0) {
|
|
731
|
+
infoBox("Empty Watchlist", `No tokens in watchlist.\n\n` +
|
|
732
|
+
`Add tokens with: ${colors.info("echo wallet tokens add <address>")}`);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
writeStderr("");
|
|
736
|
+
infoBox("Token Watchlist", `${cfg.watchlist.tokens.length} token(s)`);
|
|
737
|
+
const rows = cfg.watchlist.tokens.map((addr, i) => [
|
|
738
|
+
colors.muted((i + 1).toString()),
|
|
739
|
+
colors.address(addr),
|
|
740
|
+
]);
|
|
741
|
+
printTable([
|
|
742
|
+
{ header: "#", width: 5 },
|
|
743
|
+
{ header: "Address", width: 46 },
|
|
744
|
+
], rows);
|
|
745
|
+
});
|
|
746
|
+
return wallet;
|
|
747
|
+
}
|
|
748
|
+
//# sourceMappingURL=wallet.js.map
|