Skip to content

Add the Edge CLI, on Node-safe extractions from the GUI - #6082

Open
paullinator wants to merge 56 commits into
developfrom
paul/cli
Open

paullinator wants to merge 56 commits into
developfrom
paul/cli

Conversation

@paullinator

@paullinator paullinator commented Jul 22, 2026 •

Copy link
Copy Markdown
Member

Description

Adds the Edge CLI — edge-cli and the edge-engine daemon behind it — and
the work that had to come first: nine commits that move display metadata,
denominations, spam-threshold resolution, local and synced settings, the
transaction export pipeline, transaction tagging, locale selection, exchange
rates and the network helpers out of GUI-only modules so they load under
plain Node, with no behaviour change to the app.

The engine is a long-lived process owning an EdgeContext, serving JSON over
a Unix socket (with an optional token-authenticated loopback TCP listener).
The client is thin: it spawns the engine on demand, forwards one request and
prints the answer. 117 routes, each its own route({…}) declaration carrying
its cleaners, its error list and its prose; the command table, the help text
and the OpenAPI reference are generated from those declarations and
committed.

Five documentation gates hold over the surface — the generated artifacts are
current, the surface matches, every response field carries prose, each call
matches its edge-core-js signature or records why it differs, and every
command either reaches its handler offline against the fake world or is
listed with the suite that drives it. npm run docs:api:gates runs them;
cli:manifest:check and docs:api:committed run beside them.

The twelve commits are meant to be read in order, and each one stands on its
own: npm test, npm run docs:api:gates, npm run test:cli:node-safe and
npm run test:cli:offline pass on every tree, with the npm scripts, the
CHANGELOG entries and the CI steps landing in the commit that adds the file
each one names.

Not included, and waiting on a decision: the published package carries no
native HMAC signer. Its shards reconstruct the same apiSecret the iOS and
Android release builds sign with, so shipping it in a public tarball would
hand that secret to npm pack plus strings. publish:cli --with-signer
exists and refuses unless --signer-secret-is-cli-only says the secret is
the CLI's own — which needs an apiKey/apiSecret pair issued for
@edgeapp/cli. Until then a published CLI needs an edgeApiKey of its own
in keys.json or -k, and says so when it has neither.

Documentation: docs/EDGE_CLI.md is the
guide and the published README; docs/api/README.md
is the declaration format.

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

Dependencies

none

Requirements

If you have made any visual changes to the GUI. Make sure you have:

  • Tested on iOS device
  • Tested on Android device
  • Tested on small-screen device (iPod Touch)
  • Tested on large-screen device (tablet)

Test plan

  • npm test — the jest suites, including 50-odd new ones under
    src/__tests__/cli and src/__tests__/util for the engine's handlers,
    the client, the rate queue, the locale boot and the export pipeline.
  • npm run docs:api:gates — the five documentation gates.
  • npm run test:cli:node-safe — both CLI entry points loaded under plain
    Node with a react-native import poisoned, so a module-scope leak into
    the shared code fails here.
  • npm run test:cli:offline — testCliFake and testCliSubscribe drive a
    real engine daemon on a Unix socket under a temporary data directory,
    against the in-process fake world. No network, no API key.
  • npm run test:cli:offline:built — the same suites against the rollup
    bundle, which is what a published CLI runs.
  • npm run test:cli:network — the one-shot, CAPTCHA and edge-login suites
    against the tester fleet. Needs the network and an Edge API key.

CLI session on the tester servers

Run on 2026-10-10 against 9beff6d7d:

  • Build: this branch, with edge-currency-accountbased paul/cli (da1b8fe8, rebased on its master) packed in through pack-dependencies, then npm run build:cli:all.
  • Suites: test:cli:network against the tester fleet passed: one-shot 13/13, CAPTCHA, edge-login. test:cli:offline against the built bundle passed: 242 fake-world checks plus the subscribe suite.

Then the session below, run with the built CLI as clitester, sending funds to and from sharetest and swapping LTC to BTC. Every command is shown as typed, with its full output and exit code. Passwords went through EDGE_CLI_PASSWORD and show as <redacted>; session ids are shortened. The full log, byte arrays included, is at https://balls.edge.app/pr-artifacts/edge-react-gui/cliTesterSession/202610101758-cli-session.log.

Every command below was run with the built CLI (lib/edgeCli.js, shown as edge-cli), from edge-react-gui paul/cli at 5bda66390 with edge-currency-accountbased paul/cli at da1b8fe8 packed in (https://balls.edge.app/20261010174309-edge-currency-accountbased-4.102.0.tgz).

  • -t points the client and engine at the tester servers: login-tester, info-tester, sync-tester.
  • Passwords reach the CLI through EDGE_CLI_PASSWORD and appear as <redacted>. PINs never appear, and live session ids are shortened to sess_….
  • Two accounts: clitester (funded) and sharetest (the receiver), each with its own data directory.
  • Each entry shows the command as typed, the complete output (stdout, then stderr), and the exit code.
Step Command Result
Tester servers engine-config login-tester, info-tester, sync-tester-us1…3
Log in login-with-password (both accounts, password from EDGE_CLI_PASSWORD) sessions created, wallets loaded (including Ethereum, which needs the packed accountbased)
Addresses get-addresses sharetest LTC ltc1qmcs05z9stqwxlm48f3ja66qnu9w9edpvc6c456; clitester LTC ltc1qfkvdvrcr8g82mr72q7u67p8yzpg7yl3n3nm9nw, BTC bc1q5kjamtm0424shsjp4dng3z85xxrqhgdzm7j7cr
Payment request encode-uri litecoin:ltc1qmcs…?amount=0.005&label=sharetest
Send, dry run spend --dry-run built, not signed: 0.005 LTC plus a 0.00035063 fee at 250 sat/vB
Send spend clitester → sharetest, 0.005 LTC txid 9ca11b2d32db160c39434b4b989ef737a08debc346411d792097ff5c21cd7c83; seen by sharetest
Send back spend sharetest → clitester, 0.002 LTC txid b9acbae3efc486c7e0bc423a64b0ca6131d7049b06ae4a64c2e09fd8a01a98f2
Swap fetch-swap-quotes and approve-swap-quote, 0.05 LTC → BTC (THORChain) LTC txid 0f0a34872761fb57db762bb82660ea5142fc6fd894324ca1238a217f91236262; 2738 sats paid out, txid 9ef7af518502d91bfd6513c624c08ca7ccf94122d3c6bd0d3e4a101c02d40d88
Expired quote approve-swap-quote after the exchange's 60 s 410 QUOTE_EXPIRED, exit 4, nothing signed
Log out logout both sessions ended

Balances: clitester LTC 2 went from 0.45255611 to 0.39912806, its BTC from 0.00109035 to 0.00111773, and sharetest LTC from 0 to 0.00264937.

The session: 31 commands and their output

2026-10-10 17:50:15 (clitester)

$ edge-cli -t -d ~/.edge-cli-clitester --solve-captcha engine-config
{
  "appId": "",
  "testMode": true,
  "directory": "/Users/paul/.edge-cli-clitester",
  "servers": {
    "loginServer": "https://login-tester.edge.app",
    "infoServer": "https://info-tester.edge.app",
    "changeServer": "https://change-tester.edge.app",
    "syncServer": [
      "https://sync-tester-us1.edge.app",
      "https://sync-tester-us2.edge.app",
      "https://sync-tester-us3.edge.app"
    ]
  },
  "plugins": [
    "badcoin",
    "bitcoin",
    "bitcoincash",
    "bitcoincashtestnet",
    "bitcoingold",
    "bitcoingoldtestnet",
    "bitcoinsv",
    "bitcointestnet",
    "bitcointestnet4",
    "dash",
    "digibyte",
    "dogecoin",
    "eboost",
    "ecash",
    "feathercoin",
    "groestlcoin",
    "litecoin",
    "pivx",
    "qtum",
    "ravencoin",
    "smartcash",
    "ufo",
    "vertcoin",
    "zcoin",
    "eos",
    "telos",
    "wax",
    "abstract",
    "amoy",
    "arbitrum",
    "arc",
    "avalanche",
    "base",
    "binancesmartchain",
    "bobevm",
    "botanix",
    "celo",
    "ethDev",
    "ethereum",
    "ethereumclassic",
    "ethereumpow",
    "fantom",
    "filecoinfevm",
    "filecoinfevmcalibration",
    "holesky",
    "hyperevm",
    "monad",
    "opbnb",
    "optimism",
    "polygon",
    "pulsechain",
    "robinhood",
    "sepolia",
    "rsk",
    "sonic",
    "zksync",
    "axelar",
    "mayachain",
    "coreum",
    "cosmoshub",
    "nym",
    "osmosis",
    "thorchainrune",
    "thorchainrunestagenet",
    "algorand",
    "binance",
    "cardano",
    "cardanotestnet",
    "filecoin",
    "calibration",
    "fio",
    "hedera",
    "liberland",
    "liberlandtestnet",
    "monero",
    "piratechain",
    "polkadot",
    "ripple",
    "solana",
    "stellar",
    "sui",
    "suitestnet",
    "tezos",
    "ton",
    "tron",
    "zano",
    "zcash",
    "bridgeless",
    "changehero",
    "changelly",
    "changenow",
    "exolix",
    "godex",
    "lifi",
    "nexchange",
    "nymswap",
    "rango",
    "sideshift",
    "swapuz",
    "mayaprotocol",
    "thorchain",
    "swapkit",
    "tombSwap",
    "xgram",
    "xrpdex",
    "0xgasless"
  ],
  "configFiles": {
    "cwd": "/Users/paul/git-worktrees/edge-react-gui-cli",
    "keys": [
      "/Users/paul/git-worktrees/edge-react-gui-cli/keys.json",
      "/Users/paul/.edge-cli/keys.json"
    ],
    "appConfig": "/Users/paul/git-worktrees/edge-react-gui-cli/config.json",
    "cliConfig": null
  }
}

[edge-cli] /Users/paul/.edge-cli-clitester is mode 755, so other local users can read the login stashes in it. The CLI leaves the mode of a directory it did not create alone: run `chmod 700` on it, or pass a -d of its own.

exit 0, 4.1s

2026-10-10 17:50:36 (clitester)

$ EDGE_CLI_PASSWORD=<redacted> edge-cli -t -d ~/.edge-cli-clitester --solve-captcha login-with-password --username=clitester
{
  "sessionId": "sess_Qdxa…",
  "username": "clitester",
  "rootLoginId": "2Nzutzi4qbtG31mHJMpfcV6e2nhjVMhYQYwmUMr3FrH3",
  "loginMethod": "password",
  "autoLogoutSeconds": 3600,
  "autoLogoutRead": true,
  "expiresAt": "2026-10-11T01:50:36.191Z",
  "lastActivityAt": "2026-10-11T00:50:36.191Z",
  "createdAt": "2026-10-11T00:50:36.191Z"
}

exit 0, 16.9s

2026-10-10 17:50:36 (clitester)

$ edge-cli -t -d ~/.edge-cli-clitester --solve-captcha wait-for-all-wallets
{
  "unloadedWallets": [
    {
      "walletId": "+I4cOaHjcEJc8r6Gqj0ItweWLfX2W0MuvqUmKi07UrA=",
      "walletType": "wallet:monero",
      "pluginId": "monero",
      "pluginRegistered": true,
      "pluginHasLoadedWallet": false
    },
    {
      "walletId": "R6lSMQGOtdPD46sEfabvxEw/nihdq6cpwxC6Gca5cMI=",
      "walletType": "wallet:zano",
      "pluginId": "zano",
      "pluginRegistered": true,
      "pluginHasLoadedWallet": false
    },
    {
      "walletId": "Zt7/dSqLD62IfUi8zPC+ALNpRNURhxANYVUNmnXIyrI=",
      "walletType": "wallet:zcash",
      "pluginId": "zcash",
      "pluginRegistered": true,
      "pluginHasLoadedWallet": false
    },
    {
      "walletId": "yoXwwIxf29Wj8fsHgTciI4wlzMmO6VhOGa8vG8RYkes=",
      "walletType": "wallet:monero",
      "pluginId": "monero",
      "pluginRegistered": true,
      "pluginHasLoadedWallet": false
    },
    {
      "walletId": "M/96jnAaXICOLFKJt9BUPoSfRoJ8KoZHWDpYRwjpr04=",
      "walletType": "wallet:zano",
      "pluginId": "zano",
      "pluginRegistered": true,
      "pluginHasLoadedWallet": false
    },
    {
      "walletId": "5w8Kx3jB5mCiUWGiooxMMafU9ckeS3RrAAF0UTatz6A=",
      "walletType": "wallet:zcash",
      "pluginId": "zcash",
      "pluginRegistered": true,
      "pluginHasLoadedWallet": false
    },
    {
      "walletId": "tIwzb7yUI9jK4foI2qexrErdMXgW51sDr06oGxJKFEk=",
      "walletType": "wallet:piratechain",
      "pluginId": "piratechain",
      "pluginRegistered": true,
      "pluginHasLoadedWallet": false
    },
    {
      "walletId": "zetC0WIMm0nNNaSun2y+nLk3WAOX2xN7fS0B6JGkjeU=",
      "walletType": "wallet:piratechain",
      "pluginId": "piratechain",
      "pluginRegistered": true,
      "pluginHasLoadedWallet": false
    },
    {
      "walletId": "yemaJy7C3goKaYo3/0knJW0OF4hQD1GiBiVpRi9ie/s=",
      "walletType": "wallet:piratechain",
      "pluginId": "piratechain",
      "pluginRegistered": true,
      "pluginHasLoadedWallet": false
    },
    {
      "walletId": "fxLd74wgxuH4f8EBc1dLixqcmiZrQXr3g1tRMacrBJE=",
      "walletType": "wallet:piratechain",
      "pluginId": "piratechain",
      "pluginRegistered": true,
      "pluginHasLoadedWallet": false
    }
  ]
}

exit 0, 0.3s

2026-10-10 17:50:42 (clitester)

$ edge-cli -t -d ~/.edge-cli-clitester --solve-captcha currency-wallets
{
  "currencyWallets": [
    {
      "walletId": "7o7i6/tlI+qiFwlvZOdiLF7hcBY/JjBEIYZyB9e7t+8=",
      "id": "7o7i6/tlI+qiFwlvZOdiLF7hcBY/JjBEIYZyB9e7t+8=",
      "type": "wallet:bitcoin",
      "name": "CLI Tester BTC",
      "pluginId": "bitcoin",
      "currencyCode": "BTC",
      "fiatCurrencyCode": "iso:USD",
      "blockHeight": 970852,
      "syncStatus": {
        "totalRatio": 1
      },
      "syncRatio": "100%",
      "paused": false,
      "imported": false,
      "created": "2026-08-24T15:03:56.294Z",
      "enabledTokenIds": [],
      "detectedTokenIds": [],
      "unactivatedTokenIds": []
    },
    {
      "walletId": "C002fn2kSztYXik+gyMQ8HGMo2jYssc6wv7zBxtVaK0=",
      "id": "C002fn2kSztYXik+gyMQ8HGMo2jYssc6wv7zBxtVaK0=",
      "type": "wallet:ethereum",
      "name": "My Ether",
      "pluginId": "ethereum",
      "currencyCode": "ETH",
      "fiatCurrencyCode": "iso:USD",
      "blockHeight": 0,
      "syncStatus": {
        "totalRatio": 1
      },
      "syncRatio": "100%",
      "paused": false,
      "imported": false,
      "created": "2026-08-27T22:25:25.602Z",
      "enabledTokenIds": [],
      "detectedTokenIds": [],
      "unactivatedTokenIds": []
    },
    {
      "walletId": "jOluX13Yskt5weYARoYIzuWrZW1O/BlgSjJnlbRE1jk=",
      "id": "jOluX13Yskt5weYARoYIzuWrZW1O/BlgSjJnlbRE1jk=",
      "type": "wallet:ethereum",
      "name": "My Ether 2",
      "pluginId": "ethereum",
      "currencyCode": "ETH",
      "fiatCurrencyCode": "iso:USD",
      "blockHeight": 0,
      "syncStatus": {
        "totalRatio": 1
      },
      "syncRatio": "100%",
      "paused": false,
      "imported": false,
      "created": "2026-09-01T04:38:58.890Z",
      "enabledTokenIds": [],
      "detectedTokenIds": [],
      "unactivatedTokenIds": []
    },
    {
      "walletId": "xouxuRPAphzmK46sMSdskhMZiKTW15BgNk6/fffwHgQ=",
      "id": "xouxuRPAphzmK46sMSdskhMZiKTW15BgNk6/fffwHgQ=",
      "type": "wallet:bitcoin",
      "name": "CLI Tester BTC 2",
      "pluginId": "bitcoin",
      "currencyCode": "BTC",
      "fiatCurrencyCode": "iso:USD",
      "blockHeight": 970852,
      "syncStatus": {
        "totalRatio": 1
      },
      "syncRatio": "100%",
      "paused": false,
      "imported": false,
      "created": "2026-09-02T16:42:40.586Z",
      "enabledTokenIds": [],
      "detectedTokenIds": [],
      "unactivatedTokenIds": []
    },
    {
      "walletId": "DOhkEgaWy3whGKlI5COESjgnW37PawgjGakQfB1SusM=",
      "id": "DOhkEgaWy3whGKlI5COESjgnW37PawgjGakQfB1SusM=",
      "type": "wallet:litecoin",
      "name": "CLI Tester LTC",
      "pluginId": "litecoin",
      "currencyCode": "LTC",
      "fiatCurrencyCode": "iso:USD",
      "blockHeight": 3192984,
      "syncStatus": {
        "totalRatio": 1
      },
      "syncRatio": "100%",
      "paused": false,
      "imported": false,
      "created": "2026-09-02T19:46:43.809Z",
      "enabledTokenIds": [],
      "detectedTokenIds": [],
      "unactivatedTokenIds": []
    },
    {
      "walletId": "m+z6RkRpEomOSRSggpFvYihi7mmzttq0r751AnigBI8=",
      "id": "m+z6RkRpEomOSRSggpFvYihi7mmzttq0r751AnigBI8=",
      "type": "wallet:litecoin",
      "name": "CLI Tester LTC 2",
      "pluginId": "litecoin",
      "currencyCode": "LTC",
      "fiatCurrencyCode": "iso:USD",
      "blockHeight": 3192984,
      "syncStatus": {
        "totalRatio": 1
      },
      "syncRatio": "100%",
      "paused": false,
      "imported": false,
      "created": "2026-09-02T19:47:57.385Z",
      "enabledTokenIds": [],
      "detectedTokenIds": [],
      "unactivatedTokenIds": []
    },
    {
      "walletId": "2mvD7iFKAyQiH0vXMOs5rWlTmF6YJaovXJavUVABy8s=",
      "id": "2mvD7iFKAyQiH0vXMOs5rWlTmF6YJaovXJavUVABy8s=",
      "type": "wallet:dogecoin",
      "name": "My Doge",
      "pluginId": "dogecoin",
      "currencyCode": "DOGE",
      "fiatCurrencyCode": "iso:USD",
      "blockHeight": 6410049,
      "syncStatus": {
        "totalRatio": 0.275
      },
      "syncRatio": "28%",
      "paused": false,
      "imported": false,
      "created": "2026-09-11T19:43:36.532Z",
      "enabledTokenIds": [],
      "detectedTokenIds": [],
      "unactivatedTokenIds": []
    },
    {
      "walletId": "95YpOSuzU7m1ryJ+MUsFzf2QQ/+Re2TIFhP+jcwRB78=",
      "id": "95YpOSuzU7m1ryJ+MUsFzf2QQ/+Re2TIFhP+jcwRB78=",
      "type": "wallet:dogecoin",
      "name": "My Doge 2",
      "pluginId": "dogecoin",
      "currencyCode": "DOGE",
      "fiatCurrencyCode": "iso:USD",
      "blockHeight": 6410049,
      "syncStatus": {
        "totalRatio": 1
      },
      "syncRatio": "100%",
      "paused": false,
      "imported": true,
      "created": "2026-09-11T19:44:37.295Z",
      "enabledTokenIds": [],
      "detectedTokenIds": [],
      "unactivatedTokenIds": []
    },
    {
      "walletId": "lXNtBlC55BV/Nx1x+BqgpMcXEDlW+K3Xl7E5OHk/4BE=",
      "id": "lXNtBlC55BV/Nx1x+BqgpMcXEDlW+K3Xl7E5OHk/4BE=",
      "type": "wallet:stellar",
      "name": "TxDb XLM",
      "pluginId": "stellar",
      "currencyCode": "XLM",
      "fiatCurrencyCode": "iso:USD",
      "blockHeight": 64878810,
      "syncStatus": {
        "totalRatio": 1
      },
      "syncRatio": "100%",
      "paused": false,
      "imported": false,
      "created": "2026-09-24T20:03:00.688Z",
      "enabledTokenIds": [],
      "detectedTokenIds": [],
      "unactivatedTokenIds": []
    }
  ],
  "unloadedWallets": [
    {
      "walletId": "+I4cOaHjcEJc8r6Gqj0ItweWLfX2W0MuvqUmKi07UrA=",
      "walletType": "wallet:monero",
      "pluginId": "monero",
      "pluginRegistered": true,
      "pluginHasLoadedWallet": false
    },
    {
      "walletId": "R6lSMQGOtdPD46sEfabvxEw/nihdq6cpwxC6Gca5cMI=",
      "walletType": "wallet:zano",
      "pluginId": "zano",
      "pluginRegistered": true,
      "pluginHasLoadedWallet": false
    },
    {
      "walletId": "Zt7/dSqLD62IfUi8zPC+ALNpRNURhxANYVUNmnXIyrI=",
      "walletType": "wallet:zcash",
      "pluginId": "zcash",
      "pluginRegistered": true,
      "pluginHasLoadedWallet": false
    },
    {
      "walletId": "yoXwwIxf29Wj8fsHgTciI4wlzMmO6VhOGa8vG8RYkes=",
      "walletType": "wallet:monero",
      "pluginId": "monero",
      "pluginRegistered": true,
      "pluginHasLoadedWallet": false
    },
    {
      "walletId": "M/96jnAaXICOLFKJt9BUPoSfRoJ8KoZHWDpYRwjpr04=",
      "walletType": "wallet:zano",
      "pluginId": "zano",
      "pluginRegistered": true,
      "pluginHasLoadedWallet": false
    },
    {
      "walletId": "5w8Kx3jB5mCiUWGiooxMMafU9ckeS3RrAAF0UTatz6A=",
      "walletType": "wallet:zcash",
      "pluginId": "zcash",
      "pluginRegistered": true,
      "pluginHasLoadedWallet": false
    },
    {
      "walletId": "tIwzb7yUI9jK4foI2qexrErdMXgW51sDr06oGxJKFEk=",
      "walletType": "wallet:piratechain",
      "pluginId": "piratechain",
      "pluginRegistered": true,
      "pluginHasLoadedWallet": false
    },
    {
      "walletId": "zetC0WIMm0nNNaSun2y+nLk3WAOX2xN7fS0B6JGkjeU=",
      "walletType": "wallet:piratechain",
      "pluginId": "piratechain",
      "pluginRegistered": true,
      "pluginHasLoadedWallet": false
    },
    {
      "walletId": "yemaJy7C3goKaYo3/0knJW0OF4hQD1GiBiVpRi9ie/s=",
      "walletType": "wallet:piratechain",
      "pluginId": "piratechain",
      "pluginRegistered": true,
      "pluginHasLoadedWallet": false
    },
    {
      "walletId": "fxLd74wgxuH4f8EBc1dLixqcmiZrQXr3g1tRMacrBJE=",
      "walletType": "wallet:piratechain",
      "pluginId": "piratechain",
      "pluginRegistered": true,
      "pluginHasLoadedWallet": false
    }
  ]
}

exit 0, 0.3s

2026-10-10 17:50:47 (clitester)

$ edge-cli -t -d ~/.edge-cli-clitester --solve-captcha balance-map --wallet-id=DOhkEgaWy3whGKlI5COESjgnW37PawgjGakQfB1SusM=
{
  "balances": [
    {
      "tokenId": null,
      "currencyCode": "LTC",
      "nativeAmount": "1957906",
      "displayAmount": "0.01957906",
      "unknownToken": false
    }
  ]
}

exit 0, 0.2s

2026-10-10 17:50:47 (clitester)

$ edge-cli -t -d ~/.edge-cli-clitester --solve-captcha balance-map --wallet-id=m+z6RkRpEomOSRSggpFvYihi7mmzttq0r751AnigBI8=
{
  "balances": [
    {
      "tokenId": null,
      "currencyCode": "LTC",
      "nativeAmount": "45255611",
      "displayAmount": "0.45255611",
      "unknownToken": false
    }
  ]
}

exit 0, 0.1s

2026-10-10 17:50:48 (clitester)

$ edge-cli -t -d ~/.edge-cli-clitester --solve-captcha balance-map --wallet-id=7o7i6/tlI+qiFwlvZOdiLF7hcBY/JjBEIYZyB9e7t+8=
{
  "balances": [
    {
      "tokenId": null,
      "currencyCode": "BTC",
      "nativeAmount": "109035",
      "displayAmount": "0.00109035",
      "unknownToken": false
    }
  ]
}

exit 0, 0.1s

2026-10-10 17:51:06 (sharetest)

$ EDGE_CLI_PASSWORD=<redacted> edge-cli -t -d ~/.edge-cli-sharetest --solve-captcha login-with-password --username=sharetest
{
  "sessionId": "sess_2BWw…",
  "username": "sharetest",
  "rootLoginId": "F87uRAGkXbeHvn3fnMWh2NzWHCfKMthksAT2ECdKEf8k",
  "loginMethod": "password",
  "autoLogoutSeconds": 3600,
  "autoLogoutRead": true,
  "expiresAt": "2026-10-11T01:51:05.951Z",
  "lastActivityAt": "2026-10-11T00:51:05.951Z",
  "createdAt": "2026-10-11T00:51:05.951Z"
}

exit 0, 7.9s

2026-10-10 17:51:06 (sharetest)

$ edge-cli -t -d ~/.edge-cli-sharetest --solve-captcha wait-for-all-wallets
{
  "unloadedWallets": []
}

exit 0, 0.1s

2026-10-10 17:51:06 (sharetest)

$ edge-cli -t -d ~/.edge-cli-sharetest --solve-captcha balance-map --wallet-id=NaMX9JxtRBIdcafwnuVB/5ARZ9nnVi5khHbA2garRg0=
{
  "balances": [
    {
      "tokenId": null,
      "currencyCode": "LTC",
      "nativeAmount": "0",
      "displayAmount": "0",
      "unknownToken": false
    }
  ]
}

exit 0, 0.1s

2026-10-10 17:51:06 (sharetest)

$ edge-cli -t -d ~/.edge-cli-sharetest --solve-captcha get-addresses --wallet-id=NaMX9JxtRBIdcafwnuVB/5ARZ9nnVi5khHbA2garRg0=
{
  "addresses": [
    {
      "addressType": "segwitAddress",
      "publicAddress": "ltc1qmcs05z9stqwxlm48f3ja66qnu9w9edpvc6c456",
      "nativeBalance": "0"
    },
    {
      "addressType": "publicAddress",
      "publicAddress": "MRuZUzvds8aCLsMXyU5MATiSmMCGwjLiVY",
      "nativeBalance": "0"
    },
    {
      "addressType": "legacyAddress",
      "publicAddress": "3KhRB7Wfv1imYN5dsb61LpU3SebpyvWrCs",
      "nativeBalance": "0"
    }
  ]
}

exit 0, 0.1s

2026-10-10 17:51:13 (clitester)

$ edge-cli -t -d ~/.edge-cli-clitester --solve-captcha get-addresses --wallet-id=m+z6RkRpEomOSRSggpFvYihi7mmzttq0r751AnigBI8=
{
  "addresses": [
    {
      "addressType": "segwitAddress",
      "publicAddress": "ltc1qfkvdvrcr8g82mr72q7u67p8yzpg7yl3n3nm9nw",
      "nativeBalance": "0"
    },
    {
      "addressType": "publicAddress",
      "publicAddress": "M9ZPrtd23pVuhj4WSLm15deEcc56uvgPJg",
      "nativeBalance": "0"
    },
    {
      "addressType": "legacyAddress",
      "publicAddress": "33MFZ1D46heUuDncLTmfFzPqHuUeyNLYcJ",
      "nativeBalance": "0"
    }
  ]
}

exit 0, 0.1s

2026-10-10 17:51:13 (clitester)

$ edge-cli -t -d ~/.edge-cli-clitester --solve-captcha get-addresses --wallet-id=7o7i6/tlI+qiFwlvZOdiLF7hcBY/JjBEIYZyB9e7t+8=
{
  "addresses": [
    {
      "addressType": "segwitAddress",
      "publicAddress": "bc1q5kjamtm0424shsjp4dng3z85xxrqhgdzm7j7cr",
      "nativeBalance": "0"
    },
    {
      "addressType": "publicAddress",
      "publicAddress": "3HsDrCZWss7mCNTLLaAatB4c8YDJrs33Wp",
      "nativeBalance": "0"
    }
  ]
}

exit 0, 0.1s

2026-10-10 17:51:13 (clitester)

$ edge-cli -t -d ~/.edge-cli-clitester --solve-captcha encode-uri --wallet-id=m+z6RkRpEomOSRSggpFvYihi7mmzttq0r751AnigBI8= --public-address=ltc1qmcs05z9stqwxlm48f3ja66qnu9w9edpvc6c456 --native-amount=500000 --label=sharetest
{
  "uri": "litecoin:ltc1qmcs05z9stqwxlm48f3ja66qnu9w9edpvc6c456?amount=0.005&label=sharetest"
}

exit 0, 0.2s

2026-10-10 17:51:18 (clitester)

$ edge-cli -t -d ~/.edge-cli-clitester --solve-captcha spend --dry-run --wallet-id=m+z6RkRpEomOSRSggpFvYihi7mmzttq0r751AnigBI8= '--to=litecoin:ltc1qmcs05z9stqwxlm48f3ja66qnu9w9edpvc6c456?amount=0.005&label=sharetest'
{
  "objectId": "tx_4X31wp7tu6tddiVda",
  "kind": "transaction",
  "createdAt": "2026-10-11T00:51:18.587Z",
  "expiresAt": "2026-10-11T00:56:18.587Z",
  "sessionId": "sess_Qdxa…",
  "walletId": "m+z6RkRpEomOSRSggpFvYihi7mmzttq0r751AnigBI8=",
  "transaction": {
    "blockHeight": 0,
    "currencyCode": "LTC",
    "date": 1791679879,
    "feeRateUsed": {
      "satPerVByte": 250
    },
    "isSend": true,
    "memos": [],
    "nativeAmount": "-535063",
    "networkFee": "35063",
    "networkFees": [
      {
        "tokenId": null,
        "nativeAmount": "35063"
      }
    ],
    "otherParams": {
      "unsignedTx": "0200000001c312989acaf8fb0cf81980ed9d4c82959a69c2b7673286cf95618e18b9f181000100000000ffffffff0220a1070000000000160014de20fa08b0581c6feea74c65dd6813e15c5cb42c641f9b02000000001600146565bd0049b4d1232ee75a9771f806b79bc4fe3200000000",
      "psbt": {
        "base64": "cHNidP8BAHECAAAAAcMSmJrK+PsM+BmA7Z1MgpWaacK3ZzKGz5Vhjhi58YEAAQAAAAD/////AiChBwAAAAAAFgAU3iD6CLBYHG/up0xl3WgT4VxctCxkH5sCAAAAABYAFGVlvQBJtNEjLudal3H4BrebxP4yAAAAAAABAR97SaMCAAAAABYAFH6ZGqrgRTIiF3mU0PTxSRCnG4yPAQMEAQAAAAAAAA==",
        "inputs": [
          {
            "hash": {
              "type": "Buffer",
              "data": [/* 32 bytes */]
            },
            "index": 1,
            "value": 44255611,
            "script": {
              "type": "Buffer",
              "data": [/* 22 bytes */]
            },
            "scriptPubkey": {
              "type": "Buffer",
              "data": [/* 22 bytes */]
            },
            "scriptType": "p2wpkh",
            "sequence": 4294967295,
            "sighashType": 1,
            "witnessUtxo": {
              "script": {
                "type": "Buffer",
                "data": [/* 22 bytes */]
              },
              "value": 44255611
            }
          }
        ],
        "outputs": [
          {
            "script": {
              "type": "Buffer",
              "data": [/* 22 bytes */]
            },
            "value": 500000,
            "scriptPubkey": {
              "type": "Buffer",
              "data": [/* 22 bytes */]
            }
          },
          {
            "script": {
              "type": "Buffer",
              "data": [/* 22 bytes */]
            },
            "scriptPubkey": {
              "type": "Buffer",
              "data": [/* 22 bytes */]
            },
            "value": 43720548
          }
        ]
      },
      "edgeSpendInfo": {
        "currencyCode": "LTC",
        "tokenId": null,
        "memos": [],
        "metadata": {
          "name": "sharetest"
        },
        "networkFeeOption": "standard",
        "noUnconfirmed": false,
        "spendTargets": [
          {
            "nativeAmount": "500000",
            "otherParams": {},
            "publicAddress": "ltc1qmcs05z9stqwxlm48f3ja66qnu9w9edpvc6c456"
          }
        ]
      },
      "ourScriptPubkeys": [
        "00147e991aaae0453222177994d0f4f14910a71b8c8f",
        "00146565bd0049b4d1232ee75a9771f806b79bc4fe32"
      ]
    },
    "ourReceiveAddresses": [
      "ltc1qv4jm6qzfkngjxth8t2thr7qxk7dufl3jcu08ts"
    ],
    "signedTx": "",
    "tokenId": null,
    "txid": "",
    "walletId": "m+z6RkRpEomOSRSggpFvYihi7mmzttq0r751AnigBI8=",
    "networkFeeOption": "standard",
    "spendTargets": [
      {
        "currencyCode": "LTC",
        "nativeAmount": "500000",
        "publicAddress": "ltc1qmcs05z9stqwxlm48f3ja66qnu9w9edpvc6c456"
      }
    ],
    "metadata": {
      "name": "sharetest"
    }
  }
}

exit 0, 0.1s

2026-10-10 17:51:24 (clitester)

$ edge-cli -t -d ~/.edge-cli-clitester --solve-captcha spend --wallet-id=m+z6RkRpEomOSRSggpFvYihi7mmzttq0r751AnigBI8= '--to=litecoin:ltc1qmcs05z9stqwxlm48f3ja66qnu9w9edpvc6c456?amount=0.005&label=sharetest' '--metadata={"name":"sharetest","category":"Transfer:Sharetest","notes":"CLI sanity send"}'
{
  "transaction": {
    "blockHeight": 0,
    "currencyCode": "LTC",
    "date": 1791679885,
    "feeRateUsed": {
      "satPerVByte": 250
    },
    "isSend": true,
    "memos": [],
    "nativeAmount": "-535063",
    "networkFee": "35063",
    "networkFees": [
      {
        "tokenId": null,
        "nativeAmount": "35063"
      }
    ],
    "otherParams": {
      "unsignedTx": "0200000001c312989acaf8fb0cf81980ed9d4c82959a69c2b7673286cf95618e18b9f181000100000000ffffffff0220a1070000000000160014de20fa08b0581c6feea74c65dd6813e15c5cb42c641f9b02000000001600146565bd0049b4d1232ee75a9771f806b79bc4fe3200000000",
      "psbt": {
        "base64": "cHNidP8BAHECAAAAAcMSmJrK+PsM+BmA7Z1MgpWaacK3ZzKGz5Vhjhi58YEAAQAAAAD/////AiChBwAAAAAAFgAU3iD6CLBYHG/up0xl3WgT4VxctCxkH5sCAAAAABYAFGVlvQBJtNEjLudal3H4BrebxP4yAAAAAAABAR97SaMCAAAAABYAFH6ZGqrgRTIiF3mU0PTxSRCnG4yPAQMEAQAAAAAAAA==",
        "inputs": [
          {
            "hash": {
              "type": "Buffer",
              "data": [/* 32 bytes */]
            },
            "index": 1,
            "value": 44255611,
            "script": {
              "type": "Buffer",
              "data": [/* 22 bytes */]
            },
            "scriptPubkey": {
              "type": "Buffer",
              "data": [/* 22 bytes */]
            },
            "scriptType": "p2wpkh",
            "sequence": 4294967295,
            "sighashType": 1,
            "witnessUtxo": {
              "script": {
                "type": "Buffer",
                "data": [/* 22 bytes */]
              },
              "value": 44255611
            }
          }
        ],
        "outputs": [
          {
            "script": {
              "type": "Buffer",
              "data": [/* 22 bytes */]
            },
            "value": 500000,
            "scriptPubkey": {
              "type": "Buffer",
              "data": [/* 22 bytes */]
            }
          },
          {
            "script": {
              "type": "Buffer",
              "data": [/* 22 bytes */]
            },
            "scriptPubkey": {
              "type": "Buffer",
              "data": [/* 22 bytes */]
            },
            "value": 43720548
          }
        ]
      },
      "edgeSpendInfo": {
        "currencyCode": "LTC",
        "tokenId": null,
        "memos": [],
        "metadata": {
          "name": "sharetest",
          "category": "Transfer:Sharetest",
          "notes": "CLI sanity send"
        },
        "networkFeeOption": "standard",
        "noUnconfirmed": false,
        "spendTargets": [
          {
            "nativeAmount": "500000",
            "otherParams": {},
            "publicAddress": "ltc1qmcs05z9stqwxlm48f3ja66qnu9w9edpvc6c456"
          }
        ]
      },
      "ourScriptPubkeys": [
        "00147e991aaae0453222177994d0f4f14910a71b8c8f",
        "00146565bd0049b4d1232ee75a9771f806b79bc4fe32"
      ]
    },
    "ourReceiveAddresses": [
      "ltc1qv4jm6qzfkngjxth8t2thr7qxk7dufl3jcu08ts"
    ],
    "signedTx": "02000000000101c312989acaf8fb0cf81980ed9d4c82959a69c2b7673286cf95618e18b9f181000100000000ffffffff0220a1070000000000160014de20fa08b0581c6feea74c65dd6813e15c5cb42c641f9b02000000001600146565bd0049b4d1232ee75a9771f806b79bc4fe3202483045022100f8002ef97063375975572320a276bc5af60b7c5e3b85d363f22c15083384277102205ce80a792e128f3e9e39cd283063213cede9f0a48e7141723eaa2cd450e544140121032f45c6b16ec4130fc1b78a98a3b76aaa53eae2c7caae5f6b03ab828b72b8990500000000",
    "tokenId": null,
    "txid": "9ca11b2d32db160c39434b4b989ef737a08debc346411d792097ff5c21cd7c83",
    "walletId": "m+z6RkRpEomOSRSggpFvYihi7mmzttq0r751AnigBI8=",
    "networkFeeOption": "standard",
    "spendTargets": [
      {
        "currencyCode": "LTC",
        "nativeAmount": "500000",
        "publicAddress": "ltc1qmcs05z9stqwxlm48f3ja66qnu9w9edpvc6c456"
      }
    ],
    "metadata": {
      "name": "sharetest",
      "category": "Transfer:Sharetest",
      "notes": "CLI sanity send"
    }
  }
}

exit 0, 0.5s

2026-10-10 17:51:37 (clitester)

$ edge-cli -t -d ~/.edge-cli-clitester --solve-captcha get-transactions --wallet-id=m+z6RkRpEomOSRSggpFvYihi7mmzttq0r751AnigBI8= --limit=1
{
  "transactions": [
    {
      "blockHeight": 0,
      "confirmations": "unconfirmed",
      "currencyCode": "LTC",
      "feeRateUsed": {
        "satPerVByte": 248
      },
      "date": 1791679884.906,
      "isSend": true,
      "memos": [],
      "metadata": {
        "exchangeAmount": {
          "iso:USD": -0.3402789208155502
        },
        "category": "Transfer:Sharetest",
        "name": "sharetest",
        "notes": "CLI sanity send"
      },
      "nativeAmount": "-535063",
      "networkFee": "35063",
      "networkFees": [
        {
          "tokenId": null,
          "nativeAmount": "35063"
        }
      ],
      "otherParams": {},
      "ourReceiveAddresses": [
        "ltc1qv4jm6qzfkngjxth8t2thr7qxk7dufl3jcu08ts"
      ],
      "signedTx": "02000000000101c312989acaf8fb0cf81980ed9d4c82959a69c2b7673286cf95618e18b9f181000100000000ffffffff0220a1070000000000160014de20fa08b0581c6feea74c65dd6813e15c5cb42c641f9b02000000001600146565bd0049b4d1232ee75a9771f806b79bc4fe3202483045022100f8002ef97063375975572320a276bc5af60b7c5e3b85d363f22c15083384277102205ce80a792e128f3e9e39cd283063213cede9f0a48e7141723eaa2cd450e544140121032f45c6b16ec4130fc1b78a98a3b76aaa53eae2c7caae5f6b03ab828b72b8990500000000",
      "tokenId": null,
      "txid": "9ca11b2d32db160c39434b4b989ef737a08debc346411d792097ff5c21cd7c83",
      "walletId": "m+z6RkRpEomOSRSggpFvYihi7mmzttq0r751AnigBI8=",
      "networkFeeOption": "standard",
      "spendTargets": [
        {
          "currencyCode": "LTC",
          "nativeAmount": "500000",
          "publicAddress": "ltc1qmcs05z9stqwxlm48f3ja66qnu9w9edpvc6c456"
        }
      ],
      "displayInfo": {
        "direction": "send",
        "category": {
          "category": "transfer",
          "subcategory": "Sharetest"
        }
      }
    }
  ],
  "total": 5,
  "isoFiat": "iso:USD",
  "unpricedCount": 0
}

exit 0, 1.3s

2026-10-10 17:51:59 (sharetest)

$ edge-cli -t -d ~/.edge-cli-sharetest --solve-captcha get-transactions --wallet-id=NaMX9JxtRBIdcafwnuVB/5ARZ9nnVi5khHbA2garRg0= --limit=1
{
  "transactions": [
    {
      "blockHeight": 0,
      "confirmations": "unconfirmed",
      "currencyCode": "LTC",
      "feeRateUsed": {
        "satPerVByte": 248
      },
      "date": 1791679885,
      "isSend": false,
      "memos": [],
      "metadata": {
        "exchangeAmount": {
          "iso:USD": 0.31798023860325814
        },
        "category": "Income:",
        "name": "Received Litecoin"
      },
      "nativeAmount": "500000",
      "networkFee": "35063",
      "networkFees": [
        {
          "tokenId": null,
          "nativeAmount": "35063"
        }
      ],
      "otherParams": {},
      "ourReceiveAddresses": [
        "ltc1qmcs05z9stqwxlm48f3ja66qnu9w9edpvc6c456"
      ],
      "signedTx": "02000000000101c312989acaf8fb0cf81980ed9d4c82959a69c2b7673286cf95618e18b9f181000100000000ffffffff0220a1070000000000160014de20fa08b0581c6feea74c65dd6813e15c5cb42c641f9b02000000001600146565bd0049b4d1232ee75a9771f806b79bc4fe3202483045022100f8002ef97063375975572320a276bc5af60b7c5e3b85d363f22c15083384277102205ce80a792e128f3e9e39cd283063213cede9f0a48e7141723eaa2cd450e544140121032f45c6b16ec4130fc1b78a98a3b76aaa53eae2c7caae5f6b03ab828b72b8990500000000",
      "tokenId": null,
      "txid": "9ca11b2d32db160c39434b4b989ef737a08debc346411d792097ff5c21cd7c83",
      "walletId": "NaMX9JxtRBIdcafwnuVB/5ARZ9nnVi5khHbA2garRg0=",
      "displayInfo": {
        "direction": "receive",
        "category": {
          "category": "income",
          "subcategory": ""
        }
      }
    }
  ],
  "total": 1,
  "isoFiat": "iso:USD",
  "unpricedCount": 0
}

exit 0, 1.6s

2026-10-10 17:51:59 (sharetest)

$ edge-cli -t -d ~/.edge-cli-sharetest --solve-captcha balance-map --wallet-id=NaMX9JxtRBIdcafwnuVB/5ARZ9nnVi5khHbA2garRg0=
{
  "balances": [
    {
      "tokenId": null,
      "currencyCode": "LTC",
      "nativeAmount": "500000",
      "displayAmount": "0.005",
      "unknownToken": false
    }
  ]
}

exit 0, 0.1s

2026-10-10 17:52:07 (sharetest)

$ edge-cli -t -d ~/.edge-cli-sharetest --solve-captcha spend --wallet-id=NaMX9JxtRBIdcafwnuVB/5ARZ9nnVi5khHbA2garRg0= --to=ltc1qfkvdvrcr8g82mr72q7u67p8yzpg7yl3n3nm9nw --native-amount=200000 '--metadata={"name":"clitester","notes":"CLI sanity return"}'
{
  "transaction": {
    "blockHeight": 0,
    "currencyCode": "LTC",
    "date": 1791679927,
    "feeRateUsed": {
      "satPerVByte": 250
    },
    "isSend": true,
    "memos": [],
    "nativeAmount": "-235063",
    "networkFee": "35063",
    "networkFees": [
      {
        "tokenId": null,
        "nativeAmount": "35063"
      }
    ],
    "otherParams": {
      "unsignedTx": "0200000001837ccd215cff9720791d4146c3eb8da037f79e984b4b43390c16db322d1ba19c0000000000ffffffff02400d0300000000001600144d98d60f033a0ead8fca07b9af04e41051e27e33e90a0400000000001600140a52ba87d974118a8b070b0424916d6eb347b03800000000",
      "psbt": {
        "base64": "cHNidP8BAHECAAAAAYN8zSFc/5cgeR1BRsPrjaA3956YS0tDOQwW2zItG6GcAAAAAAD/////AkANAwAAAAAAFgAUTZjWDwM6Dq2Pyge5rwTkEFHifjPpCgQAAAAAABYAFApSuofZdBGKiwcLBCSRbW6zR7A4AAAAAAABAR8goQcAAAAAABYAFN4g+giwWBxv7qdMZd1oE+FcXLQsAQMEAQAAAAAAAA==",
        "inputs": [
          {
            "hash": {
              "type": "Buffer",
              "data": [/* 32 bytes */]
            },
            "index": 0,
            "value": 500000,
            "script": {
              "type": "Buffer",
              "data": [/* 22 bytes */]
            },
            "scriptPubkey": {
              "type": "Buffer",
              "data": [/* 22 bytes */]
            },
            "scriptType": "p2wpkh",
            "sequence": 4294967295,
            "sighashType": 1,
            "witnessUtxo": {
              "script": {
                "type": "Buffer",
                "data": [/* 22 bytes */]
              },
              "value": 500000
            }
          }
        ],
        "outputs": [
          {
            "script": {
              "type": "Buffer",
              "data": [/* 22 bytes */]
            },
            "value": 200000,
            "scriptPubkey": {
              "type": "Buffer",
              "data": [/* 22 bytes */]
            }
          },
          {
            "script": {
              "type": "Buffer",
              "data": [/* 22 bytes */]
            },
            "scriptPubkey": {
              "type": "Buffer",
              "data": [/* 22 bytes */]
            },
            "value": 264937
          }
        ]
      },
      "edgeSpendInfo": {
        "currencyCode": "LTC",
        "tokenId": null,
        "memos": [],
        "metadata": {
          "name": "clitester",
          "notes": "CLI sanity return"
        },
        "networkFeeOption": "standard",
        "noUnconfirmed": false,
        "spendTargets": [
          {
            "nativeAmount": "200000",
            "otherParams": {},
            "publicAddress": "ltc1qfkvdvrcr8g82mr72q7u67p8yzpg7yl3n3nm9nw"
          }
        ]
      },
      "ourScriptPubkeys": [
        "0014de20fa08b0581c6feea74c65dd6813e15c5cb42c",
        "00140a52ba87d974118a8b070b0424916d6eb347b038"
      ]
    },
    "ourReceiveAddresses": [
      "ltc1qpfft4p7ewsgc4zc8pvzzfytdd6e50vpcpcqgm2"
    ],
    "signedTx": "02000000000101837ccd215cff9720791d4146c3eb8da037f79e984b4b43390c16db322d1ba19c0000000000ffffffff02400d0300000000001600144d98d60f033a0ead8fca07b9af04e41051e27e33e90a0400000000001600140a52ba87d974118a8b070b0424916d6eb347b03802473044022050b8d8bf5cb43231051419068cf35c6bb46bffd058841e183180c2ee0474475302200f1c57906ecfd95f5ad0ad14e665991d9fa5a38c953d8d2094ee7c462ac22431012102615edc573743ed4ca4dbf174d00bdf493bf7c4db13896504f7b71db6909e401b00000000",
    "tokenId": null,
    "txid": "b9acbae3efc486c7e0bc423a64b0ca6131d7049b06ae4a64c2e09fd8a01a98f2",
    "walletId": "NaMX9JxtRBIdcafwnuVB/5ARZ9nnVi5khHbA2garRg0=",
    "networkFeeOption": "standard",
    "spendTargets": [
      {
        "currencyCode": "LTC",
        "nativeAmount": "200000",
        "publicAddress": "ltc1qfkvdvrcr8g82mr72q7u67p8yzpg7yl3n3nm9nw"
      }
    ],
    "metadata": {
      "name": "clitester",
      "notes": "CLI sanity return"
    }
  }
}

exit 0, 0.2s

2026-10-10 17:52:19 (clitester)

$ edge-cli -t -d ~/.edge-cli-clitester --solve-captcha fetch-swap-quotes --from-wallet-id=m+z6RkRpEomOSRSggpFvYihi7mmzttq0r751AnigBI8= --to-wallet-id=7o7i6/tlI+qiFwlvZOdiLF7hcBY/JjBEIYZyB9e7t+8= --native-amount=5000000
{
  "quoteCount": 1,
  "quotes": [
    {
      "objectId": "swap_3vP38GDa7XQGpqsmS",
      "kind": "swap",
      "expiresAt": "2026-10-11T00:57:19.717Z",
      "pluginId": "thorchain",
      "isEstimate": true,
      "canBePartial": false,
      "maxFulfillmentSeconds": 150,
      "minReceiveAmount": null,
      "fromNativeAmount": "5000000",
      "toNativeAmount": "2738",
      "networkFee": {
        "nativeAmount": "7742",
        "tokenId": null
      },
      "quoteExpirationDate": "2026-10-11T00:53:16.717Z",
      "swapInfo": {
        "pluginId": "thorchain",
        "displayName": "Thorchain",
        "supportEmail": "support@edge.app",
        "isDex": true
      },
      "request": {
        "fromTokenId": null,
        "toTokenId": null,
        "nativeAmount": "5000000",
        "quoteFor": "from",
        "fromWalletId": "m+z6RkRpEomOSRSggpFvYihi7mmzttq0r751AnigBI8=",
        "toWalletId": "7o7i6/tlI+qiFwlvZOdiLF7hcBY/JjBEIYZyB9e7t+8="
      }
    }
  ]
}

exit 0, 5.3s

2026-10-10 17:52:28 (clitester)

$ edge-cli -t -d ~/.edge-cli-clitester --solve-captcha approve-swap-quote swap_3vP38GDa7XQGpqsmS
{
  "ok": true,
  "objectId": "swap_3vP38GDa7XQGpqsmS",
  "orderId": "0f0a34872761fb57db762bb82660ea5142fc6fd894324ca1238a217f91236262",
  "destinationAddress": "bc1q5kjamtm0424shsjp4dng3z85xxrqhgdzm7j7cr",
  "transaction": {
    "blockHeight": 0,
    "currencyCode": "LTC",
    "date": 1791679948,
    "feeRateUsed": {
      "satPerVByte": 28
    },
    "isSend": true,
    "memos": [
      {
        "type": "text",
        "value": "=:b:bc1q5kjamtm0424shsjp4dng3z85xxrqhgdzm7j7cr:0/1/1:ej:75"
      }
    ],
    "nativeAmount": "-5007742",
    "networkFee": "7742",
    "networkFees": [
      {
        "tokenId": null,
        "nativeAmount": "7742"
      }
    ],
    "otherParams": {
      "unsignedTx": "02000000025750bb69c55d3b78976396928ac59f6bd3937500a9585df7f3f55e1cfe7e27470000000000ffffffff837ccd215cff9720791d4146c3eb8da037f79e984b4b43390c16db322d1ba19c0100000000ffffffff03404b4c000000000016001477dfabe45a632fd979340b995f592a209f75e1f300000000000000003c6a3a3d3a623a62633171356b6a616d746d303432347368736a7034646e67337a3835787872716867647a6d376a3763723a302f312f313a656a3a373526f85d020000000016001404c24d607a8d15005ae0506ece2208a8332dac7800000000",
      "psbt": {
        "base64": "cHNidP8BAN8CAAAAAldQu2nFXTt4l2OWkorFn2vTk3UAqVhd9/P1Xhz+fidHAAAAAAD/////g3zNIVz/lyB5HUFGw+uNoDf3nphLS0M5DBbbMi0boZwBAAAAAP////8DQEtMAAAAAAAWABR336vkWmMv2Xk0C5lfWSogn3Xh8wAAAAAAAAAAPGo6PTpiOmJjMXE1a2phbXRtMDQyNHNoc2pwNGRuZzN6ODV4eHJxaGdkem03ajdjcjowLzEvMTplajo3NSb4XQIAAAAAFgAUBMJNYHqNFQBa4FBuziIIqDMtrHgAAAAAAAEBH0BCDwAAAAAAFgAUyUW3QU5CRJowEbR3EtojKgX9qxMBAwQBAAAAAAEBH2QfmwIAAAAAFgAUZWW9AEm00SMu51qXcfgGt5vE/jIBAwQBAAAAAAAAAA==",
        "inputs": [
          {
            "hash": {
              "type": "Buffer",
              "data": [/* 32 bytes */]
            },
            "index": 0,
            "value": 1000000,
            "script": {
              "type": "Buffer",
              "data": [/* 22 bytes */]
            },
            "scriptPubkey": {
              "type": "Buffer",
              "data": [/* 22 bytes */]
            },
            "scriptType": "p2wpkh",
            "sequence": 4294967295,
            "sighashType": 1,
            "witnessUtxo": {
              "script": {
                "type": "Buffer",
                "data": [/* 22 bytes */]
              },
              "value": 1000000
            }
          },
          {
            "hash": {
              "type": "Buffer",
              "data": [/* 32 bytes */]
            },
            "index": 1,
            "value": 43720548,
            "script": {
              "type": "Buffer",
              "data": [/* 22 bytes */]
            },
            "scriptPubkey": {
              "type": "Buffer",
              "data": [/* 22 bytes */]
            },
            "scriptType": "p2wpkh",
            "sequence": 4294967295,
            "sighashType": 1,
            "witnessUtxo": {
              "script": {
                "type": "Buffer",
                "data": [/* 22 bytes */]
              },
              "value": 43720548
            }
          }
        ],
        "outputs": [
          {
            "script": {
              "type": "Buffer",
              "data": [/* 22 bytes */]
            },
            "value": 5000000,
            "scriptPubkey": {
              "type": "Buffer",
              "data": [/* 22 bytes */]
            }
          },
          {
            "script": {
              "type": "Buffer",
              "data": [/* 60 bytes */]
            },
            "value": 0,
            "scriptPubkey": {
              "type": "Buffer",
              "data": [/* 60 bytes */]
            }
          },
          {
            "script": {
              "type": "Buffer",
              "data": [/* 22 bytes */]
            },
            "scriptPubkey": {
              "type": "Buffer",
              "data": [/* 22 bytes */]
            },
            "value": 39712806
          }
        ]
      },
      "edgeSpendInfo": {
        "currencyCode": "LTC",
        "tokenId": null,
        "customNetworkFee": {
          "satPerByte": "28"
        },
        "memos": [
          {
            "type": "text",
            "value": "=:b:bc1q5kjamtm0424shsjp4dng3z85xxrqhgdzm7j7cr:0/1/1:ej:75"
          }
        ],
        "networkFeeOption": "custom",
        "noUnconfirmed": false,
        "otherParams": {
          "outputSort": "targets"
        },
        "spendTargets": [
          {
            "memo": "=:b:bc1q5kjamtm0424shsjp4dng3z85xxrqhgdzm7j7cr:0/1/1:ej:75",
            "nativeAmount": "5000000",
            "otherParams": {
              "uniqueIdenfitifer": "=:b:bc1q5kjamtm0424shsjp4dng3z85xxrqhgdzm7j7cr:0/1/1:ej:75"
            },
            "publicAddress": "ltc1qwl06hez6vvhaj7f5pwv47kf2yz0htc0ndyk0nz",
            "uniqueIdentifier": "=:b:bc1q5kjamtm0424shsjp4dng3z85xxrqhgdzm7j7cr:0/1/1:ej:75"
          }
        ]
      },
      "ourScriptPubkeys": [
        "0014c945b7414e42449a3011b47712da232a05fdab13",
        "00146565bd0049b4d1232ee75a9771f806b79bc4fe32",
        "001404c24d607a8d15005ae0506ece2208a8332dac78"
      ]
    },
    "ourReceiveAddresses": [
      "ltc1qqnpy6cr6352sqkhq2phvugsg4qejmtrckwfk3k"
    ],
    "signedTx": "020000000001025750bb69c55d3b78976396928ac59f6bd3937500a9585df7f3f55e1cfe7e27470000000000ffffffff837ccd215cff9720791d4146c3eb8da037f79e984b4b43390c16db322d1ba19c0100000000ffffffff03404b4c000000000016001477dfabe45a632fd979340b995f592a209f75e1f300000000000000003c6a3a3d3a623a62633171356b6a616d746d303432347368736a7034646e67337a3835787872716867647a6d376a3763723a302f312f313a656a3a373526f85d020000000016001404c24d607a8d15005ae0506ece2208a8332dac7802473044022067c9478870ec0f881b9f177d79a4bd6398a04c4d03b66377babc9476097bb29b022055e00863a8553acee510ea904d714ce7c9c2e9af843cb3ca4bdab8bf265309af012102ca5769e4a1b60fbeca75074801931e74022ea69bc4470fb0316f31effc007730024830450221009c43391e6ce0de25999640c351253a1a4e60946ac720c887d15bafbb1b82e0780220161d3b881f2c7cc592269d2ecbfe4f779c29cebc5b419b4e923ca870c4b4e040012103e98b22c86e8cd115952e5091a266b050b286c8f5c4faf8a47ac706ff7705445200000000",
    "tokenId": null,
    "txid": "0f0a34872761fb57db762bb82660ea5142fc6fd894324ca1238a217f91236262",
    "walletId": "m+z6RkRpEomOSRSggpFvYihi7mmzttq0r751AnigBI8=",
    "networkFeeOption": "custom",
    "requestedCustomFee": {
      "satPerByte": "28"
    },
    "spendTargets": [
      {
        "currencyCode": "LTC",
        "memo": "=:b:bc1q5kjamtm0424shsjp4dng3z85xxrqhgdzm7j7cr:0/1/1:ej:75",
        "nativeAmount": "5000000",
        "publicAddress": "ltc1qwl06hez6vvhaj7f5pwv47kf2yz0htc0ndyk0nz",
        "uniqueIdentifier": "=:b:bc1q5kjamtm0424shsjp4dng3z85xxrqhgdzm7j7cr:0/1/1:ej:75"
      }
    ],
    "savedAction": {
      "actionType": "swap",
      "swapInfo": {
        "pluginId": "thorchain",
        "displayName": "Thorchain",
        "isDex": true,
        "supportEmail": "support@edge.app"
      },
      "orderUri": "https://track.thorchain.org/{{TXID}}",
      "isEstimate": true,
      "fromAsset": {
        "pluginId": "litecoin",
        "tokenId": null,
        "nativeAmount": "5000000"
      },
      "toAsset": {
        "pluginId": "bitcoin",
        "tokenId": null,
        "nativeAmount": "2738"
      },
      "payoutWalletId": "7o7i6/tlI+qiFwlvZOdiLF7hcBY/JjBEIYZyB9e7t+8=",
      "payoutAddress": "bc1q5kjamtm0424shsjp4dng3z85xxrqhgdzm7j7cr"
    },
    "assetAction": {
      "assetActionType": "swap"
    },
    "metadata": {}
  }
}

exit 0, 0.2s

2026-10-10 17:52:40 (clitester)

$ edge-cli -t -d ~/.edge-cli-clitester --solve-captcha fetch-swap-quotes --from-wallet-id=m+z6RkRpEomOSRSggpFvYihi7mmzttq0r751AnigBI8= --to-wallet-id=7o7i6/tlI+qiFwlvZOdiLF7hcBY/JjBEIYZyB9e7t+8= --native-amount=5000000
{
  "quoteCount": 1,
  "quotes": [
    {
      "objectId": "swap_5mpbdk1twifxdcjD3",
      "kind": "swap",
      "expiresAt": "2026-10-11T00:57:40.678Z",
      "pluginId": "thorchain",
      "isEstimate": true,
      "canBePartial": false,
      "maxFulfillmentSeconds": 150,
      "minReceiveAmount": null,
      "fromNativeAmount": "5000000",
      "toNativeAmount": "2738",
      "networkFee": {
        "nativeAmount": "5859",
        "tokenId": null
      },
      "quoteExpirationDate": "2026-10-11T00:53:39.131Z",
      "swapInfo": {
        "pluginId": "thorchain",
        "displayName": "Thorchain",
        "supportEmail": "support@edge.app",
        "isDex": true
      },
      "request": {
        "fromTokenId": null,
        "toTokenId": null,
        "nativeAmount": "5000000",
        "quoteFor": "from",
        "fromWalletId": "m+z6RkRpEomOSRSggpFvYihi7mmzttq0r751AnigBI8=",
        "toWalletId": "7o7i6/tlI+qiFwlvZOdiLF7hcBY/JjBEIYZyB9e7t+8="
      }
    }
  ]
}

exit 0, 3.1s

2026-10-10 17:53:43 (clitester)

$ edge-cli -t -d ~/.edge-cli-clitester --solve-captcha approve-swap-quote swap_5mpbdk1twifxdcjD3

{
  "error": {
    "code": "QUOTE_EXPIRED",
    "message": "The exchange's quote expired at 2026-10-11T00:53:39.131Z; fetch a new one",
    "status": 410,
    "details": {
      "quoteExpirationDate": "2026-10-11T00:53:39.131Z"
    }
  }
}

exit 4, 0.1s

2026-10-10 17:53:50 (clitester)

$ edge-cli -t -d ~/.edge-cli-clitester --solve-captcha get-transactions --wallet-id=m+z6RkRpEomOSRSggpFvYihi7mmzttq0r751AnigBI8= --limit=1
{
  "transactions": [
    {
      "blockHeight": 0,
      "confirmations": "unconfirmed",
      "currencyCode": "LTC",
      "feeRateUsed": {
        "satPerVByte": 27
      },
      "date": 1791679948,
      "isSend": true,
      "memos": [],
      "metadata": {
        "exchangeAmount": {
          "iso:USD": -3.184841844383285
        },
        "category": "Exchange:To BTC",
        "name": "Swap Funds"
      },
      "nativeAmount": "-5007742",
      "networkFee": "7742",
      "networkFees": [
        {
          "tokenId": null,
          "nativeAmount": "7742"
        }
      ],
      "otherParams": {},
      "ourReceiveAddresses": [
        "ltc1qqnpy6cr6352sqkhq2phvugsg4qejmtrckwfk3k"
      ],
      "signedTx": "020000000001025750bb69c55d3b78976396928ac59f6bd3937500a9585df7f3f55e1cfe7e27470000000000ffffffff837ccd215cff9720791d4146c3eb8da037f79e984b4b43390c16db322d1ba19c0100000000ffffffff03404b4c000000000016001477dfabe45a632fd979340b995f592a209f75e1f300000000000000003c6a3a3d3a623a62633171356b6a616d746d303432347368736a7034646e67337a3835787872716867647a6d376a3763723a302f312f313a656a3a373526f85d020000000016001404c24d607a8d15005ae0506ece2208a8332dac7802473044022067c9478870ec0f881b9f177d79a4bd6398a04c4d03b66377babc9476097bb29b022055e00863a8553acee510ea904d714ce7c9c2e9af843cb3ca4bdab8bf265309af012102ca5769e4a1b60fbeca75074801931e74022ea69bc4470fb0316f31effc007730024830450221009c43391e6ce0de25999640c351253a1a4e60946ac720c887d15bafbb1b82e0780220161d3b881f2c7cc592269d2ecbfe4f779c29cebc5b419b4e923ca870c4b4e040012103e98b22c86e8cd115952e5091a266b050b286c8f5c4faf8a47ac706ff7705445200000000",
      "tokenId": null,
      "txid": "0f0a34872761fb57db762bb82660ea5142fc6fd894324ca1238a217f91236262",
      "walletId": "m+z6RkRpEomOSRSggpFvYihi7mmzttq0r751AnigBI8=",
      "networkFeeOption": "custom",
      "requestedCustomFee": {
        "satPerByte": "28"
      },
      "spendTargets": [
        {
          "currencyCode": "LTC",
          "memo": "=:b:bc1q5kjamtm0424shsjp4dng3z85xxrqhgdzm7j7cr:0/1/1:ej:75",
          "nativeAmount": "5000000",
          "publicAddress": "ltc1qwl06hez6vvhaj7f5pwv47kf2yz0htc0ndyk0nz",
          "uniqueIdentifier": "=:b:bc1q5kjamtm0424shsjp4dng3z85xxrqhgdzm7j7cr:0/1/1:ej:75"
        }
      ],
      "assetAction": {
        "assetActionType": "swap"
      },
      "savedAction": {
        "actionType": "swap",
        "swapInfo": {
          "pluginId": "thorchain",
          "displayName": "Thorchain",
          "isDex": true,
          "supportEmail": "support@edge.app"
        },
        "orderUri": "https://track.thorchain.org/{{TXID}}",
        "isEstimate": true,
        "fromAsset": {
          "pluginId": "litecoin",
          "tokenId": null,
          "nativeAmount": "5000000"
        },
        "toAsset": {
          "pluginId": "bitcoin",
          "tokenId": null,
          "nativeAmount": "2738"
        },
        "payoutWalletId": "7o7i6/tlI+qiFwlvZOdiLF7hcBY/JjBEIYZyB9e7t+8=",
        "payoutAddress": "bc1q5kjamtm0424shsjp4dng3z85xxrqhgdzm7j7cr"
      },
      "displayInfo": {
        "direction": "send",
        "iconPluginId": "thorchain",
        "assetActionType": "swap",
        "actionType": "swap",
        "category": {
          "category": "exchange",
          "subcategory": "To BTC"
        }
      }
    }
  ],
  "total": 7,
  "isoFiat": "iso:USD",
  "unpricedCount": 0
}

exit 0, 0.2s

2026-10-10 17:57:25 (clitester)

$ edge-cli -t -d ~/.edge-cli-clitester --solve-captcha get-transactions --wallet-id=7o7i6/tlI+qiFwlvZOdiLF7hcBY/JjBEIYZyB9e7t+8= --limit=1
{
  "transactions": [
    {
      "blockHeight": 0,
      "confirmations": "unconfirmed",
      "currencyCode": "BTC",
      "feeRateUsed": {
        "satPerVByte": 4
      },
      "date": 1791680185,
      "isSend": false,
      "memos": [],
      "metadata": {
        "exchangeAmount": {
          "iso:USD": 2.2694007757744763
        },
        "category": "Income:",
        "name": "Received Bitcoin"
      },
      "nativeAmount": "2738",
      "networkFee": "1000",
      "networkFees": [
        {
          "tokenId": null,
          "nativeAmount": "1000"
        }
      ],
      "otherParams": {},
      "ourReceiveAddresses": [
        "bc1q5kjamtm0424shsjp4dng3z85xxrqhgdzm7j7cr"
      ],
      "signedTx": "0100000000010199697c1c5b4951fd62128ca8e5b02e4fccd86490fe0dea3423d37539df19da160100000000ffffffff03b20a000000000000160014a5a5ddaf6faaab0bc241ab668888f431860ba1a2bdd0d82f01000000160014224d665483619dd00f2ddc321c19cdeab15d59260000000000000000466a444f55543a3046304133343837323736314642353744423736324242383236363045413531343246433646443839343332344341313233384132313746393132333632363202483045022100f948fda44e7357c73b253860be10da96ef006332f462c22ad269e08cc196b07502205addc227ce566274a5e5b65a9f3e771c9d63301fbba4522c67c4d52f890c4dab012103903119b955a2f380e2091996c720f7cbcf626ea729cb793a5f41661bdec00f9700000000",
      "tokenId": null,
      "txid": "9ef7af518502d91bfd6513c624c08ca7ccf94122d3c6bd0d3e4a101c02d40d88",
      "walletId": "7o7i6/tlI+qiFwlvZOdiLF7hcBY/JjBEIYZyB9e7t+8=",
      "displayInfo": {
        "direction": "receive",
        "category": {
          "category": "income",
          "subcategory": ""
        }
      }
    }
  ],
  "total": 13,
  "isoFiat": "iso:USD",
  "unpricedCount": 0
}

exit 0, 1.5s

2026-10-10 17:57:25 (clitester)

$ edge-cli -t -d ~/.edge-cli-clitester --solve-captcha balance-map --wallet-id=7o7i6/tlI+qiFwlvZOdiLF7hcBY/JjBEIYZyB9e7t+8=
{
  "balances": [
    {
      "tokenId": null,
      "currencyCode": "BTC",
      "nativeAmount": "111773",
      "displayAmount": "0.00111773",
      "unknownToken": false
    }
  ]
}

exit 0, 0.1s

2026-10-10 17:57:25 (clitester)

$ edge-cli -t -d ~/.edge-cli-clitester --solve-captcha balance-map --wallet-id=m+z6RkRpEomOSRSggpFvYihi7mmzttq0r751AnigBI8=
{
  "balances": [
    {
      "tokenId": null,
      "currencyCode": "LTC",
      "nativeAmount": "39912806",
      "displayAmount": "0.39912806",
      "unknownToken": false
    }
  ]
}

exit 0, 0.1s

2026-10-10 17:57:25 (sharetest)

$ edge-cli -t -d ~/.edge-cli-sharetest --solve-captcha balance-map --wallet-id=NaMX9JxtRBIdcafwnuVB/5ARZ9nnVi5khHbA2garRg0=
{
  "balances": [
    {
      "tokenId": null,
      "currencyCode": "LTC",
      "nativeAmount": "264937",
      "displayAmount": "0.00264937",
      "unknownToken": false
    }
  ]
}

exit 0, 0.1s

2026-10-10 17:57:26 (sharetest)

$ edge-cli -t -d ~/.edge-cli-sharetest --solve-captcha logout
{
  "ok": true
}

exit 0, 0.1s

2026-10-10 17:57:26 (clitester)

$ edge-cli -t -d ~/.edge-cli-clitester --solve-captcha logout
{
  "ok": true
}

exit 0, 0.1s

@socket-security

socket-security Bot commented Jul 22, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednanocolors@​0.1.12991009975100
Added@​rollup/​plugin-json@​6.1.010010010084100
Added@​rollup/​plugin-node-resolve@​16.0.39910010084100
Updated@​rollup/​plugin-babel@​6.0.3 ⏵ 6.1.0100 +110010085 -4100
Addednode-gyp@​11.5.09910010089100

View full report

@socket-security

socket-security Bot commented Jul 22, 2026 •

Copy link
Copy Markdown

All alerts resolved. Learn more about Socket for GitHub.

This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored.

View full report

@paullinator
paullinator force-pushed the paul/cli branch 11 times, most recently from 67406a5 to 35eeb43 Compare August 8, 2026 07:06
@paullinator
paullinator force-pushed the paul/cli branch 10 times, most recently from a8819b9 to 60adf1e Compare September 3, 2026 05:13
@paullinator
paullinator force-pushed the paul/cli branch 2 times, most recently from 7d3a2ba to c0c5f13 Compare September 5, 2026 00:08
@paullinator
paullinator force-pushed the paul/cli branch 3 times, most recently from 0574066 to 4bb295e Compare September 24, 2026 20:49
paullinator and others added 25 commits November 6, 2026 17:45
Start the log's reopen retry window only when an open fails.

The window spaces out retries after a failed reopen, so a persistent
failure costs one openSync per REOPEN_RETRY_MS rather than one per line.
A successful reopen started it too, and every roll reopens the log, so a
roll within five seconds of the previous one found the window open:
roll() had already released the stream, and every line was dropped until
the window passed. At the 8 MB ceiling that takes more than about 1.6 MB/s
of logging.
Account and session management, credentials, 2FA and vouchers, the data
store, keys and wallets, tokens, URIs, transactions and their export,
the staged spend path, swaps, exchange rates, and the `$internalStuff`
admin calls — the rest of the surface, each call its own `route({…})`
declaration carrying its own cleaners, error list and prose.

With the surface complete, the suites that drive all of it arrive too:
`testCliFake` and `testCliSubscribe` spawn a real engine daemon on a
Unix socket under a temporary data directory and walk every command
against the in-process fake world, needing no network and no API key —
and the fifth documentation gate, the one that asks whether a command's
handler is ever reached, counts them. `docs/api/README.md` is the
contributor's guide to the declaration format and to all five gates.

They hold across every route: the surface matches, each response field
carries prose, each call either matches its `edge-core-js` signature or
records why it differs, and every command either reaches its handler
offline or is listed with the suite that drives it instead. `npm run
docs:api:gates` reports the counts; they are not repeated here, because
a number in a commit message goes stale the first time a route is added.

Some handlers no offline suite can reach past a refusal: a spend needs
funds, a swap a live partner, a voucher a pending login. The coverage
gate lists each with what does reach it, either a handler suite such as
`spendHandlers.test.ts` or `ratesRoutes.test.ts`, which it checks
exists, or the reason nothing can. No command is excused as reachable
only over the network.

Where the API departs from `edge-core-js` it is recorded in `coreExtra`
with the reason — a wallet object that cannot cross HTTP as anything but
an id, the `to`/`amount` shorthand that expands into `spendTargets`,
engine-side paging and export on `get-transactions`. Anything not listed
there fails the build.

A caller's mistake is a refusal, not a success. Core's own surface is
tolerant in ways an API cannot be — `changeEnabledTokenIds` filters out
an id it does not know and answers 200, so a mistyped contract address
was indistinguishable from a no-op; a voucher id the login server has
never seen is forwarded and reported as approved, while the server
approves the real login on a timer; a `--token-id` the wallet tracks
nothing for answered an empty list and exit 0. Each of those is now a
`404` or a `400` naming the field, raised before anything is written,
and the declared error list says so. The same stance covers a pair of
fields that contradict each other: a `spendInfo` beside a `tokenId`, a
saved export preference with no format to save.

Where a value cannot be derived safely the call refuses rather than
guesses. `rates-usd-to-native` requires its `multiplier`, because this
route has no logged-in account to read a denomination from and an
assumed one returns a `nativeAmount` wrong by orders of magnitude.
`sign-bytes` rejects malformed base64 instead of signing whatever
decoded. Handles record the resolved `wallet.id`, so a wallet id and a
unique prefix of it name the same wallet on every step of a staged
spend.
Read the engine's run directory portably in the CLI suites.

The suites found session.json beside the engine's socket with
path.dirname. On Windows the socket is a named pipe that embeds the run
directory under \?\pipe\. Where the suites regexed it out of
engine-status, the capture was still JSON-escaped. They read and wrote
session.json in a directory that does not exist and opened the event
stream on a pipe that does not exist, which failed the one-shot suite and
three offline checks. cliHarness unescapes the capture and maps the pipe
back to its run directory. The open-subscription Ctrl-C check also accepts
the signalled exit on Windows, where kill('SIGINT') terminates without
running a handler.

Fixup-for: human
The CLI is built from this repository but is not this repository: rollup
inlines every module it reaches under `src/`, so a published package is
the two bundles, the CLI document as its README and `LICENSE`. Nothing
about it needs the CLI to move to a workspace or a submodule, and the
app's own `package.json` stays `private: true` — what is published is a
separate manifest assembled in a temporary directory, so the app itself
cannot reach npm by accident.

`src/cli/npmMeta.ts` holds the decisions: the scoped name, the bin name,
the licence — `BSD-3-Clause`, the CLI's own, not the app's `SEE LICENSE
IN LICENSE`, which names a file a published package does not carry.
There are no per-platform native packages. The version is not among
them. The CLI ships in lockstep with the app, so the manifest takes it
from the app's `package.json`: there is no second number to bump, and a
published CLI says which app release it corresponds to. Lockstep costs
one thing worth knowing before a release — npm will not replace an
existing version, so a CLI-only fix goes out on the next app version
bump rather than on its own.

The dependency list is derived, not written down.
`rollup.config.cli.mjs` externalises every key of the app's
`dependencies` — its whole runtime set — so the bundles leave all of
them as bare `require`s while needing fifteen, and anything the app does
not declare is inlined instead. `buildCliManifest.ts` walks the module
graph from both entry points, keeps the bare specifiers the app declares
as dependencies, drops builtins and type-only imports, and treats the
rest as bundled. Checked against the bundles' own `require` calls:
fifteen declared and none missing, which is the direction that decides
whether an install can run — three of the fifteen (`date-fns`,
`edge-info-server`, `uuid`) rollup inlines rather than externalises, so
the tarball declares a little more than it strictly needs.
Hand-maintaining that list fails as an `npm install` that succeeds and a
CLI that cannot resolve a module on first run, so `cli:manifest:check`
gates it in `verify` and the pre-commit hook. `npm run prepare`
regenerates it as it does the documentation artifacts, so in CI, as for
those, `docs:api:committed` is what catches a stale one.

`publishCli.ts` builds, stages and publishes, and by default the tarball
carries no HMAC addon. The addon's shards rebuild the same `apiSecret`
the mobile release builds sign with, from a pad that is a constant in
this public repository, so shipping it would hand that secret to anyone
with `npm pack` and `strings`. An unsigned CLI needs an `edgeApiKey` of
its own, and the staged README says so. `--with-signer
--signer-secret-is-cli-only` is the opt-in for a key pair issued to the
CLI; `--with-signer` alone is refused.

Each way a publish could go wrong is refused before it happens, because
npm will not replace a version:

- a working tree that is dirty, or that git cannot describe;
- a HEAD that cannot be read, or that no long-lived branch (`develop`,
  `master`, `staging`) or tag contains after a fetch, since the README's
  links into this repository are pinned to it and a feature branch's
  next force-push would drop the commit;
- a plugin family the bundled packages are known to break
  (`scripts/util/knownPluginBreakage.js`, which `cli:plugins:check`
  holds the installed packages to), unless `--allow-broken-plugins`;
- a signer addon whose name is missing from the manifest's `files`, the
  allowlist npm packs from;
- a flag whose value is missing or is itself a flag — `--out` with the
  path left off used to fall through to `npm publish --access public`.

`--dry-run` packs without publishing and `--out` stages for inspection.

CI moves from Node 18 to 22 for every build of this repository, because
`cli:plugins:check` enters `.travis.yml` here and loads every plugin's
tools: the Solana family reaches `rpc-websockets`, which `require`s the
ESM-only `uuid@14`, and Node can `require()` ESM only from 20.19 and
22.12. The same floor is the package's `engines` — below it an install
succeeds and Solana alone fails to load.
Today the plugin refusal stops every publish: the EVM chains need an
`edge-currency-accountbased` release that ships `lib/ethereum/abi/`.

One thing the exercise surfaced, not fixed here: installing the package
costs 2.3 GB across 69,756 files, of which about 1.3 GB is React Native
mobile binaries — iOS simulator slices and Android libraries for the
privacy coins — reached through `edge-currency-accountbased`, which also
brings `react-native` itself. A Node CLI can load none of it.
Log a failed rates pass rather than showing it.

A failed pass of the retried rates queue showed a warning drop-down, at
most once every 30 seconds. A user cannot act on a background rates fetch,
so the app leaves onPassError unset and the failure goes to reportWarning,
which logs it, as on develop. The chain giving up still shows an error.
Show the error when the most-recent wallets order cannot be saved.

Redux takes the new order at once, before the file is written, so the
user sees it. A failed save went to reportWarning, which only logs, and
the order then reverted at the next login with nothing said. It goes to
showError now, as on develop.
Clear a handle's broadcast mark when its transaction is replaced.

broadcast-tx marks a transaction handle sent, and save-tx records only a
marked handle. accelerate --object-id and sign-tx replace the handle's
transaction with one nobody has sent, and the mark stayed, so
make-spend, sign-tx, broadcast-tx, accelerate --object-id, save-tx
recorded the unsent replacement: a phantom pending send with its inputs
marked spent until a resync. txHandleResponse now sets the mark from
its caller, and only broadcast-tx sets it.
Make broadcast-tx safe to repeat.

A second broadcast-tx on a handle already sent answers the first call's
result and sends nothing. The likely repeat is a client that timed out
or lost the socket after the engine sent, and sending again reached the
plugin, which answers "already known" or "missing inputs": a failure
report for a payment that went through.

A broadcast that fails answers BROADCAST_FAILED with the txid and the
same handle, as spend does, rather than the plain core error: it may
have reached the network, and running broadcast-tx again re-sends the
same bytes. BROADCAST_FAILED replaces NETWORK_ERROR in the route's
errors, and the API reference is regenerated.
Refuse an amount that disagrees with the payment request's.

An amount in the body won silently over the amount in a payment URI, so
--to='bitcoin:<address>?amount=0.001' --amount=0.5, from a wrapper that
always sets --amount, prepared a send 500 times the invoice. The two are
refused when they differ, as useMax beside a payment request already is,
and the nativeAmount doc says so.
Report a swap approve that may have sent as SWAP_OUTCOME_UNKNOWN.

approve() signs, broadcasts and then does the plugin's bookkeeping. A
failure reached the caller as the raw core error, usually NETWORK_ERROR
with exit 6, which is the code a script treats as transient, and the
route's note then named a fresh quote as the safe recovery. After a send
that went through, that approves a second swap of the same funds.

A failure core places before the send (insufficient funds, the swap
limit, permission and currency errors) keeps its own code. Anything else
answers SWAP_OUTCOME_UNKNOWN, 502, with the handle, the plugin and the
source wallet in details, and exits 1 rather than 6. The note now says to
check get-transactions on the source wallet before quoting again, and the
code is in the published error table.
Ask for swap quotes with the app's options and gating.

fetch-swap-quotes passed core only preferPluginId. The app also turns off
the exchanges the info server's exchangeInfo disables, outright or for the
request's assets, and the ones the account's referral or a promotion
disables, prefers the account's exchange and exchange type, passes promo
codes, and waits 10 s for slow exchanges and 60 s in all. Without the
disables, the CLI could quote and execute through an exchange Edge had
turned off for an outage.

makeSwapRequestOptions builds those options for both, from the hook's own
logic, and withAssetDisabledPlugins adds the per-asset disables that
SwapProcessingScene added inline. The route reads the account's referral
files through cleaners moved into accountReferralFiles.ts, its preferred
exchange from Settings.json, and exchangeInfo from the public info rollup,
fetched again once the last one read is five minutes old, as the app
does. An asset Edge turned off for every exchange is refused with
SWAP_CURRENCY and the swap scene's message. A preferPluginId in the
request still wins, and its doc now says it prefers rather than
restricts.
Enable the plugins the app enables, from the app's own table.

The engine derived plugin enablement on its own. With no keys every
currency plugin came on, liberlandtestnet, bitcoincashtestnet and
bitcoingoldtestnet included, which the app turns off, and swap plugins
followed config.json alone, so unizen and spookySwap could be on while
transfer, cosmosibc, velodrome and fantomsonicupgrade, which the app
always enables, were off.

The app's table moves out of corePlugins.ts into corePluginsTable.ts,
whose builders take the resolved plugin maps instead of reading the
app's pluginMaps. corePlugins.ts passes pluginMaps, so the app is
unchanged. The engine's buildPluginsInit feeds the same table the app's
merge, config.json's corePlugins and swapPlugins over the keys through
resolvePluginMaps, and still narrows each keys entry through
initFromKeysEntry, which disables an entry it cannot read.
Let a CLI user start a 2FA reset.

request-otp-reset takes the resetToken an OTP_REQUIRED error carries,
and the CLI's printError replaced it with "read it from the REST body",
so a CLI-only user could not start a reset. One who fetched it over REST
had to pass it on argv, visible to ps, because --otp-reset-token had no
variable.

printError now writes the token to ~/.edge-cli/otp-reset-token, readable
only by the user, and names the file where the token was. The flag reads
EDGE_CLI_OTP_RESET_TOKEN when absent, like the other credentials, and the
guide's table and the field's doc say so.
Declare OTP_REQUIRED and CHALLENGE_REQUIRED on the other login routes.

login-with-pin, login-with-key and login-with-recovery take the same otp,
otpKey and challengeId fields as login-with-password, and core can answer
any of them with a 2FA or captcha challenge, but only
login-with-password declared the two codes. The generated reference and
help text now publish them for all four.
Exit 5, not 3, for a swap the exchange refused.

SWAP_PERMISSION had no row of its own, so its 403 fell through to exit 3,
AUTH. A geo restriction, missing verification or an asset that needs
activating is not a credential problem, and a script that logs in again
on exit 3 would loop on it. It maps to VALIDATION now, and the published
exit-code table lists it there.

QUOTE_EXPIRED keeps exit 4 through its 410, the same as OBJECT_EXPIRED:
both release the handle and mean "start again", and the code tells them
apart. The 410 row's comment says so, and a test pins the two together.
Export even when the preferences cannot be saved.

With saveExportPrefs, an unreadable exportTxInfo.json made
mergeExportTxInfo refuse the write, rightly, and the refusal failed the
request after the whole fill, so the caller got no files. The export
scene saves the preferences as a side errand and exports anyway.
get-transactions now does too: it logs the failure and answers the files
with exportPrefsError beside them, and the route's doc names the field.
Say in the CHANGELOG that an unpriced pair reads as zero for five minutes.

A pair the rates server answers without a price is cached as unpriced for
UNPRICED_TTL_MS and resolves 0 until then, where develop queued it again
without end. The entry for the fix now says what the app shows meanwhile.
Add the app's settings changes to the CHANGELOG.

The extraction changes two things the app does: a settings write refuses,
with settings_not_saved_synced_unreadable or
settings_not_saved_unreadable_file, while the file it would replace could
not be read, and the most-recent wallets list is written from the file's
own list rather than from Redux, which holds an empty list after a failed
login read. Each gets an entry.
Add the app's export changes to the CHANGELOG.

The shared export pipeline changes what the app's export scene writes:
every fiat amount takes its transaction's sign, a transfer's fee row is
priced, and the scene writes nothing when the settings could not be read
at login and drops the Bitwave file without an account ID, saying so.
Each gets an entry.
Drop "no behaviour change to the app" from the extraction's CHANGELOG entry.

The extraction does change the app, in the ways its own entries now list,
so the claim beside the move was not true.
Gate and smoke-load the modules fetch-swap-quotes now imports.

The swap gating made the CLI import src/actions/ExchangeInfoActions.ts,
src/types/TweakTypes.ts and src/types/AppleAdsAttributionTypes.ts, which
no gate path watched, so a commit staging only one of them skipped the
CLI gates, and src/util/swapRequestOptions.ts and
src/util/accountReferralFiles.ts, which the Node-safety smoke test did not
load. Both lists name them now.
Regenerate the CLI's package manifest for develop's versions.

paul/mason's develop moves edge-core-js to 2.54.0, edge-currency-accountbased
to 4.103.1 and edge-currency-plugins to 3.17.0, and the manifest pins the
versions the lockfile resolves.
Regenerate src/cli/generated/npmPackage.json for develop's dependency
versions: edge-core-js 2.55.0, edge-currency-accountbased 4.104.0,
edge-exchange-plugins 2.60.1 and edge-info-server 3.16.1.
Leave the privacy coins off in the CLI. buildPluginsInit sets monero,
piratechain, zano and zcash to false whatever the app's table, the keys
or config.json say: their engines need the native libraries
edge-currency-accountbased uses (react-native-monero,
react-native-pirate-wallet, react-native-zano and zcash-native), which
the CLI does not load. The app's table is unchanged.

With the plugins off, core keeps wallets of those types out of
activeWalletIds, so the CLI does not list them, and
create-currency-wallet for one answers "Cannot find a currency plugin
for wallet type wallet:<chain>" (checked on login-tester with a
throwaway account). The Monero LWS key logging, which applied only while
monero was enabled, goes. The unloadedWallets comments and tests, and
the guide, describe the plugins the CLI does enable.
Name zcash-native in the guide's --omit=peer paragraph:
edge-currency-accountbased 4.104.0 declares it as a peer in place of
react-native-zcash.
Comment thread scripts/util/publishArgs.ts Outdated
// `apiSecret` the iOS and Android release builds sign with and the runtime
// pad is a constant in this public repository: a public tarball would hand
// that secret to anyone with `npm pack` and `strings`.
if (flags.withSigner && !flags.signerSecretIsCliOnly) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Agentic Security Review

Severity: MEDIUM

Description: publish:cli will put the Node HMAC addon in the public @edgeapp/cli tarball when the operator passes --with-signer and --signer-secret-is-cli-only. That second flag is only a boolean. The build still embeds whatever apiSecret is in edgeKey.json, which on the app build is the same secret the iOS and Android release builds sign with, and the Node pad is the public constant app.edge.cli. Default publish (no flags) does not pack the addon.

Impact: The release job already has the mobile edgeKey.json from the Jenkins/Mason secret set. The documented signed-publish command is exactly those two flags, and nothing compares the embedded apiKey or apiSecret to a CLI-only key before npm publish --access public. A publish that skips the manual key swap leaves the mobile API HMAC secret in edge_api_signer.node. The addon reconstructs it as the XOR of the six shards with sha256("app.edge.cli"), which is also compiled in as EDGE_NODE_BUNDLE_ID. npm will not replace that version, so every consumer of the package gets the credential the mobile apps use for login-server and info-server HMAC.

Remediation: Do not treat --signer-secret-is-cli-only as proof. Give the published addon a separate input (for example edgeCliKey.json) so the mobile Jenkins edgeKey.json cannot feed it, and refuse the publish unless the embedded apiKey is on an allowlist of CLI keys or a hash of the embedded secret differs from a pinned fingerprint of the mobile release secret. Keep the default of omitting edge_api_signer.node.

Build the Node HMAC signer from edgeCliKey.json, a key pair issued for
the CLI, and never from the app's edgeKey.json. makeApiSigner.ts writes
the iOS and Android sources from edgeKey.json and, only with --cli, the
Node addon's sources from edgeCliKey.json, with no stub and a stamp of
its own. build:cli:native runs it with --cli and stops without the file.
edgeCliKey.json and its stamp are gitignored, and EDGE_CLI.md and
HMAC_SIGNING.md describe the split.

The addon's shards reconstruct the secret it was built from and its
runtime pad is a public constant, so building it from edgeKey.json put
the mobile secret into any addon built on a machine that builds the app.
test:cli:node-hmac checks the addon against edgeCliKey.json, the key it
is built from.
--with-signer needs edgeCliKey.json, and --signer-secret-is-cli-only is
gone: the separate key file is what keeps the app's secret out of the
addon. The publish refuses a CLI key that shares its apiKey or apiSecret
with edgeKey.json, builds the addon from the CLI key, and before packing
asks the staged addon to sign a probe, refusing unless the answer matches
an HMAC of edgeCliKey.json's key, so --no-build cannot stage an addon
built from another secret. Without edgeCliKey.json an unsigned publish
needs --allow-unsigned, as it did without edgeKey.json.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

There are 5 total unresolved issues (including 4 from previous reviews).

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Copied app key can ship signed
    • Restored --signer-secret-is-cli-only for --with-signer when edgeKey.json is absent so a copied app pair cannot ship on the documented CLI-key-only path.

Create PR

Or push these changes by commenting:

@cursor push 2e455a6728
Preview (2e455a6728)
diff --git a/docs/EDGE_CLI.md b/docs/EDGE_CLI.md
--- a/docs/EDGE_CLI.md
+++ b/docs/EDGE_CLI.md
@@ -73,8 +73,10 @@
 and never reads the app's `edgeKey.json`, which feeds the iOS and Android
 signers only; without the file there is nothing to compile. For local testing
 any pair the tester servers accept will do, including a copy of the app's
-development key: `publish:cli --with-signer` refuses a CLI key that shares its
-apiKey or apiSecret with `edgeKey.json`. It runs `build:cli:native` (node-gyp over
+development key. A signed publish refuses a CLI key that shares its apiKey or
+apiSecret with `edgeKey.json`, and without that file needs
+`--signer-secret-is-cli-only` to say the CLI key is a pair of its own.
+`build:cli:all` runs `build:cli:native` (node-gyp over
 `native/edge-api-signer/node/`), then `build:cli`, then
 `build:cli:copy-native`, which puts `edge_api_signer.node` beside the bundles
 where `loadNodeApiSignerNative` looks for it. `npm run test:cli:node-hmac`
@@ -892,7 +894,9 @@
 skips builtins and type-only imports, and treats the rest as bundled. The
 result is checked against the built bundles' own `require` calls.
 
-A signed publish needs `edgeCliKey.json` and nothing else:
+A signed publish needs `edgeCliKey.json`. `--with-signer` still needs
+`edgeKey.json` to compare against, or `--signer-secret-is-cli-only` to say
+the CLI key is not a copy of the app pair:
 
 ```sh
 npm run publish:cli -- --dry-run      # build, stage, pack, publish nothing
@@ -938,10 +942,13 @@
    stays the app's, and the addon never reads it.
 2. `npm run build:cli:all`, which generates the shards, compiles the addon and
    copies it beside the bundles.
-3. `npm run publish:cli -- --with-signer`.
+3. `npm run publish:cli -- --with-signer`. Without `edgeKey.json` beside
+   `edgeCliKey.json`, add `--signer-secret-is-cli-only`.
 
-`--with-signer` is refused without `edgeCliKey.json`, and refused when that
-file shares its apiKey or apiSecret with `edgeKey.json`. With it, the addon is
+`--with-signer` is refused without `edgeCliKey.json`, refused when that
+file shares its apiKey or apiSecret with `edgeKey.json`, and refused without
+`edgeKey.json` unless `--signer-secret-is-cli-only` says the CLI key is a
+pair of its own. With it, the addon is
 added to the manifest's `files` — npm's `files` is an allowlist, so staging the
 file is not enough — and the publish is refused if it did not make the list,
 since a tarball that silently lacks the addon would be published as signed.

diff --git a/docs/HMAC_SIGNING.md b/docs/HMAC_SIGNING.md
--- a/docs/HMAC_SIGNING.md
+++ b/docs/HMAC_SIGNING.md
@@ -31,9 +31,11 @@
    CLI's own `edgeCliKey.json` and never the app's `edgeKey.json`, why
    `scripts/publishCli.ts` leaves it out of the published package by default,
    and why `--with-signer` refuses without `edgeCliKey.json`, refuses a CLI key
-   that shares its apiKey or apiSecret with `edgeKey.json`, and checks that
-   the staged addon signs with the CLI key. Each run hashes its bundle id into
-   the stamp that decides whether a regeneration is needed: `.edgeApiSigner.stamp`
+   that shares its apiKey or apiSecret with `edgeKey.json`, refuses without
+   `edgeKey.json` unless `--signer-secret-is-cli-only` says the CLI key is a
+   pair of its own, and checks that the staged addon signs with the CLI key.
+   Each run hashes its bundle id into the stamp that decides whether a
+   regeneration is needed: `.edgeApiSigner.stamp`
    for the mobile pair, `.edgeCliApiSigner.stamp` for the Node one.
 3. The C sources reconstruct `secret = s0 ⊕ … ⊕ s5 ⊕ runtimePad`.
 

diff --git a/scripts/publishCli.ts b/scripts/publishCli.ts
--- a/scripts/publishCli.ts
+++ b/scripts/publishCli.ts
@@ -16,12 +16,13 @@
  *   npm run publish:cli                      # publish
  *   npm run publish:cli -- --tag next        # publish under a dist-tag
  *
- * A signed build needs `edgeCliKey.json`, the CLI's own key pair, and nothing
- * else: with it, `build:cli:all` generates the XOR-split secret shards,
- * compiles the Node HMAC addon and rolls up both bundles. The app's
- * `edgeKey.json` is never read for the addon. Without the CLI key the addon
- * cannot be built at all, so publishing then needs `--allow-unsigned` said out
- * loud rather than quietly shipping a CLI that cannot sign.
+ * A signed build needs `edgeCliKey.json`, the CLI's own key pair: with it,
+ * `build:cli:all` generates the XOR-split secret shards, compiles the Node
+ * HMAC addon and rolls up both bundles. The app's `edgeKey.json` is never
+ * read for the addon. `--with-signer` still reads it to refuse a copy, and
+ * without it needs `--signer-secret-is-cli-only`. Without the CLI key the
+ * addon cannot be built at all, so publishing then needs `--allow-unsigned`
+ * said out loud rather than quietly shipping a CLI that cannot sign.
  */
 import { spawnSync } from 'child_process'
 import fs from 'fs'
@@ -72,7 +73,9 @@
  * for a reformat that breaks the logic. `--with-signer` is off by default
  * and needs `edgeCliKey.json`, the CLI's own key pair, because the addon's
  * shards reconstruct the secret it carries and the runtime pad is a constant
- * in this public repository — see the note in `src/cli/npmMeta.ts`.
+ * in this public repository — see the note in `src/cli/npmMeta.ts`. A CLI
+ * key that is a copy of the app pair is refused when `edgeKey.json` is
+ * present, and attested with `--signer-secret-is-cli-only` when it is not.
  */
 const flags = parsePublishFlags(argv)
 // Only the ones this shell still reads for itself; the rest are the
@@ -149,7 +152,7 @@
 
 const treeState = readTreeState()
 // The addon is built from the CLI's own key pair. The app's key is read only
-// to refuse a CLI key that is a copy of it.
+// to refuse a CLI key that is a copy of it; without it, the flag attests.
 const cliKey = readKeyPair('edgeCliKey.json')
 const appKey = readKeyPair('edgeKey.json')
 // Only what this phase actually judges: the tree, the key, and the flags.
@@ -160,6 +163,7 @@
   treeState,
   headState,
   hasCliKey: cliKey != null,
+  hasAppKey: appKey != null,
   cliKeyIsAppKey: cliKey != null && appKey != null && sharesKey(cliKey, appKey),
   knownBrokenPlugins: [...KNOWN_MISSING, ...KNOWN_UNLOADABLE]
 })

diff --git a/scripts/util/publishArgs.ts b/scripts/util/publishArgs.ts
--- a/scripts/util/publishArgs.ts
+++ b/scripts/util/publishArgs.ts
@@ -22,6 +22,7 @@
   stageOnly: string | undefined
   allowUnsigned: boolean
   withSigner: boolean
+  signerSecretIsCliOnly: boolean
   allowDirty: boolean
   allowBrokenPlugins: boolean
   allowUnpushed: boolean
@@ -60,6 +61,7 @@
     stageOnly: flagValue(argv, '--out'),
     allowUnsigned: has('--allow-unsigned'),
     withSigner: has('--with-signer'),
+    signerSecretIsCliOnly: has('--signer-secret-is-cli-only'),
     allowDirty: has('--allow-dirty'),
     allowBrokenPlugins: has('--allow-broken-plugins'),
     allowUnpushed: has('--allow-unpushed'),
@@ -291,6 +293,8 @@
   headState: HeadState
   /** `edgeCliKey.json`, the CLI's own key pair, is present. */
   hasCliKey: boolean
+  /** The app's `edgeKey.json` is present, so a copy can be detected. */
+  hasAppKey: boolean
   /** That pair shares its apiKey or apiSecret with the app's `edgeKey.json`. */
   cliKeyIsAppKey: boolean
   /** `knownPluginBreakage.js`'s two lists, joined. */
@@ -301,6 +305,7 @@
     treeState,
     headState,
     hasCliKey,
+    hasAppKey,
     cliKeyIsAppKey,
     knownBrokenPlugins
   } = opts
@@ -394,6 +399,23 @@
     )
   }
 
+  // A machine that holds only the CLI key cannot run the copy check, and
+  // that is the documented signed-publish path. A CLI key that is a copy of
+  // the app pair then passes every other gate and ships. The attestation
+  // is what `--signer-secret-is-cli-only` used to cover on every
+  // `--with-signer` run; with `edgeKey.json` present the comparison is
+  // stricter, so the flag is only required when the comparison cannot run.
+  if (flags.withSigner && !hasAppKey && !flags.signerSecretIsCliOnly) {
+    return (
+      'Refusing to publish the HMAC addon: edgeKey.json is absent, so the ' +
+      'CLI key cannot be checked for a copy of the app pair. A copied pair ' +
+      'would pack the mobile apiSecret into a public tarball. Put ' +
+      'edgeKey.json beside edgeCliKey.json so the copy can be detected, or ' +
+      'pass --signer-secret-is-cli-only to say the CLI key is a pair of its ' +
+      'own.'
+    )
+  }
+
   if (!flags.skipBuild && !hasCliKey && !flags.allowUnsigned) {
     return (
       'edgeCliKey.json is absent, so the Node HMAC addon cannot be built and ' +

diff --git a/src/__tests__/cli/publishArgs.test.ts b/src/__tests__/cli/publishArgs.test.ts
--- a/src/__tests__/cli/publishArgs.test.ts
+++ b/src/__tests__/cli/publishArgs.test.ts
@@ -76,6 +76,7 @@
       '/tmp/stage',
       '--allow-unsigned',
       '--with-signer',
+      '--signer-secret-is-cli-only',
       '--allow-dirty',
       '--allow-broken-plugins',
       '--no-build',
@@ -87,6 +88,7 @@
       stageOnly: '/tmp/stage',
       allowUnsigned: true,
       withSigner: true,
+      signerSecretIsCliOnly: true,
       allowDirty: true,
       allowBrokenPlugins: true,
       allowUnpushed: false,
@@ -101,6 +103,7 @@
       stageOnly: undefined,
       allowUnsigned: false,
       withSigner: false,
+      signerSecretIsCliOnly: false,
       allowDirty: false,
       allowBrokenPlugins: false,
       allowUnpushed: false,
@@ -191,6 +194,7 @@
     treeState: treeClean,
     headState: pushed,
     hasCliKey: true,
+    hasAppKey: true,
     cliKeyIsAppKey: false,
     knownBrokenPlugins: []
   }
@@ -329,8 +333,41 @@
     const refusal = preBuildRefusal({ ...base, flags, cliKeyIsAppKey: true })
     expect(refusal).toContain('edgeKey.json')
     expect(refusal).toContain('key pair of its own')
+    // The attestation is not an override: a detected copy still ships the
+    // app secret.
+    expect(
+      preBuildRefusal({
+        ...base,
+        flags: parsePublishFlags([
+          '--with-signer',
+          '--signer-secret-is-cli-only'
+        ]),
+        cliKeyIsAppKey: true
+      })
+    ).toContain('key pair of its own')
   })
 
+  it('refuses --with-signer without edgeKey.json unless attested', () => {
+    // The copy check cannot run without the app pair, and a signed publish
+    // is documented to need only edgeCliKey.json. A CLI key that is a copy
+    // of the app pair then ships. The flag is the attestation that used to
+    // cover every --with-signer run.
+    const flags = parsePublishFlags(['--with-signer'])
+    const refusal = preBuildRefusal({ ...base, flags, hasAppKey: false })
+    expect(refusal).toContain('edgeKey.json is absent')
+    expect(refusal).toContain('--signer-secret-is-cli-only')
+    expect(
+      preBuildRefusal({
+        ...base,
+        flags: parsePublishFlags([
+          '--with-signer',
+          '--signer-secret-is-cli-only'
+        ]),
+        hasAppKey: false
+      })
+    ).toBeUndefined()
+  })
+
   it('allows the addon built from edgeCliKey.json', () => {
     const flags = parsePublishFlags(['--with-signer'])
     expect(preBuildRefusal({ ...base, flags })).toBeUndefined()

diff --git a/src/cli/npmMeta.ts b/src/cli/npmMeta.ts
--- a/src/cli/npmMeta.ts
+++ b/src/cli/npmMeta.ts
@@ -59,8 +59,9 @@
 // engine says exactly that when it has neither (`makeCoreContext.ts`), and
 // `--fake` needs nothing at all. `publishCli.ts --with-signer` can still ship
 // the addon, built from `edgeCliKey.json` — a key pair issued for the CLI —
-// and never from the app's `edgeKey.json`. It refuses without that file, or
-// when the file holds the app's key.
+// and never from the app's `edgeKey.json`. It refuses without that file, when
+// the file holds the app's key, or without `edgeKey.json` to compare against
+// unless `--signer-secret-is-cli-only` says the CLI key is a pair of its own.
 
 export const CLI_PACKAGE_META: CliPackageMeta = {
   name: '@edgeapp/cli',

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 5f12dcc. Configure here.

Comment thread scripts/publishCli.ts
treeState,
headState,
hasCliKey: cliKey != null,
cliKeyIsAppKey: cliKey != null && appKey != null && sharesKey(cliKey, appKey),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copied app key can ship signed

High Severity

The --with-signer copy check only runs when edgeKey.json is on disk, but a signed publish is documented to need edgeCliKey.json and nothing else. A CLI key that is a copy of the app pair therefore passes preBuildRefusal, the staged HMAC probe (it matches edgeCliKey.json), and ships. That is the case --signer-secret-is-cli-only used to cover, and the local-testing note still allows copying the app development key into edgeCliKey.json.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 5f12dcc. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants