Skip to content

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

Draft
paullinator wants to merge 12 commits into
developfrom
paul/cli
Draft

paullinator wants to merge 12 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.129910010080100
Added@​rollup/​plugin-json@​6.1.010010010084100
Added@​rollup/​plugin-node-resolve@​16.0.39910010085100
Updated@​rollup/​plugin-babel@​6.0.3 ⏵ 6.1.0100 +110010086 -3100
Addednode-gyp@​11.5.09910010092100

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 paullinator changed the title Add CLI on top of config keys refactor Add the Edge CLI, on Node-safe extractions from the GUI Oct 7, 2026
@paullinator
paullinator force-pushed the paul/cli branch 8 times, most recently from ef2f489 to 2324b31 Compare October 10, 2026 07:06
`typechain` emits `export * as factories from './factories'`, and the
React Native preset does not transform namespace re-exports. The plugin
was declared but never enabled, so `TransactionListTop` failed to parse
the moment `src/plugins/contracts` existed — which it does after any
`npm install`, since `prepare` generates it. Metro needs that transform
as much as jest does, so it belongs in the shared config.
src/util/hmacAuth.ts imports hashjs and src/util/utils.ts imports uuid,
but both packages were only ever resolved transitively. Declare them so
a clean install and the Node CLI bundle both get them.
@paullinator
paullinator force-pushed the paul/cli branch 7 times, most recently from f569518 to ebea9ed Compare October 10, 2026 17:16
`network.ts`, `utils.ts` and the locale boot each pulled React Native in
through their module load paths, so nothing outside the app could fetch
from the info server, format an amount, or pick a language table.

Fiat constants and helpers move to `fiatConstants.ts`, and
`getOsVersion` to `rnUtils.ts` alongside the other React Native-only
helpers, with `keysStore.ts` following it there.
`getCurrencyCodeWithAccount` moves to `txDisplay/currencyCodes.ts`,
which `utils.ts` can reach and `CurrencyInfoHelpers.ts` re-exports, so
its fifty-odd call sites are untouched and there is still only one body
deciding what an asset is called. `network.ts` takes its server lists
and device fields through `configureNetwork` and
`initInfoServer(params)` rather than reading `appConfig` and
`react-native-device-info` at module scope.

Reporting splits with them. These modules recovered from a failure by
writing to `console`, which is a terminal in the app and, in a detached
daemon, a file its own shutdown deletes. `reportWarning.ts` is one
module-level sink they call instead: `console.warn` by default, so the
GUI is unchanged, and redirected by whoever owns the process. It is a
sink rather than an injected logger because these are free functions on
both import graphs, and threading one through
`getCurrencyCodeWithAccount` and its callers would put a logger in the
signature of every display helper.

Locale boot splits the same way: `bootLocale.ts` applies a language
table and number format with no React Native imports, and
`initLocale.ts` stays GUI-only, feeding it what `react-native-localize`
reports.

Capturing those device fields is separate from starting the poll:
`configureInfoServer` records them synchronously, so `fetchPublicRollup`
works for whoever calls it first, while `initInfoServer` owns the
polling and the decision to skip the unsigned launch fetch.
`fetchWaterfall` refuses an empty server list, because
`asyncWaterfall([])` resolves `undefined` and every caller would then
fail on `response.ok` with an error naming nothing.

`asyncWaterfall` is event-driven: each server is running or settled, a
failure hands over to the next at once, and the last failure is
rethrown only when nothing is running and nothing is left to try. The
array version tracked servers by position, so a primary that was slow
and then failed took a still-running fallback's promise with it and
threw while healthy servers were answering. Refreshing the info rollup
or the coinrank list joins a fetch already in flight, so a flapping
connection cannot start several and let whichever answers last decide.

It adds `edge/no-module-scope-lstrings`, a lint rule that stops a
module reading `lstrings` while it loads, following the binding however
it was imported or aliased. Its rule tests run in a child Node process,
because jest's transform does not load `.mjs`. It is not switched on
here: `Extract Node-safe transaction tagging` adds the config block that
enables it for the trees the CLI loads.

Two helpers come with them. `errorMessage` renders whatever was thrown
in one place, so these modules' reports need no engine import to get
it. `withDeadline` is `raceTimeout` with a rejection instead of a
sentinel, and both unref their pending timer, because a deadline should
never be the reason a Node process stays alive.
Exchange rates could not be fetched outside the app: the module read its
server list and its error reporter at load time, both of which come from
React Native.

The query logic now takes what it needs through
`configureExchangeRates`, and `exchangeRatesGui` supplies the GUI's
Airship reporter at app start. A Node caller supplies its own, so the
same rate lookup answers the same way in both places.

Two answers that were one. `0` means "the server could not price this
date", which is a fact about the asset and is cached as one; a request
that failed, a chain whose budget expired and a queue that was stopped
learned nothing, and settling those at `0` as well made a wrong number
indistinguishable from a real one. They settle at `RATE_UNAVAILABLE`
instead — `NaN`, so a caller that does not check degrades to the old
behaviour rather than to something plausible — and `isRateUnavailable`
is the test. The one caller that must not write a wrong number is an
accounting export.

The budget is the caller's. `RATE_QUERY_TIMEOUT_MS` bounds one upstream
request and the chain bounds the whole fill, and a fill is one pass per
99 keys — so the ceiling that matters belongs to whoever is waiting, and
it is passed in rather than read from a module constant they cannot
reach.
`initLocale` reaches for `react-native-localize`, so nothing outside the
app could ask which locale to use. The decision itself is pure: read a
tag from argv, config or the environment, normalize it, and pick a
language table.

`nodeLocale.ts` holds that decision with no React Native imports, and
feeds the same `applyLocale` that the GUI's device lookup already calls,
so the GUI and any Node caller resolve a locale the same way rather than
approximately the same way.

Precedence is explicit and tested: an explicit tag, then config, then
`EDGE_CLI_LOCALE`, then `LC_ALL` / `LC_MESSAGES` / `LANG`, then `Intl`,
then `en-US`. `es_MX.UTF-8@euro` and `C` both resolve, which is what the
POSIX forms actually look like.

`env` is typed as the variables it reads rather than
`NodeJS.ProcessEnv`, which in this repo demands `NODE_ENV` and would
make every caller invent one.
`CategoriesActions.ts` held five hundred lines deciding what a
transaction should be called: the category, the payee, the direction,
and the label for each action type. All of it is a pure function of the
transaction, the wallet and the account, but it sat behind Redux
imports, so nothing outside the app could ask the same question and get
the same answer.

`src/util/txDisplay/` holds that logic now — `displayInfo` for the
derivation, `category` for the category strings, `txActionLabels` for
the action names, and `currencyCodes` for the ticker lookups. The call
sites move with the helpers: `CategoryModal`, `TransactionDetailsScene`,
`TransactionListRow` and `TransactionsExportScene` import
`splitCategory`, `joinCategory`, `getTxActionDisplayInfo` and `Category`
from `util/txDisplay` instead of from `CategoriesActions`, and those
moves are import paths only. `cleanFiatCurrencyCode` moves to
`fiatCode.ts` for the same reason, and `memoUtils`' labels are read when
they are shown rather than captured while the module loads.

One behaviour does change, in `splitCategory`'s fallback for a category
it does not recognise. It stripped `/^[^:]:/`, which removes only a
one-character prefix — `X:Food` came back as `Food` — and returned the
probe with its appended colon, so a bare `Shopping` became `Shopping:`.
`TransactionDetailsScene` round-trips the pair, so opening and saving
such a transaction wrote the change back. It now keeps the stored text
exactly.

The point is that two callers cannot drift. A transaction rendered in a
list, exported to CSV, or printed by a script now describes itself
identically, because it is the same code deciding.

`CategoriesActions.ts` keeps what is the app's: the account's
subcategories file and the Redux state around it. Reads and writes go
through `serializeByKey`, one at a time per account, so a second tap in
`CategoryModal` cannot drop the first one's entry and a slow mount read
cannot overwrite a list that was just added to. The file is cleaned:
JSON that is not a categories object is not read as the defaults. The
modal shows the defaults instead, and the first add keeps the unreadable
file as `Categories.json.unreadable-<ms>` before starting the list
again, so nothing is lost and the feature is not left dead on every
device. Category names are read when they are shown, never captured
while the module loads, because `applyLocale` replaces the strings in
place. `txActionConstants.ts` goes: its table is
`txDisplay/txActionLabels` now.
Three small pieces of GUI state that any caller reading transactions
needs, and none of which had a reason to be Redux-only.

`exchangeDenom` picks the denomination a currency or token reports
amounts in. `DenominationSelectors` keeps its selector shape and calls
it, so the two cannot disagree about what a multiplier is.

`spamThreshold` decides which incoming transactions are dust worth
hiding. The GUI applies it to every list; a caller that reads the same
wallet and does not apply it sees a different set of transactions, which
is the sort of difference that looks like a bug in whichever one you did
not write.

`localAccountSettings` reads the device-local settings file that holds
the spam filter toggle, and `LocalSettingsActions` reads through it
rather than duplicating the format.

The threshold needs a rate, and asks for it at the current hour rather
than the current millisecond, so repeated listings share one cache entry
instead of missing on every call. It is a different source from the
GUI's, which reads live rates from Redux, and a lookup that fails yields
no filtering — stated in the module, because the two can legitimately
disagree.

A settings file that is *there* and cannot be read is not the same as
one that was never written, and both readers say which they got.
`readLocalAccountSettingsOrDefaults` answers `trusted: false` for the
first, because its defaults have the spam filter *on*: substituting them
silently applies a floor to an account that turned the filter off, and a
caller that caches them must not then mark itself authoritative, or a
later write persists them over the real file. So the threshold declines
to filter on an untrusted read — no floor is the safe direction, the
same one a non-finite rate takes — and reports it.

A write never starts from those defaults. Every local writer goes
through `updateLocalAccountSettings(account, update)`, which applies the
caller's change to a base it establishes itself: the cache when it is
trusted, and otherwise `readLocalAccountSettingsForWrite`, which reads
the file strictly, twice. When both reads fail on the content, the file
is recovered rather than refused for ever: text that does not parse is
kept as `Settings.json.unreadable-<ms>`, and a box that will not decrypt
leaves nothing to keep and is deleted; either way the answer says which.
A failure that says nothing about the content — a permission error, a
descriptor limit — is rethrown and the file stays. Writes are serialized
per account, as `change-local-settings` does.

The synced `Settings.json` has one write door too,
`updateSyncedSettings(account, update)`, which applies the change to a
strict read made for that write, so a stale or defaults-based object can
never be written over the real file. Collections are changed inside the
updater — the most-recent wallets list, the security-check map — so a
caller's old copy cannot replace the file's. An unreadable file stops
the write with a translated message, and there is no recovery door:
this file is on the synced repo, where a sync may still bring a good
copy. The login-time denomination migration goes through the same door,
so one failed read no longer writes the whole file out as defaults.
`syncedSettingsAreTrusted` reports only the login read, which is what
fills Redux, so the export can tell whether the units and fiat it holds
are the account's own.

`syncedSettingsFile` holds the one definition of the synced
`Settings.json` that Node-safe code reads. The GUI's own cleaner sits
behind an Airship import and cannot be loaded here, so this is a
two-field view of the same file with the same defaults, and a test
asserts those defaults still match the GUI's.
`TransactionExportActions.tsx` was a five-hundred-line thunk that did
four separable jobs: fill in historical fiat values, render CSV, render
QBO, and render the Bitwave format. Only the last step needed React
Native, and only for writing the file.

`fillTxsFiat` asks the rates server what each transaction was worth on
the day it happened, which is the part that makes an export more than a
dump of native amounts. `txExport/format` renders the three formats.
`exportTxInfo` holds the Bitwave account mapping the exporter needs.

The thunk keeps the file-writing and the share sheet, and calls the same
renderers. `TransactionsExportScene` follows it. A test now covers
`fillTxsFiat` against a partial wallet, which is all it reads.

The formats matter here: an export that a person reconciles against
their books has to be byte-identical whichever tool produced it, and the
only way to be sure of that is for one renderer to produce both.

Which is not only the renderers but the *dispatch* over them, so
`buildExportFiles` is here too. The three formats want different
denominations — CSV and QBO carry the amount beside a unit, in the
user's display units, while Bitwave has no unit field at all and its
`amount` has to be the exchange denomination or a row reads `50000 BTC`
for 0.0005 BTC — and deciding that per caller is how the two came to
disagree. One exhaustive `switch` with a `never` default decides it
once, so a fourth format cannot be half-wired, and
`TX_EXPORT_FORMAT_INFO` holds each format's file suffix and media type
beside the format set rather than in three non-exhaustive copies.

`fillTxsFiat` reports what it could not price instead of writing a zero
for it, so a caller can tell a priced export from a short one.

`planTxExport` makes the decisions that come before rendering — which
formats, with which Bitwave account id, and what the user must be told
— where a test can reach them. A Bitwave format with no account id is
dropped and reported, settings that could not be read stop the export,
and rates the queue gave up on still produce the files, with the count
of unpriced rows shown, because refusing made a full-history export
unobtainable.
`SendScene2` saved a sent transaction and then attached its metadata,
its category, its notes and any swap details in a sequence that had to
happen in a particular order and had grown inline in the scene. A second
caller that saved a transaction and got the order wrong would produce a
transaction that looks right until someone exports it.

`txTagging/apply` holds that sequence. `SendScene2` calls it and loses
two dozen lines. The scene was the only definition of what a correctly
tagged transaction is, and now it is not the only caller that can
produce one.

It re-applies only the fields the caller actually supplied. Under
`EdgeMetadataChange` an empty string is a value rather than "leave
unchanged", so passing all three through would let a caller who set one
field erase the other two that core derived. The trigger is any
non-empty name, notes or category, which is wider than the scene's old
`payeeName != null` check and preserves a category that would otherwise
be lost; callers pass the metadata they were given, never computed
display metadata.

It also switches on `edge/no-module-scope-lstrings`, added with `Make
network and utils Node-safe`, as an error for `src/cli`, `src/locales`
and `src/util`, which lint clean of module-scope `lstrings` reads at
this point, so from here a regression fails lint. The
config block's comment speaks of the CLI and its `bootNodeLocale`,
which the next commit adds; these trees are the ones it loads without
the app's boot.
A long-lived engine daemon owns the `EdgeContext` and answers a JSON
REST API over a Unix socket; the `edge-cli` binary is a thin one-shot
client that spawns the engine on demand and keeps a session id in
`session.json` so commands chain. `docs/EDGE_CLI.md` describes that
architecture and deliberately documents no endpoints — the reference is
generated.

The point of this commit is the declaration format, so it carries one
group of each shape rather than the whole surface. Each is one
`route({…})`: the core call it fronts, the HTTP method and path, how it
appears on the command line, cleaners for the query, body and response,
and its error codes. The prose lives inside the declaration, beside the
field it describes, and the JSDoc above carries what belongs to the call
as a whole.

Nothing is written twice. The command line, the help text, the OpenAPI
document and the HTML reference are all derived from these declarations,
and the derived artifacts are committed so a fresh clone needs no build
step. Four gates run in the pre-commit hook and reject the ways they
could drift apart: a route with no command, a handler reading a field
its cleaner would strip, a request parameter the core call does not
have, and a generated file that is stale. The fifth asks whether a
command is exercised by a test, so it arrives with the suites that drive
the whole surface, in the commit that completes it.

The shapes worth reviewing are all here:

- no arguments, engine-local — `engine-status`, `engine-config`
- no arguments, reaching core — `local-users`, `fetch-login-messages`
- one named argument — `username-available`
- a body, and the session it establishes — `create-account`,
`login-with-password`, `logout`
- a positional path parameter — `object-get`, `object-delete`
- a held-open stream — `subscribe`

Path parameters are base58 identifiers and nothing else, because base64
wallet ids and free-text usernames contain `/` and cannot survive a URL
unescaped. Everything else is a named argument. A positional is declared
once as an ordinary field and the path is derived from it, so the two
cannot disagree.

`--fake` serves an in-process `makeFakeEdgeWorld`, which is what lets
the CLI tests run in a hook with no network, no server and no API key.

Core values with methods on them cannot cross JSON, so the engine keeps
them and hands back a handle: a staged transaction, a pending login, a
swap quote, a lobby. A handle carries its own TTL and is released when
the caller finishes with it, and a call that consumes one — approving a
swap — marks it in flight first, so a client that retries after its own
socket timeout is refused with `OBJECT_IN_USE` rather than spending
twice. A call that keeps its handle holds it the same way, so a
broadcast that outlives the TTL still returns its txid instead of
expiring between the send and the reply. Reading a handle returns a
projection, never the live object: serializing an `EdgeAccount` would
walk its `otpKey` and `recoveryKey` getters, and a swap quote reaches
both wallets and every token they know.

`cli:plugins:check` is a gate on the packages the engine registers
rather than on this repository: every relative `require` in a bundled
plugin's `lib` has to resolve, and every plugin family's `*Tools.js` has
to load. Five do not today, in `edge-currency-accountbased`, which is
why `docs/EDGE_CLI.md` carries a table of which asset families a Node
CLI can actually carry. The gate fails on a new breakage *and* on one of
the five being fixed, so the list is cleaned up when that dependency
releases instead of becoming permanent.

Responses are validated against the same cleaners that document them.
`checkResponse` runs each one and discards the cleaned value, since a
cleaner strips unknown keys and returning it would delete fields the
engine means to send; `EDGE_CLI_CHECK_RESPONSES` decides whether a
mismatch warns, fails the request, or is skipped. Drift shows up in the
log rather than reaching a caller unnoticed.

What is fatal is decided here, and it is narrow. An `uncaughtException`
unwound a stack and left state nothing can reason about, so the engine
logs it, shuts down and exits non-zero — and keeps its startup log, the
one file that then says why. An unhandled rejection is *not* fatal,
which is not Node's default: a rejection nobody awaited means only that
whatever was going to read that value has none, and this process hosts
third-party plugins, one of which rejects on a timer. Treating it as
fatal cost every session, every live stream and every handle in the
daemon. It is reported with the whole rejected value — `inspect`, not
`String`, since a plugin rejects with a plain object and `String`
renders that as `[object Object]` — and the engine stays up.

Everything else that recovers reports to one place. The engine points
`reportWarning`'s sink and `configureExchangeRates`' hooks at its own
logger at boot, so a rates server that fails, an unreadable settings
file and an asset whose token the plugin dropped all reach
`engine-<profile>.log` rather than a terminal nobody is watching.

`apiCredentials` decides what every request is signed with: an explicit
`-k` overrides the key and drops the `keys.json` secret that does not
belong to it, a key from the config file is only where the key came from
and keeps both, and `EDGE_CLI_FORCE_KEYS_JSON` or `-k` skips the native
signer. Which signer is chosen decides which `appKeys` layer the info
server serves, so it is a pure function with a table rather than four
decisions inline behind an I/O call.

One engine serves one profile, and it claims the profile by creating its
run file exclusively before opening the data directory, so two cold
invocations cannot hold two `EdgeContext`s on one set of repos or unlink
each other's socket. The idle timer counts in-flight requests as well as
sessions and subscribers, so a cold login cannot be shut down underneath
itself. Everything the engine reads from disk — its run file, the
client's session file, the account's synced settings — goes through a
cleaner, and the bearer tokens in an OTP challenge are masked on their
way to a terminal while staying in the REST body the commands read them
from.
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.
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.
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.

1 participant