diff --git a/.cursorignore b/.cursorignore index 7c4ce5e264e..d888e092815 100644 --- a/.cursorignore +++ b/.cursorignore @@ -1,2 +1,3 @@ env.json config.json +keys.json diff --git a/.gitignore b/.gitignore index 78d20f38cd2..a366be5783e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,11 @@ temp/ /android/app/google-services.json /android/google-java-format-*.jar /deploy-config.json +/edgeKey.json /env.json +/config.json +/keys.json +/keys.*.json /fastlane.json /ios/edge/GoogleService-Info.plist /ios/Pods/ @@ -17,6 +21,7 @@ IDEWorkspaceChecks.plist android-release.bundle.map ios-release.bundle.map keystores/ +/.edgeApiSigner.stamp # Debugging overrideTheme.json @@ -35,6 +40,16 @@ coverage/ # Generated headers /android/app/src/main/java/co/edgesecure/app/EdgeApiKey.java /ios/EdgeApiKey.swift +/ios/EdgeApiSecret.c +/ios/EdgeApiSecret.h +/android/app/src/main/cpp/edge_api_secret.c +/android/app/src/main/cpp/edge_api_secret.h +/native/edge-api-signer/node/edge_api_secret.c +/native/edge-api-signer/node/edge_api_secret.h +/native/edge-api-signer/node/build/ +/vendor/*.tgz +/vendor/edge-core-js-*.tgz +/*.tgz # Checkpoint jsons /android/app/src/main/assets/saplingtree/ @@ -128,3 +143,13 @@ yarn-error.log !.yarn/sdks !.yarn/versions /.husky/_ + +# Edge CLI runtime +.edge-cli/ + +# Built CLI +lib/ + + +# Maestro run output +/reports/ diff --git a/CHANGELOG.md b/CHANGELOG.md index d619f314a08..601c59a25d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,9 @@ ## 4.51.0 (staging) +- added: Native Edge API HMAC signer (`edgeKey.json` + XOR-split C shards) so login-server requests can be signed outside the JS bundle via `apiSigner`, with JS `KEYS.EDGE_API_*` remaining as a fallback. - added: Push info-server attestation tokens into edge-core-js via `setAttestationToken` so the login server can skip CAPTCHA for attested devices, and allow `LOGIN_SERVER` / `INFO_SERVER` env overrides for local E2E stacks. +- added: Remote signed `GET /v1/infoRollup/:appId` `appKeys` fetch so plugin secrets can rotate without an app release, with DeviceSettings cache and baked-in `keys.json` fallback - added: App/device attestation for gated info-server requests - added: Swapter swap provider - added: "-m" tag on the version number in the Help scene for Maestro test builds @@ -14,6 +16,8 @@ - changed: Adopt the iOS UIScene lifecycle, removing the deprecated app-delegate window and lifecycle APIs ahead of Xcode 27. - changed: Target Android 16 (API level 36), which Google Play requires for app updates submitted after Aug 30, 2026. Predictive back is opted out of for now, since React Native 0.79 cannot handle it, so the back button behaves exactly as it did before. - changed: Sign MoonPay buy/sell widget URLs and bind them to the customer's IP via the info server, for MoonPay's on-ramp IP-matching security upgrade. +- changed: Split runtime `env.json` into non-secret `config.json` and secret `keys.json`; deploy-config branch overrides move from `envJson` to `configJson`/`keysJson` (legacy `envJson` is ignored on this GUI) +- changed: Replace the flat `ENV` singleton with separate `CONFIG`, `KEYS`/`globalKeys`, and `pluginMaps` accessors (no top-level globalKeys flatten) - changed: Style the entire "Already have an account? Sign in" line in the getting-started USP carousel with the tertiary link color, not just "Sign in". - changed: Refresh the buy, sell, sort, scan-QR and FIO names icons to the updated design. - changed: Display "MoonPay" instead of "Moonpay" wherever the partner name appears in the app. diff --git a/README.md b/README.md index 986f7a9be7e..60cd0fc9c62 100644 --- a/README.md +++ b/README.md @@ -41,11 +41,11 @@ This project uses npm to manage Javascript dependencies (npm ships with Node). This bundler process needs to run in the background, so feel free to run this in its own terminal window. -### Add API key in env.json +### Add API key in keys.json A public API key is built into the edge-core-js which can be used to build and test the Edge app. This key is severely rate limited and should not be used for production. For production use, get an API key by emailing info@edge.app. -Change the `AIRBITZ_API_KEY` in `env.json` to the API key you received from Edge. To use the public API key, leave `AIRBITZ_API_KEY` blank. +`npm run prepare` creates `config.json` (non-secret) and `keys.json` (secret) with defaults. Set `EDGE_API_KEY` in `keys.json` to the key you received from Edge. To use the public API key, leave `EDGE_API_KEY` blank. If you still have a legacy `env.json`, run `npm run split-env-json` once to produce the two files. ### Run the app in debug mode diff --git a/WIP_README_DELETE_ME.md b/WIP_README_DELETE_ME.md new file mode 100644 index 00000000000..139bdf8b164 --- /dev/null +++ b/WIP_README_DELETE_ME.md @@ -0,0 +1,52 @@ +# WIP — delete this file before a production PR + +This file records temporary state that exists only while `paul/cli` is in +progress. **None of it should reach a production pull request.** When the +blockers below clear, delete this file along with the workarounds it describes. + +## This branch does not compile from a clean clone + +`tsc` reports five errors, and `npm run precommit` therefore fails: + +``` +src/cli/engine/fetchPluginKeys.ts Module '"edge-core-js"' has no exported member 'EdgeApiSigner' +src/cli/engine/nodeApiSigner.ts Module '"edge-core-js"' has no exported member 'EdgeApiSigner' +src/util/edgeApiSigner.ts Module '"edge-core-js"' has no exported member 'EdgeApiSigner' +src/util/keysServer.ts Module '"edge-core-js"' has no exported member 'EdgeApiSigner' +src/components/services/EdgeCoreManager.tsx + Property 'apiSigner' does not exist on type 'EdgeContextOptions' +``` + +`package.json` asks for `edge-core-js@^2.48.1`, which is the newest published +version and does not export `EdgeApiSigner`. The five files above are all new +on this branch and all need it. + +### Working around it + +Pack `edge-core-js` from its own worktree and install the tarball here. The +`.tgz` files in the repository root are the packs already made for this — they +are gitignored, so they exist only on machines that built them. + +```bash +npm install --no-save ./edge-core-js--.tgz +``` + +`--no-save` is deliberate: pointing `package.json` at a gitignored tarball +would break the build for everyone else. That leaves `package.json` and +`node_modules` disagreeing, which is the whole reason this file exists. + +### Clearing it + +Either is enough, and both make this section obsolete: + +- `edge-core-js` publishes a release exporting `EdgeApiSigner`, and + `package.json` moves to it; or +- the `apiSigner` work comes out of this branch and ships separately. + +## Checklist before opening a production PR + +- [ ] `EdgeApiSigner` resolves from a published `edge-core-js` +- [ ] `npx tsc --noEmit` is clean with no `--no-save` install +- [ ] `npm run precommit` passes from a fresh `npm ci` +- [ ] No `*.tgz` in the repository root +- [ ] Delete this file diff --git a/android/app/build.gradle b/android/app/build.gradle index f9e22b0d811..7feadb4d03e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -103,6 +103,19 @@ android { ndk { abiFilters 'armeabi-v7a', 'arm64-v8a' // Exclude Intel } + externalNativeBuild { + cmake { + cppFlags "" + arguments "-DANDROID_STL=c++_shared" + } + } + } + + externalNativeBuild { + cmake { + path file("src/main/cpp/CMakeLists.txt") + version "3.22.1" + } } // Edge addition: sideloadable per-ABI APKs for distribution outside @@ -395,3 +408,55 @@ if (!sentrySlug.contains('SENTRY_ORGANIZATION')) { telemetry = true } } + +// Gradle daemons started from Android Studio inherit a minimal PATH that +// usually lacks nvm / Homebrew node, so reuse the NODE_BINARY that the React +// Native iOS build already depends on before falling back to a PATH lookup. +def resolveNodeBinary(File repoRoot) { + def pattern = ~'^\\s*export\\s+NODE_BINARY=(.+)$' + for (String name : ['ios/.xcode.env.local', 'ios/.xcode.env']) { + File file = new File(repoRoot, name) + if (!file.exists()) continue + for (String line : file.readLines()) { + def matcher = pattern.matcher(line) + if (!matcher.find()) continue + String value = matcher.group(1).trim().replaceAll('^["\']|["\']$', '') + // Skip `$(command -v node)` and friends: this is not a shell. + if (value.contains('$')) continue + if (new File(value).canExecute()) return value + } + } + return 'node' +} + +// Regenerate XOR-split API secret C sources and EdgeApiKey.{swift,java} before +// every native build. Explicitly clear ALLOW_STUB so a stub from `npm prepare` +// cannot leak into the signer outputs. +def nodeBinary = resolveNodeBinary(rootProject.projectDir.parentFile) +tasks.register("generateEdgeApiSigner", Exec) { + def repoRoot = rootProject.projectDir.parentFile + workingDir repoRoot + environment "EDGE_API_SIGNER_ALLOW_STUB", "" + commandLine nodeBinary, "-r", "sucrase/register", "./scripts/makeApiSigner.ts" +} +// Same edgeKey.json feeds EdgeApiKey used by native push registration; keep it +// in lockstep with the signer so a key rotation cannot leave AppDelegate / +// MessagesWorker on the previous public key. +tasks.register("generateEdgeApiKeyHeaders", Exec) { + def repoRoot = rootProject.projectDir.parentFile + workingDir repoRoot + commandLine nodeBinary, "-r", "sucrase/register", "./scripts/makeNativeHeaders.ts" +} +generateEdgeApiKeyHeaders.dependsOn("generateEdgeApiSigner") +preBuild.dependsOn("generateEdgeApiKeyHeaders") + +// edge_api_secret.c is gitignored but listed in CMakeLists.txt, and the CMake +// configure/build tasks do not run behind preBuild, so wire them up directly +// or a fresh checkout fails with "Cannot find source file". +tasks.matching { + it.name.startsWith("configureCMake") || + it.name.startsWith("buildCMake") || + it.name.startsWith("externalNativeBuild") +}.configureEach { + dependsOn("generateEdgeApiSigner") +} diff --git a/android/app/src/main/cpp/CMakeLists.txt b/android/app/src/main/cpp/CMakeLists.txt new file mode 100644 index 00000000000..8ceef11d6d4 --- /dev/null +++ b/android/app/src/main/cpp/CMakeLists.txt @@ -0,0 +1,27 @@ +cmake_minimum_required(VERSION 3.18.1) +project(edge_api_signer) + +set(NATIVE_SIGNER_DIR "${CMAKE_SOURCE_DIR}/../../../../../native/edge-api-signer") + +add_library( + edge_api_signer + SHARED + edge_api_secret.c + edge_api_signer_jni.c + "${NATIVE_SIGNER_DIR}/edge_hmac.c" +) + +target_include_directories( + edge_api_signer + PRIVATE + ${CMAKE_SOURCE_DIR} + ${NATIVE_SIGNER_DIR} +) + +target_compile_options(edge_api_signer PRIVATE -fvisibility=hidden -O2) + +# Pixel / Android 15+: 16 KB page-size ELF alignment +target_link_options(edge_api_signer PRIVATE "-Wl,-z,max-page-size=16384") + +find_library(log-lib log) +target_link_libraries(edge_api_signer ${log-lib}) diff --git a/android/app/src/main/cpp/edge_api_signer_jni.c b/android/app/src/main/cpp/edge_api_signer_jni.c new file mode 100644 index 00000000000..17eabf56ea5 --- /dev/null +++ b/android/app/src/main/cpp/edge_api_signer_jni.c @@ -0,0 +1,95 @@ +#include +#include +#include +#include + +#include "edge_api_sign.h" + +static void throw_by_name(JNIEnv *env, const char *class_name, const char *msg) { + jclass ex = (*env)->FindClass(env, class_name); + if (ex != NULL) { + (*env)->ThrowNew(env, ex, msg); + } +} + +static void throw_illegal_argument(JNIEnv *env, const char *msg) { + throw_by_name(env, "java/lang/IllegalArgumentException", msg); +} + +static void throw_runtime(JNIEnv *env, const char *msg) { + throw_by_name(env, "java/lang/RuntimeException", msg); +} + +JNIEXPORT jbyteArray JNICALL +Java_co_edgesecure_app_EdgeApiSignerModule_nativeSignMessage( + JNIEnv *env, + jobject thiz, + jbyteArray message_utf8, + jbyteArray package_name_utf8 +) { + if (message_utf8 == NULL || package_name_utf8 == NULL) { + throw_illegal_argument(env, "messageUtf8 and packageNameUtf8 are required"); + return NULL; + } + + jsize msg_len = (*env)->GetArrayLength(env, message_utf8); + jbyte *msg_bytes = (*env)->GetByteArrayElements(env, message_utf8, NULL); + if (msg_bytes == NULL) return NULL; + + jsize pkg_len = (*env)->GetArrayLength(env, package_name_utf8); + jbyte *pkg_bytes = (*env)->GetByteArrayElements(env, package_name_utf8, NULL); + if (pkg_bytes == NULL) { + (*env)->ReleaseByteArrayElements(env, message_utf8, msg_bytes, JNI_ABORT); + return NULL; + } + + /* edge_api_hmac_sign expects a C string bundle id (NUL-terminated). */ + char *bundle_id = (char *)malloc((size_t)pkg_len + 1); + if (bundle_id == NULL) { + (*env)->ReleaseByteArrayElements(env, message_utf8, msg_bytes, JNI_ABORT); + (*env)->ReleaseByteArrayElements(env, package_name_utf8, pkg_bytes, JNI_ABORT); + throw_by_name(env, "java/lang/OutOfMemoryError", "bundle id allocation failed"); + return NULL; + } + memcpy(bundle_id, pkg_bytes, (size_t)pkg_len); + bundle_id[pkg_len] = '\0'; + + uint8_t signature[32]; + int rc = edge_api_hmac_sign( + (const uint8_t *)msg_bytes, + (size_t)msg_len, + bundle_id, + signature + ); + (*env)->ReleaseByteArrayElements(env, message_utf8, msg_bytes, JNI_ABORT); + (*env)->ReleaseByteArrayElements(env, package_name_utf8, pkg_bytes, JNI_ABORT); + free(bundle_id); + + if (rc != 0) { + throw_runtime(env, "edge_api_hmac_sign failed"); + return NULL; + } + + /* nativeSignMessage is declared non-null in Kotlin, so a bare NULL return + would surface as an NPE far from its cause. */ + jbyteArray out = (*env)->NewByteArray(env, 32); + if (out == NULL) { + throw_by_name(env, "java/lang/OutOfMemoryError", "signature allocation failed"); + return NULL; + } + (*env)->SetByteArrayRegion(env, out, 0, 32, (const jbyte *)signature); + memset(signature, 0, sizeof(signature)); + return out; +} + +JNIEXPORT jstring JNICALL +Java_co_edgesecure_app_EdgeApiSignerModule_nativeApiKey( + JNIEnv *env, + jobject thiz +) { + jstring out = (*env)->NewStringUTF(env, edge_api_key()); + if (out == NULL) { + throw_runtime(env, "apiKey allocation failed"); + } + return out; +} diff --git a/android/app/src/main/java/co/edgesecure/app/EdgeApiSignerModule.kt b/android/app/src/main/java/co/edgesecure/app/EdgeApiSignerModule.kt new file mode 100644 index 00000000000..68e3e309f90 --- /dev/null +++ b/android/app/src/main/java/co/edgesecure/app/EdgeApiSignerModule.kt @@ -0,0 +1,82 @@ +package co.edgesecure.app + +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.Promise +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.bridge.ReactContextBaseJavaModule +import com.facebook.react.bridge.ReactMethod +import com.facebook.react.bridge.WritableMap +import android.util.Base64 +import java.nio.charset.StandardCharsets + +/** + * React Native bridge to the native HMAC API signer. + * The secret never enters Java as a contiguous plaintext constant. + */ +class EdgeApiSignerModule( + reactContext: ReactApplicationContext, +) : ReactContextBaseJavaModule(reactContext) { + companion object { + /** + * React Native constructs every module while building the package list, so + * an UnsatisfiedLinkError here would kill the app at startup. Record the + * failure instead, so EdgeApiSignerPackage can leave the module unregistered + * and JS sees an honestly absent signer rather than one that rejects every + * call. + */ + val libraryLoaded: Boolean = + try { + System.loadLibrary("edge_api_signer") + true + } catch (e: UnsatisfiedLinkError) { + false + } + } + + override fun getName(): String = "EdgeApiSigner" + + @ReactMethod + fun signMessage( + message: String, + promise: Promise, + ) { + if (!libraryLoaded) { + promise.reject("EDGE_API_SIGNER", "edge_api_signer library is unavailable") + return + } + try { + // Real UTF-8 bytes for both message and packageName (not JNI Modified UTF-8). + val messageUtf8 = message.toByteArray(StandardCharsets.UTF_8) + val packageNameUtf8 = + reactApplicationContext.packageName.toByteArray(StandardCharsets.UTF_8) + val signature = nativeSignMessage(messageUtf8, packageNameUtf8) + val apiKey = nativeApiKey() + val map: WritableMap = Arguments.createMap() + map.putString("apiKey", apiKey) + map.putString("signature", Base64.encodeToString(signature, Base64.NO_WRAP)) + promise.resolve(map) + } catch (e: Throwable) { + promise.reject("EDGE_API_SIGNER", e.message, e) + } + } + + @ReactMethod + fun getApiKey(promise: Promise) { + if (!libraryLoaded) { + promise.reject("EDGE_API_SIGNER", "edge_api_signer library is unavailable") + return + } + try { + promise.resolve(nativeApiKey()) + } catch (e: Throwable) { + promise.reject("EDGE_API_SIGNER", e.message, e) + } + } + + private external fun nativeSignMessage( + messageUtf8: ByteArray, + packageNameUtf8: ByteArray, + ): ByteArray + + private external fun nativeApiKey(): String +} diff --git a/android/app/src/main/java/co/edgesecure/app/EdgeApiSignerPackage.kt b/android/app/src/main/java/co/edgesecure/app/EdgeApiSignerPackage.kt new file mode 100644 index 00000000000..cc8b928a5a3 --- /dev/null +++ b/android/app/src/main/java/co/edgesecure/app/EdgeApiSignerPackage.kt @@ -0,0 +1,21 @@ +package co.edgesecure.app + +import com.facebook.react.ReactPackage +import com.facebook.react.bridge.NativeModule +import com.facebook.react.bridge.ReactApplicationContext +import com.facebook.react.uimanager.ViewManager + +/** Registers the EdgeApiSigner native module with React Native. */ +class EdgeApiSignerPackage : ReactPackage { + /** + * Registering a module whose JNI library is missing would make + * `hasNativeApiSigner()` true and steer JS away from its credential + * fallback, so an unusable signer is simply not registered. + */ + override fun createNativeModules(reactContext: ReactApplicationContext): List = + if (EdgeApiSignerModule.libraryLoaded) listOf(EdgeApiSignerModule(reactContext)) + else emptyList() + + override fun createViewManagers(reactContext: ReactApplicationContext): List> = + emptyList() +} diff --git a/android/app/src/main/java/co/edgesecure/app/MainApplication.kt b/android/app/src/main/java/co/edgesecure/app/MainApplication.kt index 28e44718e40..b02622f53de 100644 --- a/android/app/src/main/java/co/edgesecure/app/MainApplication.kt +++ b/android/app/src/main/java/co/edgesecure/app/MainApplication.kt @@ -36,6 +36,7 @@ class MainApplication : // packages.add(new MyReactNativePackage()); val packages = PackageList(this).packages packages.add(EdgeAttestationPackage()) + packages.add(EdgeApiSignerPackage()) return packages } diff --git a/babel.config.js b/babel.config.js index 9adf2e4f163..78781a0afbd 100644 --- a/babel.config.js +++ b/babel.config.js @@ -4,6 +4,10 @@ module.exports = function (api) { return { presets: ['module:@react-native/babel-preset'], plugins: [ + // `typechain` emits `export * as factories from './factories'` in + // src/plugins/contracts, which the React Native preset does not + // transform on its own. + '@babel/plugin-transform-export-namespace-from', isAndroid ? './node_modules/r3-hack/node_modules/react-native-reanimated/plugin' : 'react-native-worklets/plugin' diff --git a/deploy-config.sample.json b/deploy-config.sample.json index 54d082a4fc9..dcd6219f587 100644 --- a/deploy-config.sample.json +++ b/deploy-config.sample.json @@ -29,19 +29,49 @@ "master": { "hockeyAppId": "xxxxxxxxx", "splitArchitectures": [ - { "abi": "arm64-v8a", "zealotChannelKey": "xxxxxxxxxx" }, - { "abi": "armeabi-v7a", "zealotChannelKey": "xxxxxxxxxx" } + { + "abi": "arm64-v8a", + "zealotChannelKey": "xxxxxxxxxx" + }, + { + "abi": "armeabi-v7a", + "zealotChannelKey": "xxxxxxxxxx" + } ] }, "develop": { "hockeyAppId": "xxxxxxxxx" } }, - "envJson": { + "configJson": { + "develop": { + "corePlugins": { "bitcoin": true }, + "swapPlugins": { "changelly": true }, + "guiApiKeys": { "banxa": true, "phaze": false }, + "rampPlugins": { "moonpay": true }, + "ENABLE_VISA_PROGRAM": true, + "BETA_FEATURES": true + }, + "beta": { + "ENABLE_VISA_PROGRAM": true, + "BETA_FEATURES": true + }, "yolo": { "YOLO_USERNAME": "", "YOLO_PASSWORD": "" } + }, + "keysJson": { + "develop": { + "corePlugins": { "bitcoin": { "nowNodesApiKey": "xxxxxxxxx" } }, + "swapPlugins": { "changelly": { "apiKey": "xxxxxxxxx" } }, + "guiApiKeys": { "banxa": { "apiKey": "xxxxxxxxx" } }, + "rampPlugins": { "moonpay": {} }, + "globalKeys": { + "WALLETCONNECT_PROJECT_ID": "xxxxxxxxx", + "COINGECKO_API_KEY": "xxxxxxxxx" + } + } } } } diff --git a/docs/CONFIG_KEYS_ARCHITECTURE.md b/docs/CONFIG_KEYS_ARCHITECTURE.md new file mode 100644 index 00000000000..0d5071c76fa --- /dev/null +++ b/docs/CONFIG_KEYS_ARCHITECTURE.md @@ -0,0 +1,715 @@ +# Edge React GUI - Config & Keys Architecture + +## Overview + +Historically the app was configured through a single, gitignored `env.json` +file that mixed non-secret settings (feature flags, hosts, debug options, +plugin enablement) with real credential material (API keys, secrets, tokens) in +one flat, `ALLCAPS_*_INIT`-keyed blob. + +This refactor splits that single file into three gitignored inputs and reshapes +the schema so that plugin configuration is keyed by real plugin ID: + +- **`config.json`** — non-secret app/debug settings and the non-secret halves of + each plugin's init options. Safe to commit to a private build-config repo. +- **`keys.json`** — every secret (API keys, tokens, credentials), including the + secret halves of plugin init options — **except** the Edge login HMAC + credentials when using the native signer. +- **`edgeKey.json`** — `{ apiKey, apiSecret }` for Edge login HMAC. Build-time + only: `scripts/makeApiSigner.ts` embeds XOR-split native shards from it and + `scripts/makeNativeHeaders.ts` reads the public `apiKey`. The Metro bundle + never loads it, so `KEYS.EDGE_API_KEY` / `KEYS.EDGE_API_SECRET` are absent in + native-signer builds and every consumer must handle that (native + `EdgeApiSigner` or JS fallback). + +HMAC request signing (login-server via core, and info-server signed +infoRollup) is documented in [HMAC_SIGNING.md](./HMAC_SIGNING.md). + +At runtime the config/keys files stay separate accessors rather than flattening into one +`ENV` singleton: + +- **`CONFIG`** (`src/config.ts`) — immutable cleaned `config.json`. Never updated + by remote appKeys overlays. +- **`KEYS`** / **`globalKeys`** (`src/keys.ts`) — mutable cleaned keys. Partner + secrets live only under `KEYS.globalKeys`; `globalKeys` is a live alias of that + same object (no top-level flatten onto `KEYS`). +- **`pluginMaps`** (`src/pluginMaps.ts`) — the four resolved plugin init maps, + produced by `resolvePluginMaps(CONFIG, KEYS)` and rebuilt in place when keys + overlays apply. + +The split is about _where a field lives_ and _which accessor a consumer imports_. +A golden-equivalence test still proves the merged plugin maps are +behavior-identical to the legacy `env.json` shape for plugin inits. + +## Data flow + +```mermaid +flowchart LR + configJson["config.json (non-secret)"] --> cleanC["asConfigJson.withRest"] + keysJson["keys.json (secret)"] --> cleanK["asKeysJson.withRest"] + cleanC --> CONFIG["CONFIG (immutable)"] + cleanK --> nest["nestGlobalKeys"] + nest --> baked["bakedKeys"] + baked --> KEYS["KEYS + globalKeys alias (mutable)"] + CONFIG --> resolve["resolvePluginMaps"] + KEYS --> resolve + resolve --> maps["pluginMaps"] + maps --> core["corePlugins.ts -> allPlugins -> edge-core"] + maps --> ramps["useRampPlugins.ts"] + maps --> gui["gift-card / revolut / walletconnect / stake consumers"] + CONFIG --> configConsumers["feature flags / hosts / PostHog host / YOLO_*"] + KEYS --> keyConsumers["EDGE_API_* / SENTRY_* / POSTHOG_API_KEY"] + globalKeys["globalKeys"] --> partner["CoinGecko / Kiln / StakeKit / …"] +``` + +Each file is validated and cleaned by its own cleaner exactly once. There is no +union `asEnvConfig` pass over a merged blob: that would re-run single-shot codecs +such as `EDGE_API_SECRET`'s `asBase16` transform (string ⇄ `Uint8Array`) a second +time and fail. Runtime types are `ConfigJson`, `KeysJson`, and `RuntimeKeys` +(after flat partner fields have been nested under `globalKeys`). + +### Plugin inits are no longer validated field-by-field + +The plugin maps hold each plugin's init options as-is. The legacy flat cleaner +declared a cleaner per `*_INIT` field, which also meant it supplied defaults for +fields a config file left out — `thorname: 'ej'`, `affiliateFeeBasis: '50'`, +`appId: 'edge'`, FIO's `tpid`, and so on. + +Those defaults were duplicates: every plugin cleans its own init options and +declares the same default itself, so an omitted field still ends up with the +same value. The one exception was `guiApiKeys.paybis.partnerUrl`, whose +consumer required the field outright, so that default now lives in +`paybisProvider.ts` where it is used. + +The remaining difference is Rango: it applies a referral only when +`referrerAddress` and `referrerFee` are both set, and no longer invents a +`referrerFee` of `'0.75'` for a config that sets an address but no fee. Such a +config was always ambiguous; it now takes no referral rather than a rate nobody +wrote down. + +## Key modules + +| File | Responsibility | +| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `src/config.ts` | Cleans `config.json` with `asConfigJson.withRest` and exports immutable `CONFIG`. | +| `src/keys.ts` | Cleans `keys.json`, nests flat partner secrets via `nestGlobalKeys`, exports immutable merge-base `bakedKeys`, mutable `KEYS`, live `globalKeys` alias, and `applyRuntimeKeys`. | +| `src/pluginMaps.ts` | Builds `pluginMaps` via `resolvePluginMaps(CONFIG, KEYS)` and exports `rebuildPluginMaps` for in-place updates after key overlays. | +| `src/util/keysStore.ts` | Tier selection, the remote/cache/baked-in resolution promise, the local-only strip list, and `applyKeys` (mutates `KEYS`/`globalKeys`, then `rebuildPluginMaps` + `rebuildAllPlugins`). Prefers native `apiSigner` for signed infoRollup when linked. | +| `src/util/keysServer.ts` | Signs and issues `GET /v1/infoRollup/:appId` (JS HMAC or `apiSigner`), extracts `appKeys`, and validates the overlay shape. | +| `src/util/edgeApiSigner.ts` | Detects the native `EdgeApiSigner` module, builds the core's `apiSigner`, and caches the public `apiKey` for push / notification callers. | +| `src/configKeysMerge.ts` | Runtime merge layer: `deepMerge`, `mergePluginInit`, `nestGlobalKeys`, `resolvePluginMaps`, and `asMergeableKeys`. Also holds redaction helpers for unit tests. | +| `src/configKeysSchema.ts` | Per-file cleaners `asConfigJson` (non-secret) and `asKeysJson` (secret), `globalKeysShape` / `asGlobalKeys`, and the `ConfigJson` / `KeysJson` / `RuntimeKeys` / `GlobalKeys` types. | +| `scripts/splitEnvJson.ts` | Migration-only CLI (`npm run split-env-json`) that classifies a legacy `env.json` and writes `config.json` + `keys.json` + `edgeKey.json`. Never prints secrets; `--force` to overwrite. Not imported by the app. | +| `src/__tests__/configKeysMerge.test.ts` | Golden-equivalence + deep-merge + redaction unit tests. | +| `scripts/configure.ts` | Runs `makeConfig(asConfigJson.withRest, 'config.json')` and `makeConfig(asKeysJson.withRest, 'keys.json')` so `prepare` can bootstrap both files without writing secrets into `config.json`. | + +## The CONFIG / KEYS / pluginMaps schema + +`asConfigJson` and `asKeysJson` (`src/configKeysSchema.ts`) define the two +on-disk shapes. Only plugin-owned data was re-keyed; everything else keeps its +historical name and shape (`ACTION_QUEUE`, `LOG_CONFIG`, `LOG_SERVER`, +`THEME_SERVER`, `DEBUG_*`, `APP_CONFIG`, `EDGE_API_KEY`, `SENTRY_*`, `KILN_*`, +`YOLO_*`, etc.) — but consumers now import the accessor that owns the field. + +**Schema is not the same as a data file.** Each cleaner validates one file. +A secret field such as `EDGE_API_KEY` or `SENTRY_DSN_URL` appearing in +`asKeysJson` does **not** mean its value lives in `config.json` — the value comes +from `keys.json`; the cleaner only types that file. + +There is no runtime union cleaner that splat-merges both shapes into one object. +Ownership is enforced by keeping the accessors separate: + +```ts +export const asConfigJson = asObject({ + corePlugins, swapPlugins, guiApiKeys, rampPlugins, // shared plugin maps + ...non-secret config fields +}) + +export const asKeysJson = asObject({ + corePlugins, swapPlugins, guiApiKeys, rampPlugins, // secret-bearing plugin maps + globalKeys: asOptional(asGlobalKeys, () => ({})), + ...globalKeysShape, // legacy flat partner keys still accepted on disk + ...secret fields // EDGE_API_*, SENTRY_*, POSTHOG_API_KEY, … +}) + +// RuntimeKeys = KeysJson without flat partner fields, with nested globalKeys +``` + +(`.withRest` on each cleaner preserves legacy/extra keys and the JSON "comment" +separators the files carry.) + +Both per-file cleaners are actually used: + +- `src/config.ts` runs `asConfigJson.withRest(CONFIG_JSON)` at startup. +- `src/keys.ts` runs `asKeysJson.withRest(KEYS_JSON)`, then `nestGlobalKeys`, so + **each file is validated on its own** (a malformed `keys.json`, or a secret + misfiled into `config.json`, fails loudly) before runtime nesting / map + resolution. +- `scripts/configure.ts` runs `makeConfig` with the same cleaners for both + files, so `makeConfig` can never default or write a secret field into + `config.json`. + +The four plugin maps, each `Record`, live on `pluginMaps` after +`resolvePluginMaps`: + +- **`corePlugins`** — edge-core currency plugin inits keyed by real edge-core + plugin ID (`bitcoin`, `ethereum`, `binancesmartchain`, `thorchainrune`, ...). + Each value is the same `object | true | false` union as before. +- **`swapPlugins`** — swap plugin inits keyed by real swap plugin ID + (`changehero`, `thorchain`, `0xgasless`, ...). +- **`guiApiKeys`** — GUI fiat / gift-card provider credentials (formerly + `PLUGIN_API_KEYS`: banxa, paybis, phaze, revolut, simplex, …). WalletConnect + is **not** in this map; its `projectId` is `globalKeys.WALLETCONNECT_PROJECT_ID`. +- **`rampPlugins`** — ramp plugin inits (formerly `RAMP_PLUGIN_INITS`). Kept + distinct from `guiApiKeys` on purpose: `banxa` exists in both maps with + different shapes, so merging them would collide. + +There are **no `*_INIT` fields** left in the schema or in any consumer. The dead +`WYRE_CLIENT_INIT` (0 consumers) and unmapped legacy `*_INIT` fields were dropped. + +## File ownership rule + +- **`config.json`** holds non-secret app/debug fields and the non-secret plugin + fields: `enabled` flags, `appId`, `affiliateFeeBasis`, `integrator`, + `thorname`, `apiHost`, `apiUrl`, `widgetUrl`, `partnerUrl`, `referralId`, + `feePercentage`, `feeReceiveAddress`, `host`, `port`, and the like. +- **`keys.json`** holds all credential material: `apiKey`, `nowNodesApiKey`, + `evmScanApiKey`, `ninerealmsClientId`, `thorswapApiKey`, `privateKeyB64`, + `hmacUser`, `jwtTokenProvider`, `clientSecret`, `heliusApiKey`, + `alchemyApiKey`, `blockfrostProjectId`, `glifApiKey`, `subscanApiKey`, + `WALLETCONNECT_PROJECT_ID` (from `WALLET_CONNECT_INIT.projectId`), auth/telemetry top-level + fields (`EDGE_API_KEY`/`EDGE_API_SECRET`, `SENTRY_*`, `BUGSNAG_API_KEY`, + `POSTHOG_API_KEY`), and the partner secrets — the "global keys". On disk those + partner secrets may still appear **flat** at the top level for legacy files; + load and overlay paths run `nestGlobalKeys` so the runtime `KEYS` object keeps + them only under `KEYS.globalKeys` (`AZTECO_API_KEY`, `COINGECKO_API_KEY`, + `IP_API_KEY`, `STAKEKIT_API_KEY`, `UNSTOPPABLE_DOMAINS_API_KEY`, + `WALLETCONNECT_PROJECT_ID`, `KILN_*`, …). + A signed infoRollup `appKeys` overlay delivers the same partner secrets nested + under a `globalKeys` section; the client keeps that nesting (no top-level + flatten onto `KEYS`). `YOLO_*` and `POSTHOG_API_HOST` live in `config.json` + (local-only developer / host wiring, never served). + +Both files are gitignored (`.gitignore` lists `/config.json` and `/keys.json` +alongside the retained `/env.json`). + +## Merge semantics (`resolvePluginMaps` / `nestGlobalKeys`) + +`src/configKeysMerge.ts` combines config enablement with keys secrets into the +resolved `pluginMaps`, and normalizes partner secrets under `globalKeys`: + +1. **`CONFIG` top-level fields** stay on `CONFIG` only. They are never overwritten + by appKeys overlays (`keysStore` also drops non-`asKeysJson` fields from + overlays via `keepKeysFields`). +2. **`KEYS` top-level secret fields** (`EDGE_API_*`, `SENTRY_*`, `POSTHOG_API_KEY`, + plugin maps, …) live on `KEYS`. Remote/cache overlays deep-merge onto + `bakedKeys` with keys winning on collision. +3. **Partner `globalKeys`** — flat on-disk partner fields and any nested + `globalKeys` section are normalized by `nestGlobalKeys`. Consumers read + `globalKeys.COINGECKO_API_KEY` (or `KEYS.globalKeys.…`); there is no + top-level `KEYS.COINGECKO_API_KEY` after nesting. +4. **Currency & swap plugins** — for each ID present in config or keys + `corePlugins` / `swapPlugins` (union), the non-secret config value is + combined with the matching secret from `KEYS.corePlugins[id]` / + `KEYS.swapPlugins[id]` via `mergePluginInit`: + - a `false` config value keeps the plugin disabled (secrets ignored); + - a `true`/absent config value with an object secret becomes the secret + object (an object always wins over a bare boolean enablement flag); + - otherwise the two are deep-merged with the keys side winning. + Extra remote IDs on `pluginMaps.corePlugins` do **not** register a new + engine — `corePlugins.ts` is a hardcoded table. +5. **GUI provider keys (`guiApiKeys`)** — union of config and keys IDs, merged + per ID. Currency/swap secrets do not live here. +6. **Ramp plugins (`rampPlugins`)** — `CONFIG.rampPlugins[id]` deep-merged with + `KEYS.rampPlugins[id]` per ID. + +Objects are merged field-by-field; arrays and primitives replace wholesale; +`undefined` on either side yields the other side. + +## How the legacy file was split (`scripts/splitEnvJson.ts`) + +`splitEnv` converts a flat legacy `env.json` object into `{ config, keys }`: + +- `CURRENCY_INIT_MAP` / `SWAP_INIT_MAP` map each `*_INIT` field name to its real + edge-core plugin ID (e.g. `THORCHAIN_INIT` → `thorchainrune` for currency and + `thorchain` for swap). +- `isSecretField` (a field-name regex) and `isSecretTopLevel` classify each + field. Secret-looking fields go to `keys.json`; the rest go to `config.json`. +- `PLUGIN_API_KEYS` → `guiApiKeys`, `RAMP_PLUGIN_INITS` → `rampPlugins`. +- `POSTHOG_INIT` → `config.POSTHOG_API_HOST` + a flat `keys.POSTHOG_API_KEY` + (PostHog is not a plugin; the api key stays top-level on `KEYS` at runtime). +- `WALLET_CONNECT_INIT.projectId` → flat `keys.WALLETCONNECT_PROJECT_ID` (then + nested under `globalKeys` at load). No config flag; disable = omit the key. +- Loose partner secrets (`AZTECO_*`, `KILN_*`, CoinGecko, …) → flat top-level + fields in `keys.json` (nested under `globalKeys` at runtime load). +- `YOLO_*` stays in `config.json`. +- `WYRE_CLIENT_INIT` and any remaining unmapped `*_INIT` fields are dropped. + +This same function is what the golden test uses to synthesize `config`/`keys` +in memory from the historical `env.json`, and what +`scripts/splitEnvJson.ts` (`npm run split-env-json`) uses to write the real +files on disk — guaranteeing the split is value-preserving. + +## Consumers + +Every reader was re-pointed from the old flat `ENV` / `*_INIT` / +`PLUGIN_API_KEYS` / `RAMP_PLUGIN_INITS` surface to the matching accessor: + +- Import **`CONFIG`** for non-secret settings (`APP_CONFIG`, `DEBUG_*`, + `LOG_SERVER`, `POSTHOG_API_HOST`, `YOLO_*`, feature flags, …). +- Import **`KEYS`** for top-level secrets (`EDGE_API_KEY`, `EDGE_API_SECRET`, + `SENTRY_*`, `POSTHOG_API_KEY`, …). +- Import **`globalKeys`** for partner secrets (`COINGECKO_API_KEY`, `KILN_*`, + `STAKEKIT_API_KEY`, …). +- Import **`pluginMaps`** for resolved plugin inits: + - `src/util/corePlugins.ts` — maps each edge-core plugin ID to + `pluginMaps.corePlugins[id]` / `pluginMaps.swapPlugins[id]`, preserving the + existing `true`/`false` hardcodes. Note `thorchainrune` and + `thorchainrunestagenet` both read `corePlugins.thorchainrune`. + - `src/hooks/useRampPlugins.ts` — `pluginMaps.rampPlugins[pluginId]`. + - `src/plugins/gui/util/initializeProviders.ts`, `fetchRevolut.ts`, and the + gift-card paths — `pluginMaps.guiApiKeys.*`. + - Inner-field readers: `FioAddressUtils.ts` (`pluginMaps.corePlugins.fio`), + `thorchainYield.ts` + `stakePlugins.ts` (`pluginMaps.swapPlugins.thorchain`), + `fantomEcosystem.ts` (`pluginMaps.corePlugins.fantom`), + `WalletConnectService.tsx` (`globalKeys.WALLETCONNECT_PROJECT_ID`), + `tracking.ts` (`KEYS.POSTHOG_API_KEY` + `CONFIG.POSTHOG_API_HOST`). + +## Scripts + +All build/deploy scripts were retargeted from `env.json` to the new files: +`secretFiles.ts` (copies `config.json` + `keys.json` + `edgeKey.json`), +`makeNativeHeaders.ts` (reads `{apiKey, apiSecret}` from `edgeKey.json`), +`patchFiles.ts` (`SENTRY_*` from `keys.json`), `loggingServer.ts` + `themeServer.ts` (point at `config.json`), +`configure.ts` (config- and keys-scoped cleaners), and `deploy.ts` + `cleaners.ts` +(`configJson` / `keysJson` branch-override fields — already shaped like the +files they patch). + +After `npm run split-env-json`, `npm run split-baked-and-server-keys` rewrites +`keys.json` to the local-only keep-list (`slimKeysJson` / `localOnlyKeys`) and +writes `appKeys.json` for the info-server Couch default layer: `corePlugins`, +`swapPlugins`, `guiApiKeys`, `rampPlugins`, and nested `globalKeys`. It never +prints secret values. + +--- + +## Status of remaining Env config code + +The following pieces still reference the old configuration world. Each is +listed with why it remains. + +### 1. `env.json` on disk — retained intentionally + +`env.json` is still present in the worktree and still gitignored +(`.gitignore` and `.cursorignore`). **No active runtime code reads it.** It is +kept as the migration source and a historical copy, per the plan's locked +decision. The golden-equivalence test reads it opportunistically (guarded by an +existence check) to prove parity, but the app itself does not depend on it. + +### 2. `scripts/splitEnvJson.ts` — committed legacy bridge + +The CLI deliberately contains the legacy `*_INIT` maps and the `splitEnv` +classifier. It remains because it is the bridge that: + +- powers the golden-equivalence test (`src/__tests__/configKeysMerge.test.ts`), + and +- regenerates local `config.json` / `keys.json` from an `env.json` + (`npm run split-env-json`). + +It references legacy names by design; it is the one place that is _supposed_ to +know about the old shape. If `env.json` is ever fully retired, this module, the +CLI script, and the golden test can be removed together. + +### 3. Temporary `[pipe]` runtime-verification logging — removed + +The temporary `[pipe]` harness (`logPipe` and its call sites) has been removed. +`redactKey` / `redactValue` remain in `src/configKeysMerge.ts` for unit tests +only. + +### 4. Comment / documentation references to `env.json` + +Non-functional mentions of `env.json` still exist in `README.md`, +`ios/Sentry.swift`, `android/.../MainApplication.kt`, and `CHANGELOG.md`. These +are comments/docs, not runtime code, so they do not affect behavior. They are +reasonable follow-up cleanup (update README setup instructions and native +comments to reference `config.json` / `keys.json`) but were out of the strict +refactor scope. + +## Verification status + +- **Static:** deep-merge, nest/global-keys, and redaction unit tests pass; + `tsc --noEmit` and lint are clean across the edited files. Local + `config.json` / `keys.json` golden checks in `configKeysMerge.test.ts` are + skipped when those files are absent (typical CI), so they do not substitute + for a built-in fixture — run them on a developer machine that has real local + files when validating a split. +- **Cross-repo:** the HMAC signing vector is asserted from both sides — + `src/__tests__/util/hmacAuth.test.ts` here and `src/__tests__/hmacAuth.test.ts` + in edge-info-server assert the same base64 digest, so the canonical signed + string cannot drift on one side unnoticed. +- **Runtime pipe comparison:** abandoned; temporary logging removed. + +## Follow-up notes + +- Temporary `[pipe]` logging and the iOS/Android runtime pipe comparison are + removed. +- Private build-config repos must ship `config.json` + `keys.json` instead of + `env.json` before release builds use this branch. +- Deploy deep-merges explicit `configJson` / `keysJson` per-branch overrides into + the matching files and does not run overrides through `splitEnv`. Outer keys + are **git branch names** (`develop`, `beta`, `yolo`, …). Inner `keysJson[branch]` + is the same overlay as `info_keys` layer `keys` / signed rollup `appKeys` + (four maps + `globalKeys.WALLETCONNECT_PROJECT_ID`). Inner `configJson[branch]` + is enablement / non-secret init. See `deploy-config.sample.json`. Never-serve + fields (`POSTHOG_API_KEY`, `EDGE_API_*`, `SENTRY_*`, `YOLO_*`) do not belong + in `keysJson`. Legacy `envJson` is ignored (with a migration error when a + branch block exists only there) so the same file can still serve older GUI + builds that read it. +- Optional: update `README.md` and native comments to reference the new files; + eventually retire `env.json` + `scripts/splitEnvJson.ts` together. + +## Remote keys via the info server (signed `infoRollup` `appKeys`) + +Client support for remote keys is implemented on this branch (`keysStore`, +`keysServer`, DeviceSettings `keysCache`, EdgeCoreManager gate). The design +notes below remain the source of truth for layering and fallbacks. + +The goal is to move the secrets in `keys.json` onto the Edge info server, which +serves them from a new authenticated endpoint. `config.json` / `CONFIG` are +unaffected — they hold no secrets and stay local, synchronous, and immutable. + +### Resolution order + +Keys resolve through three tiers, and `getKeysTier()` reports which one won so a +runtime check can prove the remote path was exercised: + +| Tier | Source | When it applies | +| ---------- | ------------------------------------ | ------------------------------------------------------------------- | +| `cache` | `keysCache` in `DeviceSettings.json` | Any launch with a mergeable on-disk cache (does not expire) | +| `remote` | Signed `GET /v1/infoRollup/:appId` `appKeys` | Cold start (no usable cache), fetch succeeded within budget | +| `baked-in` | `keys.json` compiled into the binary | Cold start where the fetch failed/missed budget and no usable cache | + +The cache takes precedence over the network rather than the other way round. +That is deliberate and follows from "never hot-swap a running core" (see +[Launch sequencing](#launch-sequencing)): a launch that already has keys must not +stall on the network, so it serves the cache and refreshes in the background for +the _next_ launch. Only a cold start with no cache has anything to wait for. + +A cache whose payload will not merge counts as no cache at all, so that launch +takes the cold-start path and pays its budget. The alternative — keeping the +cache's fast launch and skipping the fetch — would strand the app on baked-in +keys for as long as the bad payload sits on disk, since only a successful fetch +overwrites it. Paying the budget once repairs it. + +Both tiers are held to the same definition of "will not merge", `asMergeableKeys` +in `configKeysMerge.ts`: a top-level object whose `corePlugins`, `swapPlugins`, +`guiApiKeys`, `rampPlugins`, and `globalKeys` are objects if present. It is checked in `applyKeys`, which +every tier passes through, and again at the fetch so a bad response never reaches +disk. Validating only the fetch would leave the cache unguarded, and because +`deepMerge` replaces rather than merges when the two sides disagree on type, a +map that came back as a string or `null` would overwrite the whole baked-in map +and strip every secret in it while the launch still reported tier `cache`. + +A cold start that falls through to `baked-in` because the budget expired keeps +waiting on that fetch in the background and caches whatever it returns. The gate +closing does not cancel the request, so without this the answer would be +discarded and every later launch would pay the full budget again. The late +payload is only written to disk, never folded into the running `KEYS` / +`pluginMaps`, which is the same rule the warm path follows. A fetch that failed +outright has nothing to wait for and simply does nothing. + +The on-disk cache does **not** expire. Any mergeable `keysCache` is used as a +warm start so later launches never block on the network; a background refresh +updates the cache for the _next_ launch. `fetchedAt` may still be recorded for +diagnostics, but it does not gate the warm path. There is no TTL. + +The baked-in file is the **base layer** of a `deepMerge`, not a wholesale +replacement, so a partial remote payload degrades gracefully instead of blanking +fields the build already knew. Boot never blocks on the network and never shows +an error scene for key retrieval; a failed fetch falls to the next tier and +retries in the background. + +Two consequences worth stating plainly: + +- **`keys.json` does not go away.** It keeps its full schema with every field + optional. `EDGE_API_KEY` / `EDGE_API_SECRET` authenticate signed infoRollup and login + when the native signer is **not** linked. Native-signer builds embed those + credentials at compile time from `edgeKey.json` and omit them from the Metro + bundle; see [HMAC signing](HMAC_SIGNING.md). +- **A shipped binary may therefore still contain partner secrets.** This work + _reduces_ secret exposure and enables server-side rotation; it does not make + the IPA/APK secret-free. + +### Authentication + +The endpoint uses HMAC `Authorization` plus a required `X-Timestamp`. That is +the existing login-server scheme (`with-api-key.ts`) with one extra signed line. +Canonical server behavior, layer matching, and the Couch schema live in +[edge-info-server `docs/INFO_ROLLUP.md`](https://github.com/EdgeApp/edge-info-server/blob/master/docs/INFO_ROLLUP.md). + +``` +GET /v1/infoRollup/{appId}?os={ios|android}&osVersion={x.y.z}&appVersion={semver} +Authorization: HMAC {edgeApiKey} {base64(hmacSha256(signedString, secret))} +X-Timestamp: {unix seconds} +x-attestation-token: {ES256 JWT} // optional; invalid token → HTTP 401 +``` + +Signed string (empty GET body): + +``` +GET\n/v1/infoRollup/{appId}?os=…&osVersion=…&appVersion=…\n\n{timestamp} +``` + +The client signs that path **including** `/v1` (`keysServer.ts` `signPath`). +The info server verifies `req.originalUrl`. Login-server HMAC has **no** +timestamp line and **no** freshness window. + +`appId` in the path is the info-rollup partner id (`config.appId ?? 'edge'`). +It must match `info_keys` document `_id`. The attested bundle id is a different +field: the JWT `appId` claim (`co.edgesecure.app`). + +Disk cache holds the **`appKeys` overlay only** (`DeviceSettings.keysCache`). +The public rollup (promo/APY/…) stays **in-memory** on `infoServerData.rollup`. +A 5-minute / NetInfo unsigned poll may live-update those public fields; KEYS +never hot-swap this session. Boot is a single signed infoRollup when HMAC +credentials exist (no parallel unsigned fetch at t=0). + +Native `apiSigner` is preferred when `EdgeApiSigner` is linked; otherwise JS +HMAC uses `KEYS.EDGE_API_KEY` / `KEYS.EDGE_API_SECRET`. + +### Attestation-level layering + +Payloads are **not** a named ladder (`default` then `debug` then `hardware`) +nested under each app. The info server walks an ordered `layers` array and +deep-merges every row that independently matches: + +1. Layer `apiKeys` lists the **memo name** of the HMAC that signed the request + (not the presented public id). +2. Token assurance ≥ `minAssurance` (`default` is unattested, below `debug`). +3. `bundleIds` is `"*"` (only legal at `minAssurance: default`) **or** the JWT + `appId` is in the layer’s bundle list. Unattested callers never match a + non-wildcard bundle list. + +Unknown bundles are not a 403; they receive only wildcard/`default` rows. +A present-but-invalid attestation token is HTTP 401 — the GUI must not treat +that as “unattested floor.” + +**Invariant:** anything on a `"bundleIds": "*"` / `minAssurance: default` row is +reachable by any holder of a listed HMAC. App-scoped credentials belong on rows +that list real bundle IDs and `minAssurance` of `debug` or above. + +### `info_keys` document shape + +Couch `info_keys/` (`_id` is the rollup app ID: `edge`, +`com.testy.wallet`, …). Lookup is the presented HMAC public id via +`_design/api-key`. Secrets live here, not on login-server Couch (rotate both +when minting a pair). + +``` +apiKeys: + : + type: hmac + key: + secret: + enabled: true | false | returningOnly +layers: + - comment, bundleIds, apiKeys: [, ...], minAssurance, keys: { ... } +``` + +See the sample document and compile-error rules in the info-server INFO_ROLLUP +doc. The GUI `LAYER-*` launch log (`[keys] … markers=…`) is how e2e confirms +which overlay rows fired. + +### Never served + +The endpoint strips these even if an operator pastes them into a document: + +- `EDGE_API_KEY` and `EDGE_API_SECRET` — they _are_ the credentials. +- All telemetry keys — `SENTRY_*`, `BUGSNAG_API_KEY`, and `POSTHOG_API_KEY` + (stripped from the payload's top-level and any `globalKeys` section; legacy + `pluginApiKeys.posthog` is also stripped). These stay permanently local + because `Sentry.init` + (`src/app.ts`) and the PostHog setup (`src/util/tracking.ts`) both run at + module scope, before any gate can exist, and crash reporting must cover the + launch path that fetches the keys. The consequence is that rotating a Sentry DSN + requires an app update. +- Any pasted `YOLO_*` / `SENTRY_*` top-level fields (matched by prefix). YOLO + credentials themselves live in `config.json` on the client and are not part of + the keys payload. + +Partner globals such as `KILN_*`, `STAKEKIT_API_KEY`, and `COINGECKO_API_KEY` +**are** served in the payload's `globalKeys` section. The client keeps them +nested under `KEYS.globalKeys` / the exported `globalKeys` alias (no top-level +flatten). + +### Impact on this document's architecture + +The one structural change to what is described above: secrets on `KEYS` / +`globalKeys` / `pluginMaps` cannot be assumed final at module-evaluation time, +because the remote fetch is asynchronous. `CONFIG` reads stay synchronous and +immutable, while secrets move behind an awaited keys store that must be +populated before `EdgeCoreManager` builds `allPlugins`. + +#### Consumers must read secrets lazily + +`applyKeys` mutates `KEYS` / `globalKeys` in place and then rebuilds +`pluginMaps` (and `allPlugins`), so a consumer that reads +`KEYS.SOME_SECRET`, `globalKeys.SOME_SECRET`, or `pluginMaps.…` **inside a +function** picks up the remote value, while one that copies it into a +module-scope constant does not. Metro evaluates the whole static import graph +synchronously during bundle load, which is strictly before any network fetch can +resolve, so a module-scope copy is always the baked-in value — permanently, and +silently. + +This is a real constraint, not a theoretical one: `stakeKitUtils.ts`, +`cardanoKilnPool.ts`, `ethereumKilnPool.ts`, `thorchainYield.ts`, and +`fantomEcosystem.ts` all originally captured secrets this way and had to be +converted to functions or property getters. `corePlugins.ts` is the one case that +does not need this for the compiled plugin table, because `applyKeys` calls +`rebuildAllPlugins()` and the `allPlugins` export is a live binding. + +When adding a consumer of a remotely-servable secret, read it at the point of +use. Anything that genuinely must be read at module scope belongs in the +never-served set below, alongside `SENTRY_*` and PostHog. + +### Launch sequencing + +Cold start (no cache) blocks on the network fetch before core plugins are built. +Every later launch blocks only on the cache read and refreshes keys in the +background, writing the result for the _next_ launch; refreshed keys are never +hot-swapped into a running core. + +The resolution promise starts at module scope in +`src/components/services/EdgeCoreManager.tsx`, which Metro evaluates during the +initial bundle load, so the disk read and the signed infoRollup fetch overlap +the rest of +startup. The WebView is gated behind keys and does **not** overlap that work. +The component's effect then awaits the same single-flighted promise, which has +usually already resolved, making the warm-start gate approximately free. The +native splash is still up at that point, so the gate is not visible. + +`initializeKeys()` is idempotent and **never rejects**. Both properties matter: +it has two callers, and `EdgeCoreManager` renders `LoadingSplashScreen` until it +resolves, so a rejection cached in the memoized promise would leave the app on +the splash screen with no way to recover. Every failure inside it simply selects +a lower tier. + +Cold-start budget, worst case: + +| Stage | Budget | Constant (`keysStore.ts`) | Enforced | +| -------------------------- | ------ | ----------------------------- | ----------------- | +| Wait for a first token | 5 s | `ATTESTATION_BUDGET_MS` | yes, inside fetch | +| Signed infoRollup | 8 s | `COLD_FETCH_TIMEOUT_MS` | no, share only | +| **Deadline raced** | 13 s | `COLD_TOTAL_TIMEOUT_MS` | yes, the gate | +| Settings read (warm path) | 2 s | `SETTINGS_READ_TIMEOUT_MS` | yes, first cap | +| Settings salvage (cold) | 2 s | `SETTINGS_SALVAGE_TIMEOUT_MS` | yes, second cap | + +Only two things are actually timed: the attestation wait, and the combined +deadline the app waits on. The two stages share that one deadline rather than +being timed separately, because attestation happens inside the promise being +raced, and the deadline is their sum so that a slow first attestation cannot +spend the fetch's share and abandon a request that was about to answer. The +fetch's 8 s is therefore a share used to size the total, not a timer of its own: +whatever is left of the 13 s once attestation settles is what the fetch gets. + +The network call uses `FETCH_TIMEOUT_MS` (5 s) in `keysServer.ts` as the +`asyncWaterfall` per-server stagger (same as the helper's default), not as a +hard ceiling on the whole signed infoRollup call. With more than one server configured the +waterfall can outlast the 8 s share, which is why the 13 s gate — not the +stagger — is what bounds the launch. + +These are ceilings on a first install with no network, not typical cost. The +cache write is deliberately left outside the race and not awaited: a slow disk +must not be able to discard keys already in hand, and losing the write costs one +refetch on the next launch. + +#### Settings read vs salvage + +`SETTINGS_READ_TIMEOUT_MS` and `SETTINGS_SALVAGE_TIMEOUT_MS` are two separate +2-second waits on the **same** `DeviceSettings.json` load, at different points +in boot. They have the same duration. Salvage does **not** start a second disk +read. + +**Settings read** is the first cap. At the start of `initializeKeys`, the store +races `awaitDeviceSettingsDisk()` against 2 seconds. + +- Disk wins in time and the cache is mergeable → **warm start**: apply cache + now, refresh in the background, never enter the cold network gate. +- 2 seconds elapse first → boot **continues without cache**. The disk read is + still in flight; in-memory `keysCache` may still be empty. + +That first timeout exists so a hung or slow settings file cannot stall the +splash on every launch. + +**Settings salvage** is a **second** 2-second wait, only on the cold path, +**after** the network race has settled (fetch failed, hit +`COLD_TOTAL_TIMEOUT_MS`, or returned keys). `applyCacheFallback` races that +same in-flight `settingsLoad` against another 2 seconds, then reads +`getKeysCache()`. + +That exists because the first timeout can fire while the file is only a little +late. Without salvage, a fast network failure right after the settings timeout +left **no** remaining disk budget, and boot fell through to baked-in even +though cache was about to appear. Salvage gives that late read another chance +to win **this** launch: + +- Fetch failed or timed out → prefer late cache over baked-in. +- Fetch succeeded → still prefer late cache for this launch (warm-start rule), + and write the remote payload for the **next** launch. + +If attestation finishes inside its budget the fetch goes out attested and +receives the full payload immediately; otherwise it goes out unattested and takes +the `default` tier, and the background refresh upgrades the cached payload for the +next launch. A feature needing a key absent from the current payload can trigger +an on-demand foreground escalation. + +### Where the cache lives + +The cache is a `keysCache` field inside **`DeviceSettings.json`**. That file is +already read for theme setup in `src/app.ts`; the keys promise itself starts in +`EdgeCoreManager.tsx` and awaits the same single-flighted `initDeviceSettings` +load. + +The other three launch-window files (`remoteConfigSticky.json`, `firstOpen3.json`, +`utilityServer.json`) are deliberately **left untouched**. Folding them in was +considered and rejected for two reasons: + +- Two of them silently regenerate sticky data when their read fails. + `experimentConfig` re-randomizes the A/B variant, and `firstOpen` mints a new + `deviceId`, resets `firstOpenEpoch`, and reports `isFirstOpen: 'true'`, making an + existing install look brand new. Leaving them alone removes that failure mode + instead of mitigating it. +- There is no latency to gain. `firstOpen3.json` and `utilityServer.json` are read + from a `Providers` effect after the core already exists, and the + `experimentConfig` read fires during initial bundle evaluation through a fully + static import chain, so it has long resolved before `Main` checks its gate. That + gate therefore stays as-is. + +`logins/*` and `fingerprint.json` are read by `edge-core-js` and +`edge-login-ui-rn` respectively, and are outside this repo's control. + +The tradeoff of hosting the cache here is write amplification rather than read +cost. `DeviceSettings.json` is the most frequently written of the four, and +`writeDefaultScreen` fires on every tap of the Home or Assets tab, so writes are +**serialized immediately** (not debounced) and `DeviceSettingsActions.ts` is the +single owner of the file, holding the authoritative in-memory copy and chaining +writes so concurrent patches cannot interleave `setText` calls. The keys store +mutates the cache through that owner rather than writing the file itself. + +`readDeviceSettings` collapses any read failure into `asDeviceSettings({})`, so a +corrupt file already resets user preferences today. Since the payload is now larger +and rewritten more often, `keysCache` is cleaned with `asMaybe` so a malformed +cache degrades to a miss instead of wiping preferences, and a malformed preference +does not discard the cache. Losing the cache is recoverable by refetching or +falling back to the baked-in file — which is precisely why this file is a safe host +and the sticky files are not. + +Migration is additive: an existing `DeviceSettings.json` simply lacks `keysCache`, +which reads as a miss and triggers a fetch. + +`initDeviceSettings` is single-flighted, because it now has two callers: the +existing fire-and-forget theme setup in `app.ts` and the awaited call in the +keys store. Without that, the read that resolved last would replace the whole +in-memory settings object and could discard a `keysCache` written in between. +The theme setup itself is still fire-and-forget, so the pre-existing `themeMode` +flash on first render is unchanged by this work. diff --git a/docs/EDGE_CLI.md b/docs/EDGE_CLI.md new file mode 100644 index 00000000000..91074167db4 --- /dev/null +++ b/docs/EDGE_CLI.md @@ -0,0 +1,368 @@ +# Edge CLI + +A command-line interface for the Edge platform. Useful for account management, +wallet operations, debugging, and scripting against edge-core-js. + +The CLI is a **thin one-shot client**. A long-lived **engine daemon** owns the +`EdgeContext`, keeps logged-in accounts alive across invocations, and exposes a +JSON REST API over a Unix domain socket (TCP is optional). + +For the full surface — every command, its REST call, and the `edge-core-js` +call behind it — see the generated reference at +[docs/api/dist/index.html](./api/dist/index.html), built from `docs/api/`. + +## Overview + +| Piece | Role | +|-------|------| +| `edge-engine` | Long-lived daemon. Owns one `EdgeContext` and N `EdgeAccount`s keyed by `sessionId`. Serves HTTP. | +| `edge-cli` | One-shot client. Parses argv, auto-spawns the engine if needed, talks over the Unix socket, prints results. | + +By default the client uses only the Unix socket at +`~/.edge-cli/run//engine.sock`. Enable loopback TCP with +`--tcp=9008` on the engine (useful for `curl` / scripts). + +## Running + +**Development (from source):** + +```bash +npm run cli -- help # One-shot via client (auto-spawns engine) +npm run cli -- login-with-password --username=u --password=p # sessionId is persisted +npm run cli -- balance-map --wallet-id= # Reuses the engine + session + +npm run engine # Start the engine alone +npm run engine -- -t # Engine against tester servers +npm run engine -- --tcp=9008 # Also listen on 127.0.0.1:9008 +``` + +**Built artifact:** + +```bash +npm run build:cli # → lib/edgeCli.js + lib/edgeEngine.js +node lib/edgeCli.js help +node lib/edgeEngine.js -t --tcp=9008 +``` + +**Published (npm):** + +```bash +npx edge-cli help +npx edge-cli -t login-with-password --username= --password= +``` + +### Engine / client flags + +| Flag | Who | Description | +|------|-----|-------------| +| `-t, --test` | both | Use the six `-tester` servers (see below) | +| `--fake` | both | Emulate the login, info and sync servers in-process; no network, no API key. Its own engine profile, so it never shares a socket with a real one | +| `-d, --directory` | both | Working directory for local Edge data | +| `-a, --app-id` | both | Application ID | +| `-k, --api-key` | both | Override API key from `keys.json` | +| `--locale ` | both | Language tag (BCP 47 or POSIX). Also `EDGE_CLI_LOCALE` or `locale` in the config file | +| `--tcp=9008` | engine | Bind TCP on `127.0.0.1` (off by default; bare `--tcp` is an error; `--tcp=0` = ephemeral) | +| `--idle-timeout ` | engine | Self-shutdown after idle with no sessions (default `300`; `0` = never) | +| `--no-spawn` | client | Do not auto-start the engine; fail if none is running | +| `--session ` | client | Override the persisted sessionId | +| `--solve-captcha` | client | On `CHALLENGE_REQUIRED`, auto-solve ALTCHA PoW and retry | +| `-c, --config ` | both | Configuration file | +| `--tcp-host=` | engine | TCP bind host (default `127.0.0.1`) | +| `-u, --username` / `-p, --password` | client | Legacy one-shot login helpers | +| `-h, --help` | both | Show options | + +API keys load from `./keys.json`, then `~/.edge-cli/keys.json` +(`edgeApiKey`, `edgeApiSecret`, `pluginApiKeys`). + +When the native Edge API HMAC signer is available, the engine prefers it over +`keys.json` secrets for **both** `edge-core-js` and `GET /v1/getKeys` on the +info server. Plugin secrets (including Monero LWS `edgeApiKey`) come from that +fetch and overlay local `pluginApiKeys`. Set `EDGE_CLI_FORCE_KEYS_JSON=1` +(or pass `-k`) to force the JSON key/secret pair instead — useful for tester +embeds and debugging. `-t` signs getKeys against `info-tester.edge.app`. + +Locale (one tag drives language tables and number format): `--locale`, then +`locale` in `edge-cli.conf`, then `EDGE_CLI_LOCALE`, then `LC_ALL` / +`LC_MESSAGES` / `LANG`, then `Intl`, then `en-US`. An already-running engine +keeps its locale; the client warns on mismatch and continues. `GET /engine/status` +reports `locale`, `decimalSeparator`, and `groupingSeparator`. + +## Tester servers + +**Always use `-t` / `--test` for testing. Never hit production in tests.** + +`-t` points the engine at these six hosts (the only `*-tester.edge.app` +names that resolve): + +| Host | `EdgeContextOptions` field | +|------|----------------------------| +| `https://login-tester.edge.app` | `loginServer` | +| `https://info-tester.edge.app` | `infoServer` | +| `https://sync-tester-us1.edge.app` | `syncServer` (array) | +| `https://sync-tester-us2.edge.app` | `syncServer` | +| `https://sync-tester-us3.edge.app` | `syncServer` | +| `https://change-tester.edge.app` | `changeServer` | + +```bash +npm run cli -- -t create-account alice --password='pass' --pin=1234 +npm run cli -- -t login-with-password --username=alice --password='pass' +``` + +Confirm with `edge-cli engine-config` — `testMode` should be true and every +server URL should be a `*-tester.edge.app` host. + +## Architecture + +```mermaid +flowchart LR + cli["edge-cli (one-shot)"] -->|"HTTP / unix socket"| engine + script["scripts / curl"] -->|"HTTP / TCP (opt-in --tcp=9008)"| engine + subgraph engine [edge-engine daemon] + router[Router] --> sessions[SessionStore] + sessions --> account1["EdgeAccount (sess_A)"] + sessions --> account2["EdgeAccount (sess_B)"] + router --> context["EdgeContext (single)"] + end + context --> core[edge-core-js + currency plugins] +``` + +ASCII equivalent: + +``` +edge-cli ──HTTP──► engine.sock ──► edge-engine + │ + ├─ EdgeContext (one) + └─ accounts by sessionId + (sess_… → EdgeAccount) +``` + +A *profile* is a hash of `{ appId, directory, testMode, loginServer }`. +Distinct profiles get distinct run directories, so a tester engine and a +production engine can coexist. + +## Discovery + +Under `~/.edge-cli/run//` (files mode `0600`): + +| File | Purpose | +|------|---------| +| `engine.json` | Discovery / lock: pid, apiVersion, socketPath, tcpPort, appId, testMode, startedAt | +| `engine.sock` | Unix domain socket (always on) | +| `session.json` | Last `sessionId` written by the client | + +Example `engine.json`: + +```json +{ + "pid": 40123, + "apiVersion": "1.0.0", + "socketPath": "/Users/you/.edge-cli/run/8f3a.../engine.sock", + "tcpPort": null, + "appId": "", + "testMode": true, + "startedAt": "2026-08-06T04:55:00.000Z" +} +``` + +Client flow: read `engine.json` → `GET /engine/status` → on miss, spawn the +engine (unless `--no-spawn`), poll readiness up to 30 s, retry. + +```bash +# Manual status check over the socket +curl --unix-socket ~/.edge-cli/run//engine.sock \ + http://localhost/engine/status +``` + +## Sessions + +Successful login returns an opaque `sessionId` (`sess_` + base58 of 16 random +bytes). Account-scoped REST paths look like: + +``` +/account/{sessionId}/wallet/balance-map?walletId= +``` + +There is **no transport-level auth**. Core authenticates via password / PIN / +key / recovery; `sessionId` scopes everything after that. + +The client persists the latest id in `session.json` so commands chain without +re-typing. Override with `--session ` or `EDGE_CLI_SESSION`. + +**Auto-logout** mirrors the GUI: the engine reads `autoLogoutTimeInSeconds` +from the account’s synced `Settings.json` (default `3600`, `0` = disabled) and +logs the account out after that much idle time since the last REST call that +touched the session. `edge-cli touch` is an explicit keepalive. + +**Engine idle shutdown:** after ~5 minutes with no sessions, no subscribers and +no traffic, the engine closes the context, unlinks the socket / run file, and +exits. Configure with `--idle-timeout` (`0` = never). A live `subscribe` holds +it open — see [Subscribing to events](#subscribing-to-events). + +```bash +edge-cli -t login-with-password --username=alice --password='pass' # stores sessionId +edge-cli currency-wallets # uses persisted session +edge-cli engine-sessions +edge-cli touch +edge-cli logout +``` + +## CAPTCHA + +`usernameAvailable`, `createAccount`, and `loginWithPassword` can raise a +login-server CAPTCHA. The engine does **not** solve it. It returns: + +```json +{ + "error": { + "code": "CHALLENGE_REQUIRED", + "status": 403, + "message": "Login requires a CAPTCHA", + "details": { + "challengeId": "GTNMhqW1...", + "challengeUri": "https://login-tester.edge.app/api/v2/captcha/..." + } + } +} +``` + +Options: + +1. **CLI helper** — `--solve-captcha` on any login command headlessly + solves ALTCHA PoW at `challengeUri` and retries with `challengeId`. +2. **Manual** — open the URI in a browser, then re-run the command with + `--challenge-id ` (or pass `challengeId` in the REST body). +3. **Prefetch** — `edge-cli fetch-challenge` → `POST /fetch-challenge`. + +Automated tests use the same ALTCHA solver (see `src/cli/client/solveCaptcha.ts`). + +## Edge login (QR / barcode) + +`edge-cli request-edge-login` requests a pending Edge login and prints JSON the +approving device can use: + +```json +{ + "pendingId": "sess-pending_7Qk3...", + "lobbyId": "HbC9mVJ2xR4tN8pL", + "uri": "edge://edge/HbC9mVJ2xR4tN8pL", + "state": "pending" +} +``` + +Approve from another logged-in Edge device (Scan QR), or paste `uri` / +`lobbyId` via **Scan QR → Enter** (useful with Maestro on the iOS simulator). +Poll with `GET /pending-edge-login/{pendingId}` until `state` is `done` +(it then carries the session) or `error`. + +## Command shape + +Commands are not listed here. The full reference — every command paired with +the REST call it makes and the `edge-core-js` call behind it, with request and +response types and an example — is generated from the route declarations: + +**[docs/api/dist/index.html](./api/dist/index.html)** + +```bash +npm run docs:api # rebuild it +npm run docs:api:gates # check it still matches src/cli +``` + +Every command follows one shape: + +``` +edge-cli [global flags] [--flag=value ...] +``` + +| Form | Example | +|------|---------| +| Preferred | `--wallet-id=7o7i6` | +| Also accepted | `--wallet-id 7o7i6` | +| Optional boolean | `--paused` (presence means true) | +| Required boolean | `--paused=true` — a bare flag cannot say false | +| Repeatable | `--answer=rex --answer=oak` | +| Lists | comma-separated, no spaces: `--export-format=csv,qbo` | +| JSON | single-quoted: `--spend-info='{"tokenId":null}'` | + +Arguments are named. A command takes a bare positional only where the value is +a base58 identifier the engine issued — an object handle, a pending login — +because only those are safe as a URL path segment. A wallet id is base64 and a +username is free text, so both are flags. `edge-cli help ` prints the +exact usage for any of them, and that text is generated from the same source +as the reference. + +For the native asset, omit `--token-id` rather than passing the literal +`null`. An empty `--name=` is a usage error, as are unknown flags and extra +positionals. + +### Subscribing to events + +`edge-cli subscribe` holds a Server-Sent Events stream open and prints one JSON +object per line until you interrupt it. It runs concurrently with ordinary +one-shot commands, so a subscriber in one terminal watches what another +terminal does: + +```bash +# terminal 1 +edge-cli subscribe --type=session.created --type=session.expired + +# terminal 2 +edge-cli -t login-with-password --username=alice --password='pass' +edge-cli logout +``` + +A live subscription keeps the **engine** alive past its idle timeout — the +stream would otherwise die under the subscriber. It does **not** keep an +**account** logged in: the auto-logout timer still fires on schedule, and when +it does, subscriptions that depend on that account or one of its wallets are +closed with a `subscription.closed` frame. Context-level subscriptions survive, +because the `EdgeContext` outlives every account. + +`subscribe` exits `0` on Ctrl-C, `3` when a session ended the stream, and `7` +when the engine went away. + +### Exit codes + +| Code | Meaning | +|------|---------| +| `0` | Success | +| `1` | Generic failure | +| `2` | Usage / bad argv | +| `3` | Auth / session | +| `4` | Not found | +| `5` | Validation / funds | +| `6` | Network | +| `7` | Engine unavailable | + +## Source layout + +``` +src/cli/ + engine/ + index.ts # Daemon entry, argv, signals + makeCoreContext.ts # Plugin registration + makeEdgeContext + server.ts # HTTP handler; unix (+ optional TCP) listeners + router.ts # Method + path dispatch + sessions.ts # SessionStore + auto-logout ticker + idleShutdown.ts # Idle self-shutdown + discovery.ts # Profile hash, run-file, socket paths + errors.ts # EngineError + core → HTTP mapping + json.ts # Body parse / Uint8Array·Date·Map codec + resolve.ts # walletId prefix, tokenId parsing + events.ts # SSE hub + testerServers.ts # The six -tester hosts + routes/ # status, login, account, wallets, … + client/ + apiClient.ts # HTTP over socketPath or TCP + spawnEngine.ts # Auto-spawn + readiness poll + sessionFile.ts # Persisted sessionId + output.ts # JSON / table / plain + exit codes + commands/ # Argv → apiClient → output (no core imports) + index.ts # One-shot (+ REPL) front-end +``` + +## REST API + +Full method/path/body/error documentation is generated: +**[docs/api/dist/index.html](./api/dist/index.html)**, with an OpenAPI 3.1 +document beside it at `docs/api/dist/openapi.json`. The source of truth is +`docs/api/`; see [docs/api/README.md](./api/README.md). diff --git a/docs/GUI_PLUGINS_ARCHITECTURE.md b/docs/GUI_PLUGINS_ARCHITECTURE.md index 620c3d70f05..5df9196da35 100644 --- a/docs/GUI_PLUGINS_ARCHITECTURE.md +++ b/docs/GUI_PLUGINS_ARCHITECTURE.md @@ -67,6 +67,9 @@ interface FiatProviderSupportedRegions { ### 3. Plugin Configuration System +GUI fiat / gift-card provider credentials live in `pluginMaps.guiApiKeys` +(see [CONFIG_KEYS_ARCHITECTURE.md](./CONFIG_KEYS_ARCHITECTURE.md)). + #### Sell Plugin List (`sellPluginList.json`) Each payment method is configured with: diff --git a/docs/HMAC_SIGNING.md b/docs/HMAC_SIGNING.md new file mode 100644 index 00000000000..4d13527948c --- /dev/null +++ b/docs/HMAC_SIGNING.md @@ -0,0 +1,124 @@ +# HMAC signing for Edge APIs + +The GUI signs requests to the login server (via edge-core-js) and to the +info-server `GET /v1/infoRollup/:appId` route with HMAC-SHA256. Native release and beta +builds keep the HMAC secret out of the Metro bundle by embedding XOR-split +shards from `edgeKey.json` (gitignored). JavaScript-only debug builds can fall +back to `EDGE_API_KEY` / `EDGE_API_SECRET` in `keys.json`. + +This is the GUI-side contract. Core wiring is in +[edge-core-js `docs/api-signer.md`](https://github.com/EdgeApp/edge-core-js/blob/master/docs/api-signer.md). +Key file layout is in [CONFIG_KEYS_ARCHITECTURE.md](./CONFIG_KEYS_ARCHITECTURE.md). +appKeys layer matching lives in +[edge-info-server `docs/INFO_ROLLUP.md`](https://github.com/EdgeApp/edge-info-server/blob/master/docs/INFO_ROLLUP.md). + +## Native signer (`edgeKey.json`) + +`edgeKey.json` is `{ "apiKey": "", "apiSecret": "" }`. +`scripts/makeApiSigner.ts` runs from Gradle/Xcode generate tasks (and +`prepare.sh`) when that file exists. It XOR-shards the secret: + +1. Five random pads plus a stored remainder (`SHARD_COUNT = 6`). +2. A runtime pad of `sha256(bundleId)` (Android `applicationId` and iOS + `PRODUCT_BUNDLE_IDENTIFIER` must match). +3. The C sources reconstruct `secret = s0 ⊕ … ⊕ s5 ⊕ runtimePad`. + +Generated (gitignored) outputs: + +- `ios/EdgeApiSecret.c` + `ios/EdgeApiSecret.h` +- `android/app/src/main/cpp/edge_api_secret.c` + `edge_api_secret.h` + +Native modules (`ios/edge/EdgeApiSigner.m`, +`android/.../EdgeApiSignerModule.kt`) expose `signMessage` and `getApiKey`. +`src/util/edgeApiSigner.ts` wraps that module as an `EdgeApiSigner` whose +`signMessage(message)` returns `{ apiKey, signature }` (base64 HMAC-SHA256). +Release/beta generate tasks fail if `edgeKey.json` is missing. Debug may set +`EDGE_API_SIGNER_ALLOW_STUB=1` to compile a non-signing stub. + +The GUI passes that object into `MakeEdgeContext` as `apiSigner`. Core prefers +it over `apiKey` / `apiSecret` for login-server HMAC. + +## JavaScript fallback + +When the native module is absent or returns an unusable key (typical debug +without `edgeKey.json`), `src/util/hmacAuth.ts` signs with +`KEYS.EDGE_API_KEY` and `KEYS.EDGE_API_SECRET` from `keys.json`. Those values +must match a `login-api-keys` row on the login server and an +`info_keys.apiKeys[].key` on the info server. + +`makeNativeApiSigner()` is not used in that build; `MakeEdgeContext` is +called without `apiSigner`, and core falls back to the JS secret pair (or +legacy `Token {apiKey}` if there is no secret). + +## Two HMAC string formats + +Do not reuse one canonical string for both services. Same presented key and +secret; different signed UTF-8 string and headers. + +### Login server (core `loginFetchInner`) + +``` +{METHOD}\n/api{path}\n{BODY} +``` + +- `METHOD` is upper-case (`POST`, `GET`, …). +- Path is `/api` plus the login route (`/api/v2/login`, `/api/v2/login/create`, + …). Query string is included when present. +- `BODY` is the JSON body string, or empty when the method is GET or there is + no body. + +Header: + +``` +Authorization: HMAC {apiKey} {base64(hmac-sha256(secret, data))} +``` + +There is **no** timestamp and **no** `X-Timestamp` header. The login server +verifies this exact three-line string (`with-api-key.ts`). A missing secret +falls back to the legacy `Authorization: Token {apiKey}` header (still accepted +for some routes such as `messages`). + +When an attestation JWT is loaded via `EdgeContext.setAttestationToken`, core +also sends `x-attestation-token`. Login-server challenge rates may use that +token; a missing or invalid token is treated as unattested (the request still +proceeds). That fail-open behavior is **not** how signed infoRollup treats a +bad token. + +### Info-server `GET /v1/infoRollup/:appId` (GUI `keysServer.ts`) + +``` +{METHOD}\n{URI}\n{BODY}\n{TIMESTAMP} +``` + +- `METHOD` is `GET`. +- `URI` is `req.originalUrl` on the server (`/v1/infoRollup/{appId}?os=&osVersion=&appVersion=`, + including the `/v1` prefix). The client signs `/${fetchPath}` to match. +- `BODY` is empty. +- `TIMESTAMP` is Unix seconds as a decimal string, also sent as `X-Timestamp`. + +Headers: + +``` +Authorization: HMAC {apiKey} {base64(hmac-sha256(secret, data))} +X-Timestamp: {unixSeconds} +x-attestation-token: {ES256 JWT} # optional +``` + +A valid HMAC is not enough to receive hardware-gated keys. The info server +walks an ordered `layers` array; see the info-server INFO_ROLLUP doc. A +present-but-invalid attestation token is **HTTP 401** — the GUI must not treat +that as the unattested floor. + +Native `apiSigner.signMessage` is preferred when `EdgeApiSigner` is linked +(`keysServer.ts`); otherwise `signHmacAuthorization` in `hmacAuth.ts`. + +## Request coverage + +| Caller | Signed with | Endpoint | +|---------------------------|------------------------------------|----------------------------| +| Core `loginFetch` | Native `apiSigner` or JS apiSecret | login-server `/api/v2/*` | +| GUI `fetchRemoteKeys` | Native signer or `hmacAuth.ts` | info-server `GET /v1/infoRollup/:appId` | +| GUI `infoServer.ts` (rates, …) | not HMAC | other info-server routes | + +Core does not call infoRollup. The GUI does, then writes the `appKeys` overlay into +`KEYS` / `pluginMaps` through `keysStore`. diff --git a/docs/api/README.md b/docs/api/README.md new file mode 100644 index 00000000000..543a49c0eaa --- /dev/null +++ b/docs/api/README.md @@ -0,0 +1,138 @@ +# Edge CLI API docs + +The `edge-cli` command line and the `edge-engine` REST API, defined once and +rendered together. Each call is a single record holding both forms, so the CLI +usage and the HTTP request cannot drift apart in the documentation. + +```bash +npm run docs:api # build dist/index.html + dist/openapi.json +npm run docs:api:verify # check the docs still match src/cli +``` + +Open `docs/api/dist/index.html` in a browser. The command line comes first in +every entry, the REST call second, and each states the `edge-core-js` call it +fronts. + +**`dist/` is committed on purpose** so the reference can be read on GitHub and +linked to without a build step. Rebuild and commit it in the same change as any +route or command edit — `npm run docs:api:verify` will fail otherwise. + +## Naming + +Routes are named after the core call they front, kebab-cased, and the command +matches: `context.forgetAccount` becomes `POST /forget-account` and +`forget-account`. Parameters keep core's names. + +A path parameter is a base58 identifier, and nothing else — `sessionId`, +`objectId`, `pendingId`, `lobbyId`, `syncKey`. Base58 has no `/`, `?` or `#`, +so it survives a URL as written. A base64 wallet id or a free-text username +does not, so those are named arguments: the query for `GET`, the body for +`POST`. Where a path parameter is allowed it comes last, in the order the +command reads. Collection segments are singular, since each call acts on one. Only `GET` and `POST` are used, since core has no +HTTP verbs, and a core method returning `void` answers `204`. + +Endpoints with no core equivalent set `coreCall: null` and must explain +themselves in `coreNote` — the verifier enforces that. + +## Why generated, not hand-written + +The previous hand-maintained `docs/EDGE_CLI_API.md` drifted badly: response +shapes that no route returned, status codes off by a category, body fields +under the wrong name, and a documented `confirm=true` guard on account deletion +that the engine never implemented. None of that is visible by reading either +the doc or the code alone — only by diffing them. + +`scripts/verifyApiDocs.ts` does that diff. It reads `router.add(…)` out of +`src/cli/engine/routes/` and `command(…)` out of `src/cli/commands/`, then +asserts the documentation covers exactly that surface. Run it in CI and adding +a route without documenting it fails the build. + +What it checks today: + +- every registered route is documented exactly once, and nothing is documented + that is not registered +- every cited `edge-cli` command exists, and every registered command is cited + by at least one endpoint +- each `usage` string starts with its own command name +- every endpoint names a real `edge-core-js` member, or sets `coreCall: null` + with a `coreNote` saying why +- error codes come from the shared catalogue +- `204` endpoints declare no body, `200` endpoints declare a schema or prose +- every `{pathParam}` in a path is declared + +What it cannot check yet: that a response *schema* matches what the engine +really returns. See "Runtime validation" below. + +## Layout + +``` +docs/api/ + schema.ts the schema DSL (s.object, s.ref, …) + types.ts what an Endpoint is + shared.ts shapes reused across routes, error + exit-code tables + endpoints/ one file per file in src/cli/engine/routes/ + index.ts group order, which is also render order + dist/ generated — do not edit +scripts/ + buildApiDocs.ts -> dist/index.html and dist/openapi.json + verifyApiDocs.ts docs vs. code drift check +``` + +`endpoints/` mirrors `src/cli/engine/routes/` deliberately: when you touch a +route file, the doc file to update sits at the same name. + +## Adding an endpoint + +Add the route in `src/cli/engine/routes/`, then add the record beside it: + +```ts +endpoint({ + id: 'balanceMap', // anchor + OpenAPI operationId + summary: 'Balances for every asset in the wallet', + description: 'Optional prose. Markdown.', + method: 'GET', + path: '/account/{sessionId}/wallets/{walletId}/balance-map', + source: 'src/cli/engine/routes/wallets.ts', + coreCall: 'wallet.balanceMap', // or null + coreNote + cli: [ + { + command: 'balance-map', // must match command(…) in src/cli/commands/ + usage: 'balance-map [--token-id=]', + flags: [{ flag: '--token-id=', maps: 'tokenId', target: 'client' }], + example: 'edge-cli balance-map abc123' + } + ], + pathParams: [sessionId, walletId], + success: { status: 200, schema: s.object([f('balances', s.array(s.ref('Balance')))]) }, + errors: ['WALLET_NOT_FOUND', 'AMBIGUOUS_WALLET_ID'] +}) +``` + +Then `npm run docs:api:verify`. + +Conventions worth keeping: + +- `cli: []` means REST-only. The verifier will not let a documented command + name be wrong, but it cannot yet prove a route has *no* command — check by + hand before writing `[]`. +- Reuse `s.ref('…')` for anything in `shared.ts` rather than restating fields. + Nine schemas already cover most of the surface. +- Put anything a caller would get wrong from the schema alone in `notes` — + surprising defaults, fields that look symmetric but are not, calls that write + when they look like reads. +- Two commands may share a route (`spend` / `spend-max`), and one command may + cover two routes (`balance`, `spam-filter`). Both are fine: list every + binding on the route it actually calls. + +## Runtime validation + +`schema.ts` is deliberately close in shape to `cleaners`, which the repo +already depends on. The engine does not currently validate its own responses — +it returns plain objects assembled from edge-core-js types — so there was no +existing runtime schema to point these docs at. + +The next step, if this format earns its keep, is an `asSchema()` that compiles +a `Schema` into a cleaner and a test that drives a tester-server session +through every endpoint, asserting real responses satisfy the documented shape. +That closes the last gap: today the docs are provably complete, but only the +*shapes* are still trusted rather than verified. diff --git a/docs/api/dist/index.html b/docs/api/dist/index.html new file mode 100644 index 00000000000..51bf0e6333b --- /dev/null +++ b/docs/api/dist/index.html @@ -0,0 +1,8207 @@ + + + + +Edge CLI API + + +
+ +
+

Overview

+

Every entry is one API call shown twice: as an edge-cli command, then as the JSON REST request that command sends. Both are generated from a single declaration in src/cli/engine/routes/, so the two forms cannot drift apart.

+

Routes are named after the edge-core-js call they front, kebab-cased: context.forgetAccount becomes POST /forget-account, and the command is forget-account. Parameters carry core's own names. Every entry states its core call, or says why there is none. Only GET and POST appear — core has no HTTP verbs, so reads are GET and everything else is POST.

+

The edge-cli client is a thin one-shot process. A long-lived edge-engine daemon owns the EdgeContext and every logged-in account, serving this API over a Unix socket at ~/.edge-cli/run/<profile>/engine.sock, plus loopback TCP when started with --tcp=9008.

+

There is no transport authentication. The socket is owner-only (0600) and TCP is loopback, so anything that can reach the engine can act as every logged-in account.

+

+Ephemeral object handles. In edge-core-js a method-bearing value is identified by object reference — you call wallet.signTx(tx) on the very tx that makeSpend returned. That does not survive HTTP, so the engine parks such values under an objectId with a 5 minute TTL and later steps name the id. Reads do not extend the TTL; only a step that updates the value does. Finishing a workflow, or POST …/objects/{objectId}/delete, releases the handle early. Expired handles return 410 OBJECT_EXPIRED.

+

Serialization. Uint8Array becomes base64, Date becomes an ISO-8601 string, Map becomes an object, amounts are always decimal strings, and EdgeTokenId is JSON null for a native asset.

+

Testing. Always pass -t / --test to point at the *-tester.edge.app servers.

+
+

Engine

+

The edge-engine daemon itself. None of these have an edge-core-js equivalent — they describe the process — and none need a session.

+
+

Lifecycle

+

Lifecycle and configuration of the edge-engine daemon. None of these have an edge-core-js equivalent — they describe the daemon itself — and none need a session.

+
+
+
+

Engine liveness and summary.

+
engine-statussrc/cli/engine/routes/status.ts
+
+

coreEngine lifecycle; the daemon is not part of the core API.

+

The readiness probe the client polls after auto-spawning the engine.

+
+
+

Command line

+
engine-status
+ + +
+

REST

+

GET/engine/status

+ + + + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/engine/status'
+
+
+

Response 200

+

idleShutdownAt is null while a session or a subscription holds the engine open, and tcpPort is null unless started with --tcp.

+
+
Response body
+
{
+  pid: number
+  apiVersion: string
+  uptimeSeconds: number
+  sessionCount: number
+  testMode: boolean
+  idleShutdownAt: string | null
+  tcpPort: number | null
+  socketPath: string
+  locale: string
+  decimalSeparator: string
+  groupingSeparator: string
+}
+
Example
{
+  "pid": 0,
+  "apiVersion": "string",
+  "uptimeSeconds": 1,
+  "sessionCount": 1,
+  "testMode": true,
+  "idleShutdownAt": "2026-09-02T16:35:00.000Z",
+  "tcpPort": 0,
+  "socketPath": "string",
+  "locale": "string",
+  "decimalSeparator": "string",
+  "groupingSeparator": "string"
+}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
pidnumberThe daemon process, for kill when it will not stop.
apiVersionstringThe API this engine speaks. A client refusing to talk to an older engine checks this.
uptimeSecondsnumberHow long the daemon has been running.
sessionCountnumberLogged-in accounts held open right now.
testModebooleanTrue when pointed at the tester fleet.
idleShutdownAtstring | nullWhen the engine will exit for want of work. Null while a session or a subscription is holding it open, and null when the timeout is disabled.
tcpPortnumber | nullThe loopback port, null unless started with --tcp.
socketPathstringUnix socket the CLI connects to.
localestringLanguage tag the engine resolved at boot.
decimalSeparatorstringDecimal mark for that locale.
groupingSeparatorstringThousands mark for that locale.
+
+
Errors

503ENGINE_SHUTTING_DOWN

+
+ +
+
+

Configured context options.

+
engine-configsrc/cli/engine/routes/status.ts
+
+

coreReflects the EdgeContextOptions the engine supplied at startup.

+

What the engine passed to makeEdgeContext. Contains no secrets. Use it to assert tester hosts before a test run.

+
+
+

Command line

+
engine-config
+ + +
+

REST

+

GET/engine/config

+ + + + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/engine/config'
+
+
+

Response 200

+
+
Response body
+
{
+  appId: string
+  testMode: boolean
+  directory: string
+  servers: {
+    [keys: string]: string | string[]
+  }
+  plugins: string[]
+}
+
Example
{
+  "appId": "FS8xJ2kQ…",
+  "testMode": true,
+  "directory": "string",
+  "servers": {},
+  "plugins": [
+    "string"
+  ]
+}
+ + + + + + + + + + + + + + + + + + + + + + + + +
appIdstringApplication ID the engine was started with.
testModebooleanTrue when the engine is pointed at the tester fleet.
directorystringWorking directory holding the core data.
servers{ [keys: string]: string | string[]; }The URLs this engine talks to, keyed by role. syncServer is a list, since core rotates across the sync fleet.
pluginsstring[]Plugin IDs the engine loaded, sorted.
+
+ +
+

Notes

  • Outside -t / --test, servers is an empty object — core is using its built-in production defaults, so there is nothing to echo back.
+
+
+

Stop the engine.

+
engine-stopsrc/cli/engine/routes/status.ts
+
+

coreEngine lifecycle. Internally calls context.close().

+

Logs out every session, closes the context, unlinks the socket and run-file, then exits. The engine answers before it starts tearing down, so a response is not proof the process is gone.

+
+
+

Command line

+
engine-stop
+ + +
+

REST

+

POST/engine/stop

+ + + + +
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  'http://localhost/engine/stop'
+
+
+

Response 200

+
+
Response body
+
{
+  ok: boolean
+}
+
Example
{
+  "ok": true
+}
+ + + + +
okbooleanAlways true; a failure arrives as an error envelope.
+
+ +
+

Notes

  • In-flight callers may see 503 ENGINE_SHUTTING_DOWN once teardown starts.
+

Event stream

+

A Server-Sent Events feed of engine activity, served outside the router because the response never ends.

+
+
+
+

Subscribe to engine events.

+
subscribesrc/cli/engine/routes/events.ts
+
+

coreEngine-side fan-out; core.log frames carry core's onLog output.

+

Holds a Server-Sent Events stream open until the caller disconnects or the engine closes it. Runs concurrently with one-shot calls, so a subscriber in one terminal watches what another terminal does.

+

A live subscription holds the engine open past its idle timeout. It does not hold an account logged in: the auto-logout timer still fires, and closes any subscription scoped to that account or one of its wallets. Context-scoped subscriptions survive, because the context outlives every account.

+
+
+

Command line

+
subscribe [--type=<value>]
+
Client-only flags
--typeoptionalClient-side filter; the engine always sends everything the scope allows.
+

Prints newline-delimited JSON and runs until interrupted. Exits 0 on SIGINT, 3 when a session ended the stream, 7 when the engine went away.

+
+
+

REST

+

GET/engine/events

+ + + + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/engine/events'
+
+
+

Response 200

+

One frame per event, as event: then data: lines.

+
+
Response body
+
{
+  type: string
+  data: unknown
+}
+
Example
{
+  "type": "string",
+  "data": {}
+}
+ + + + + + + + + +
typestringThe event name.
dataunknownPayload, shaped by the event type.
+
+ +
+

Notes

  • Frame types: core.log, session.created, session.expired, engine.shutdown, and subscription.closed when the engine ends it.
  • sessionId in event payloads is truncated to its first 10 characters.
  • A client more than 1 MiB behind is disconnected rather than buffered.
  • Served directly by the HTTP handler rather than through the router, because the response never ends.
+

Context

+

Calls on the shared EdgeContext: device state, username queries, and every way of logging in. None of them need a session, because a session is what they produce.

+
+

Device and usernames

+

Calls on the shared EdgeContext: local device state and login-server queries that do not need a session.

+
+
+
+

List local users on this device.

+
local-userssrc/cli/engine/routes/context.ts
+
+

corecontext.localUsers

+ +
+

Command line

+
local-users
+ + +
+

REST

+

GET/local-users

+ + + + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/local-users'
+
+
+

Response 200

+

Everything context.localUsers reports, including which login methods each user has enabled on this device.

+
+
Response body
+
{
+  localUsers: unknown[]
+}
+
Example
{
+  "localUsers": [
+    {}
+  ]
+}
+ + + + +
localUsersunknown[]EdgeUserInfo[]: one entry per account cached on this device.
+
+ +
+ +
+
+

Forget an account on this device.

+
forget-accountsrc/cli/engine/routes/context.ts
+
+

corecontext.forgetAccount

+

Removes locally cached credentials. The remote account is untouched.

+
+
+

Command line

+
forget-account --root-login-id=<rootLoginId>
+ + +
+

REST

+

POST/forget-account

+ + + +
+
Request body
+
{
+  rootLoginId: string
+}
+
Example
{
+  "rootLoginId": "FS8xJ2kQ…"
+}
+ + + + +
rootLoginIdstringCore takes a rootLoginId. A username is also accepted and resolved against localUsers first, so callers need not hash it.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"rootLoginId":"FS8xJ2kQ…"}' \
+  'http://localhost/forget-account'
+
+
+

Response 204

+

No body.

+
Errors

404USER_NOT_FOUND 400BAD_REQUEST

+
+ +
+
+

Check whether a username is free.

+
username-availablesrc/cli/engine/routes/context.ts
+
+

corecontext.usernameAvailable

+ +
+

Command line

+
username-available --username=<username> [--challenge-id=<challengeId>]
+ + +
+

REST

+

GET/username-available

+ +
+
Query
+
{
+  username: string
+  challengeId?: string
+}
+
Example
{
+  "username": "string",
+  "challengeId": "FS8xJ2kQ…"
+}
+ + + + + + + + + +
usernamestringThe name to check.
challengeIdstring optionalSupply after solving a CAPTCHA to retry the same check.
+
+ + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/username-available?username=…'
+
+
+

Response 200

+
+
Response body
+
{
+  username: string
+  available: boolean
+}
+
Example
{
+  "username": "string",
+  "available": true
+}
+ + + + + + + + + +
usernamestringThe name that was checked, echoed back.
availablebooleanTrue when nobody holds this name. It is not reserved by asking.
+
+
Errors

400USERNAME_ERROR 403CHALLENGE_REQUIRED 503NETWORK_ERROR

+
+ +
+
+

Normalize a username.

+
fix-usernamesrc/cli/engine/routes/context.ts
+
+

corecontext.fixUsername

+

Applies the same rules the login server does, so a caller can show the user what their name will actually be before creating an account.

+
+
+

Command line

+
fix-username --username=<username>
+ + +
+

REST

+

GET/fix-username

+ +
+
Query
+
{
+  username: string
+}
+
Example
{
+  "username": "string"
+}
+ + + + +
usernamestringThe name to normalize.
+
+ + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/fix-username?username=…'
+
+
+

Response 200

+
+
Response body
+
{
+  username: string
+}
+
Example
{
+  "username": "string"
+}
+ + + + +
usernamestringThe normalized value. The input is not echoed.
+
+ +
+ +
+
+

Score a candidate password.

+
check-password-rulessrc/cli/engine/routes/context.ts
+
+

corecontext.checkPasswordRules

+ +
+

Command line

+
check-password-rules --password=<password>
+ + +
+

REST

+

GET/check-password-rules

+ +
+
Query
+
{
+  password: string
+}
+
Example
{
+  "password": "string"
+}
+ + + + +
passwordstringThe candidate password to score.
+
+ + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/check-password-rules?password=…'
+
+
+

Response 200

+

EdgePasswordRules from core: passed, tooShort, noNumber, noLowerCase, noUpperCase, secondsToCrack.

+
unknown
+ +
+

Notes

  • Send it with curl --get --data-urlencode rather than putting it in a shell-visible URL.
+
+
+

Fetch login-server messages for every local user.

+
fetch-login-messagessrc/cli/engine/routes/context.ts
+
+

corecontext.fetchLoginMessages

+ +
+

Command line

+
fetch-login-messages
+ + +
+

REST

+

GET/fetch-login-messages

+ + + + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/fetch-login-messages'
+
+
+

Response 200

+

EdgeLoginMessages from core, keyed by loginId; each value carries otpResetPending and pendingVouchers.

+
unknown
+
Errors

503NETWORK_ERROR

+
+ +
+
+

Request a 2FA reset.

+
request-otp-resetsrc/cli/engine/routes/context.ts
+
+

corecontext.requestOtpReset

+

Starts the timed reset a user falls back on after losing their authenticator.

+
+
+

Command line

+
request-otp-reset --username=<username> --otp-reset-token=<otpResetToken>
+ + +
+

REST

+

POST/request-otp-reset

+ + + +
+
Request body
+
{
+  username: string
+  otpResetToken: string
+}
+
Example
{
+  "username": "string",
+  "otpResetToken": "string"
+}
+ + + + + + + + + +
usernamestringWhose 2FA to reset.
otpResetTokenstringFrom details.resetToken on an OTP_REQUIRED error.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"username":"string","otpResetToken":"string"}' \
+  'http://localhost/request-otp-reset'
+
+
+

Response 200

+

When the reset completes if nobody cancels it.

+
+
Response body
+
{
+  resetDate: string
+}
+
Example
{
+  "resetDate": "2026-09-02T16:35:00.000Z"
+}
+ + + + +
resetDatestringWhen 2FA will actually come off. The login server enforces a waiting period so the real owner has time to cancel.
+
+
Errors

400USERNAME_ERROR 400BAD_REQUEST 503NETWORK_ERROR

+
+ +
+
+

Fetch a user’s recovery questions.

+
fetch-recovery-questionssrc/cli/engine/routes/context.ts
+
+

corecontext.fetchRecovery2Questions Our surface drops the 2 from the path, command and recoveryKey parameter; a future Recovery1 would be suffixed V1.

Differs from core:

  • recoveryKey — Core calls it recovery2Key. The 2 is dropped throughout.
+ +
+

Command line

+
fetch-recovery-questions --recovery-key=<recoveryKey> --username=<username>
+ + +
+

REST

+

GET/fetch-recovery-questions

+ +
+
Query
+
{
+  recoveryKey: string
+  username: string
+}
+
Example
{
+  "recoveryKey": "string",
+  "username": "string"
+}
+ + + + + + + + + +
recoveryKeystringFrom change-recovery, stored by the user out of band.
usernamestringWhose questions to fetch.
+
+ + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/fetch-recovery-questions?recoveryKey=…&username=…'
+
+
+

Response 200

+
+
Response body
+
{
+  questions: string[]
+}
+
Example
{
+  "questions": [
+    "string"
+  ]
+}
+ + + + +
questionsstring[]The questions in the order login-with-recovery expects the answers.
+
+
Errors

400USERNAME_ERROR 503NETWORK_ERROR

+
+ +
+
+

Pre-fetch a CAPTCHA challenge.

+
fetch-challengesrc/cli/engine/routes/context.ts
+
+

corecontext.fetchChallenge

+

Lets a client solve a challenge before it hits 403 CHALLENGE_REQUIRED mid-flow.

+
+
+

Command line

+
fetch-challenge
+ + +
+

REST

+

POST/fetch-challenge

+ + + + +
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  'http://localhost/fetch-challenge'
+
+
+

Response 200

+

challengeUri is absent when the server considers the challenge already satisfied.

+
+
Response body
+
{
+  challengeId: string
+  challengeUri?: string
+}
+
Example
{
+  "challengeId": "FS8xJ2kQ…",
+  "challengeUri": "string"
+}
+ + + + + + + + + +
challengeIdstringPass to the call that demanded a challenge once the user has solved it.
challengeUristring optionalWhere to send the user to solve the CAPTCHA. Absent when the server issued a challenge that needs no interaction.
+
+
Errors

503NETWORK_ERROR

+
+ +
+
+

List plugin ids usable for wallet creation.

+
currency-configssrc/cli/engine/routes/context.ts
+
+

coreEngine view of the enabled plugin set; core exposes account.currencyConfig per plugin instead.

+

Currency and accountbased plugins only — swap plugins are excluded.

+
+
+

Command line

+
currency-configs
+ + +
+

REST

+

GET/currency-configs

+ + + + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/currency-configs'
+
+
+

Response 200

+
+
Response body
+
{
+  pluginIds: string[]
+}
+
Example
{
+  "pluginIds": [
+    "string"
+  ]
+}
+ + + + +
pluginIdsstring[]Currency plugins this engine loaded.
+
+ +
+ +

Login methods

+

Every successful login returns a Session and registers it in the engine, so later calls need only the sessionId. The CLI writes that id to session.json automatically.

+
+
+
+

Log in with a password.

+
login-with-passwordsrc/cli/engine/routes/login.ts
+
+

corecontext.loginWithPassword

+ +
+

Command line

+
login-with-password [--otp=<otp>] [--otp-key=<otpKey>] [--challenge-id=<challengeId>] --username=<username> --password=<password>
+ + +
+

REST

+

POST/login-with-password

+ + + +
+
Request body
+
{
+  otp?: string
+  otpKey?: string
+  challengeId?: string
+  username: string
+  password: string
+}
+
Example
{
+  "otp": "string",
+  "otpKey": "string",
+  "challengeId": "FS8xJ2kQ…",
+  "username": "string",
+  "password": "string"
+}
+ + + + + + + + + + + + + + + + + + + + + + + + +
otpstring optionalA current 2FA code.
otpKeystring optionalThe 2FA secret itself, instead of a code.
challengeIdstring optionalSupply after solving a CAPTCHA to retry the same request.
usernamestringThe account name.
passwordstringThe account password.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"otp":"string","otpKey":"string","challengeId":"FS8xJ2kQ…","username":"string","password":"string"}' \
+  'http://localhost/login-with-password'
+
+
+

Response 200

+

A session with loginMethod: "password".

+
+
Response body
+
{
+  sessionId: string
+  username?: string
+  rootLoginId: string
+  loginMethod: "password" | "pin" | "key" | "recovery" | "edge" | "create"
+  autoLogoutSeconds: number
+  expiresAt: string | null
+  lastActivityAt: string
+  createdAt: string
+}
+
Example
{
+  "sessionId": "FS8xJ2kQ…",
+  "username": "string",
+  "rootLoginId": "FS8xJ2kQ…",
+  "loginMethod": "<\"password\">",
+  "autoLogoutSeconds": 1,
+  "expiresAt": "2026-09-02T16:35:00.000Z",
+  "lastActivityAt": "2026-09-02T16:35:00.000Z",
+  "createdAt": "2026-09-02T16:35:00.000Z"
+}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
sessionIdstringIdentifies this login. Every account-scoped call carries it, and the CLI stores the most recent one so commands can omit it.
usernamestring optionalAbsent for a light account, which has no username.
rootLoginIdstringThe account root, stable across appIds. Two sessions sharing it are the same account.
loginMethod"password" | "pin" | "key" | "recovery" | "edge" | "create"How this session was established.
autoLogoutSecondsnumberIdle time before the engine logs the account out. 0 disables it.
expiresAtstring | nullWhen auto-logout will fire, or null when it is disabled.
lastActivityAtstringLast call on this session, which is what auto-logout measures from.
createdAtstringWhen the login completed.
+
+
Errors

401PASSWORD_ERROR 400USERNAME_ERROR 401OTP_REQUIRED 403CHALLENGE_REQUIRED 503NETWORK_ERROR

+
+

Notes

  • With --solve-captcha the client solves a CHALLENGE_REQUIRED response headlessly (ALTCHA proof-of-work) and retries once.
+
+
+

Log in with a device PIN.

+
login-with-pinsrc/cli/engine/routes/login.ts
+
+

corecontext.loginWithPIN

+

Only works on a device that has already saved a PIN for the account.

+
+
+

Command line

+
login-with-pin [--otp=<otp>] [--otp-key=<otpKey>] [--challenge-id=<challengeId>] --username-or-login-id=<usernameOrLoginId> --pin=<pin> [--use-login-id]
+ + +
+

REST

+

POST/login-with-pin

+ + + +
+
Request body
+
{
+  otp?: string
+  otpKey?: string
+  challengeId?: string
+  usernameOrLoginId: string
+  pin: string
+  useLoginId?: boolean
+}
+
Example
{
+  "otp": "string",
+  "otpKey": "string",
+  "challengeId": "FS8xJ2kQ…",
+  "usernameOrLoginId": "FS8xJ2kQ…",
+  "pin": "string",
+  "useLoginId": true
+}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
otpstring optionalA current 2FA code.
otpKeystring optionalThe 2FA secret itself, instead of a code.
challengeIdstring optionalSupply after solving a CAPTCHA to retry the same request.
usernameOrLoginIdstringA username, or a login id.
pinstringThe device PIN.
useLoginIdboolean optionalTreat the value as a login id.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"otp":"string","otpKey":"string","challengeId":"FS8xJ2kQ…","usernameOrLoginId":"FS8xJ2kQ…","pin":"string","useLoginId":true}' \
+  'http://localhost/login-with-pin'
+
+
+

Response 200

+

A session with loginMethod: "pin".

+
+
Response body
+
{
+  sessionId: string
+  username?: string
+  rootLoginId: string
+  loginMethod: "password" | "pin" | "key" | "recovery" | "edge" | "create"
+  autoLogoutSeconds: number
+  expiresAt: string | null
+  lastActivityAt: string
+  createdAt: string
+}
+
Example
{
+  "sessionId": "FS8xJ2kQ…",
+  "username": "string",
+  "rootLoginId": "FS8xJ2kQ…",
+  "loginMethod": "<\"password\">",
+  "autoLogoutSeconds": 1,
+  "expiresAt": "2026-09-02T16:35:00.000Z",
+  "lastActivityAt": "2026-09-02T16:35:00.000Z",
+  "createdAt": "2026-09-02T16:35:00.000Z"
+}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
sessionIdstringIdentifies this login. Every account-scoped call carries it, and the CLI stores the most recent one so commands can omit it.
usernamestring optionalAbsent for a light account, which has no username.
rootLoginIdstringThe account root, stable across appIds. Two sessions sharing it are the same account.
loginMethod"password" | "pin" | "key" | "recovery" | "edge" | "create"How this session was established.
autoLogoutSecondsnumberIdle time before the engine logs the account out. 0 disables it.
expiresAtstring | nullWhen auto-logout will fire, or null when it is disabled.
lastActivityAtstringLast call on this session, which is what auto-logout measures from.
createdAtstringWhen the login completed.
+
+
Errors

401PASSWORD_ERROR 403PIN_DISABLED 400USERNAME_ERROR 400BAD_REQUEST 503NETWORK_ERROR

+
+ +
+
+

Log in with an account login key.

+
login-with-keysrc/cli/engine/routes/login.ts
+
+

corecontext.loginWithKey

+

The key comes from get-login-key on an already-authenticated session.

+
+
+

Command line

+
login-with-key [--otp=<otp>] [--otp-key=<otpKey>] [--challenge-id=<challengeId>] --username-or-login-id=<usernameOrLoginId> --login-key=<loginKey> [--use-login-id]
+ + +
+

REST

+

POST/login-with-key

+ + + +
+
Request body
+
{
+  otp?: string
+  otpKey?: string
+  challengeId?: string
+  usernameOrLoginId: string
+  loginKey: string
+  useLoginId?: boolean
+}
+
Example
{
+  "otp": "string",
+  "otpKey": "string",
+  "challengeId": "FS8xJ2kQ…",
+  "usernameOrLoginId": "FS8xJ2kQ…",
+  "loginKey": "string",
+  "useLoginId": true
+}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
otpstring optionalA current 2FA code.
otpKeystring optionalThe 2FA secret itself, instead of a code.
challengeIdstring optionalSupply after solving a CAPTCHA to retry the same request.
usernameOrLoginIdstringA username, or a login id.
loginKeystringFrom get-login-key.
useLoginIdboolean optionalTreat the value as a login id.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"otp":"string","otpKey":"string","challengeId":"FS8xJ2kQ…","usernameOrLoginId":"FS8xJ2kQ…","loginKey":"string","useLoginId":true}' \
+  'http://localhost/login-with-key'
+
+
+

Response 200

+

A session with loginMethod: "key".

+
+
Response body
+
{
+  sessionId: string
+  username?: string
+  rootLoginId: string
+  loginMethod: "password" | "pin" | "key" | "recovery" | "edge" | "create"
+  autoLogoutSeconds: number
+  expiresAt: string | null
+  lastActivityAt: string
+  createdAt: string
+}
+
Example
{
+  "sessionId": "FS8xJ2kQ…",
+  "username": "string",
+  "rootLoginId": "FS8xJ2kQ…",
+  "loginMethod": "<\"password\">",
+  "autoLogoutSeconds": 1,
+  "expiresAt": "2026-09-02T16:35:00.000Z",
+  "lastActivityAt": "2026-09-02T16:35:00.000Z",
+  "createdAt": "2026-09-02T16:35:00.000Z"
+}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
sessionIdstringIdentifies this login. Every account-scoped call carries it, and the CLI stores the most recent one so commands can omit it.
usernamestring optionalAbsent for a light account, which has no username.
rootLoginIdstringThe account root, stable across appIds. Two sessions sharing it are the same account.
loginMethod"password" | "pin" | "key" | "recovery" | "edge" | "create"How this session was established.
autoLogoutSecondsnumberIdle time before the engine logs the account out. 0 disables it.
expiresAtstring | nullWhen auto-logout will fire, or null when it is disabled.
lastActivityAtstringLast call on this session, which is what auto-logout measures from.
createdAtstringWhen the login completed.
+
+
Errors

401PASSWORD_ERROR 400USERNAME_ERROR 503NETWORK_ERROR

+
+ +
+
+

Log in with recovery answers.

+
login-with-recoverysrc/cli/engine/routes/login.ts
+
+

corecontext.loginWithRecovery2 Our surface drops the 2 from core's recovery2 naming, and calls the key recoveryKey to match what change-recovery returns.

Differs from core:

  • recoveryKey — Core calls it recovery2Key. The 2 is dropped throughout.
+

Needs both the recovery key and the answers; neither works alone.

+
+
+

Command line

+
login-with-recovery [--otp=<otp>] [--otp-key=<otpKey>] [--challenge-id=<challengeId>] --recovery-key=<recoveryKey> --username=<username> --answer=<answers> …
+ + +
+

REST

+

POST/login-with-recovery

+ + + +
+
Request body
+
{
+  otp?: string
+  otpKey?: string
+  challengeId?: string
+  recoveryKey: string
+  username: string
+  answers: string[]
+}
+
Example
{
+  "otp": "string",
+  "otpKey": "string",
+  "challengeId": "FS8xJ2kQ…",
+  "recoveryKey": "string",
+  "username": "string",
+  "answers": [
+    "string"
+  ]
+}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
otpstring optionalA current 2FA code.
otpKeystring optionalThe 2FA secret itself, instead of a code.
challengeIdstring optionalSupply after solving a CAPTCHA to retry the same request.
recoveryKeystringFrom change-recovery.
usernamestringThe account name.
answersstring[]In the same order as the questions.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"otp":"string","otpKey":"string","challengeId":"FS8xJ2kQ…","recoveryKey":"string","username":"string","answers":["string"]}' \
+  'http://localhost/login-with-recovery'
+
+
+

Response 200

+

A session with loginMethod: "recovery".

+
+
Response body
+
{
+  sessionId: string
+  username?: string
+  rootLoginId: string
+  loginMethod: "password" | "pin" | "key" | "recovery" | "edge" | "create"
+  autoLogoutSeconds: number
+  expiresAt: string | null
+  lastActivityAt: string
+  createdAt: string
+}
+
Example
{
+  "sessionId": "FS8xJ2kQ…",
+  "username": "string",
+  "rootLoginId": "FS8xJ2kQ…",
+  "loginMethod": "<\"password\">",
+  "autoLogoutSeconds": 1,
+  "expiresAt": "2026-09-02T16:35:00.000Z",
+  "lastActivityAt": "2026-09-02T16:35:00.000Z",
+  "createdAt": "2026-09-02T16:35:00.000Z"
+}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
sessionIdstringIdentifies this login. Every account-scoped call carries it, and the CLI stores the most recent one so commands can omit it.
usernamestring optionalAbsent for a light account, which has no username.
rootLoginIdstringThe account root, stable across appIds. Two sessions sharing it are the same account.
loginMethod"password" | "pin" | "key" | "recovery" | "edge" | "create"How this session was established.
autoLogoutSecondsnumberIdle time before the engine logs the account out. 0 disables it.
expiresAtstring | nullWhen auto-logout will fire, or null when it is disabled.
lastActivityAtstringLast call on this session, which is what auto-logout measures from.
createdAtstringWhen the login completed.
+
+
Errors

401PASSWORD_ERROR 400USERNAME_ERROR 503NETWORK_ERROR

+
+ +
+
+

Create an account.

+
create-accountsrc/cli/engine/routes/login.ts
+
+

corecontext.createAccount

+

Every credential is optional over REST: omitting all three creates a light account with no username.

+
+
+

Command line

+
create-account [--otp=<otp>] [--otp-key=<otpKey>] [--challenge-id=<challengeId>] [--username=<username>] [--password=<password>] [--pin=<pin>]
+ + +
+

REST

+

POST/create-account

+ + + +
+
Request body
+
{
+  otp?: string
+  otpKey?: string
+  challengeId?: string
+  username?: string
+  password?: string
+  pin?: string
+}
+
Example
{
+  "otp": "string",
+  "otpKey": "string",
+  "challengeId": "FS8xJ2kQ…",
+  "username": "string",
+  "password": "string",
+  "pin": "string"
+}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
otpstring optionalA current 2FA code.
otpKeystring optionalThe 2FA secret itself, instead of a code.
challengeIdstring optionalSupply after solving a CAPTCHA to retry the same request.
usernamestring optionalThe name to claim.
passwordstring optionalThe account password.
pinstring optionalA device PIN to save.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"otp":"string","otpKey":"string","challengeId":"FS8xJ2kQ…","username":"string","password":"string","pin":"string"}' \
+  'http://localhost/create-account'
+
+
+

Response 200

+

A session with loginMethod: "create".

+
+
Response body
+
{
+  sessionId: string
+  username?: string
+  rootLoginId: string
+  loginMethod: "password" | "pin" | "key" | "recovery" | "edge" | "create"
+  autoLogoutSeconds: number
+  expiresAt: string | null
+  lastActivityAt: string
+  createdAt: string
+}
+
Example
{
+  "sessionId": "FS8xJ2kQ…",
+  "username": "string",
+  "rootLoginId": "FS8xJ2kQ…",
+  "loginMethod": "<\"password\">",
+  "autoLogoutSeconds": 1,
+  "expiresAt": "2026-09-02T16:35:00.000Z",
+  "lastActivityAt": "2026-09-02T16:35:00.000Z",
+  "createdAt": "2026-09-02T16:35:00.000Z"
+}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
sessionIdstringIdentifies this login. Every account-scoped call carries it, and the CLI stores the most recent one so commands can omit it.
usernamestring optionalAbsent for a light account, which has no username.
rootLoginIdstringThe account root, stable across appIds. Two sessions sharing it are the same account.
loginMethod"password" | "pin" | "key" | "recovery" | "edge" | "create"How this session was established.
autoLogoutSecondsnumberIdle time before the engine logs the account out. 0 disables it.
expiresAtstring | nullWhen auto-logout will fire, or null when it is disabled.
lastActivityAtstringLast call on this session, which is what auto-logout measures from.
createdAtstringWhen the login completed.
+
+
Errors

400USERNAME_ERROR 403CHALLENGE_REQUIRED 400BAD_REQUEST 503NETWORK_ERROR

+
+

Notes

  • The command requires a username, password and PIN. Creating a light account is REST-only.
+
+
+

Start a QR login.

+
request-edge-loginsrc/cli/engine/routes/login.ts
+
+

corecontext.requestEdgeLogin

+

Asks the login server for a lobby another logged-in Edge device can approve. The returned lobbyId is what goes in the QR code.

+
+
+

Command line

+
request-edge-login [--no-wait]
+
Client-only flags
--no-waitoptionalPrint the lobby and exit instead of polling, so the QR can be displayed while poll-edge-login watches the same handle from another process.
+

Prints the pending login, then polls every 2s for up to 5 minutes. On done it stores the session. With --no-wait it returns immediately and poll-edge-login takes over.

+
+
+

REST

+

POST/request-edge-login

+ + + + +
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  'http://localhost/request-edge-login'
+
+
+

Response 200

+
+
Response body
+
{
+  objectId: string
+  pendingId: string
+  kind: string
+  expiresAt: string | null
+  lobbyId: string
+  uri: string
+  state: string
+  username: string | null
+  session: {
+    sessionId: string;
+    username: string | undefined;
+    rootLoginId: string;
+    loginMethod: "password" | "pin" | "key" | "recovery" | "edge" | "create";
+    autoLogoutSeconds: number;
+    expiresAt: string | null;
+    lastActivityAt: string;
+    createdAt: string
+  } | null
+  error: string | null
+}
+
Example
{
+  "objectId": "FS8xJ2kQ…",
+  "pendingId": "FS8xJ2kQ…",
+  "kind": "string",
+  "expiresAt": "2026-09-02T16:35:00.000Z",
+  "lobbyId": "FS8xJ2kQ…",
+  "uri": "string",
+  "state": "string",
+  "username": "string",
+  "session": {},
+  "error": "string"
+}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
objectIdstringHandle for the value the engine is holding. Pass it to the calls that consume it.
pendingIdstringSame value as objectId, under the name the poll command takes.
kindstringWhat the handle refers to, which decides the calls that accept it.
expiresAtstring | nullWhen the lobby closes and the QR code stops working.
lobbyIdstringLobby the phone connects to.
uristringThe edge:// URI to render as a QR code for the phone to scan.
statestringHow far the login has got: pending before the phone scans, started once it has, and done when session is filled in.
usernamestring | nullAccount that approved the login, known once the phone has scanned.
session{ sessionId: string; username: string | undefined; rootLoginId: string; loginMethod: "password" | "pin" | "key" | "recovery" | "edge" | "create"; autoLogoutSeconds: number; expiresAt: string | null; lastActivityAt: string; createdAt: string; } | nullThe session, null until state is done.
errorstring | nullWhy the login failed, set only when state is error.
+
+
Errors

503NETWORK_ERROR

+
+

Notes

  • The pending login is an object handle with a 5 minute TTL. On expiry the engine cancels the request on the login server for you.
+
+
+

Poll a pending QR login.

+
poll-edge-loginsrc/cli/engine/routes/login.ts
+
+

coreEngine state for an in-flight requestEdgeLogin; core exposes it as EdgePendingEdgeLogin properties.

+

Once state reaches done the engine has already created the session, so the response carries one ready to use.

+
+
+

Command line

+
poll-edge-login <pendingId>
+ + +
+

REST

+

GET/pending-edge-login/{pendingId}

+
Path
pendingIdstringThe pendingId returned when the QR login was requested.
+ + + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/pending-edge-login/$PENDINGID'
+
+
+

Response 200

+
+
Response body
+
{
+  objectId: string
+  pendingId: string
+  kind: string
+  expiresAt: string | null
+  lobbyId: string
+  uri: string
+  state: string
+  username: string | null
+  session: {
+    sessionId: string;
+    username: string | undefined;
+    rootLoginId: string;
+    loginMethod: "password" | "pin" | "key" | "recovery" | "edge" | "create";
+    autoLogoutSeconds: number;
+    expiresAt: string | null;
+    lastActivityAt: string;
+    createdAt: string
+  } | null
+  error: string | null
+}
+
Example
{
+  "objectId": "FS8xJ2kQ…",
+  "pendingId": "FS8xJ2kQ…",
+  "kind": "string",
+  "expiresAt": "2026-09-02T16:35:00.000Z",
+  "lobbyId": "FS8xJ2kQ…",
+  "uri": "string",
+  "state": "string",
+  "username": "string",
+  "session": {},
+  "error": "string"
+}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
objectIdstringHandle for the value the engine is holding. Pass it to the calls that consume it.
pendingIdstringSame value as objectId, under the name the poll command takes.
kindstringWhat the handle refers to, which decides the calls that accept it.
expiresAtstring | nullWhen the lobby closes and the QR code stops working.
lobbyIdstringLobby the phone connects to.
uristringThe edge:// URI to render as a QR code for the phone to scan.
statestringHow far the login has got: pending before the phone scans, started once it has, and done when session is filled in.
usernamestring | nullAccount that approved the login, known once the phone has scanned.
session{ sessionId: string; username: string | undefined; rootLoginId: string; loginMethod: "password" | "pin" | "key" | "recovery" | "edge" | "create"; autoLogoutSeconds: number; expiresAt: string | null; lastActivityAt: string; createdAt: string; } | nullThe session, null until state is done.
errorstring | nullWhy the login failed, set only when state is error.
+
+
Errors

404PENDING_LOGIN_NOT_FOUND 410OBJECT_EXPIRED

+
+

Notes

  • Session creation is attempted once. A failure is sticky, so later polls report the same error rather than retrying.
  • Polling does not extend the handle TTL; only the original 5 minute window applies.
+
+
+

Cancel a pending QR login.

+
cancel-requestsrc/cli/engine/routes/login.ts
+
+

coreEdgePendingEdgeLogin.cancelRequest

+ +
+

Command line

+
cancel-request <pendingId>
+ + +
+

REST

+

POST/pending-edge-login/cancel-request/{pendingId}

+
Path
pendingIdstringThe pendingId returned when the QR login was requested.
+ + + +
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  'http://localhost/pending-edge-login/cancel-request/$PENDINGID'
+
+
+

Response 204

+

No body.

+
Errors

404PENDING_LOGIN_NOT_FOUND

+
+

Notes

  • If the login already completed and a session exists, that session is force-logged-out too, so cancelling cannot leave an orphan visible in engine-sessions.
+
+
+

List active sessions.

+
engine-sessionssrc/cli/engine/routes/login.ts
+
+

coreThe session registry is an engine construct; core has no multi-account session concept.

+ +
+

Command line

+
engine-sessions
+ + +
+

REST

+

GET/engine/sessions

+ + + + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/engine/sessions'
+
+
+

Response 200

+

A bare array, not wrapped in a key.

+
{
+  sessionId: string;
+  username: string | undefined;
+  rootLoginId: string;
+  loginMethod: "password" | "pin" | "key" | "recovery" | "edge" | "create";
+  autoLogoutSeconds: number;
+  expiresAt: string | null;
+  lastActivityAt: string;
+  createdAt: string
+}[]
+ +
+ +

Account

+

Calls on a logged-in EdgeAccount, addressed by sessionId. All of these can also return 401 INVALID_SESSION or 401 SESSION_EXPIRED.

+
+

Session

+

Calls on a logged-in EdgeAccount, addressed by sessionId. All of these can also return 401 INVALID_SESSION or 401 SESSION_EXPIRED.

+
+
+
+

Account and session summary.

+
account-infosrc/cli/engine/routes/account.ts
+
+

coreEngine composite of the session record plus EdgeAccount properties.

+

Session fields are spread at the top level alongside the account's own properties — there is no nested session object.

+
+
+

Command line

+
account-info
+ + +
+

REST

+

GET/account/{sessionId}

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/account/$SESS'
+
+
+

Response 200

+
+
Response body
+
{
+  appId: string
+  created: string | null
+  lastLogin: string
+  loggedIn: boolean
+  recoveryKey: string | null
+  otpEnabled: boolean
+  otpResetPending: boolean
+  canDuressLogin: boolean
+  isDuressAccount: boolean
+  edgeLogin: boolean
+  keyLogin: boolean
+  newAccount: boolean
+  passwordLogin: boolean
+  pinLogin: boolean
+  recoveryLogin: boolean
+}
+
Example
{
+  "appId": "FS8xJ2kQ…",
+  "created": "string",
+  "lastLogin": "string",
+  "loggedIn": true,
+  "recoveryKey": "string",
+  "otpEnabled": true,
+  "otpResetPending": true,
+  "canDuressLogin": true,
+  "isDuressAccount": true,
+  "edgeLogin": true,
+  "keyLogin": true,
+  "newAccount": true,
+  "passwordLogin": true,
+  "pinLogin": true,
+  "recoveryLogin": true
+}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
appIdstringApplication this session logged into.
createdstring | nullWhen the account was created, null for accounts predating the field.
lastLoginstringThe previous login, not this one.
loggedInbooleanFalse once the account has been logged out; the session object outlives it briefly.
recoveryKeystring | nullPresent only while recovery is configured.
otpEnabledboolean2FA is on for this account.
otpResetPendingbooleanTrue while somebody has a reset pending against this account.
canDuressLoginbooleanA duress PIN is configured, so this account can be opened in duress mode.
isDuressAccountbooleanTrue when this very session is the duress account rather than the real one.
edgeLoginbooleanThis account was reached by QR login.
keyLoginbooleanThis session was reached with a login key.
newAccountbooleanThis session created the account rather than logging into an existing one.
passwordLoginbooleanThis session was reached with a password.
pinLoginbooleanThis session was reached with a PIN.
recoveryLoginbooleanThis session was reached by answering recovery questions.
+
+ +
+

Notes

  • The otpEnabled and otpResetPending flags here are derived. For the secret itself use otp-key.
+
+
+

Log out.

+
logoutsrc/cli/engine/routes/account.ts
+
+

coreaccount.logout

+

Ends the session and drops it from the engine. Any subscription scoped to this account or its wallets is closed with it.

+
+
+

Command line

+
logout
+ +

Also clears the stored id from session.json.

+
+
+

REST

+

POST/account/{sessionId}/logout

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + + +
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  'http://localhost/account/$SESS/logout'
+
+
+

Response 204

+

No body.

+ +
+ +
+
+

Keepalive.

+
touchsrc/cli/engine/routes/account.ts
+
+

coreEngine auto-logout timer; core has no idle concept.

+

Resets the idle auto-logout timer without doing any other work.

+
+
+

Command line

+
touch
+ + +
+

REST

+

POST/account/{sessionId}/touch

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + + +
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  'http://localhost/account/$SESS/touch'
+
+
+

Response 200

+

The session, with a refreshed expiresAt.

+
+
Response body
+
{
+  sessionId: string
+  username?: string
+  rootLoginId: string
+  loginMethod: "password" | "pin" | "key" | "recovery" | "edge" | "create"
+  autoLogoutSeconds: number
+  expiresAt: string | null
+  lastActivityAt: string
+  createdAt: string
+}
+
Example
{
+  "sessionId": "FS8xJ2kQ…",
+  "username": "string",
+  "rootLoginId": "FS8xJ2kQ…",
+  "loginMethod": "<\"password\">",
+  "autoLogoutSeconds": 1,
+  "expiresAt": "2026-09-02T16:35:00.000Z",
+  "lastActivityAt": "2026-09-02T16:35:00.000Z",
+  "createdAt": "2026-09-02T16:35:00.000Z"
+}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
sessionIdstringIdentifies this login. Every account-scoped call carries it, and the CLI stores the most recent one so commands can omit it.
usernamestring optionalAbsent for a light account, which has no username.
rootLoginIdstringThe account root, stable across appIds. Two sessions sharing it are the same account.
loginMethod"password" | "pin" | "key" | "recovery" | "edge" | "create"How this session was established.
autoLogoutSecondsnumberIdle time before the engine logs the account out. 0 disables it.
expiresAtstring | nullWhen auto-logout will fire, or null when it is disabled.
lastActivityAtstringLast call on this session, which is what auto-logout measures from.
createdAtstringWhen the login completed.
+
+ +
+ +
+
+

Read the account login key.

+
get-login-keysrc/cli/engine/routes/account.ts
+
+

coreaccount.getLoginKey

+

The key login-with-key takes. It grants full account access, so treat the output as secret.

+
+
+

Command line

+
get-login-key
+ + +
+

REST

+

GET/account/{sessionId}/get-login-key

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/account/$SESS/get-login-key'
+
+
+

Response 200

+
+
Response body
+
{
+  loginKey: string
+}
+
Example
{
+  "loginKey": "string"
+}
+ + + + +
loginKeystringbase58. Full account access — keep it safe.
+
+ +
+ +
+
+

Force an account data sync.

+
syncsrc/cli/engine/routes/account.ts
+
+

coreaccount.sync

+

Pushes and pulls the account repos immediately rather than waiting for the next scheduled sync.

+
+
+

Command line

+
sync
+ +

Named sync for the account; the wallet one is wallet-sync.

+
+
+

REST

+

POST/account/{sessionId}/sync

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + + +
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  'http://localhost/account/$SESS/sync'
+
+
+

Response 204

+

No body.

+
Errors

503NETWORK_ERROR

+
+ +
+
+

Permanently delete the remote account.

+
delete-remote-accountsrc/cli/engine/routes/account.ts
+
+

coreaccount.deleteRemoteAccount

+

Irreversible. The account is removed from the login server, and funds in its wallets are unrecoverable without the keys. The session is logged out afterwards.

+
+
+

Command line

+
delete-remote-account --yes
+
Client-only flags
--yesrequiredConfirms intent. Without it the command refuses to run.
+ +
+

REST

+

POST/account/{sessionId}/delete-remote-account

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + + +
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  'http://localhost/account/$SESS/delete-remote-account'
+
+
+

Response 204

+

No body.

+
Errors

503NETWORK_ERROR

+
+

Notes

  • The engine performs no confirmation check — the call runs as soon as it arrives, so any guard has to live in the caller. The command requires --yes for exactly this reason.
+
+
+

Wait for every wallet to finish loading.

+
wait-for-all-walletssrc/cli/engine/routes/account.ts
+
+

coreaccount.waitForAllWallets

+

Wallets load in the background after login, so a list taken straight afterwards can be short. This resolves once each active wallet has either loaded or failed — balances may still be syncing afterwards.

+
+
+

Command line

+
wait-for-all-wallets
+ + +
+

REST

+

POST/account/{sessionId}/wait-for-all-wallets

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + + +
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  'http://localhost/account/$SESS/wait-for-all-wallets'
+
+
+

Response 204

+

No body.

+ +
+

Notes

  • There is no timeout: a wallet that never resolves holds this open. The engine's own idle shutdown does not fire while a request is in flight, so give the client one.
  • Nothing is returned. Call currency-wallets afterwards to see the result, including any wallet that failed to load.
+
+
+

List the account's wallets.

+
currency-walletssrc/cli/engine/routes/account.ts
+
+

coreaccount.currencyWallets Filtered by account.activeWalletIds / archivedWalletIds / hiddenWalletIds.

Differs from core:

  • filter — Core has no filter: it exposes activeWalletIds, archivedWalletIds and hiddenWalletIds as separate lists. This picks between them.
+ +
+

Command line

+
currency-wallets [--filter=active|archived|hidden|all]
+ + +
+

REST

+

GET/account/{sessionId}/currency-wallets

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+
+
Query
+
{
+  filter?: "active" | "archived" | "hidden" | "all"
+}
+
Example
{
+  "filter": "<\"active\">"
+}
+ + + + +
filter"active" | "archived" | "hidden" | "all" optionalWhich of the account’s wallet lists to read. Defaults to active.
+
+ + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/account/$SESS/currency-wallets'
+
+
+

Response 200

+
+
Response body
+
{
+  currencyWallets: {
+    walletId: string;
+    id: string;
+    type: string;
+    name: string | null;
+    pluginId: string;
+    currencyCode: string;
+    fiatCurrencyCode: string;
+    blockHeight: number;
+    syncStatus: unknown;
+    syncRatio: string | undefined;
+    paused: boolean;
+    imported: boolean | undefined;
+    created: string | null;
+    enabledTokenIds: string[];
+    detectedTokenIds: string[];
+    unactivatedTokenIds: string[]
+  }[]
+}
+
Example
{
+  "currencyWallets": [
+    {}
+  ]
+}
+ + + + +
currencyWallets{ walletId: string; id: string; type: string; name: string | null; pluginId: string; currencyCode: string; fiatCurrencyCode: string; blockHeight: number; syncStatus: unknown; syncRatio: string | undefined; paused: boolean; imported: boolean | undefined; created: string | null; enabledTokenIds: string[]; detectedTokenIds: string[]; unactivatedTokenIds: string[]; }[]Every wallet in the account, including paused ones.
+
+ +
+

Notes

  • Wallets load in the background after login, so a list taken straight afterwards can be short. Call wait-for-all-wallets first to be sure the account has finished loading.
+
+
+

Create a currency wallet.

+
create-currency-walletsrc/cli/engine/routes/account.ts
+
+

coreaccount.createCurrencyWallet

+ +
+

Command line

+
create-currency-wallet --wallet-type=<walletType> [--name=<name>] [--import-text=<importText>]
+ + +
+

REST

+

POST/account/{sessionId}/create-currency-wallet

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  walletType: string
+  name?: string
+  importText?: string
+}
+
Example
{
+  "walletType": "string",
+  "name": "string",
+  "importText": "string"
+}
+ + + + + + + + + + + + + + +
walletTypestringFrom currency-configs, e.g. wallet:bitcoin.
namestring optionalDisplay name.
importTextstring optionalSeed or key text to import instead of generating.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"walletType":"string","name":"string","importText":"string"}' \
+  'http://localhost/account/$SESS/create-currency-wallet'
+
+
+

Response 200

+
+
Response body
+
{
+  walletId: string
+  id: string
+  type: string
+  name: string | null
+  pluginId: string
+  currencyCode: string
+  fiatCurrencyCode: string
+  blockHeight: number
+  syncStatus: unknown
+  syncRatio?: string
+  paused: boolean
+  imported?: boolean
+  created: string | null
+  enabledTokenIds: string[]
+  detectedTokenIds: string[]
+  unactivatedTokenIds: string[]
+}
+
Example
{
+  "walletId": "FS8xJ2kQ…",
+  "id": "FS8xJ2kQ…",
+  "type": "string",
+  "name": "string",
+  "pluginId": "FS8xJ2kQ…",
+  "currencyCode": "string",
+  "fiatCurrencyCode": "string",
+  "blockHeight": 1,
+  "syncStatus": {},
+  "syncRatio": "string",
+  "paused": true,
+  "imported": true,
+  "created": "string",
+  "enabledTokenIds": [
+    "string"
+  ],
+  "detectedTokenIds": [
+    "string"
+  ],
+  "unactivatedTokenIds": [
+    "string"
+  ]
+}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
walletIdstringThe full wallet id. Commands taking a wallet accept any unique prefix.
idstringSame value as walletId; core exposes both names.
typestringKey type, such as wallet:bitcoin.
namestring | nullUser-assigned name, null until one is set.
pluginIdstringCurrency plugin backing this wallet.
currencyCodestringTicker for the native asset.
fiatCurrencyCodestringFiat the wallet reports value in, as iso:USD.
blockHeightnumberChain height this wallet has seen.
syncStatusunknownEdgeWalletSyncStatus from core.
syncRatiostring optionalSync progress as a percentage, for display.
pausedbooleanTrue while the engine is not syncing this wallet.
importedboolean optionalTrue when the keys came from an import rather than being generated here.
createdstring | nullWhen the wallet was created, null for wallets predating the field.
enabledTokenIdsstring[]Tokens the user turned on.
detectedTokenIdsstring[]Tokens found on-chain that are not enabled yet.
unactivatedTokenIdsstring[]Enabled tokens still awaiting on-chain activation.
+
+
Errors

400BAD_REQUEST

+
+

Notes

  • The fiat currency is not set here. Core still accepts it on create, but that path is deprecated — use set-fiat-currency-code afterwards, so there is one way to do it.
+
+
+

Create several wallets at once.

+
create-currency-walletssrc/cli/engine/routes/account.ts
+
+

coreaccount.createCurrencyWallets

+

Partial success is normal: each entry reports its own outcome, and one failure does not roll back the others.

+
+
+

Command line

+
create-currency-wallets --create-wallets='<json>'
+ + +
+

REST

+

POST/account/{sessionId}/create-currency-wallets

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  createWallets: unknown[]
+}
+
Example
{
+  "createWallets": [
+    {}
+  ]
+}
+ + + + +
createWalletsunknown[]EdgeCreateCurrencyWallet[]: walletType, name, fiatCurrencyCode.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"createWallets":[{}]}' \
+  'http://localhost/account/$SESS/create-currency-wallets'
+
+
+

Response 200

+
+
Response body
+
{
+  results: unknown[]
+}
+
Example
{
+  "results": [
+    {}
+  ]
+}
+ + + + +
resultsunknown[]Mirrors core's EdgeResult[]: { ok, wallet } or { ok: false, error }.
+
+
Errors

400BAD_REQUEST

+
+ +

Credentials

+

Password, PIN, username and recovery changes on a logged-in account.

+
+
+
+

Set or change the password.

+
change-passwordsrc/cli/engine/routes/credentials.ts
+
+

coreaccount.changePassword

+

The login server enforces its own rules; check-password-rules scores a candidate first.

+
+
+

Command line

+
change-password --password=<password>
+ + +
+

REST

+

POST/account/{sessionId}/change-password

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  password: string
+}
+
Example
{
+  "password": "string"
+}
+ + + + +
passwordstringThe new password.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"password":"string"}' \
+  'http://localhost/account/$SESS/change-password'
+
+
+

Response 204

+

No body.

+
Errors

400BAD_REQUEST 503NETWORK_ERROR

+
+ +
+
+

Remove password login.

+
delete-passwordsrc/cli/engine/routes/credentials.ts
+
+

coreaccount.deletePassword

+

The account keeps its other login methods; only the password stops working.

+
+
+

Command line

+
delete-password
+ + +
+

REST

+

POST/account/{sessionId}/delete-password

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + + +
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  'http://localhost/account/$SESS/delete-password'
+
+
+

Response 204

+

No body.

+
Errors

400BAD_REQUEST 503NETWORK_ERROR

+
+ +
+
+

Verify a password.

+
check-passwordsrc/cli/engine/routes/credentials.ts
+
+

coreaccount.checkPassword

+

Checks without changing anything, which is how a caller gates a destructive action behind a re-entry prompt.

+
+
+

Command line

+
check-password --password=<password>
+ + +
+

REST

+

POST/account/{sessionId}/check-password

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  password: string
+}
+
Example
{
+  "password": "string"
+}
+ + + + +
passwordstringThe account password.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"password":"string"}' \
+  'http://localhost/account/$SESS/check-password'
+
+
+

Response 200

+
+
Response body
+
{
+  ok: boolean
+}
+
Example
{
+  "ok": true
+}
+ + + + +
okbooleanFalse for a wrong password — not an error response.
+
+ +
+ +
+
+

Read the account PIN.

+
get-pinsrc/cli/engine/routes/credentials.ts
+
+

coreaccount.getPin

+

Returns the PIN itself, not a status flag, so treat the output as secret.

+
+
+

Command line

+
get-pin
+ + +
+

REST

+

GET/account/{sessionId}/get-pin

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/account/$SESS/get-pin'
+
+
+

Response 200

+
+
Response body
+
{
+  pin: string | null
+}
+
Example
{
+  "pin": "string"
+}
+ + + + +
pinstring | nullNull when no PIN is set.
+
+ +
+ +
+
+

Set or change the PIN.

+
change-pinsrc/cli/engine/routes/credentials.ts
+
+

coreaccount.changePin

+ +
+

Command line

+
change-pin --pin=<pin> [--enable-login] [--for-duress-account]
+ + +
+

REST

+

POST/account/{sessionId}/change-pin

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  pin: string
+  enableLogin?: boolean
+  forDuressAccount?: boolean
+}
+
Example
{
+  "pin": "string",
+  "enableLogin": true,
+  "forDuressAccount": true
+}
+ + + + + + + + + + + + + + +
pinstringThe new PIN.
enableLoginboolean optionalAllow logging in with this PIN on this device.
forDuressAccountboolean optionalAct on the duress account rather than the real one.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"pin":"string","enableLogin":true,"forDuressAccount":true}' \
+  'http://localhost/account/$SESS/change-pin'
+
+
+

Response 200

+
+
Response body
+
{
+  pin2Key: string
+}
+
Example
{
+  "pin2Key": "string"
+}
+ + + + +
pin2KeystringThe new PIN login key core returns.
+
+
Errors

400BAD_REQUEST

+
+ +
+
+

Remove the PIN.

+
delete-pinsrc/cli/engine/routes/credentials.ts
+
+

coreaccount.deletePin

+

PIN login stops working on this device; other methods are untouched.

+
+
+

Command line

+
delete-pin
+ + +
+

REST

+

POST/account/{sessionId}/delete-pin

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + + +
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  'http://localhost/account/$SESS/delete-pin'
+
+
+

Response 204

+

No body.

+ +
+ +
+
+

Verify a PIN.

+
check-pinsrc/cli/engine/routes/credentials.ts
+
+

coreaccount.checkPin

+ +
+

Command line

+
check-pin --pin=<pin> [--for-duress-account]
+ + +
+

REST

+

POST/account/{sessionId}/check-pin

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  pin: string
+  forDuressAccount?: boolean
+}
+
Example
{
+  "pin": "string",
+  "forDuressAccount": true
+}
+ + + + + + + + + +
pinstringThe device PIN, usually four digits.
forDuressAccountboolean optionalAct on the duress account rather than the real one.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"pin":"string","forDuressAccount":true}' \
+  'http://localhost/account/$SESS/check-pin'
+
+
+

Response 200

+
+
Response body
+
{
+  ok: boolean
+}
+
Example
{
+  "ok": true
+}
+ + + + +
okbooleanFalse for a wrong PIN — not an error response.
+
+ +
+ +
+
+

Change the username.

+
change-usernamesrc/cli/engine/routes/credentials.ts
+
+

coreaccount.changeUsername

+

The old name is released, so it becomes available to anyone else.

+
+
+

Command line

+
change-username --username=<username> [--password=<password>]
+ + +
+

REST

+

POST/account/{sessionId}/change-username

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  username: string
+  password?: string
+}
+
Example
{
+  "username": "string",
+  "password": "string"
+}
+ + + + + + + + + +
usernamestringThe new username.
passwordstring optionalRequired by core when the account has a password.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"username":"string","password":"string"}' \
+  'http://localhost/account/$SESS/change-username'
+
+
+

Response 204

+

No body.

+
Errors

400USERNAME_ERROR 400BAD_REQUEST 503NETWORK_ERROR

+
+ +
+
+

Set recovery questions and answers.

+
change-recoverysrc/cli/engine/routes/credentials.ts
+
+

coreaccount.changeRecovery Our surface drops the 2 from core's recovery2 naming; a future Recovery1 would be suffixed V1.

+

The returned key is half of the credential: without it the answers alone cannot recover the account, so it has to be stored somewhere else.

+
+
+

Command line

+
change-recovery --question=<questions> … --answer=<answers> …
+ + +
+

REST

+

POST/account/{sessionId}/change-recovery

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  questions: string[]
+  answers: string[]
+}
+
Example
{
+  "questions": [
+    "string"
+  ],
+  "answers": [
+    "string"
+  ]
+}
+ + + + + + + + + +
questionsstring[]The questions to ask.
answersstring[]Same length and order as questions.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"questions":["string"],"answers":["string"]}' \
+  'http://localhost/account/$SESS/change-recovery'
+
+
+

Response 200

+
+
Response body
+
{
+  recoveryKey: string
+}
+
Example
{
+  "recoveryKey": "string"
+}
+ + + + +
recoveryKeystringStore this out of band. login-with-recovery needs it alongside the answers.
+
+
Errors

400BAD_REQUEST

+
+ +
+
+

Disable recovery login.

+
delete-recoverysrc/cli/engine/routes/credentials.ts
+
+

coreaccount.deleteRecovery

+

The existing recovery key stops working.

+
+
+

Command line

+
delete-recovery
+ + +
+

REST

+

POST/account/{sessionId}/delete-recovery

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + + +
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  'http://localhost/account/$SESS/delete-recovery'
+
+
+

Response 204

+

No body.

+ +
+ +

Two-factor authentication

+

OTP state and the reset flow a user falls back on after losing their authenticator.

+
+
+
+

Read the 2FA secret and reset state.

+
otp-keysrc/cli/engine/routes/otp.ts
+
+

coreaccount.otpKey Also carries account.otpResetDate.

+ +
+

Command line

+
otp-key
+ + +
+

REST

+

GET/account/{sessionId}/otp-key

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/account/$SESS/otp-key'
+
+
+

Response 200

+
+
Response body
+
{
+  otpKey: string | null
+  otpResetDate: string | null
+}
+
Example
{
+  "otpKey": "string",
+  "otpResetDate": "2026-09-02T16:35:00.000Z"
+}
+ + + + + + + + + +
otpKeystring | nullNull when 2FA is off. The 2FA secret itself. Secret material — record it safely.
otpResetDatestring | nullSet once somebody has requested a reset; cancel it with cancel-otp-reset.
+
+ +
+ +
+
+

Enable 2FA.

+
enable-otpsrc/cli/engine/routes/otp.ts
+
+

coreaccount.enableOtp

+

Record the returned key before leaving the terminal: it is the only copy.

+
+
+

Command line

+
enable-otp [--timeout=<timeout>]
+ + +
+

REST

+

POST/account/{sessionId}/enable-otp

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  timeout?: number
+}
+
Example
{
+  "timeout": 0
+}
+ + + + +
timeoutnumber optionalHow long a reset request must wait before it completes. Core supplies the default when omitted.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"timeout":0}' \
+  'http://localhost/account/$SESS/enable-otp'
+
+
+

Response 200

+
+
Response body
+
{
+  otpKey: string | null
+}
+
Example
{
+  "otpKey": "string"
+}
+ + + + +
otpKeystring | nullThe new secret. The 2FA secret itself. Secret material — record it safely.
+
+ +
+ +
+
+

Disable 2FA.

+
disable-otpsrc/cli/engine/routes/otp.ts
+
+

coreaccount.disableOtp

+

Logins stop requiring a code immediately.

+
+
+

Command line

+
disable-otp
+ + +
+

REST

+

POST/account/{sessionId}/disable-otp

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + + +
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  'http://localhost/account/$SESS/disable-otp'
+
+
+

Response 204

+

No body.

+ +
+ +
+
+

Cancel a pending 2FA reset.

+
cancel-otp-resetsrc/cli/engine/routes/otp.ts
+
+

coreaccount.cancelOtpReset

+

The defence against somebody else requesting a reset on your account: as long as you cancel before the timer runs out, their reset never lands.

+
+
+

Command line

+
cancel-otp-reset
+ + +
+

REST

+

POST/account/{sessionId}/cancel-otp-reset

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + + +
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  'http://localhost/account/$SESS/cancel-otp-reset'
+
+
+

Response 204

+

No body.

+ +
+ +
+
+

Re-point the account at a known 2FA secret.

+
repair-otpsrc/cli/engine/routes/otp.ts
+
+

coreaccount.repairOtp

+

For a device whose stored secret has drifted from the server's.

+
+
+

Command line

+
repair-otp --otp-key=<otpKey>
+ + +
+

REST

+

POST/account/{sessionId}/repair-otp

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  otpKey: string
+}
+
Example
{
+  "otpKey": "string"
+}
+ + + + +
otpKeystringThe secret the account should use.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"otpKey":"string"}' \
+  'http://localhost/account/$SESS/repair-otp'
+
+
+

Response 204

+

No body.

+
Errors

401OTP_REQUIRED 400BAD_REQUEST

+
+ +

Vouchers

+

When 2FA blocks a login, the login server issues a voucher an already-trusted device can approve or reject.

+
+
+
+

List pending 2FA vouchers.

+
pending-voucherssrc/cli/engine/routes/vouchers.ts
+
+

coreaccount.pendingVouchers

+

When 2FA blocks a login, the login server issues a voucher that an already-trusted device can approve or reject.

+
+
+

Command line

+
pending-vouchers
+ + +
+

REST

+

GET/account/{sessionId}/pending-vouchers

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/account/$SESS/pending-vouchers'
+
+
+

Response 200

+
+
Response body
+
{
+  pendingVouchers: unknown[]
+}
+
Example
{
+  "pendingVouchers": [
+    {}
+  ]
+}
+ + + + +
pendingVouchersunknown[]EdgePendingVoucher[]: voucherId, activates, created, deviceDescription, ipDescription.
+
+ +
+ +
+
+

Approve a voucher.

+
approve-vouchersrc/cli/engine/routes/vouchers.ts
+
+

coreaccount.approveVoucher

+

Lets the waiting device finish logging in.

+
+
+

Command line

+
approve-voucher --voucher-id=<voucherId>
+ + +
+

REST

+

POST/account/{sessionId}/approve-voucher

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  voucherId: string
+}
+
Example
{
+  "voucherId": "FS8xJ2kQ…"
+}
+ + + + +
voucherIdstringFrom pending-vouchers, or an OTP_REQUIRED error’s details.voucherId.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"voucherId":"FS8xJ2kQ…"}' \
+  'http://localhost/account/$SESS/approve-voucher'
+
+
+

Response 204

+

No body.

+
Errors

400BAD_REQUEST 503NETWORK_ERROR

+
+ +
+
+

Reject a voucher.

+
reject-vouchersrc/cli/engine/routes/vouchers.ts
+
+

coreaccount.rejectVoucher

+

Denies the waiting device. The login it was issued for cannot complete.

+
+
+

Command line

+
reject-voucher --voucher-id=<voucherId>
+ + +
+

REST

+

POST/account/{sessionId}/reject-voucher

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  voucherId: string
+}
+
Example
{
+  "voucherId": "FS8xJ2kQ…"
+}
+ + + + +
voucherIdstringFrom pending-vouchers, or an OTP_REQUIRED error’s details.voucherId.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"voucherId":"FS8xJ2kQ…"}' \
+  'http://localhost/account/$SESS/reject-voucher'
+
+
+

Response 204

+

No body.

+
Errors

400BAD_REQUEST 503NETWORK_ERROR

+
+ +

Approving a login

+

The other side of request-edge-login: a logged-in account inspecting and approving a login somebody scanned.

+
+
+
+

Inspect a login request.

+
fetch-lobbysrc/cli/engine/routes/lobby.ts
+
+

coreaccount.fetchLobby

+

The other side of request-edge-login: shows who is asking, so a human can decide before approving.

+
+
+

Command line

+
fetch-lobby <lobbyId>
+ + +
+

REST

+

GET/account/{sessionId}/fetch-lobby/{lobbyId}

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
lobbyIdstringFrom the QR code, or an edge://edge/<lobbyId> link.
+ + + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/account/$SESS/fetch-lobby/$LOBBYID?lobbyId=…'
+
+
+

Response 200

+
+
Response body
+
{
+  lobbyId: string
+  loginRequest: {
+    appId: string;
+    displayName: string;
+    displayImageDarkUrl: string | null;
+    displayImageLightUrl: string | null
+  } | null
+}
+
Example
{
+  "lobbyId": "FS8xJ2kQ…",
+  "loginRequest": {}
+}
+ + + + + + + + + +
lobbyIdstringThe lobby that was fetched, echoed back.
loginRequest{ appId: string; displayName: string; displayImageDarkUrl: string | null; displayImageLightUrl: string | null; } | nullNull when the lobby carries no pending login request.
+
+
Errors

400BAD_REQUEST 503NETWORK_ERROR

+
+ +
+
+

Approve a login request.

+
approve-login-requestsrc/cli/engine/routes/lobby.ts
+
+

coreEdgeLoginRequest.approve Reached through account.fetchLobby(lobbyId).loginRequest.

Differs from core:

  • lobbyId — Core calls approve() on a request object. Over HTTP there is no object to hold, so the lobby names which one to approve.
+

Grants the requesting device access to this account.

+
+
+

Command line

+
approve-login-request <lobbyId>
+ + +
+

REST

+

POST/account/{sessionId}/approve-login-request/{lobbyId}

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
lobbyIdstringFrom the QR code, or an edge://edge/<lobbyId> link.
+ + + +
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"lobbyId":"FS8xJ2kQ…"}' \
+  'http://localhost/account/$SESS/approve-login-request/$LOBBYID'
+
+
+

Response 200

+
+
Response body
+
{
+  ok: boolean
+}
+
Example
{
+  "ok": true
+}
+ + + + +
okbooleanAlways true; a failure arrives as an error envelope.
+
+
Errors

404NO_LOGIN_REQUEST 400BAD_REQUEST 503NETWORK_ERROR

+
+

Notes

  • The lobby is re-fetched on approve, so a request that expired between inspecting and approving fails with 404 NO_LOGIN_REQUEST.
+

Keys

+

Raw key infrastructure beneath the wallet API. Several of these return private key material, and the engine has no transport auth — treat any process that can reach the socket as fully trusted.

+
+
+
+

List every key in the account.

+
all-keyssrc/cli/engine/routes/keys.ts
+
+

coreaccount.allKeys

+

Includes archived and deleted keys, unlike currency-wallets.

+
+
+

Command line

+
all-keys
+ + +
+

REST

+

GET/account/{sessionId}/all-keys

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/account/$SESS/all-keys'
+
+
+

Response 200

+
+
Response body
+
{
+  allKeys: unknown[]
+}
+
Example
{
+  "allKeys": [
+    {}
+  ]
+}
+ + + + +
allKeysunknown[]EdgeWalletInfoFull[]: id, type, keys, archived, deleted, hidden, sortIndex.
+
+ +
+ +
+
+

Create a wallet from raw key JSON.

+
create-walletsrc/cli/engine/routes/keys.ts
+
+

coreaccount.createWallet

+

The import path. Use create-currency-wallet to make a fresh wallet with generated keys.

+
+
+

Command line

+
create-wallet --type=<type> [--keys='<json>']
+ + +
+

REST

+

POST/account/{sessionId}/create-wallet

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  type: string
+  keys?: unknown
+}
+
Example
{
+  "type": "string",
+  "keys": {}
+}
+ + + + + + + + + +
typestringWallet type, e.g. wallet:bitcoin.
keysunknown optionalPlugin key material. Omit to let core generate it.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"type":"string","keys":{}}' \
+  'http://localhost/account/$SESS/create-wallet'
+
+
+

Response 200

+
+
Response body
+
{
+  walletId: string
+}
+
Example
{
+  "walletId": "FS8xJ2kQ…"
+}
+ + + + +
walletIdstringThe new wallet. Its keys are already saved.
+
+
Errors

400BAD_REQUEST

+
+ +
+
+

Read one wallet's key info.

+
get-wallet-infosrc/cli/engine/routes/keys.ts
+
+

coreaccount.getWalletInfo

+ +
+

Command line

+
get-wallet-info --id=<id>
+ + +
+

REST

+

GET/account/{sessionId}/get-wallet-info

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+
+
Query
+
{
+  id: string
+}
+
Example
{
+  "id": "FS8xJ2kQ…"
+}
+ + + + +
idstringThe key id, from all-keys. Base64, like a wallet id.
+
+ + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/account/$SESS/get-wallet-info?id=…'
+
+
+

Response 200

+

EdgeWalletInfoFull, verbatim from core — including the keys object.

+
unknown
+
Errors

404WALLET_NOT_FOUND 409AMBIGUOUS_WALLET_ID

+
+

Notes

  • An exact lookup: unlike the wallet-scoped routes this does not accept an id prefix.
+
+
+

Read raw private key material.

+
get-raw-private-keysrc/cli/engine/routes/keys.ts
+
+

coreaccount.getRawPrivateKey

+

Secret. Whatever the plugin stores — seed, mnemonic, xpriv.

+
+
+

Command line

+
get-raw-private-key --wallet-id=<walletId>
+ + +
+

REST

+

GET/account/{sessionId}/get-raw-private-key

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+
+
Query
+
{
+  walletId: string
+}
+
Example
{
+  "walletId": "FS8xJ2kQ…"
+}
+ + + + +
walletIdstringThe wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns 409 AMBIGUOUS_WALLET_ID with details.candidates.
+
+ + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/account/$SESS/get-raw-private-key?walletId=…'
+
+
+

Response 200

+

The plugin's key object, at the top level.

+
unknown
+
Errors

404WALLET_NOT_FOUND 409AMBIGUOUS_WALLET_ID

+
+ +
+
+

Read raw public key material.

+
get-raw-public-keysrc/cli/engine/routes/keys.ts
+
+

coreaccount.getRawPublicKey

+ +
+

Command line

+
get-raw-public-key --wallet-id=<walletId>
+ + +
+

REST

+

GET/account/{sessionId}/get-raw-public-key

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+
+
Query
+
{
+  walletId: string
+}
+
Example
{
+  "walletId": "FS8xJ2kQ…"
+}
+ + + + +
walletIdstringThe wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns 409 AMBIGUOUS_WALLET_ID with details.candidates.
+
+ + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/account/$SESS/get-raw-public-key?walletId=…'
+
+
+

Response 200

+

The plugin's public key object.

+
unknown
+
Errors

404WALLET_NOT_FOUND 409AMBIGUOUS_WALLET_ID

+
+ +
+
+

Export the private key for display.

+
get-display-private-keysrc/cli/engine/routes/keys.ts
+
+

coreaccount.getDisplayPrivateKey

+

Secret. The human-facing form — WIF, seed phrase, whatever the plugin shows on its export screen.

+
+
+

Command line

+
get-display-private-key --wallet-id=<walletId>
+ + +
+

REST

+

GET/account/{sessionId}/get-display-private-key

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+
+
Query
+
{
+  walletId: string
+}
+
Example
{
+  "walletId": "FS8xJ2kQ…"
+}
+ + + + +
walletIdstringThe wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns 409 AMBIGUOUS_WALLET_ID with details.candidates.
+
+ + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/account/$SESS/get-display-private-key?walletId=…'
+
+
+

Response 200

+
+
Response body
+
{
+  key: string
+}
+
Example
{
+  "key": "string"
+}
+ + + + +
keystringThe displayable private key.
+
+
Errors

404WALLET_NOT_FOUND 409AMBIGUOUS_WALLET_ID

+
+ +
+
+

Export the public key for display.

+
get-display-public-keysrc/cli/engine/routes/keys.ts
+
+

coreaccount.getDisplayPublicKey

+

The xpub or equivalent — safe to share for watch-only use.

+
+
+

Command line

+
get-display-public-key --wallet-id=<walletId>
+ + +
+

REST

+

GET/account/{sessionId}/get-display-public-key

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+
+
Query
+
{
+  walletId: string
+}
+
Example
{
+  "walletId": "FS8xJ2kQ…"
+}
+ + + + +
walletIdstringThe wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns 409 AMBIGUOUS_WALLET_ID with details.candidates.
+
+ + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/account/$SESS/get-display-public-key?walletId=…'
+
+
+

Response 200

+
+
Response body
+
{
+  key: string
+}
+
Example
{
+  "key": "string"
+}
+ + + + +
keystringThe displayable public key.
+
+
Errors

404WALLET_NOT_FOUND 409AMBIGUOUS_WALLET_ID

+
+ +
+
+

List chains a wallet can split into.

+
list-splittable-wallet-typessrc/cli/engine/routes/keys.ts
+
+

coreaccount.listSplittableWalletTypes

+

Forked-chain support: which wallet types can be derived from these keys.

+
+
+

Command line

+
list-splittable-wallet-types --wallet-id=<walletId>
+ + +
+

REST

+

GET/account/{sessionId}/list-splittable-wallet-types

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+
+
Query
+
{
+  walletId: string
+}
+
Example
{
+  "walletId": "FS8xJ2kQ…"
+}
+ + + + +
walletIdstringThe wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns 409 AMBIGUOUS_WALLET_ID with details.candidates.
+
+ + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/account/$SESS/list-splittable-wallet-types?walletId=…'
+
+
+

Response 200

+
+
Response body
+
{
+  walletTypes: string[]
+}
+
Example
{
+  "walletTypes": [
+    "string"
+  ]
+}
+ + + + +
walletTypesstring[]Types valid for split.
+
+
Errors

404WALLET_NOT_FOUND 409AMBIGUOUS_WALLET_ID

+
+ +
+
+

Archive, delete, hide, or reorder wallets.

+
change-wallet-statessrc/cli/engine/routes/keys.ts
+
+

coreaccount.changeWalletStates

+

The canonical backend for every wallet flag; there are no separate archive, unarchive or undelete verbs.

+
+
+

Command line

+
change-wallet-states [--wallet-states='<json>'] --wallet-id=<value> [--archived=<value>] [--deleted=<value>] [--hidden=<value>] [--sort-index=<value>]
+
Client-only flags
--wallet-idrequiredThe wallet to change. The command makes it the key of a single-entry walletStates map.
--archivedoptionalHide from the active list.
--deletedoptionalMark deleted.
--hiddenoptionalHide from the wallet picker.
--sort-indexoptionalPosition in the wallet list.
+

The command builds a single-wallet walletStates map from these flags, and needs at least one.

+
+
+

REST

+

POST/account/{sessionId}/change-wallet-states

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  walletStates?: {
+    [keys: string]: {
+      archived: boolean | undefined;
+      deleted: boolean | undefined;
+      hidden: boolean | undefined;
+      sortIndex: number | undefined
+    }
+  }
+}
+
Example
{
+  "walletStates": {}
+}
+ + + + +
walletStates{ [keys: string]: { archived: boolean | undefined; deleted: boolean | undefined; hidden: boolean | undefined; sortIndex: number | undefined; }; } optionalEdgeWalletStates: wallet ids to the flags being changed.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"walletStates":{}}' \
+  'http://localhost/account/$SESS/change-wallet-states'
+
+
+

Response 204

+

No body.

+
Errors

400BAD_REQUEST

+
+ +

Swap quotes

+

Cross-asset exchange. Quotes are live objects held server-side under a swap_ handle, so approving one means naming its objectId rather than re-uploading the quote.

+
+
+
+

Fetch swap quotes.

+
fetch-swap-quotessrc/cli/engine/routes/swap.ts
+
+

coreaccount.fetchSwapQuotes

Differs from core:

  • fromWalletId — Core takes the wallet object; over HTTP it is an id.
  • toWalletId — Core takes the wallet object; over HTTP it is an id.
+

Polls every enabled swap plugin and parks each result under its own swap_ handle with a 5 minute TTL.

+
+
+

Command line

+
fetch-swap-quotes --from-wallet-id=<fromWalletId> --to-wallet-id=<toWalletId> --native-amount=<nativeAmount> [--from-token-id=<fromTokenId>] [--to-token-id=<toTokenId>] [--quote-for=<quoteFor>] [--plugin-id=<preferPluginId>]
+ + +
+

REST

+

POST/account/{sessionId}/fetch-swap-quotes

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  fromWalletId: string
+  toWalletId: string
+  nativeAmount: string
+  fromTokenId?: string | null
+  toTokenId?: string | null
+  quoteFor?: string
+  preferPluginId?: string
+}
+
Example
{
+  "fromWalletId": "FS8xJ2kQ…",
+  "toWalletId": "FS8xJ2kQ…",
+  "nativeAmount": "12345",
+  "fromTokenId": "FS8xJ2kQ…",
+  "toTokenId": "FS8xJ2kQ…",
+  "quoteFor": "string",
+  "preferPluginId": "FS8xJ2kQ…"
+}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
fromWalletIdstringSource wallet. Accepts a unique prefix.
toWalletIdstringDestination wallet.
nativeAmountstringHow much, in native units.
fromTokenIdstring | null optionalDefaults to the native asset.
toTokenIdstring | null optionalDefaults to the native asset.
quoteForstring optionalfrom spends this much of the source, to receives this much at the destination, max sends everything. Defaults to from.
preferPluginIdstring optionalRestrict to one exchange.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"fromWalletId":"FS8xJ2kQ…","toWalletId":"FS8xJ2kQ…","nativeAmount":"12345","fromTokenId":"FS8xJ2kQ…","toTokenId":"FS8xJ2kQ…","quoteFor":"string","preferPluginId":"FS8xJ2kQ…"}' \
+  'http://localhost/account/$SESS/fetch-swap-quotes'
+
+
+

Response 200

+
+
Response body
+
{
+  quoteCount: number
+  quotes: {
+    objectId: string;
+    kind: string;
+    expiresAt: string;
+    pluginId: string;
+    isEstimate: boolean;
+    canBePartial: boolean | null;
+    maxFulfillmentSeconds: number | null;
+    minReceiveAmount: string | null;
+    fromNativeAmount: string;
+    toNativeAmount: string;
+    networkFee: {
+      nativeAmount: string;
+      tokenId: string | null
+    };
+    quoteExpirationDate: string | null;
+    swapInfo: {
+      pluginId: string;
+      displayName: string;
+      supportEmail: string;
+      isDex: boolean | null
+    };
+    request: {
+      fromTokenId: string | null;
+      toTokenId: string | null;
+      nativeAmount: string;
+      quoteFor: "to" | "from" | "max";
+      fromWalletId: string;
+      toWalletId: string
+    }
+  }[]
+}
+
Example
{
+  "quoteCount": 1,
+  "quotes": [
+    {}
+  ]
+}
+ + + + + + + + + +
quoteCountnumberHow many plugins answered.
quotes{ objectId: string; kind: string; expiresAt: string; pluginId: string; isEstimate: boolean; canBePartial: boolean | null; maxFulfillmentSeconds: number | null; minReceiveAmount: string | null; fromNativeAmount: string; toNativeAmount: string; networkFee: { nativeAmount: string; tokenId: string | null; }; quoteExpirationDate: string | null; swapInfo: { pluginId: string; displayName: string; supportEmail: string; isDex: boolean | null; }; request: { fromTokenId: string | null; toTokenId: string | null; nativeAmount: string; quoteFor: "to" | "from" | "max"; fromWalletId: string; toWalletId: string; }; }[]One quote per plugin that answered, each already parked under its own handle. Plugins that failed or had nothing to offer are simply absent.
+
+
Errors

400BAD_REQUEST 422SWAP_BELOW_LIMIT 422SWAP_ABOVE_LIMIT 422SWAP_CURRENCY 403SWAP_PERMISSION 422SWAP_ADDRESS 400SAME_CURRENCY 422INSUFFICIENT_FUNDS 404WALLET_NOT_FOUND 503NETWORK_ERROR

+
+

Notes

  • Every returned quote holds an open plugin object. Approving one releases only that handle; close the rest, or let them expire.
  • An empty quotes array with quoteCount: 0 is a success, not an error — no plugin could serve the pair.
+
+
+

Re-read a quote.

+
swap-quote-getsrc/cli/engine/routes/swap.ts
+
+

coreEngine handle store; the quote is a live EdgeSwapQuote held server-side.

+ +
+

Command line

+
swap-quote-get <objectId>
+ + +
+

REST

+

GET/account/{sessionId}/swap-quote/{objectId}

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
objectIdstringAn ephemeral object handle id.
+ + + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/account/$SESS/swap-quote/$OBJECTID'
+
+
+

Response 200

+
+
Response body
+
{
+  objectId: string
+  kind: string
+  expiresAt: string
+  pluginId: string
+  isEstimate: boolean
+  canBePartial: boolean | null
+  maxFulfillmentSeconds: number | null
+  minReceiveAmount: string | null
+  fromNativeAmount: string
+  toNativeAmount: string
+  networkFee: {
+    nativeAmount: string;
+    tokenId: string | null
+  }
+  quoteExpirationDate: string | null
+  swapInfo: {
+    pluginId: string;
+    displayName: string;
+    supportEmail: string;
+    isDex: boolean | null
+  }
+  request: {
+    fromTokenId: string | null;
+    toTokenId: string | null;
+    nativeAmount: string;
+    quoteFor: "to" | "from" | "max";
+    fromWalletId: string;
+    toWalletId: string
+  }
+}
+
Example
{
+  "objectId": "FS8xJ2kQ…",
+  "kind": "string",
+  "expiresAt": "2026-09-02T16:35:00.000Z",
+  "pluginId": "FS8xJ2kQ…",
+  "isEstimate": true,
+  "canBePartial": true,
+  "maxFulfillmentSeconds": 1,
+  "minReceiveAmount": "12345",
+  "fromNativeAmount": "12345",
+  "toNativeAmount": "12345",
+  "networkFee": {},
+  "quoteExpirationDate": "2026-09-02T16:35:00.000Z",
+  "swapInfo": {},
+  "request": {}
+}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
objectIdstringHandle for the value the engine is holding. Pass it to the calls that consume it.
kindstringWhat the handle refers to, which decides the calls that accept it.
expiresAtstringWhen the engine drops the handle. Handles live 5 minutes.
pluginIdstringSwap provider that produced this quote.
isEstimatebooleanTrue when the provider may settle at a different rate than quoted.
canBePartialboolean | nullTrue when the provider may fill only part of the order. Null when it does not say.
maxFulfillmentSecondsnumber | nullLongest the provider expects a partial fill to take.
minReceiveAmountstring | nullLeast the provider guarantees to deliver, in the destination’s native units.
fromNativeAmountstringAmount leaving the source wallet.
toNativeAmountstringAmount arriving in the destination wallet.
networkFee{ nativeAmount: string; tokenId: string | null; }On-chain fee for the sending transaction. It is not the provider’s own spread, which is already in the rate.
quoteExpirationDatestring | nullWhen the provider stops honouring the rate. Null when it does not expire.
swapInfo{ pluginId: string; displayName: string; supportEmail: string; isDex: boolean | null; }EdgeSwapInfo: how to name the provider and where to send complaints.
request{ fromTokenId: string | null; toTokenId: string | null; nativeAmount: string; quoteFor: "to" | "from" | "max"; fromWalletId: string; toWalletId: string; }The EdgeSwapRequest this quote answers, echoed back so quotes from different plugins can be compared without tracking what was asked.
+
+
Errors

404OBJECT_NOT_FOUND 410OBJECT_EXPIRED 400OBJECT_KIND_MISMATCH 400OBJECT_SESSION_MISMATCH

+
+

Notes

  • Check quoteExpirationDate as well as expiresAt: the plugin's price can go stale before the handle does.
+
+
+

Execute a quote.

+
approve-swap-quotesrc/cli/engine/routes/swap.ts
+
+

coreEdgeSwapQuote.approve

+

Moves funds. The handle is released afterwards whether or not the response is read, so record orderId from it.

+
+
+

Command line

+
approve-swap-quote <objectId>
+ + +
+

REST

+

POST/account/{sessionId}/swap-quote/approve/{objectId}

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
objectIdstringAn ephemeral object handle id.
+ + + +
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  'http://localhost/account/$SESS/swap-quote/approve/$OBJECTID'
+
+
+

Response 200

+
+
Response body
+
{
+  ok: unknown
+  objectId: string
+  orderId: unknown
+  destinationAddress: unknown
+  transaction: unknown
+}
+
Example
{
+  "ok": {},
+  "objectId": "FS8xJ2kQ…",
+  "orderId": {},
+  "destinationAddress": {},
+  "transaction": {}
+}
+ + + + + + + + + + + + + + + + + + + + + + + + +
okunknownTrue once the swap is submitted and the send broadcast.
objectIdstringThe handle that was consumed.
orderIdunknownThe exchange's order reference, when it gives one.
destinationAddressunknownAddress the funds were sent to, when the exchange reports one.
transactionunknownThe on-chain send to the exchange.
+
+
Errors

404OBJECT_NOT_FOUND 410OBJECT_EXPIRED 400OBJECT_KIND_MISMATCH 400OBJECT_SESSION_MISMATCH 422INSUFFICIENT_FUNDS 503NETWORK_ERROR

+
+

Notes

  • The plugin attaches its own savedAction and assetAction metadata; the engine adds none.
+
+
+

Discard a quote.

+
close-swap-quotesrc/cli/engine/routes/swap.ts
+
+

coreEdgeSwapQuote.close

+

Closes the plugin object without executing, freeing whatever the exchange was holding.

+
+
+

Command line

+
close-swap-quote <objectId>
+ + +
+

REST

+

POST/account/{sessionId}/swap-quote/close/{objectId}

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
objectIdstringAn ephemeral object handle id.
+ + + +
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  'http://localhost/account/$SESS/swap-quote/close/$OBJECTID'
+
+
+

Response 200

+
+
Response body
+
{
+  ok: boolean
+  objectId: string
+}
+
Example
{
+  "ok": true,
+  "objectId": "FS8xJ2kQ…"
+}
+ + + + + + + + + +
okbooleanAlways true; a failure arrives as an error envelope.
objectIdstringThe handle this call consumed. It is now expired.
+
+
Errors

404OBJECT_NOT_FOUND 410OBJECT_EXPIRED 400OBJECT_KIND_MISMATCH 400OBJECT_SESSION_MISMATCH

+
+ +

Data store

+

The account’s synced key-value store, where plugins keep their own state. One route per EdgeDataStore method.

+
+
+
+

List data-store ids.

+
list-store-idssrc/cli/engine/routes/dataStore.ts
+
+

coreaccount.dataStore.listStoreIds

+

The account's synced key-value store, where plugins keep their own state.

+
+
+

Command line

+
list-store-ids
+ + +
+

REST

+

GET/account/{sessionId}/list-store-ids

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/account/$SESS/list-store-ids'
+
+
+

Response 200

+
+
Response body
+
{
+  storeIds: string[]
+}
+
Example
{
+  "storeIds": [
+    "string"
+  ]
+}
+ + + + +
storeIdsstring[]Every store holding at least one item.
+
+ +
+ +
+
+

List item ids in a store.

+
list-item-idssrc/cli/engine/routes/dataStore.ts
+
+

coreaccount.dataStore.listItemIds

+ +
+

Command line

+
list-item-ids --store-id=<storeId>
+ + +
+

REST

+

GET/account/{sessionId}/list-item-ids

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+
+
Query
+
{
+  storeId: string
+}
+
Example
{
+  "storeId": "FS8xJ2kQ…"
+}
+ + + + +
storeIdstringPlugin or app namespace within the account data store.
+
+ + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/account/$SESS/list-item-ids?storeId=…'
+
+
+

Response 200

+
+
Response body
+
{
+  itemIds: string[]
+}
+
Example
{
+  "itemIds": [
+    "string"
+  ]
+}
+ + + + +
itemIdsstring[]Keys in this store. Empty if it has none.
+
+ +
+ +
+
+

Read an item.

+
get-itemsrc/cli/engine/routes/dataStore.ts
+
+

coreaccount.dataStore.getItem

+

Values are opaque strings; encoding is the caller's business.

+
+
+

Command line

+
get-item --store-id=<storeId> --item-id=<itemId>
+ + +
+

REST

+

GET/account/{sessionId}/get-item

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+
+
Query
+
{
+  storeId: string
+  itemId: string
+}
+
Example
{
+  "storeId": "FS8xJ2kQ…",
+  "itemId": "FS8xJ2kQ…"
+}
+ + + + + + + + + +
storeIdstringPlugin or app namespace within the account data store.
itemIdstringKey within the store.
+
+ + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/account/$SESS/get-item?storeId=…&itemId=…'
+
+
+

Response 200

+
+
Response body
+
{
+  value: string
+}
+
Example
{
+  "value": "string"
+}
+ + + + +
valuestringThe stored string.
+
+
Errors

404NOT_FOUND 400BAD_REQUEST

+
+ +
+
+

Write an item.

+
set-itemsrc/cli/engine/routes/dataStore.ts
+
+

coreaccount.dataStore.setItem

+

Creates the store if it does not exist.

+
+
+

Command line

+
set-item --store-id=<storeId> --item-id=<itemId> --value=<value>
+ + +
+

REST

+

POST/account/{sessionId}/set-item

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  storeId: string
+  itemId: string
+  value: string
+}
+
Example
{
+  "storeId": "FS8xJ2kQ…",
+  "itemId": "FS8xJ2kQ…",
+  "value": "string"
+}
+ + + + + + + + + + + + + + +
storeIdstringPlugin or app namespace within the account data store.
itemIdstringKey within the store.
valuestringThe string to store.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"storeId":"FS8xJ2kQ…","itemId":"FS8xJ2kQ…","value":"string"}' \
+  'http://localhost/account/$SESS/set-item'
+
+
+

Response 204

+

No body.

+
Errors

400BAD_REQUEST

+
+ +
+
+

Delete an item.

+
delete-itemsrc/cli/engine/routes/dataStore.ts
+
+

coreaccount.dataStore.deleteItem

+ +
+

Command line

+
delete-item --store-id=<storeId> --item-id=<itemId>
+ + +
+

REST

+

POST/account/{sessionId}/delete-item

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  storeId: string
+  itemId: string
+}
+
Example
{
+  "storeId": "FS8xJ2kQ…",
+  "itemId": "FS8xJ2kQ…"
+}
+ + + + + + + + + +
storeIdstringPlugin or app namespace within the account data store.
itemIdstringKey within the store.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"storeId":"FS8xJ2kQ…","itemId":"FS8xJ2kQ…"}' \
+  'http://localhost/account/$SESS/delete-item'
+
+
+

Response 204

+

No body.

+
Errors

400BAD_REQUEST

+
+ +
+
+

Delete an entire store.

+
delete-storesrc/cli/engine/routes/dataStore.ts
+
+

coreaccount.dataStore.deleteStore

+

Removes every item in it, which cannot be undone from this API.

+
+
+

Command line

+
delete-store --store-id=<storeId>
+ + +
+

REST

+

POST/account/{sessionId}/delete-store

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  storeId: string
+}
+
Example
{
+  "storeId": "FS8xJ2kQ…"
+}
+ + + + +
storeIdstringPlugin or app namespace within the account data store.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"storeId":"FS8xJ2kQ…"}' \
+  'http://localhost/account/$SESS/delete-store'
+
+
+

Response 204

+

No body.

+
Errors

400BAD_REQUEST

+
+ +

Wallet

+

Calls on a single EdgeCurrencyWallet. Each names its wallet with --wallet-id, which accepts a full id or any unique prefix.

+
+

Wallet state

+

Account-level wallet listing and creation, then per-wallet calls. A {walletId} segment accepts a unique prefix, so those routes can also return 404 WALLET_NOT_FOUND or 409 AMBIGUOUS_WALLET_ID.

+
+
+
+

Wallet detail.

+
wallet-infosrc/cli/engine/routes/wallets.ts
+
+

coreEngine composite of EdgeCurrencyWallet properties plus its EdgeCurrencyConfig token map.

+ +
+

Command line

+
wallet-info --wallet-id=<walletId>
+ + +
+

REST

+

GET/account/{sessionId}/wallet

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+
+
Query
+
{
+  walletId: string
+}
+
Example
{
+  "walletId": "FS8xJ2kQ…"
+}
+ + + + +
walletIdstringThe wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns 409 AMBIGUOUS_WALLET_ID with details.candidates.
+
+ + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/account/$SESS/wallet?walletId=…'
+
+
+

Response 200

+

Every WalletSummary field, plus denominations, walletSettings and allTokens.

+
unknown
+ +
+ +
+
+

Rename a wallet.

+
rename-walletsrc/cli/engine/routes/wallets.ts
+
+

corewallet.renameWallet

+ +
+

Command line

+
rename-wallet --wallet-id=<walletId> --name=<name>
+ + +
+

REST

+

POST/account/{sessionId}/wallet/rename-wallet

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  walletId: string
+  name: string
+}
+
Example
{
+  "walletId": "FS8xJ2kQ…",
+  "name": "string"
+}
+ + + + + + + + + +
walletIdstringThe wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns 409 AMBIGUOUS_WALLET_ID with details.candidates.
namestringThe new display name.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"walletId":"FS8xJ2kQ…","name":"string"}' \
+  'http://localhost/account/$SESS/wallet/rename-wallet'
+
+
+

Response 204

+

No body.

+
Errors

400BAD_REQUEST

+
+ +
+
+

Change a wallet's fiat currency.

+
set-fiat-currency-codesrc/cli/engine/routes/wallets.ts
+
+

corewallet.setFiatCurrencyCode

+

Affects how balances and history are priced, not the asset itself.

+
+
+

Command line

+
set-fiat-currency-code --wallet-id=<walletId> --fiat-currency-code=<fiatCurrencyCode>
+ + +
+

REST

+

POST/account/{sessionId}/wallet/set-fiat-currency-code

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  walletId: string
+  fiatCurrencyCode: string
+}
+
Example
{
+  "walletId": "FS8xJ2kQ…",
+  "fiatCurrencyCode": "string"
+}
+ + + + + + + + + +
walletIdstringThe wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns 409 AMBIGUOUS_WALLET_ID with details.candidates.
fiatCurrencyCodestringe.g. iso:EUR.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"walletId":"FS8xJ2kQ…","fiatCurrencyCode":"string"}' \
+  'http://localhost/account/$SESS/wallet/set-fiat-currency-code'
+
+
+

Response 204

+

No body.

+
Errors

400BAD_REQUEST

+
+ +
+
+

Pause or resume a wallet engine.

+
change-pausedsrc/cli/engine/routes/wallets.ts
+
+

corewallet.changePaused

+

A paused wallet stops syncing, which is how a caller quiets a chain it does not currently care about.

+
+
+

Command line

+
change-paused --wallet-id=<walletId> --paused=true|false
+ + +
+

REST

+

POST/account/{sessionId}/wallet/change-paused

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  walletId: string
+  paused: boolean
+}
+
Example
{
+  "walletId": "FS8xJ2kQ…",
+  "paused": true
+}
+ + + + + + + + + +
walletIdstringThe wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns 409 AMBIGUOUS_WALLET_ID with details.candidates.
pausedbooleanTrue to stop syncing.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"walletId":"FS8xJ2kQ…","paused":true}' \
+  'http://localhost/account/$SESS/wallet/change-paused'
+
+
+

Response 204

+

No body.

+
Errors

400BAD_REQUEST

+
+ +
+
+

Nudge one wallet to sync.

+
wallet-syncsrc/cli/engine/routes/wallets.ts
+
+

corewallet.sync

+ +
+

Command line

+
wallet-sync --wallet-id=<walletId>
+ + +
+

REST

+

POST/account/{sessionId}/wallet/sync

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  walletId: string
+}
+
Example
{
+  "walletId": "FS8xJ2kQ…"
+}
+ + + + +
walletIdstringThe wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns 409 AMBIGUOUS_WALLET_ID with details.candidates.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"walletId":"FS8xJ2kQ…"}' \
+  'http://localhost/account/$SESS/wallet/sync'
+
+
+

Response 204

+

No body.

+ +
+

Notes

  • Named wallet-sync on the CLI because sync is account.sync.
+
+
+

Rescan the blockchain from scratch.

+
resync-blockchainsrc/cli/engine/routes/wallets.ts
+
+

corewallet.resyncBlockchain

+

Drops cached chain state and re-scans. Expensive, and the wallet reports an incomplete balance until it finishes.

+
+
+

Command line

+
resync-blockchain --wallet-id=<walletId>
+ + +
+

REST

+

POST/account/{sessionId}/wallet/resync-blockchain

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  walletId: string
+}
+
Example
{
+  "walletId": "FS8xJ2kQ…"
+}
+ + + + +
walletIdstringThe wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns 409 AMBIGUOUS_WALLET_ID with details.candidates.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"walletId":"FS8xJ2kQ…"}' \
+  'http://localhost/account/$SESS/wallet/resync-blockchain'
+
+
+

Response 204

+

No body.

+ +
+

Notes

  • Returns when the resync is requested, not when it completes. Watch syncRatio for progress.
+
+
+

Split a wallet into another chain.

+
splitsrc/cli/engine/routes/wallets.ts
+
+

corewallet.split

+

Forked-chain support: derive a wallet of a different type from the same keys. list-splittable-wallet-types says which are valid.

+
+
+

Command line

+
split --wallet-id=<walletId> --split-wallets='<json>'
+ + +
+

REST

+

POST/account/{sessionId}/wallet/split

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  walletId: string
+  splitWallets: unknown[]
+}
+
Example
{
+  "walletId": "FS8xJ2kQ…",
+  "splitWallets": [
+    {}
+  ]
+}
+ + + + + + + + + +
walletIdstringThe wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns 409 AMBIGUOUS_WALLET_ID with details.candidates.
splitWalletsunknown[]EdgeSplitCurrencyWallet[]: walletType, name, fiatCurrencyCode.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"walletId":"FS8xJ2kQ…","splitWallets":[{}]}' \
+  'http://localhost/account/$SESS/wallet/split'
+
+
+

Response 200

+
+
Response body
+
{
+  results: unknown[]
+}
+
Example
{
+  "results": [
+    {}
+  ]
+}
+ + + + +
resultsunknown[]Per-entry outcomes, like batch create.
+
+
Errors

400BAD_REQUEST

+
+ +
+
+

Dump wallet engine state.

+
dump-datasrc/cli/engine/routes/wallets.ts
+
+

corewallet.dumpData

+

Plugin-defined debug output. Shape varies by plugin and can be very large.

+
+
+

Command line

+
dump-data --wallet-id=<walletId>
+ + +
+

REST

+

GET/account/{sessionId}/wallet/dump-data

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+
+
Query
+
{
+  walletId: string
+}
+
Example
{
+  "walletId": "FS8xJ2kQ…"
+}
+ + + + +
walletIdstringThe wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns 409 AMBIGUOUS_WALLET_ID with details.candidates.
+
+ + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/account/$SESS/wallet/dump-data?walletId=…'
+
+
+

Response 200

+

EdgeDataDump, straight from the plugin.

+
unknown
+ +
+ +
+
+

Balances for every asset in the wallet.

+
balance-mapsrc/cli/engine/routes/wallets.ts
+
+

corewallet.balanceMap Rendered as an array, with currencyCode and displayAmount added from the wallet's denominations.

+

The native currency plus every enabled token.

+
+
+

Command line

+
balance-map --wallet-id=<walletId> [--token-id=<value>]
+
Client-only flags
--token-idoptionalClient-side filter; core has no single-balance accessor.
+ +
+

REST

+

GET/account/{sessionId}/wallet/balance-map

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+
+
Query
+
{
+  walletId: string
+}
+
Example
{
+  "walletId": "FS8xJ2kQ…"
+}
+ + + + +
walletIdstringThe wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns 409 AMBIGUOUS_WALLET_ID with details.candidates.
+
+ + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/account/$SESS/wallet/balance-map?walletId=…'
+
+
+

Response 200

+
+
Response body
+
{
+  balances: {
+    tokenId: string | null;
+    currencyCode: string;
+    nativeAmount: string;
+    displayAmount: string
+  }[]
+}
+
Example
{
+  "balances": [
+    {}
+  ]
+}
+ + + + +
balances{ tokenId: string | null; currencyCode: string; nativeAmount: string; displayAmount: string; }[]One entry per asset the wallet holds, native coin first.
+
+ +
+

Notes

  • On the CLI, omit --token-id for the native asset rather than passing the literal null.
+
+
+

Receive addresses.

+
get-addressessrc/cli/engine/routes/wallets.ts
+
+

corewallet.getAddresses

+ +
+

Command line

+
get-addresses --wallet-id=<walletId> [--token-id=<tokenId>] [--force-index=<forceIndex>]
+ + +
+

REST

+

GET/account/{sessionId}/wallet/get-addresses

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+
+
Query
+
{
+  walletId: string
+  tokenId?: string | null
+  forceIndex?: number
+}
+
Example
{
+  "walletId": "FS8xJ2kQ…",
+  "tokenId": "FS8xJ2kQ…",
+  "forceIndex": 0
+}
+ + + + + + + + + + + + + + +
walletIdstringThe wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns 409 AMBIGUOUS_WALLET_ID with details.candidates.
tokenIdstring | null optionalDefaults to the native asset.
forceIndexnumber optionalDerive at a specific index.
+
+ + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/account/$SESS/wallet/get-addresses?walletId=…'
+
+
+

Response 200

+
+
Response body
+
{
+  addresses: unknown[]
+}
+
Example
{
+  "addresses": [
+    {}
+  ]
+}
+ + + + +
addressesunknown[]EdgeAddress[]: addressType, publicAddress, nativeBalance.
+
+ +
+ +

Tokens

+

Which tokens a wallet tracks. Enabled tokens are the ones it syncs balances for; detected ones were seen on-chain but are not yet enabled.

+
+
+
+

List a wallet's tokens.

+
wallet-tokenssrc/cli/engine/routes/tokens.ts
+
+

coreEngine composite of the EdgeCurrencyConfig token maps plus wallet.enabledTokenIds and wallet.detectedTokenIds.

+

"Enabled" tokens are the ones the wallet syncs balances for; "detected" ones were seen on-chain but are not yet enabled.

+
+
+

Command line

+
wallet-tokens --wallet-id=<walletId>
+ + +
+

REST

+

GET/account/{sessionId}/wallet/tokens

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+
+
Query
+
{
+  walletId: string
+}
+
Example
{
+  "walletId": "FS8xJ2kQ…"
+}
+ + + + +
walletIdstringThe wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns 409 AMBIGUOUS_WALLET_ID with details.candidates.
+
+ + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/account/$SESS/wallet/tokens?walletId=…'
+
+
+

Response 200

+
+
Response body
+
{
+  allTokens: {
+    [keys: string]: unknown
+  }
+  builtinTokens: {
+    [keys: string]: unknown
+  }
+  customTokens: {
+    [keys: string]: unknown
+  }
+  enabledTokenIds: string[]
+  detectedTokenIds: string[]
+}
+
Example
{
+  "allTokens": {},
+  "builtinTokens": {},
+  "customTokens": {},
+  "enabledTokenIds": [
+    "string"
+  ],
+  "detectedTokenIds": [
+    "string"
+  ]
+}
+ + + + + + + + + + + + + + + + + + + + + + + + +
allTokens{ [keys: string]: unknown; }Built-in and custom together, keyed by tokenId. Large on EVM chains.
builtinTokens{ [keys: string]: unknown; }EdgeToken by tokenId: everything the plugin ships with.
customTokens{ [keys: string]: unknown; }EdgeToken by tokenId: tokens this account added by hand.
enabledTokenIdsstring[]Which of the above the wallet is actually tracking.
detectedTokenIdsstring[]Seen on-chain but not enabled, so their balances are not synced.
+
+
Errors

404WALLET_NOT_FOUND 409AMBIGUOUS_WALLET_ID

+
+ +
+
+

Set the enabled token set.

+
change-enabled-token-idssrc/cli/engine/routes/tokens.ts
+
+

corewallet.changeEnabledTokenIds

+

Absolute: anything missing from tokenIds is disabled. Core has only this setter, so there is no add or remove call.

+
+
+

Command line

+
change-enabled-token-ids --wallet-id=<walletId> --token-ids='<json>' [--add=<value>] [--remove=<value>]
+
Client-only flags
--addoptionalRead the current set, add this id, write it back.
--removeoptionalRead the current set, drop this id, write it back.
+ +
+

REST

+

POST/account/{sessionId}/wallet/change-enabled-token-ids

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  walletId: string
+  tokenIds: string[]
+}
+
Example
{
+  "walletId": "FS8xJ2kQ…",
+  "tokenIds": [
+    "string"
+  ]
+}
+ + + + + + + + + +
walletIdstringThe wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns 409 AMBIGUOUS_WALLET_ID with details.candidates.
tokenIdsstring[]The complete desired set.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"walletId":"FS8xJ2kQ…","tokenIds":["string"]}' \
+  'http://localhost/account/$SESS/wallet/change-enabled-token-ids'
+
+
+

Response 200

+
+
Response body
+
{
+  enabledTokenIds: string[]
+}
+
Example
{
+  "enabledTokenIds": [
+    "string"
+  ]
+}
+ + + + +
enabledTokenIdsstring[]The wallet’s enabled tokens after the change, not just what changed.
+
+
Errors

400BAD_REQUEST 404WALLET_NOT_FOUND 409AMBIGUOUS_WALLET_ID

+
+

Notes

  • The command's --add and --remove are client-side sugar over this one route, and cost an extra read first.
+

Transactions

+

Reading transaction history, exporting it, and editing its metadata.

+
+
+
+

List or export a wallet's transactions.

+
get-transactionssrc/cli/engine/routes/transactions.ts
+
+

corewallet.getTransactions

Differs from core:

  • limit — Engine-side paging; core returns every match.
  • offset — Engine-side paging; core returns every match.
  • fiat — Selects the currency the engine values each transaction in.
  • exportFormat — Engine-side rendering to CSV, QBO or Bitwave.
  • bitwaveAccountId — Required by the Bitwave export format.
+

Reads history, overlays the display metadata the GUI shows, fills historical fiat, and optionally formats the result — all on this one call.

+
+
+

Command line

+
get-transactions --wallet-id=<walletId> [--token-id=<tokenId>] [--limit=<limit>] [--offset=<offset>] [--start-date=<startDate>] [--end-date=<endDate>] [--search-string=<searchString>] [--spam-threshold=<spamThreshold>] [--fiat=<fiat>] [--export-format=<exportFormat>] [--bitwave-account=<bitwaveAccountId>] [--out=<value>]
+
Client-only flags
--outoptionalWhere to write the returned files. One format: the path. Several: a stem, plus .csv / .qbo / .bitwave.csv.
+ +
+

REST

+

GET/account/{sessionId}/wallet/get-transactions

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+
+
Query
+
{
+  walletId: string
+  tokenId?: string | null
+  limit?: number
+  offset?: number
+  startDate?: Date
+  endDate?: Date
+  searchString?: string
+  spamThreshold?: string
+  fiat?: string
+  exportFormat?: string
+  bitwaveAccountId?: string
+}
+
Example
{
+  "walletId": "FS8xJ2kQ…",
+  "tokenId": "FS8xJ2kQ…",
+  "limit": 0,
+  "offset": 0,
+  "startDate": "<Date>",
+  "endDate": "<Date>",
+  "searchString": "string",
+  "spamThreshold": "string",
+  "fiat": "2026-09-02T16:35:00.000Z",
+  "exportFormat": "2026-09-02T16:35:00.000Z",
+  "bitwaveAccountId": "FS8xJ2kQ…"
+}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
walletIdstringThe wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns 409 AMBIGUOUS_WALLET_ID with details.candidates.
tokenIdstring | null optionalDefaults to the native asset.
limitnumber optionalOmitting it returns every transaction from offset on.
offsetnumber optionalWhere to start. Defaults to 0.
startDateDate optionalISO-8601, or epoch milliseconds.
endDateDate optionalISO-8601, or epoch milliseconds.
searchStringstring optionalMatches payee, category, notes and txid.
spamThresholdstring optionalNative-amount floor. Omitted, the account spam-filter setting applies; passing it always overrides.
fiatstring optionalThree-letter ISO 4217 code. Defaults to the account defaultIsoFiat.
exportFormatstring optionalComma list of csv, qbo, bitwave.
bitwaveAccountIdstring optionalA 400 unless exportFormat includes bitwave.
+
+ + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/account/$SESS/wallet/get-transactions?walletId=…'
+
+
+

Response 200

+

{ transactions, total, isoFiat }, or { ok, isoFiat, total, files } when exportFormat is set.

+
unknown
+
Errors

400BAD_REQUEST 400MISSING_BITWAVE_ACCOUNT_ID 404WALLET_NOT_FOUND 409AMBIGUOUS_WALLET_ID

+
+

Notes

  • The metadata overlay and the fiat fill are response-only. Neither writes to disk.
  • limit and offset apply before the fiat fill, so a large page costs proportionally more rates-server work.
  • This is the one GET that can write: passing bitwaveAccountId persists it to exportTxInfo.json on the wallet disklet.
+
+
+

Count transactions in a wallet.

+
get-num-transactionssrc/cli/engine/routes/transactions.ts
+
+

corewallet.getNumTransactions

+

Cheaper than listing when only the total matters.

+
+
+

Command line

+
get-num-transactions --wallet-id=<walletId> [--token-id=<tokenId>]
+ + +
+

REST

+

GET/account/{sessionId}/wallet/get-num-transactions

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+
+
Query
+
{
+  walletId: string
+  tokenId?: string | null
+}
+
Example
{
+  "walletId": "FS8xJ2kQ…",
+  "tokenId": "FS8xJ2kQ…"
+}
+ + + + + + + + + +
walletIdstringThe wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns 409 AMBIGUOUS_WALLET_ID with details.candidates.
tokenIdstring | null optionalDefaults to the native asset.
+
+ + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/account/$SESS/wallet/get-num-transactions?walletId=…'
+
+
+

Response 200

+
+
Response body
+
{
+  numTransactions: number
+}
+
Example
{
+  "numTransactions": 0
+}
+ + + + +
numTransactionsnumberEvery transaction the wallet knows of.
+
+
Errors

404WALLET_NOT_FOUND 409AMBIGUOUS_WALLET_ID

+
+

Notes

  • Unfiltered: spamThreshold, dates and searchString do not apply, so this can exceed total from get-transactions.
+
+
+

Save transaction metadata.

+
save-tx-metadatasrc/cli/engine/routes/transactions.ts
+
+

corewallet.saveTxMetadata

+

One of only two routes that write transaction metadata to disk.

+
+
+

Command line

+
save-tx-metadata --wallet-id=<walletId> --txid=<txid> [--token-id=<tokenId>] --metadata='<json>'
+ + +
+

REST

+

POST/account/{sessionId}/wallet/save-tx-metadata

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  walletId: string
+  txid: string
+  tokenId?: string | null
+  metadata: unknown
+}
+
Example
{
+  "walletId": "FS8xJ2kQ…",
+  "txid": "FS8xJ2kQ…",
+  "tokenId": "FS8xJ2kQ…",
+  "metadata": {}
+}
+ + + + + + + + + + + + + + + + + + + +
walletIdstringThe wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns 409 AMBIGUOUS_WALLET_ID with details.candidates.
txidstringWhich transaction to tag.
tokenIdstring | null optionalDefaults to the native asset.
metadataunknownEdgeMetadataChange: name, category, notes, exchangeAmount.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"walletId":"FS8xJ2kQ…","txid":"FS8xJ2kQ…","tokenId":"FS8xJ2kQ…","metadata":{}}' \
+  'http://localhost/account/$SESS/wallet/save-tx-metadata'
+
+
+

Response 204

+

No body.

+
Errors

400BAD_REQUEST 404WALLET_NOT_FOUND 409AMBIGUOUS_WALLET_ID

+
+

Notes

  • metadata is an EdgeMetadataChange, so an explicit null clears a field while an omitted one is left alone.
+
+
+

Save a transaction action.

+
save-tx-actionsrc/cli/engine/routes/transactions.ts
+
+

corewallet.saveTxAction

+

Records what a transaction was — a swap, a stake — beyond its metadata.

+
+
+

Command line

+
save-tx-action --wallet-id=<walletId> --txid=<txid> [--token-id=<tokenId>] --saved-action='<json>' [--asset-action='<json>']
+ + +
+

REST

+

POST/account/{sessionId}/wallet/save-tx-action

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  walletId: string
+  txid: string
+  tokenId?: string | null
+  savedAction: unknown
+  assetAction?: unknown
+}
+
Example
{
+  "walletId": "FS8xJ2kQ…",
+  "txid": "FS8xJ2kQ…",
+  "tokenId": "FS8xJ2kQ…",
+  "savedAction": {},
+  "assetAction": {}
+}
+ + + + + + + + + + + + + + + + + + + + + + + + +
walletIdstringThe wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns 409 AMBIGUOUS_WALLET_ID with details.candidates.
txidstringWhich transaction to annotate.
tokenIdstring | null optionalDefaults to the native asset.
savedActionunknownEdgeTxAction describing what happened.
assetActionunknown optionalEdgeAssetAction.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"walletId":"FS8xJ2kQ…","txid":"FS8xJ2kQ…","tokenId":"FS8xJ2kQ…","savedAction":{},"assetAction":{}}' \
+  'http://localhost/account/$SESS/wallet/save-tx-action'
+
+
+

Response 204

+

No body.

+
Errors

400BAD_REQUEST 404WALLET_NOT_FOUND 409AMBIGUOUS_WALLET_ID

+
+

Notes

  • When assetAction is omitted it defaults to { assetActionType: 'transfer' }.
+

Spending

+

Two ways to send funds. spend does the whole thing in one call; the staged workflow — make-spend, sign-tx, broadcast-tx, save-tx — hands back an object handle at each step so fees can be inspected before committing.

+
+
+
+

Largest sendable amount.

+
get-max-spendablesrc/cli/engine/routes/spend.ts
+
+

corewallet.getMaxSpendable

Differs from core:

  • to — Shorthand the engine expands into spendTargets, so a one-output send needs no nested JSON.
  • nativeAmount — Amount for the to shorthand, in the smallest unit.
  • amount — Amount for the to shorthand, in whole coins.
+

What empties the wallet after fees. A destination is still required, since fees depend on it.

+
+
+

Command line

+
get-max-spendable --wallet-id=<walletId> [--spend-info='<json>'] [--to=<to>] [--native-amount=<nativeAmount>] [--amount=<amount>] [--token-id=<tokenId>] [--metadata='<json>']
+ + +
+

REST

+

POST/account/{sessionId}/wallet/get-max-spendable

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  walletId: string
+  spendInfo?: unknown
+  to?: string
+  nativeAmount?: string
+  amount?: string
+  tokenId?: string | null
+  metadata?: unknown
+}
+
Example
{
+  "walletId": "FS8xJ2kQ…",
+  "spendInfo": {},
+  "to": "string",
+  "nativeAmount": "12345",
+  "amount": "12345",
+  "tokenId": "FS8xJ2kQ…",
+  "metadata": {}
+}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
walletIdstringThe wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns 409 AMBIGUOUS_WALLET_ID with details.candidates.
spendInfounknown optionalA full EdgeSpendInfo, used as-is when present.
tostring optionalAddress or BIP21 URI, run through wallet.parseUri.
nativeAmountstring optionalHow much, in native units.
amountstring optionalAlias of nativeAmount.
tokenIdstring | null optionalDefaults to the native asset.
metadataunknown optionalWins over anything parsed out of the URI.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"walletId":"FS8xJ2kQ…","spendInfo":{},"to":"string","nativeAmount":"12345","amount":"12345","tokenId":"FS8xJ2kQ…","metadata":{}}' \
+  'http://localhost/account/$SESS/wallet/get-max-spendable'
+
+
+

Response 200

+
+
Response body
+
{
+  nativeAmount: string
+}
+
Example
{
+  "nativeAmount": "12345"
+}
+ + + + +
nativeAmountstringThe most this wallet can send.
+
+
Errors

422INSUFFICIENT_FUNDS 400BAD_REQUEST 503NETWORK_ERROR

+
+ +
+
+

Send funds.

+
spendsrc/cli/engine/routes/spend.ts
+
+

coreGUI composite: makeSpend, signTx, broadcastTx and saveTx together.

+

makeSpend, then signTx, then optionally broadcastTx and saveTx, in one request. broadcast and save both default to true, so a bare body with a destination and an amount moves real money. A completed spend leaves no handle behind.

+
+
+

Command line

+
spend --wallet-id=<walletId> [--spend-info='<json>'] [--to=<to>] [--native-amount=<nativeAmount>] [--amount=<amount>] [--token-id=<tokenId>] [--metadata='<json>'] [--use-max] [--dry-run] [--broadcast] [--save]
+ + +
+

REST

+

POST/account/{sessionId}/wallet/spend

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  walletId: string
+  spendInfo?: unknown
+  to?: string
+  nativeAmount?: string
+  amount?: string
+  tokenId?: string | null
+  metadata?: unknown
+  useMax?: boolean
+  dryRun?: boolean
+  broadcast?: boolean
+  save?: boolean
+}
+
Example
{
+  "walletId": "FS8xJ2kQ…",
+  "spendInfo": {},
+  "to": "string",
+  "nativeAmount": "12345",
+  "amount": "12345",
+  "tokenId": "FS8xJ2kQ…",
+  "metadata": {},
+  "useMax": true,
+  "dryRun": true,
+  "broadcast": true,
+  "save": true
+}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
walletIdstringThe wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns 409 AMBIGUOUS_WALLET_ID with details.candidates.
spendInfounknown optionalA full EdgeSpendInfo, used as-is when present.
tostring optionalAddress or BIP21 URI, run through wallet.parseUri.
nativeAmountstring optionalHow much, in native units.
amountstring optionalAlias of nativeAmount.
tokenIdstring | null optionalDefaults to the native asset.
metadataunknown optionalWins over anything parsed out of the URI.
useMaxboolean optionalReplace the first target's amount with the maximum.
dryRunboolean optionalBuild only. Never signs or broadcasts.
broadcastboolean optionalDefaults to true.
saveboolean optionalDefaults to true.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"walletId":"FS8xJ2kQ…","spendInfo":{},"to":"string","nativeAmount":"12345","amount":"12345","tokenId":"FS8xJ2kQ…","metadata":{},"useMax":true,"dryRun":true,"broadcast":true,"save":true}' \
+  'http://localhost/account/$SESS/wallet/spend'
+
+
+

Response 200

+

{ transaction }, plus saveError when the broadcast succeeded but saving failed. With dryRun, a TransactionHandle instead.

+
unknown
+
Errors

422INSUFFICIENT_FUNDS 422DUST_SPEND 422PENDING_FUNDS 422SPEND_TO_SELF 400NO_AMOUNT_SPECIFIED 400BAD_REQUEST 503NETWORK_ERROR

+
+

Notes

  • BIP21 label and message from to become metadata name and notes. An explicit metadata object wins.
  • saveError is the case to handle. Once broadcast, the money is gone, so a failure inside saveTx cannot throw — it would hide the txid of a real payment. The response is 200 with the transaction plus saveError.
  • With dryRun, only makeSpend runs and the response is a transaction handle that expires in 5 minutes.
+
+
+

Build an unsigned transaction.

+
make-spendsrc/cli/engine/routes/spend.ts
+
+

corewallet.makeSpend

Differs from core:

  • to — Shorthand the engine expands into spendTargets, so a one-output send needs no nested JSON.
  • nativeAmount — Amount for the to shorthand, in the smallest unit.
  • amount — Amount for the to shorthand, in whole coins.
+

First step of the staged workflow: nothing is signed and no funds move. Inspect transaction.networkFee on the result before signing.

+
+
+

Command line

+
make-spend --wallet-id=<walletId> [--spend-info='<json>'] [--to=<to>] [--native-amount=<nativeAmount>] [--amount=<amount>] [--token-id=<tokenId>] [--metadata='<json>']
+ + +
+

REST

+

POST/account/{sessionId}/wallet/make-spend

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  walletId: string
+  spendInfo?: unknown
+  to?: string
+  nativeAmount?: string
+  amount?: string
+  tokenId?: string | null
+  metadata?: unknown
+}
+
Example
{
+  "walletId": "FS8xJ2kQ…",
+  "spendInfo": {},
+  "to": "string",
+  "nativeAmount": "12345",
+  "amount": "12345",
+  "tokenId": "FS8xJ2kQ…",
+  "metadata": {}
+}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
walletIdstringThe wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns 409 AMBIGUOUS_WALLET_ID with details.candidates.
spendInfounknown optionalA full EdgeSpendInfo, used as-is when present.
tostring optionalAddress or BIP21 URI, run through wallet.parseUri.
nativeAmountstring optionalHow much, in native units.
amountstring optionalAlias of nativeAmount.
tokenIdstring | null optionalDefaults to the native asset.
metadataunknown optionalWins over anything parsed out of the URI.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"walletId":"FS8xJ2kQ…","spendInfo":{},"to":"string","nativeAmount":"12345","amount":"12345","tokenId":"FS8xJ2kQ…","metadata":{}}' \
+  'http://localhost/account/$SESS/wallet/make-spend'
+
+
+

Response 200

+
+
Response body
+
{
+  objectId: string
+  kind: string
+  expiresAt: string
+  sessionId?: string
+  walletId?: string
+  transaction: unknown
+}
+
Example
{
+  "objectId": "FS8xJ2kQ…",
+  "kind": "string",
+  "expiresAt": "2026-09-02T16:35:00.000Z",
+  "sessionId": "FS8xJ2kQ…",
+  "walletId": "FS8xJ2kQ…",
+  "transaction": {}
+}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
objectIdstringHandle for the value the engine is holding. Pass it to the calls that consume it.
kindstringWhat the handle refers to, which decides the calls that accept it.
expiresAtstringWhen the engine drops the handle. Handles live 5 minutes.
sessionIdstring optionalSession that created the handle; only that session may use it.
walletIdstring optionalWallet the handle is bound to, when it belongs to one.
transactionunknownEdgeTransaction as it stands after this step. Unsigned after make-spend, signed after sign-tx, and carrying a txid once broadcast.
+
+
Errors

422INSUFFICIENT_FUNDS 422DUST_SPEND 400NO_AMOUNT_SPECIFIED 400BAD_REQUEST

+
+ +
+
+

Sign a staged transaction.

+
sign-txsrc/cli/engine/routes/spend.ts
+
+

corewallet.signTx

+

Keeps the same handle and pushes its expiry out another five minutes.

+
+
+

Command line

+
sign-tx <objectId>
+ + +
+

REST

+

POST/account/{sessionId}/sign-tx/{objectId}

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
objectIdstringFrom make-spend.
+ + + +
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"objectId":"FS8xJ2kQ…"}' \
+  'http://localhost/account/$SESS/sign-tx/$OBJECTID'
+
+
+

Response 200

+
+
Response body
+
{
+  objectId: string
+  kind: string
+  expiresAt: string
+  sessionId?: string
+  walletId?: string
+  transaction: unknown
+}
+
Example
{
+  "objectId": "FS8xJ2kQ…",
+  "kind": "string",
+  "expiresAt": "2026-09-02T16:35:00.000Z",
+  "sessionId": "FS8xJ2kQ…",
+  "walletId": "FS8xJ2kQ…",
+  "transaction": {}
+}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
objectIdstringHandle for the value the engine is holding. Pass it to the calls that consume it.
kindstringWhat the handle refers to, which decides the calls that accept it.
expiresAtstringWhen the engine drops the handle. Handles live 5 minutes.
sessionIdstring optionalSession that created the handle; only that session may use it.
walletIdstring optionalWallet the handle is bound to, when it belongs to one.
transactionunknownEdgeTransaction as it stands after this step. Unsigned after make-spend, signed after sign-tx, and carrying a txid once broadcast.
+
+
Errors

400BAD_REQUEST

+
+ +
+
+

Broadcast a signed transaction.

+
broadcast-txsrc/cli/engine/routes/spend.ts
+
+

corewallet.broadcastTx

+

The irreversible step: once this returns, the funds have left the wallet.

+
+
+

Command line

+
broadcast-tx <objectId>
+ + +
+

REST

+

POST/account/{sessionId}/broadcast-tx/{objectId}

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
objectIdstringFrom sign-tx.
+ + + +
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"objectId":"FS8xJ2kQ…"}' \
+  'http://localhost/account/$SESS/broadcast-tx/$OBJECTID'
+
+
+

Response 200

+

The handle survives, so save-tx can still run.

+
+
Response body
+
{
+  objectId: string
+  kind: string
+  expiresAt: string
+  sessionId?: string
+  walletId?: string
+  transaction: unknown
+}
+
Example
{
+  "objectId": "FS8xJ2kQ…",
+  "kind": "string",
+  "expiresAt": "2026-09-02T16:35:00.000Z",
+  "sessionId": "FS8xJ2kQ…",
+  "walletId": "FS8xJ2kQ…",
+  "transaction": {}
+}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
objectIdstringHandle for the value the engine is holding. Pass it to the calls that consume it.
kindstringWhat the handle refers to, which decides the calls that accept it.
expiresAtstringWhen the engine drops the handle. Handles live 5 minutes.
sessionIdstring optionalSession that created the handle; only that session may use it.
walletIdstring optionalWallet the handle is bound to, when it belongs to one.
transactionunknownEdgeTransaction as it stands after this step. Unsigned after make-spend, signed after sign-tx, and carrying a txid once broadcast.
+
+
Errors

400BAD_REQUEST 503NETWORK_ERROR

+
+

Notes

  • Broadcasting does not record the transaction locally. Follow with save-tx, or it stays missing from history until a sync finds it.
+
+
+

Record a transaction and release its handle.

+
save-txsrc/cli/engine/routes/spend.ts
+
+

corewallet.saveTx

+

Final step. The handle is gone afterwards, so a second call is a 404.

+
+
+

Command line

+
save-tx <objectId>
+ + +
+

REST

+

POST/account/{sessionId}/save-tx/{objectId}

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
objectIdstringThe handle to persist and release.
+ + + +
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"objectId":"FS8xJ2kQ…"}' \
+  'http://localhost/account/$SESS/save-tx/$OBJECTID'
+
+
+

Response 200

+
+
Response body
+
{
+  ok: boolean
+  objectId: string
+}
+
Example
{
+  "ok": true,
+  "objectId": "FS8xJ2kQ…"
+}
+ + + + + + + + + +
okbooleanAlways true; a failure arrives as an error envelope.
objectIdstringThe handle this call consumed. It is now expired.
+
+
Errors

400BAD_REQUEST

+
+ +
+
+

Fee-bump a pending transaction.

+
acceleratesrc/cli/engine/routes/spend.ts
+
+

corewallet.accelerate

Differs from core:

  • transaction — Core names the parameter tx. Spelled out here to match the transaction field every staged-transaction response returns.
+

Replace-by-fee, where the plugin supports it. Returns a new unsigned transaction to sign and broadcast.

+
+
+

Command line

+
accelerate --wallet-id=<walletId> [--object-id=<objectId>] [--transaction='<json>']
+ + +
+

REST

+

POST/account/{sessionId}/wallet/accelerate

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  walletId: string
+  objectId?: string
+  transaction?: unknown
+}
+
Example
{
+  "walletId": "FS8xJ2kQ…",
+  "objectId": "FS8xJ2kQ…",
+  "transaction": {}
+}
+ + + + + + + + + + + + + + +
walletIdstringThe wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns 409 AMBIGUOUS_WALLET_ID with details.candidates.
objectIdstring optionalHandle of the transaction to bump.
transactionunknown optionalOr the transaction itself.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"walletId":"FS8xJ2kQ…","objectId":"FS8xJ2kQ…","transaction":{}}' \
+  'http://localhost/account/$SESS/wallet/accelerate'
+
+
+

Response 200

+

Given objectId the same handle is updated; given a transaction a new one is created.

+
+
Response body
+
{
+  objectId: string
+  kind: string
+  expiresAt: string
+  sessionId?: string
+  walletId?: string
+  transaction: unknown
+}
+
Example
{
+  "objectId": "FS8xJ2kQ…",
+  "kind": "string",
+  "expiresAt": "2026-09-02T16:35:00.000Z",
+  "sessionId": "FS8xJ2kQ…",
+  "walletId": "FS8xJ2kQ…",
+  "transaction": {}
+}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
objectIdstringHandle for the value the engine is holding. Pass it to the calls that consume it.
kindstringWhat the handle refers to, which decides the calls that accept it.
expiresAtstringWhen the engine drops the handle. Handles live 5 minutes.
sessionIdstring optionalSession that created the handle; only that session may use it.
walletIdstring optionalWallet the handle is bound to, when it belongs to one.
transactionunknownEdgeTransaction as it stands after this step. Unsigned after make-spend, signed after sign-tx, and carrying a txid once broadcast.
+
+
Errors

400BAD_REQUEST

+
+

Notes

  • A plugin that cannot accelerate returns 400 rather than a null transaction.
+
+
+

Sweep private keys into this wallet.

+
sweep-private-keyssrc/cli/engine/routes/spend.ts
+
+

corewallet.sweepPrivateKeys

Differs from core:

  • spendInfo — Core names this one edgeSpendInfo while makeSpend names the same type spendInfo. Both are spendInfo here.
+

Builds a transaction moving everything from an external key. Returns an unsigned handle: sign, broadcast and save it like any staged spend.

+
+
+

Command line

+
sweep-private-keys --wallet-id=<walletId> --spend-info='<json>'
+ + +
+

REST

+

POST/account/{sessionId}/wallet/sweep-private-keys

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  walletId: string
+  spendInfo: unknown
+}
+
Example
{
+  "walletId": "FS8xJ2kQ…",
+  "spendInfo": {}
+}
+ + + + + + + + + +
walletIdstringThe wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns 409 AMBIGUOUS_WALLET_ID with details.candidates.
spendInfounknownA full EdgeSpendInfo, with the keys to sweep in privateKeys.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"walletId":"FS8xJ2kQ…","spendInfo":{}}' \
+  'http://localhost/account/$SESS/wallet/sweep-private-keys'
+
+
+

Response 200

+
+
Response body
+
{
+  objectId: string
+  kind: string
+  expiresAt: string
+  sessionId?: string
+  walletId?: string
+  transaction: unknown
+}
+
Example
{
+  "objectId": "FS8xJ2kQ…",
+  "kind": "string",
+  "expiresAt": "2026-09-02T16:35:00.000Z",
+  "sessionId": "FS8xJ2kQ…",
+  "walletId": "FS8xJ2kQ…",
+  "transaction": {}
+}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
objectIdstringHandle for the value the engine is holding. Pass it to the calls that consume it.
kindstringWhat the handle refers to, which decides the calls that accept it.
expiresAtstringWhen the engine drops the handle. Handles live 5 minutes.
sessionIdstring optionalSession that created the handle; only that session may use it.
walletIdstring optionalWallet the handle is bound to, when it belongs to one.
transactionunknownEdgeTransaction as it stands after this step. Unsigned after make-spend, signed after sign-tx, and carrying a txid once broadcast.
+
+
Errors

400BAD_REQUEST 422INSUFFICIENT_FUNDS 503NETWORK_ERROR

+
+ +
+
+

Sign arbitrary bytes.

+
sign-bytessrc/cli/engine/routes/spend.ts
+
+

corewallet.signBytes

Differs from core:

  • bytes — Core takes a Uint8Array named buf. JSON cannot carry bytes, so this is base64 text.
+

Message signing and proof-of-ownership, for plugins that support it.

+
+
+

Command line

+
sign-bytes --wallet-id=<walletId> [--bytes=<bytes>] [--other-params='<json>']
+ + +
+

REST

+

POST/account/{sessionId}/wallet/sign-bytes

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  walletId: string
+  bytes?: string
+  otherParams?: unknown
+}
+
Example
{
+  "walletId": "FS8xJ2kQ…",
+  "bytes": "string",
+  "otherParams": {}
+}
+ + + + + + + + + + + + + + +
walletIdstringThe wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns 409 AMBIGUOUS_WALLET_ID with details.candidates.
bytesstring optionalBase64. Defaults to empty when absent.
otherParamsunknown optionalPlugin-specific options. Bitcoin needs { publicAddress }; other plugins take nothing, or refuse the call entirely.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"walletId":"FS8xJ2kQ…","bytes":"string","otherParams":{}}' \
+  'http://localhost/account/$SESS/wallet/sign-bytes'
+
+
+

Response 200

+
+
Response body
+
{
+  signature: string
+}
+
Example
{
+  "signature": "string"
+}
+ + + + +
signaturestringBase64.
+
+
Errors

400BAD_REQUEST

+
+

Notes

  • Invalid base64 decodes to empty rather than erroring, so validate before sending.
  • Support is per plugin, and failures surface as 500 INTERNAL_ERROR from the plugin rather than as a typed error: litecoin answers "litecoin doesn't support signBytes", and bitcoin requires otherParams.publicAddress naming which address to sign with.
+
+
+

Fetch a BIP70 payment request.

+
get-payment-protocol-infosrc/cli/engine/routes/spend.ts
+
+

corewallet.getPaymentProtocolInfo

+

Feed spendTargets from the result into make-spend to pay it.

+
+
+

Command line

+
get-payment-protocol-info --wallet-id=<walletId> --payment-protocol-url=<paymentProtocolUrl>
+ + +
+

REST

+

GET/account/{sessionId}/wallet/get-payment-protocol-info

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+
+
Query
+
{
+  walletId: string
+  paymentProtocolUrl: string
+}
+
Example
{
+  "walletId": "FS8xJ2kQ…",
+  "paymentProtocolUrl": "string"
+}
+ + + + + + + + + +
walletIdstringThe wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns 409 AMBIGUOUS_WALLET_ID with details.candidates.
paymentProtocolUrlstringThe payment-request URL.
+
+ + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/account/$SESS/wallet/get-payment-protocol-info?walletId=…&paymentProtocolUrl=…'
+
+
+

Response 200

+

EdgePaymentProtocolInfo: domain, memo, merchant, nativeAmount, spendTargets.

+
unknown
+
Errors

400BAD_REQUEST 503NETWORK_ERROR

+
+ +

URIs

+

Parsing and building BIP21-style payment URIs through the wallet’s own plugin, so chain-specific quirks are handled for you.

+
+
+
+

Parse a payment URI or address.

+
parse-urisrc/cli/engine/routes/uri.ts
+
+

corewallet.parseUri

+

What the GUI address tile does when you paste or scan something.

+
+
+

Command line

+
parse-uri --wallet-id=<walletId> --uri=<uri> [--currency-code=<currencyCode>]
+ + +
+

REST

+

POST/account/{sessionId}/wallet/parse-uri

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  walletId: string
+  uri: string
+  currencyCode?: string
+}
+
Example
{
+  "walletId": "FS8xJ2kQ…",
+  "uri": "string",
+  "currencyCode": "string"
+}
+ + + + + + + + + + + + + + +
walletIdstringThe wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns 409 AMBIGUOUS_WALLET_ID with details.candidates.
uristringA payment URI or a bare address.
currencyCodestring optionalDisambiguates on chains that carry several assets.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"walletId":"FS8xJ2kQ…","uri":"string","currencyCode":"string"}' \
+  'http://localhost/account/$SESS/wallet/parse-uri'
+
+
+

Response 200

+

EdgeParsedUri: publicAddress, nativeAmount, currencyCode, metadata, paymentProtocolUrl, …

+
unknown
+
Errors

400BAD_REQUEST 404WALLET_NOT_FOUND 409AMBIGUOUS_WALLET_ID

+
+

Notes

  • spend and make-spend run their to field through this same call, so parsing separately is only needed to inspect or confirm first.
+
+
+

Build a payment URI.

+
encode-urisrc/cli/engine/routes/uri.ts
+
+

corewallet.encodeUri

+

For a receive screen or a QR code.

+
+
+

Command line

+
encode-uri --wallet-id=<walletId> --public-address=<publicAddress> [--native-amount=<nativeAmount>] [--label=<label>] [--message=<message>] [--currency-code=<currencyCode>]
+ + +
+

REST

+

POST/account/{sessionId}/wallet/encode-uri

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  walletId: string
+  publicAddress: string
+  nativeAmount?: string
+  label?: string
+  message?: string
+  currencyCode?: string
+}
+
Example
{
+  "walletId": "FS8xJ2kQ…",
+  "publicAddress": "string",
+  "nativeAmount": "12345",
+  "label": "string",
+  "message": "string",
+  "currencyCode": "string"
+}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
walletIdstringThe wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns 409 AMBIGUOUS_WALLET_ID with details.candidates.
publicAddressstringWhere the payment should go.
nativeAmountstring optionalAmount, in the native unit.
labelstring optionalBIP21 label; becomes metadata.name when parsed back.
messagestring optionalBIP21 message; becomes metadata.notes.
currencyCodestring optionalDisambiguates on chains that carry several assets.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"walletId":"FS8xJ2kQ…","publicAddress":"string","nativeAmount":"12345","label":"string","message":"string","currencyCode":"string"}' \
+  'http://localhost/account/$SESS/wallet/encode-uri'
+
+
+

Response 200

+
+
Response body
+
{
+  uri: string
+}
+
Example
{
+  "uri": "string"
+}
+ + + + +
uristringThe encoded URI, ready for a QR code.
+
+
Errors

400BAD_REQUEST 404WALLET_NOT_FOUND 409AMBIGUOUS_WALLET_ID

+
+

Notes

  • Only these five fields are read; a fuller EdgeEncodeUri has its extras ignored.
+

Local settings

+

Device-local account settings, stored outside the synced repos. They follow the account but never leave the machine.

+
+

Local settings

+

Device-local account settings, stored outside the synced repos.

+
+
+
+

Local settings.

+
local-settingssrc/cli/engine/routes/localSettings.ts
+
+

coreGUI code (src/util/localAccountSettings), reached through account.localDisklet.

+

Device-local account settings, stored in Settings.json on account.localDisklet. They are not synced — a phone and a CLI keep separate copies unless they share an Edge data directory.

+
+
+

Command line

+
local-settings
+ + +
+

REST

+

GET/account/{sessionId}/local-settings

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/account/$SESS/local-settings'
+
+
+

Response 200

+
+
Response body
+
{
+  spamFilterOn: boolean
+}
+
Example
{
+  "spamFilterOn": true
+}
+ + + + +
spamFilterOnbooleanHide spam transactions in get-transactions results. Defaults to true, matching the GUI. The filter hides rows; it never changes stored metadata.
+
+ +
+ +
+
+

Change local settings.

+
local-settingssrc/cli/engine/routes/localSettings.ts
+
+

coreGUI code (src/util/localAccountSettings).

+

Writes device-local account settings. Every option is a field on the body; spamFilterOn is the only one today, and new options are added alongside it.

+
+
+

Command line

+
local-settings --spam-filter-on=true|false
+ +

With no flag the command reads; with one it writes.

+
+
+

REST

+

POST/account/{sessionId}/change-local-settings

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
+ + +
+
Request body
+
{
+  spamFilterOn: boolean
+}
+
Example
{
+  "spamFilterOn": true
+}
+ + + + +
spamFilterOnbooleanHide spam transactions in get-transactions results. Defaults to true, matching the GUI. The filter hides rows; it never changes stored metadata.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"spamFilterOn":true}' \
+  'http://localhost/account/$SESS/change-local-settings'
+
+
+

Response 200

+
+
Response body
+
{
+  spamFilterOn: boolean
+}
+
Example
{
+  "spamFilterOn": true
+}
+ + + + +
spamFilterOnbooleanHide spam transactions in get-transactions results. Defaults to true, matching the GUI. The filter hides rows; it never changes stored metadata.
+
+ +
+

Notes

  • Omitting a field is a 400, not a no-op, so a caller cannot clear a setting by accident.
+

Exchange rates

+

Fiat and crypto pricing, current and historical.

+
+

Exchange rates

+

Historical and current rates through the same batching queue the GUI uses. No session required.

+
+
+
+

Batch crypto and fiat rate lookups.

+
rates-querysrc/cli/engine/routes/rates.ts
+
+

coreGUI code (src/util/exchangeRates): getHistoricalCryptoRate and getHistoricalFiatRate.

+

Concurrent lookups share one rates-server queue, so asking for many rates at once costs a single upstream request.

+
+
+

Command line

+
rates-query [--crypto='<json>'] [--fiat='<json>']
+ + +
+

REST

+

POST/rates/query

+ + + +
+
Request body
+
{
+  crypto?: {
+    pluginId: string;
+    tokenId: string | null | undefined;
+    targetFiat: string | undefined;
+    date: string | undefined
+  }[]
+  fiat?: {
+    fiatCode: string;
+    targetFiat: string | undefined;
+    date: string | undefined
+  }[]
+}
+
Example
{
+  "crypto": [
+    {}
+  ],
+  "fiat": [
+    {}
+  ]
+}
+ + + + + + + + + +
crypto{ pluginId: string; tokenId: string | null | undefined; targetFiat: string | undefined; date: string | undefined; }[] optionalCrypto rates to fetch.
fiat{ fiatCode: string; targetFiat: string | undefined; date: string | undefined; }[] optionalFiat rates to fetch.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"crypto":[{}],"fiat":[{}]}' \
+  'http://localhost/rates/query'
+
+
+

Response 200

+
+
Response body
+
{
+  crypto: {
+    pluginId: string;
+    tokenId: string | null;
+    targetFiat: string;
+    date: string;
+    rate: number
+  }[]
+  fiat: {
+    fiatCode: string;
+    targetFiat: string;
+    date: string;
+    rate: number
+  }[]
+}
+
Example
{
+  "crypto": [
+    {}
+  ],
+  "fiat": [
+    {}
+  ]
+}
+ + + + + + + + + +
crypto{ pluginId: string; tokenId: string | null; targetFiat: string; date: string; rate: number; }[]Always present; empty when no crypto rates were requested.
fiat{ fiatCode: string; targetFiat: string; date: string; rate: number; }[]Always present; empty when no fiat rates were requested.
+
+
Errors

400BAD_REQUEST 503NETWORK_ERROR

+
+

Notes

  • A rate the server cannot supply comes back as 0 rather than an error, so check for zero before dividing.
+
+
+

Convert a USD amount into native units.

+
rates-usd-to-nativesrc/cli/engine/routes/rates.ts
+
+

coreGUI code (src/util/exchangeRates): getHistoricalCryptoRate.

+

Turns a fiat notional into the native amount a spend needs.

+
+
+

Command line

+
rates-usd-to-native --usd-amount=<usdAmount> --plugin-id=<pluginId> [--token-id=<tokenId>] [--multiplier=<multiplier>] [--date=<date>]
+ + +
+

REST

+

POST/rates/usd-to-native

+ + + +
+
Request body
+
{
+  usdAmount: string
+  pluginId: string
+  tokenId?: string | null
+  multiplier?: string
+  date?: string
+}
+
Example
{
+  "usdAmount": "12345",
+  "pluginId": "FS8xJ2kQ…",
+  "tokenId": "FS8xJ2kQ…",
+  "multiplier": "string",
+  "date": "2026-09-02T16:35:00.000Z"
+}
+ + + + + + + + + + + + + + + + + + + + + + + + +
usdAmountstringA string, which must parse to a positive finite number.
pluginIdstringWhich chain to price.
tokenIdstring | null optionalDefaults to the native asset.
multiplierstring optionalNative units per whole coin. Defaults per plugin.
datestring optionalISO-8601. Omitted, the current time is sent to the rates server.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"usdAmount":"12345","pluginId":"FS8xJ2kQ…","tokenId":"FS8xJ2kQ…","multiplier":"string","date":"2026-09-02T16:35:00.000Z"}' \
+  'http://localhost/rates/usd-to-native'
+
+
+

Response 200

+
+
Response body
+
{
+  usdAmount: number
+  pluginId: string
+  tokenId: string | null
+  multiplier: string
+  date: string
+  rate: number
+  displayAmount: string
+  nativeAmount: string
+}
+
Example
{
+  "usdAmount": 0,
+  "pluginId": "FS8xJ2kQ…",
+  "tokenId": "FS8xJ2kQ…",
+  "multiplier": "string",
+  "date": "2026-09-02T16:35:00.000Z",
+  "rate": 1,
+  "displayAmount": "12345",
+  "nativeAmount": "12345"
+}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
usdAmountnumberEchoed as a number, though it is sent as a string.
pluginIdstringCurrency plugin the amount was converted for.
tokenIdstring | nullThe asset, or null for the chain’s own coin.
multiplierstringNative units per whole coin, which is what the conversion divided by.
datestringThe timestamp actually used for the rate.
ratenumberUSD per whole coin at that date.
displayAmountstringWhole coins, to 8 decimal places.
nativeAmountstringWhat a spend actually takes.
+
+
Errors

400BAD_REQUEST 404NOT_FOUND 503NETWORK_ERROR

+
+

Notes

  • displayAmount is rounded to 8 decimals before conversion, so assets with finer precision lose the tail. For an exact figure use rates-query and do the arithmetic yourself.
  • Default multipliers cover bitcoin, ethereum, bitcoincash, litecoin and dogecoin; pass multiplier explicitly for anything else.
+

Object handles

+

A core value with methods on it cannot cross JSON, so the engine keeps it and hands back an id. These read and release any of them.

+
+

Object handles

+

A core value with methods on it — a staged transaction, a swap quote, a pending login — cannot cross JSON, so the engine keeps it and hands back an id. These read and release any of them.

+
+
+
+

Inspect an object handle.

+
object-getsrc/cli/engine/routes/objects.ts
+
+

coreEngine handle store; core identifies these values by object reference.

+

Works for every kind: transactions, pending logins, swap quotes.

+
+
+

Command line

+
object-get <objectId>
+ + +
+

REST

+

GET/account/{sessionId}/object/{objectId}

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
objectIdstringAn ephemeral object handle id.
+ + + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/account/$SESS/object/$OBJECTID'
+
+
+

Response 200

+

The handle fields, plus a value holding the live core object.

+
+
Response body
+
{
+  objectId: string
+  kind: string
+  expiresAt: string
+  sessionId?: string
+  walletId?: string
+}
+
Example
{
+  "objectId": "FS8xJ2kQ…",
+  "kind": "string",
+  "expiresAt": "2026-09-02T16:35:00.000Z",
+  "sessionId": "FS8xJ2kQ…",
+  "walletId": "FS8xJ2kQ…"
+}
+ + + + + + + + + + + + + + + + + + + + + + + + +
objectIdstringHandle for the value the engine is holding. Pass it to the calls that consume it.
kindstringWhat the handle refers to, which decides the calls that accept it.
expiresAtstringWhen the engine drops the handle. Handles live 5 minutes.
sessionIdstring optionalSession that created the handle; only that session may use it.
walletIdstring optionalWallet the handle is bound to, when it belongs to one.
+
+
Errors

404OBJECT_NOT_FOUND 410OBJECT_EXPIRED 400OBJECT_SESSION_MISMATCH

+
+

Notes

  • Reading does not extend the TTL. Only a step that updates the value does.
+
+
+

Release an object handle.

+
object-deletesrc/cli/engine/routes/objects.ts
+
+

coreEngine handle store.

+

Runs the handle's cleanup — closing a swap quote, cancelling a pending login — instead of waiting out the TTL.

+
+
+

Command line

+
object-delete <objectId>
+ + +
+

REST

+

POST/account/{sessionId}/object/delete/{objectId}

+
Path
sessionIdstringFrom a successful login. The CLI supplies this from session.json, --session, or EDGE_CLI_SESSION.
objectIdstringAn ephemeral object handle id.
+ + + +
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  'http://localhost/account/$SESS/object/delete/$OBJECTID'
+
+
+

Response 200

+
+
Response body
+
{
+  ok: boolean
+  objectId: string
+}
+
Example
{
+  "ok": true,
+  "objectId": "FS8xJ2kQ…"
+}
+ + + + + + + + + +
okbooleanAlways true; a failure arrives as an error envelope.
objectIdstringThe handle this call consumed. It is now expired.
+
+
Errors

404OBJECT_NOT_FOUND 410OBJECT_EXPIRED 400OBJECT_SESSION_MISMATCH

+
+ +

Admin

+

The $internalStuff escape hatch: login-server and sync-repo access that no ordinary caller needs.

+
+

Admin

+

Debugging only — not for production apps. These reach into context.$internalStuff, the private surface of edge-core-js, and can corrupt an account’s synced repos. They take no sessionId: they act on the context, not on a logged-in account.

+
+
+
+

Raw login-server request.

+
admin-auth-requestsrc/cli/engine/routes/admin.ts
+
+

corecontext.$internalStuff.authRequest

+

Sends an arbitrary request with the context's credentials attached. Debugging only — this is core's private surface.

+
+
+

Command line

+
admin-auth-request --method=<method> --path=<path> [--body='<json>']
+ + +
+

REST

+

POST/admin/auth-request

+ + + +
+
Request body
+
{
+  method: string
+  path: string
+  body?: unknown
+}
+
Example
{
+  "method": "string",
+  "path": "string",
+  "body": {}
+}
+ + + + + + + + + + + + + + +
methodstringHTTP method, e.g. GET.
pathstringLogin-server path, not an engine path.
bodyunknown optionalRequest body, when the method takes one.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"method":"string","path":"string","body":{}}' \
+  'http://localhost/admin/auth-request'
+
+
+

Response 200

+

Whatever the login server returned.

+
unknown
+
Errors

400BAD_REQUEST 503NETWORK_ERROR

+
+ +
+
+

Hash a username.

+
admin-hash-usernamesrc/cli/engine/routes/admin.ts
+
+

corecontext.$internalStuff.hashUsername

+

Reproduces the login server's hashing, to derive a login id offline.

+
+
+

Command line

+
admin-hash-username --username=<username>
+ + +
+

REST

+

GET/admin/hash-username

+ +
+
Query
+
{
+  username: string
+}
+
Example
{
+  "username": "string"
+}
+ + + + +
usernamestringThe name to hash.
+
+ + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/admin/hash-username?username=…'
+
+
+

Response 200

+
+
Response body
+
{
+  loginId: string
+}
+
Example
{
+  "loginId": "FS8xJ2kQ…"
+}
+ + + + +
loginIdstringBase58.
+
+ +
+ +
+
+

Create a lobby.

+
admin-make-lobbysrc/cli/engine/routes/admin.ts
+
+

corecontext.$internalStuff.makeLobby

+

A lobby polls the login server until closed, so the engine parks it under a lobby_ handle and closes it on expiry rather than leaking the poll.

+
+
+

Command line

+
admin-make-lobby [--lobby-request='<json>'] [--period-seconds='<json>']
+ + +
+

REST

+

POST/admin/make-lobby

+ + + +
+
Request body
+
{
+  lobbyRequest?: unknown
+  period?: unknown
+}
+
Example
{
+  "lobbyRequest": {},
+  "period": {}
+}
+ + + + + + + + + +
lobbyRequestunknown optionalDefaults to {}.
periodunknown optionalPoll interval in seconds.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"lobbyRequest":{},"period":{}}' \
+  'http://localhost/admin/make-lobby'
+
+
+

Response 200

+
+
Response body
+
{
+  objectId: string
+  expiresAt: string
+  lobbyId: string
+  replies: unknown[]
+}
+
Example
{
+  "objectId": "FS8xJ2kQ…",
+  "expiresAt": "2026-09-02T16:35:00.000Z",
+  "lobbyId": "FS8xJ2kQ…",
+  "replies": [
+    {}
+  ]
+}
+ + + + + + + + + + + + + + + + + + + +
objectIdstringThe parked handle.
expiresAtstringWhen the engine closes the lobby and stops polling.
lobbyIdstringIdentifies the lobby to the party joining it.
repliesunknown[]Empty at creation; re-read to see replies.
+
+
Errors

503NETWORK_ERROR

+
+

Notes

  • Release it with admin-lobby-handle-delete, or the poll runs for the full five minutes.
+
+
+

Close a parked lobby.

+
admin-lobby-handle-deletesrc/cli/engine/routes/admin.ts
+
+

coreEngine handle store for a lobby created via makeLobby.

+ +
+

Command line

+
admin-lobby-handle-delete <objectId>
+ + +
+

REST

+

POST/admin/lobby-handle/delete/{objectId}

+
Path
objectIdstringAn ephemeral object handle id.
+ + + +
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  'http://localhost/admin/lobby-handle/delete/$OBJECTID'
+
+
+

Response 200

+
+
Response body
+
{
+  ok: boolean
+}
+
Example
{
+  "ok": true
+}
+ + + + +
okbooleanAlways true; a failure arrives as an error envelope.
+
+
Errors

404OBJECT_NOT_FOUND

+
+

Notes

  • Not under /account/{sessionId}/objects/, because admin lobbies belong to no session.
+
+
+

Read a lobby's contents.

+
admin-fetch-lobby-requestsrc/cli/engine/routes/admin.ts
+
+

corecontext.$internalStuff.fetchLobbyRequest

+ +
+

Command line

+
admin-fetch-lobby-request <lobbyId>
+ + +
+

REST

+

GET/admin/fetch-lobby-request/{lobbyId}

+
Path
lobbyIdstringWhich lobby to read.
+ + + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/admin/fetch-lobby-request/$LOBBYID?lobbyId=…'
+
+
+

Response 200

+

The raw lobby request.

+
unknown
+
Errors

503NETWORK_ERROR

+
+ +
+
+

Reply to a lobby.

+
admin-send-lobby-replysrc/cli/engine/routes/admin.ts
+
+

corecontext.$internalStuff.sendLobbyReply

+ +
+

Command line

+
admin-send-lobby-reply <lobbyId> --lobby-request='<json>' [--reply-data='<json>']
+ + +
+

REST

+

POST/admin/send-lobby-reply/{lobbyId}

+
Path
lobbyIdstringWhich lobby to answer.
+ + +
+
Request body
+
{
+  lobbyRequest: unknown
+  replyData?: unknown
+}
+
Example
{
+  "lobbyRequest": {},
+  "replyData": {}
+}
+ + + + + + + + + +
lobbyRequestunknownNormally the object from admin-fetch-lobby-request.
replyDataunknown optionalPayload for the requester.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"lobbyId":"FS8xJ2kQ…","lobbyRequest":{},"replyData":{}}' \
+  'http://localhost/admin/send-lobby-reply/$LOBBYID'
+
+
+

Response 204

+

No body.

+
Errors

400BAD_REQUEST 503NETWORK_ERROR

+
+ +
+
+

Sync a repo.

+
admin-sync-reposrc/cli/engine/routes/admin.ts
+
+

corecontext.$internalStuff.syncRepo

+ +
+

Command line

+
admin-sync-repo <syncKey>
+ + +
+

REST

+

POST/admin/sync-repo/{syncKey}

+
Path
syncKeystringBase58 repo sync key.
+ + + +
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"syncKey":"string"}' \
+  'http://localhost/admin/sync-repo/$SYNCKEY'
+
+
+

Response 200

+

The changeset summary.

+
unknown
+
Errors

400BAD_REQUEST 503NETWORK_ERROR

+
+ +
+
+

List repo contents.

+
admin-repo-listsrc/cli/engine/routes/admin.ts
+
+

corecontext.$internalStuff.getRepoDisklet

+ +
+

Command line

+
admin-repo-list <syncKey> [--path=<path>] --data-key=<dataKey>
+ + +
+

REST

+

GET/admin/repo-list/{syncKey}

+
Path
syncKeystringBase58 repo sync key.
+
+
Query
+
{
+  path?: string
+  dataKey: string
+}
+
Example
{
+  "path": "string",
+  "dataKey": "string"
+}
+ + + + + + + + + +
pathstring optionalSubdirectory. Defaults to the repo root.
dataKeystringBase58 repo data key.
+
+ + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/admin/repo-list/$SYNCKEY?syncKey=…&dataKey=…'
+
+
+

Response 200

+
+
Response body
+
{
+  listing: unknown
+}
+
Example
{
+  "listing": {}
+}
+ + + + +
listingunknownPath to entry type: file or folder.
+
+
Errors

400BAD_REQUEST

+
+ +
+
+

Read a repo file.

+
admin-repo-getsrc/cli/engine/routes/admin.ts
+
+

corecontext.$internalStuff.getRepoDisklet

+ +
+

Command line

+
admin-repo-get <syncKey> --path=<path> --data-key=<dataKey>
+ + +
+

REST

+

GET/admin/repo-get/{syncKey}

+
Path
syncKeystringBase58 repo sync key.
+
+
Query
+
{
+  path: string
+  dataKey: string
+}
+
Example
{
+  "path": "string",
+  "dataKey": "string"
+}
+ + + + + + + + + +
pathstringPath within the repo.
dataKeystringBase58 repo data key.
+
+ + +
Example
+
curl --unix-socket "$SOCK" \
+  'http://localhost/admin/repo-get/$SYNCKEY?path=…&syncKey=…&dataKey=…'
+
+
+

Response 200

+
+
Response body
+
{
+  text: string
+}
+
Example
{
+  "text": "string"
+}
+ + + + +
textstringThe file contents.
+
+
Errors

404NOT_FOUND 400BAD_REQUEST

+
+ +
+
+

Write a repo file.

+
admin-repo-setsrc/cli/engine/routes/admin.ts
+
+

corecontext.$internalStuff.getRepoDisklet

+

Writes directly into a synced repo, bypassing every core-level invariant. A malformed write can break the account for real clients.

+
+
+

Command line

+
admin-repo-set <syncKey> --path=<path> --text=<text> --data-key=<dataKey>
+ + +
+

REST

+

POST/admin/repo-set/{syncKey}

+
Path
syncKeystringBase58 repo sync key.
+ + +
+
Request body
+
{
+  path: string
+  text: string
+  dataKey: string
+}
+
Example
{
+  "path": "string",
+  "text": "string",
+  "dataKey": "string"
+}
+ + + + + + + + + + + + + + +
pathstringPath within the repo.
textstringThe contents to write.
dataKeystringBase58 repo data key.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"path":"string","text":"string","syncKey":"string","dataKey":"string"}' \
+  'http://localhost/admin/repo-set/$SYNCKEY'
+
+
+

Response 204

+

No body.

+
Errors

400BAD_REQUEST

+
+ +
+
+

Delete a repo file.

+
admin-repo-deletesrc/cli/engine/routes/admin.ts
+
+

corecontext.$internalStuff.getRepoDisklet

+

Destructive, and not undoable from this API.

+
+
+

Command line

+
admin-repo-delete <syncKey> --path=<path> --data-key=<dataKey>
+ + +
+

REST

+

POST/admin/repo-delete/{syncKey}

+
Path
syncKeystringBase58 repo sync key.
+ + +
+
Request body
+
{
+  path: string
+  dataKey: string
+}
+
Example
{
+  "path": "string",
+  "dataKey": "string"
+}
+ + + + + + + + + +
pathstringPath within the repo.
dataKeystringBase58 repo data key.
+
+
Example
+
curl --unix-socket "$SOCK" \
+  -X POST \
+  -H 'Content-Type: application/json' \
+  -d '{"path":"string","syncKey":"string","dataKey":"string"}' \
+  'http://localhost/admin/repo-delete/$SYNCKEY'
+
+
+

Response 204

+

No body.

+
Errors

400BAD_REQUEST

+
+ +
+

Error codes

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BAD_REQUEST400engineMalformed JSON, or a missing / wrongly typed field.
MISSING_BITWAVE_ACCOUNT_ID400engineBitwave export requested with no account id in the query and none saved in the wallet’s exportTxInfo.json.
OBJECT_KIND_MISMATCH400engineThe handle exists but is a different kind (e.g. a swap quote passed to sign-tx).
OBJECT_SESSION_MISMATCH400engineThe handle belongs to a different session.
OBJECT_WALLET_MISMATCH400engineThe transaction handle belongs to a different wallet.
INVALID_SESSION401engineUnknown sessionId.
SESSION_EXPIRED401engineAuto-logged-out, or explicitly logged out.
NOT_FOUND404engineNo route matched, or a generic missing resource.
NO_LOGIN_REQUEST404engineThe lobby exists but carries no pending login request.
OBJECT_NOT_FOUND404engineNo handle with that objectId.
PENDING_LOGIN_NOT_FOUND404engineNo pending Edge login with that pendingId.
TOKEN_NOT_ENABLED404engineTried to disable a token that was not enabled.
TOKEN_NOT_FOUND404engineUnknown token id for this wallet.
USER_NOT_FOUND404engineNo local user matches that username or login id.
WALLET_NOT_FOUND404engineNo wallet matches that id or prefix.
METHOD_NOT_ALLOWED405engineThe path exists but not for this HTTP method.
AMBIGUOUS_WALLET_ID409engineA wallet id prefix matched more than one wallet. details: details.candidates
OBJECT_EXPIRED410engineThe handle passed its 5 minute TTL and was released.
PAYLOAD_TOO_LARGE413engineRequest body over 4 MiB.
UNSUPPORTED_MEDIA_TYPE415engineBody present but not application/json.
INTERNAL_ERROR500engineUnmapped engine or plugin failure.
ENGINE_SHUTTING_DOWN503engineIdle or explicit shutdown already in progress.
USERNAME_ERROR400coreUnknown username, or an invalid recovery key.
NO_AMOUNT_SPECIFIED400coreZero-amount spend.
SAME_CURRENCY400coreSwap between identical currencies.
PASSWORD_ERROR401coreWrong password, PIN, or recovery answers. details: details.wait (seconds) when rate-limited
OTP_REQUIRED401coreMissing or wrong 2FA token. details: reason (ip|otp), loginId, resetToken, resetDate, voucherId, voucherAuth, voucherActivates
CHALLENGE_REQUIRED403coreThe login server wants a CAPTCHA. Retry with challengeId. details: challengeId, challengeUri
PIN_DISABLED403corePIN login is not enabled on this device.
SWAP_PERMISSION403coreThe swap plugin refused the request. details: pluginId, reason: geoRestriction | noVerification | needsActivation
INSUFFICIENT_FUNDS422coreNot enough balance to cover amount plus fee. details: tokenId, networkFee
DUST_SPEND422coreAmount below the network dust threshold.
PENDING_FUNDS422coreBalance exists but is unconfirmed.
SPEND_TO_SELF422coreDestination address belongs to the source wallet.
SWAP_ABOVE_LIMIT422coreAmount exceeds the plugin maximum. details: swapPluginId, nativeMax, direction
SWAP_BELOW_LIMIT422coreAmount below the plugin minimum. details: swapPluginId, nativeMin, direction
SWAP_CURRENCY422coreThe plugin does not support that pair. details: pluginId, fromTokenId, toTokenId
SWAP_ADDRESS422coreAddress unusable for this swap. details: swapPluginId, reason: mustMatch | mustBeActivated
OBSOLETE_API426coreThe login server rejected this client version.
NETWORK_ERROR503coreCould not reach an Edge server.
+

CLI exit codes

+
0OKSuccess.
1GENERICAny failure with no more specific mapping.
2USAGEBad argv: unknown flag, missing value, extra positional.
3AUTHINVALID_SESSION, SESSION_EXPIRED, PASSWORD_ERROR, OTP_REQUIRED, CHALLENGE_REQUIRED, PIN_DISABLED.
4NOT_FOUNDNOT_FOUND, WALLET_NOT_FOUND, TOKEN_NOT_FOUND.
5VALIDATIONBAD_REQUEST, INSUFFICIENT_FUNDS, DUST_SPEND, PENDING_FUNDS, SPEND_TO_SELF, NO_AMOUNT_SPECIFIED, AMBIGUOUS_WALLET_ID, USERNAME_ERROR.
6NETWORKNETWORK_ERROR, or any response with HTTP status 503.
7ENGINECould not connect to or spawn the engine.
+
+
+ + \ No newline at end of file diff --git a/docs/api/dist/openapi.json b/docs/api/dist/openapi.json new file mode 100644 index 00000000000..4b2b01a4b13 --- /dev/null +++ b/docs/api/dist/openapi.json @@ -0,0 +1,9909 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Edge CLI", + "version": "1.0.0", + "description": "The `edge-cli` command line and the `edge-engine` JSON REST API, generated from the route declarations in `src/cli/engine/routes/`." + }, + "servers": [ + { + "url": "http://localhost", + "description": "Unix socket at ~/.edge-cli/run//engine.sock" + }, + { + "url": "http://127.0.0.1:9008", + "description": "Loopback TCP, when started with --tcp=9008" + } + ], + "tags": [ + { + "name": "Lifecycle", + "description": "Lifecycle and configuration of the `edge-engine` daemon. None of these have an `edge-core-js` equivalent — they describe the daemon itself — and none need a session." + }, + { + "name": "Device and usernames", + "description": "Calls on the shared `EdgeContext`: local device state and login-server queries that do not need a session." + }, + { + "name": "Login methods", + "description": "Every successful login returns a [Session](#schema-Session) and registers it in the engine, so later calls need only the `sessionId`. The CLI writes that id to `session.json` automatically." + }, + { + "name": "Session", + "description": "Calls on a logged-in `EdgeAccount`, addressed by `sessionId`. All of these can also return `401 INVALID_SESSION` or `401 SESSION_EXPIRED`." + }, + { + "name": "Local settings", + "description": "Device-local account settings, stored outside the synced repos." + }, + { + "name": "Credentials", + "description": "Password, PIN, username and recovery changes on a logged-in account." + }, + { + "name": "Two-factor authentication", + "description": "OTP state and the reset flow a user falls back on after losing their authenticator." + }, + { + "name": "Vouchers", + "description": "When 2FA blocks a login, the login server issues a voucher an already-trusted device can approve or reject." + }, + { + "name": "Approving a login", + "description": "The other side of `request-edge-login`: a logged-in account inspecting and approving a login somebody scanned." + }, + { + "name": "Keys", + "description": "Raw key infrastructure beneath the wallet API. Several of these return private key material, and the engine has no transport auth — treat any process that can reach the socket as fully trusted." + }, + { + "name": "Wallet state", + "description": "Account-level wallet listing and creation, then per-wallet calls. A `{walletId}` segment accepts a unique prefix, so those routes can also return `404 WALLET_NOT_FOUND` or `409 AMBIGUOUS_WALLET_ID`." + }, + { + "name": "Tokens", + "description": "Which tokens a wallet tracks. Enabled tokens are the ones it syncs balances for; detected ones were seen on-chain but are not yet enabled." + }, + { + "name": "Transactions", + "description": "Reading transaction history, exporting it, and editing its metadata." + }, + { + "name": "Object handles", + "description": "A core value with methods on it — a staged transaction, a swap quote, a pending login — cannot cross JSON, so the engine keeps it and hands back an id. These read and release any of them." + }, + { + "name": "Spending", + "description": "Two ways to send funds. `spend` does the whole thing in one call; the staged workflow — `make-spend`, `sign-tx`, `broadcast-tx`, `save-tx` — hands back an object handle at each step so fees can be inspected before committing." + }, + { + "name": "Swap quotes", + "description": "Cross-asset exchange. Quotes are live objects held server-side under a `swap_` handle, so approving one means naming its `objectId` rather than re-uploading the quote." + }, + { + "name": "URIs", + "description": "Parsing and building BIP21-style payment URIs through the wallet’s own plugin, so chain-specific quirks are handled for you." + }, + { + "name": "Exchange rates", + "description": "Historical and current rates through the same batching queue the GUI uses. No session required." + }, + { + "name": "Data store", + "description": "The account’s synced key-value store, where plugins keep their own state. One route per `EdgeDataStore` method." + }, + { + "name": "Admin", + "description": "**Debugging only — not for production apps.** These reach into `context.$internalStuff`, the private surface of `edge-core-js`, and can corrupt an account’s synced repos. They take no `sessionId`: they act on the context, not on a logged-in account." + }, + { + "name": "Event stream", + "description": "A Server-Sent Events feed of engine activity, served outside the router because the response never ends." + } + ], + "paths": { + "/engine/status": { + "get": { + "operationId": "engineStatus", + "summary": "Engine liveness and summary.", + "description": "**Core call:** _none — Engine lifecycle; the daemon is not part of the core API._\n\n**Command line**\n\n```\nengine-status\n```\n\nThe readiness probe the client polls after auto-spawning the engine.", + "tags": [ + "Lifecycle" + ], + "x-cli": { + "command": "engine-status", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": null, + "x-core-note": "Engine lifecycle; the daemon is not part of the core API.", + "x-source": "src/cli/engine/routes/status.ts", + "parameters": [], + "responses": { + "200": { + "description": "`idleShutdownAt` is null while a session or a subscription holds the engine open, and `tcpPort` is null unless started with `--tcp`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "pid": { + "type": "number", + "description": "The daemon process, for `kill` when it will not stop." + }, + "apiVersion": { + "type": "string", + "description": "The API this engine speaks. A client refusing to talk to an older engine checks this." + }, + "uptimeSeconds": { + "type": "number", + "description": "How long the daemon has been running." + }, + "sessionCount": { + "type": "number", + "description": "Logged-in accounts held open right now." + }, + "testMode": { + "type": "boolean", + "description": "True when pointed at the tester fleet." + }, + "idleShutdownAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "When the engine will exit for want of work. Null while a session or a subscription is holding it open, and null when the timeout is disabled." + }, + "tcpPort": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "description": "The loopback port, null unless started with `--tcp`." + }, + "socketPath": { + "type": "string", + "description": "Unix socket the CLI connects to." + }, + "locale": { + "type": "string", + "description": "Language tag the engine resolved at boot." + }, + "decimalSeparator": { + "type": "string", + "description": "Decimal mark for that locale." + }, + "groupingSeparator": { + "type": "string", + "description": "Thousands mark for that locale." + } + }, + "required": [ + "pid", + "apiVersion", + "uptimeSeconds", + "sessionCount", + "testMode", + "idleShutdownAt", + "tcpPort", + "socketPath", + "locale", + "decimalSeparator", + "groupingSeparator" + ] + } + } + } + }, + "default": { + "description": "ENGINE_SHUTTING_DOWN", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/engine/config": { + "get": { + "operationId": "engineConfig", + "summary": "Configured context options.", + "description": "**Core call:** _none — Reflects the EdgeContextOptions the engine supplied at startup._\n\n**Command line**\n\n```\nengine-config\n```\n\nWhat the engine passed to `makeEdgeContext`. Contains no secrets. Use it to assert tester hosts before a test run.", + "tags": [ + "Lifecycle" + ], + "x-cli": { + "command": "engine-config", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": null, + "x-core-note": "Reflects the EdgeContextOptions the engine supplied at startup.", + "x-source": "src/cli/engine/routes/status.ts", + "parameters": [], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "appId": { + "type": "string", + "description": "Application ID the engine was started with." + }, + "testMode": { + "type": "boolean", + "description": "True when the engine is pointed at the tester fleet." + }, + "directory": { + "type": "string", + "description": "Working directory holding the core data." + }, + "servers": { + "anyOf": [ + { + "description": "{ [keys: string]: string" + }, + { + "description": "string[]; }" + } + ], + "description": "The URLs this engine talks to, keyed by role. `syncServer` is a list, since core rotates across the sync fleet." + }, + "plugins": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Plugin IDs the engine loaded, sorted." + } + }, + "required": [ + "appId", + "testMode", + "directory", + "servers", + "plugins" + ] + } + } + } + }, + "default": { + "description": "Error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/engine/stop": { + "post": { + "operationId": "engineStop", + "summary": "Stop the engine.", + "description": "**Core call:** _none — Engine lifecycle. Internally calls `context.close()`._\n\n**Command line**\n\n```\nengine-stop\n```\n\nLogs out every session, closes the context, unlinks the socket and run-file, then exits. The engine answers before it starts tearing down, so a response is not proof the process is gone.", + "tags": [ + "Lifecycle" + ], + "x-cli": { + "command": "engine-stop", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": null, + "x-core-note": "Engine lifecycle. Internally calls `context.close()`.", + "x-source": "src/cli/engine/routes/status.ts", + "parameters": [], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ok": { + "type": "boolean", + "description": "Always true; a failure arrives as an error envelope." + } + }, + "required": [ + "ok" + ] + } + } + } + }, + "default": { + "description": "Error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/local-users": { + "get": { + "operationId": "localUsers", + "summary": "List local users on this device.", + "description": "**Core call:** `context.localUsers`\n\n**Command line**\n\n```\nlocal-users\n```\n\n", + "tags": [ + "Device and usernames" + ], + "x-cli": { + "command": "local-users", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "context.localUsers", + "x-source": "src/cli/engine/routes/context.ts", + "parameters": [], + "responses": { + "200": { + "description": "Everything `context.localUsers` reports, including which login methods each user has enabled on this device.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "localUsers": { + "type": "array", + "items": {}, + "description": "`EdgeUserInfo[]`: one entry per account cached on this device." + } + }, + "required": [ + "localUsers" + ] + } + } + } + }, + "default": { + "description": "Error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/forget-account": { + "post": { + "operationId": "forgetAccount", + "summary": "Forget an account on this device.", + "description": "**Core call:** `context.forgetAccount`\n\n**Command line**\n\n```\nforget-account --root-login-id=\n```\n\nRemoves locally cached credentials. The remote account is untouched.", + "tags": [ + "Device and usernames" + ], + "x-cli": { + "command": "forget-account", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "context.forgetAccount", + "x-source": "src/cli/engine/routes/context.ts", + "parameters": [], + "responses": { + "204": { + "description": "No content." + }, + "default": { + "description": "USER_NOT_FOUND, BAD_REQUEST", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "rootLoginId": { + "type": "string", + "description": "Core takes a `rootLoginId`. A username is also accepted and resolved against `localUsers` first, so callers need not hash it." + } + }, + "required": [ + "rootLoginId" + ] + } + } + } + } + } + }, + "/username-available": { + "get": { + "operationId": "usernameAvailable", + "summary": "Check whether a username is free.", + "description": "**Core call:** `context.usernameAvailable`\n\n**Command line**\n\n```\nusername-available --username= [--challenge-id=]\n```\n\n", + "tags": [ + "Device and usernames" + ], + "x-cli": { + "command": "username-available", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "context.usernameAvailable", + "x-source": "src/cli/engine/routes/context.ts", + "parameters": [ + { + "name": "username", + "in": "query", + "required": true, + "description": "The name to check.", + "schema": { + "type": "string" + } + }, + { + "name": "challengeId", + "in": "query", + "required": false, + "description": "Supply after solving a CAPTCHA to retry the same check.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "username": { + "type": "string", + "description": "The name that was checked, echoed back." + }, + "available": { + "type": "boolean", + "description": "True when nobody holds this name. It is not reserved by asking." + } + }, + "required": [ + "username", + "available" + ] + } + } + } + }, + "default": { + "description": "USERNAME_ERROR, CHALLENGE_REQUIRED, NETWORK_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/fix-username": { + "get": { + "operationId": "fixUsername", + "summary": "Normalize a username.", + "description": "**Core call:** `context.fixUsername`\n\n**Command line**\n\n```\nfix-username --username=\n```\n\nApplies the same rules the login server does, so a caller can show the user what their name will actually be before creating an account.", + "tags": [ + "Device and usernames" + ], + "x-cli": { + "command": "fix-username", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "context.fixUsername", + "x-source": "src/cli/engine/routes/context.ts", + "parameters": [ + { + "name": "username", + "in": "query", + "required": true, + "description": "The name to normalize.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "username": { + "type": "string", + "description": "The normalized value. The input is not echoed." + } + }, + "required": [ + "username" + ] + } + } + } + }, + "default": { + "description": "Error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/check-password-rules": { + "get": { + "operationId": "checkPasswordRules", + "summary": "Score a candidate password.", + "description": "**Core call:** `context.checkPasswordRules`\n\n**Command line**\n\n```\ncheck-password-rules --password=\n```\n\n", + "tags": [ + "Device and usernames" + ], + "x-cli": { + "command": "check-password-rules", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "context.checkPasswordRules", + "x-source": "src/cli/engine/routes/context.ts", + "parameters": [ + { + "name": "password", + "in": "query", + "required": true, + "description": "The candidate password to score.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "`EdgePasswordRules` from core: passed, tooShort, noNumber, noLowerCase, noUpperCase, secondsToCrack.", + "content": { + "application/json": { + "schema": {} + } + } + }, + "default": { + "description": "Error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/fetch-login-messages": { + "get": { + "operationId": "fetchLoginMessages", + "summary": "Fetch login-server messages for every local user.", + "description": "**Core call:** `context.fetchLoginMessages`\n\n**Command line**\n\n```\nfetch-login-messages\n```\n\n", + "tags": [ + "Device and usernames" + ], + "x-cli": { + "command": "fetch-login-messages", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "context.fetchLoginMessages", + "x-source": "src/cli/engine/routes/context.ts", + "parameters": [], + "responses": { + "200": { + "description": "`EdgeLoginMessages` from core, keyed by loginId; each value carries otpResetPending and pendingVouchers.", + "content": { + "application/json": { + "schema": {} + } + } + }, + "default": { + "description": "NETWORK_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/request-otp-reset": { + "post": { + "operationId": "requestOtpReset", + "summary": "Request a 2FA reset.", + "description": "**Core call:** `context.requestOtpReset`\n\n**Command line**\n\n```\nrequest-otp-reset --username= --otp-reset-token=\n```\n\nStarts the timed reset a user falls back on after losing their authenticator.", + "tags": [ + "Device and usernames" + ], + "x-cli": { + "command": "request-otp-reset", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "context.requestOtpReset", + "x-source": "src/cli/engine/routes/context.ts", + "parameters": [], + "responses": { + "200": { + "description": "When the reset completes if nobody cancels it.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "resetDate": { + "type": "string", + "description": "When 2FA will actually come off. The login server enforces a waiting period so the real owner has time to cancel." + } + }, + "required": [ + "resetDate" + ] + } + } + } + }, + "default": { + "description": "USERNAME_ERROR, BAD_REQUEST, NETWORK_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "username": { + "type": "string", + "description": "Whose 2FA to reset." + }, + "otpResetToken": { + "type": "string", + "description": "From `details.resetToken` on an `OTP_REQUIRED` error." + } + }, + "required": [ + "username", + "otpResetToken" + ] + } + } + } + } + } + }, + "/fetch-recovery-questions": { + "get": { + "operationId": "fetchRecoveryQuestions", + "summary": "Fetch a user’s recovery questions.", + "description": "**Core call:** `context.fetchRecovery2Questions`\n\n**Command line**\n\n```\nfetch-recovery-questions --recovery-key= --username=\n```\n\n", + "tags": [ + "Device and usernames" + ], + "x-cli": { + "command": "fetch-recovery-questions", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "context.fetchRecovery2Questions", + "x-core-note": "Our surface drops the `2` from the path, command and `recoveryKey` parameter; a future Recovery1 would be suffixed `V1`.", + "x-source": "src/cli/engine/routes/context.ts", + "parameters": [ + { + "name": "recoveryKey", + "in": "query", + "required": true, + "description": "From `change-recovery`, stored by the user out of band.", + "schema": { + "type": "string" + } + }, + { + "name": "username", + "in": "query", + "required": true, + "description": "Whose questions to fetch.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "questions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The questions in the order `login-with-recovery` expects the answers." + } + }, + "required": [ + "questions" + ] + } + } + } + }, + "default": { + "description": "USERNAME_ERROR, NETWORK_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/fetch-challenge": { + "post": { + "operationId": "fetchChallenge", + "summary": "Pre-fetch a CAPTCHA challenge.", + "description": "**Core call:** `context.fetchChallenge`\n\n**Command line**\n\n```\nfetch-challenge\n```\n\nLets a client solve a challenge before it hits `403 CHALLENGE_REQUIRED` mid-flow.", + "tags": [ + "Device and usernames" + ], + "x-cli": { + "command": "fetch-challenge", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "context.fetchChallenge", + "x-source": "src/cli/engine/routes/context.ts", + "parameters": [], + "responses": { + "200": { + "description": "`challengeUri` is absent when the server considers the challenge already satisfied.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "challengeId": { + "type": "string", + "description": "Pass to the call that demanded a challenge once the user has solved it." + }, + "challengeUri": { + "type": "string", + "description": "Where to send the user to solve the CAPTCHA. Absent when the server issued a challenge that needs no interaction." + } + }, + "required": [ + "challengeId" + ] + } + } + } + }, + "default": { + "description": "NETWORK_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/currency-configs": { + "get": { + "operationId": "currencyConfigs", + "summary": "List plugin ids usable for wallet creation.", + "description": "**Core call:** _none — Engine view of the enabled plugin set; core exposes `account.currencyConfig` per plugin instead._\n\n**Command line**\n\n```\ncurrency-configs\n```\n\nCurrency and accountbased plugins only — swap plugins are excluded.", + "tags": [ + "Device and usernames" + ], + "x-cli": { + "command": "currency-configs", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": null, + "x-core-note": "Engine view of the enabled plugin set; core exposes `account.currencyConfig` per plugin instead.", + "x-source": "src/cli/engine/routes/context.ts", + "parameters": [], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "pluginIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Currency plugins this engine loaded." + } + }, + "required": [ + "pluginIds" + ] + } + } + } + }, + "default": { + "description": "Error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/login-with-password": { + "post": { + "operationId": "loginWithPassword", + "summary": "Log in with a password.", + "description": "**Core call:** `context.loginWithPassword`\n\n**Command line**\n\n```\nlogin-with-password [--otp=] [--otp-key=] [--challenge-id=] --username= --password=\n```\n\n", + "tags": [ + "Login methods" + ], + "x-cli": { + "command": "login-with-password", + "flags": [], + "extra": [], + "custom": true, + "preset": {} + }, + "x-core-call": "context.loginWithPassword", + "x-source": "src/cli/engine/routes/login.ts", + "parameters": [], + "responses": { + "200": { + "description": "A session with `loginMethod: \"password\"`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sessionId": { + "type": "string", + "description": "Identifies this login. Every account-scoped call carries it, and the CLI stores the most recent one so commands can omit it." + }, + "username": { + "type": "string", + "description": "Absent for a light account, which has no username." + }, + "rootLoginId": { + "type": "string", + "description": "The account root, stable across appIds. Two sessions sharing it are the same account." + }, + "loginMethod": { + "anyOf": [ + { + "description": "\"password\"" + }, + { + "description": "\"pin\"" + }, + { + "description": "\"key\"" + }, + { + "description": "\"recovery\"" + }, + { + "description": "\"edge\"" + }, + { + "description": "\"create\"" + } + ], + "description": "How this session was established." + }, + "autoLogoutSeconds": { + "type": "number", + "description": "Idle time before the engine logs the account out. 0 disables it." + }, + "expiresAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "When auto-logout will fire, or null when it is disabled." + }, + "lastActivityAt": { + "type": "string", + "description": "Last call on this session, which is what auto-logout measures from." + }, + "createdAt": { + "type": "string", + "description": "When the login completed." + } + }, + "required": [ + "sessionId", + "rootLoginId", + "loginMethod", + "autoLogoutSeconds", + "expiresAt", + "lastActivityAt", + "createdAt" + ] + } + } + } + }, + "default": { + "description": "PASSWORD_ERROR, USERNAME_ERROR, OTP_REQUIRED, CHALLENGE_REQUIRED, NETWORK_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "otp": { + "type": "string", + "description": "A current 2FA code." + }, + "otpKey": { + "type": "string", + "description": "The 2FA secret itself, instead of a code." + }, + "challengeId": { + "type": "string", + "description": "Supply after solving a CAPTCHA to retry the same request." + }, + "username": { + "type": "string", + "description": "The account name." + }, + "password": { + "type": "string", + "description": "The account password." + } + }, + "required": [ + "username", + "password" + ] + } + } + } + } + } + }, + "/login-with-pin": { + "post": { + "operationId": "loginWithPin", + "summary": "Log in with a device PIN.", + "description": "**Core call:** `context.loginWithPIN`\n\n**Command line**\n\n```\nlogin-with-pin [--otp=] [--otp-key=] [--challenge-id=] --username-or-login-id= --pin= [--use-login-id]\n```\n\nOnly works on a device that has already saved a PIN for the account.", + "tags": [ + "Login methods" + ], + "x-cli": { + "command": "login-with-pin", + "flags": [], + "extra": [], + "custom": true, + "preset": {} + }, + "x-core-call": "context.loginWithPIN", + "x-source": "src/cli/engine/routes/login.ts", + "parameters": [], + "responses": { + "200": { + "description": "A session with `loginMethod: \"pin\"`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sessionId": { + "type": "string", + "description": "Identifies this login. Every account-scoped call carries it, and the CLI stores the most recent one so commands can omit it." + }, + "username": { + "type": "string", + "description": "Absent for a light account, which has no username." + }, + "rootLoginId": { + "type": "string", + "description": "The account root, stable across appIds. Two sessions sharing it are the same account." + }, + "loginMethod": { + "anyOf": [ + { + "description": "\"password\"" + }, + { + "description": "\"pin\"" + }, + { + "description": "\"key\"" + }, + { + "description": "\"recovery\"" + }, + { + "description": "\"edge\"" + }, + { + "description": "\"create\"" + } + ], + "description": "How this session was established." + }, + "autoLogoutSeconds": { + "type": "number", + "description": "Idle time before the engine logs the account out. 0 disables it." + }, + "expiresAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "When auto-logout will fire, or null when it is disabled." + }, + "lastActivityAt": { + "type": "string", + "description": "Last call on this session, which is what auto-logout measures from." + }, + "createdAt": { + "type": "string", + "description": "When the login completed." + } + }, + "required": [ + "sessionId", + "rootLoginId", + "loginMethod", + "autoLogoutSeconds", + "expiresAt", + "lastActivityAt", + "createdAt" + ] + } + } + } + }, + "default": { + "description": "PASSWORD_ERROR, PIN_DISABLED, USERNAME_ERROR, BAD_REQUEST, NETWORK_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "otp": { + "type": "string", + "description": "A current 2FA code." + }, + "otpKey": { + "type": "string", + "description": "The 2FA secret itself, instead of a code." + }, + "challengeId": { + "type": "string", + "description": "Supply after solving a CAPTCHA to retry the same request." + }, + "usernameOrLoginId": { + "type": "string", + "description": "A username, or a login id." + }, + "pin": { + "type": "string", + "description": "The device PIN." + }, + "useLoginId": { + "type": "boolean", + "description": "Treat the value as a login id." + } + }, + "required": [ + "usernameOrLoginId", + "pin" + ] + } + } + } + } + } + }, + "/login-with-key": { + "post": { + "operationId": "loginWithKey", + "summary": "Log in with an account login key.", + "description": "**Core call:** `context.loginWithKey`\n\n**Command line**\n\n```\nlogin-with-key [--otp=] [--otp-key=] [--challenge-id=] --username-or-login-id= --login-key= [--use-login-id]\n```\n\nThe key comes from `get-login-key` on an already-authenticated session.", + "tags": [ + "Login methods" + ], + "x-cli": { + "command": "login-with-key", + "flags": [], + "extra": [], + "custom": true, + "preset": {} + }, + "x-core-call": "context.loginWithKey", + "x-source": "src/cli/engine/routes/login.ts", + "parameters": [], + "responses": { + "200": { + "description": "A session with `loginMethod: \"key\"`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sessionId": { + "type": "string", + "description": "Identifies this login. Every account-scoped call carries it, and the CLI stores the most recent one so commands can omit it." + }, + "username": { + "type": "string", + "description": "Absent for a light account, which has no username." + }, + "rootLoginId": { + "type": "string", + "description": "The account root, stable across appIds. Two sessions sharing it are the same account." + }, + "loginMethod": { + "anyOf": [ + { + "description": "\"password\"" + }, + { + "description": "\"pin\"" + }, + { + "description": "\"key\"" + }, + { + "description": "\"recovery\"" + }, + { + "description": "\"edge\"" + }, + { + "description": "\"create\"" + } + ], + "description": "How this session was established." + }, + "autoLogoutSeconds": { + "type": "number", + "description": "Idle time before the engine logs the account out. 0 disables it." + }, + "expiresAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "When auto-logout will fire, or null when it is disabled." + }, + "lastActivityAt": { + "type": "string", + "description": "Last call on this session, which is what auto-logout measures from." + }, + "createdAt": { + "type": "string", + "description": "When the login completed." + } + }, + "required": [ + "sessionId", + "rootLoginId", + "loginMethod", + "autoLogoutSeconds", + "expiresAt", + "lastActivityAt", + "createdAt" + ] + } + } + } + }, + "default": { + "description": "PASSWORD_ERROR, USERNAME_ERROR, NETWORK_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "otp": { + "type": "string", + "description": "A current 2FA code." + }, + "otpKey": { + "type": "string", + "description": "The 2FA secret itself, instead of a code." + }, + "challengeId": { + "type": "string", + "description": "Supply after solving a CAPTCHA to retry the same request." + }, + "usernameOrLoginId": { + "type": "string", + "description": "A username, or a login id." + }, + "loginKey": { + "type": "string", + "description": "From `get-login-key`." + }, + "useLoginId": { + "type": "boolean", + "description": "Treat the value as a login id." + } + }, + "required": [ + "usernameOrLoginId", + "loginKey" + ] + } + } + } + } + } + }, + "/login-with-recovery": { + "post": { + "operationId": "loginWithRecovery", + "summary": "Log in with recovery answers.", + "description": "**Core call:** `context.loginWithRecovery2`\n\n**Command line**\n\n```\nlogin-with-recovery [--otp=] [--otp-key=] [--challenge-id=] --recovery-key= --username= --answer= …\n```\n\nNeeds both the recovery key and the answers; neither works alone.", + "tags": [ + "Login methods" + ], + "x-cli": { + "command": "login-with-recovery", + "flags": [ + { + "name": "answer", + "maps": "answers", + "repeat": true + } + ], + "extra": [], + "custom": true, + "preset": {} + }, + "x-core-call": "context.loginWithRecovery2", + "x-core-note": "Our surface drops the `2` from core's recovery2 naming, and calls the key `recoveryKey` to match what `change-recovery` returns.", + "x-source": "src/cli/engine/routes/login.ts", + "parameters": [], + "responses": { + "200": { + "description": "A session with `loginMethod: \"recovery\"`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sessionId": { + "type": "string", + "description": "Identifies this login. Every account-scoped call carries it, and the CLI stores the most recent one so commands can omit it." + }, + "username": { + "type": "string", + "description": "Absent for a light account, which has no username." + }, + "rootLoginId": { + "type": "string", + "description": "The account root, stable across appIds. Two sessions sharing it are the same account." + }, + "loginMethod": { + "anyOf": [ + { + "description": "\"password\"" + }, + { + "description": "\"pin\"" + }, + { + "description": "\"key\"" + }, + { + "description": "\"recovery\"" + }, + { + "description": "\"edge\"" + }, + { + "description": "\"create\"" + } + ], + "description": "How this session was established." + }, + "autoLogoutSeconds": { + "type": "number", + "description": "Idle time before the engine logs the account out. 0 disables it." + }, + "expiresAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "When auto-logout will fire, or null when it is disabled." + }, + "lastActivityAt": { + "type": "string", + "description": "Last call on this session, which is what auto-logout measures from." + }, + "createdAt": { + "type": "string", + "description": "When the login completed." + } + }, + "required": [ + "sessionId", + "rootLoginId", + "loginMethod", + "autoLogoutSeconds", + "expiresAt", + "lastActivityAt", + "createdAt" + ] + } + } + } + }, + "default": { + "description": "PASSWORD_ERROR, USERNAME_ERROR, NETWORK_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "otp": { + "type": "string", + "description": "A current 2FA code." + }, + "otpKey": { + "type": "string", + "description": "The 2FA secret itself, instead of a code." + }, + "challengeId": { + "type": "string", + "description": "Supply after solving a CAPTCHA to retry the same request." + }, + "recoveryKey": { + "type": "string", + "description": "From `change-recovery`." + }, + "username": { + "type": "string", + "description": "The account name." + }, + "answers": { + "type": "array", + "items": { + "type": "string" + }, + "description": "In the same order as the questions." + } + }, + "required": [ + "recoveryKey", + "username", + "answers" + ] + } + } + } + } + } + }, + "/create-account": { + "post": { + "operationId": "createAccount", + "summary": "Create an account.", + "description": "**Core call:** `context.createAccount`\n\n**Command line**\n\n```\ncreate-account [--otp=] [--otp-key=] [--challenge-id=] [--username=] [--password=] [--pin=]\n```\n\nEvery credential is optional over REST: omitting all three creates a light account with no username.", + "tags": [ + "Login methods" + ], + "x-cli": { + "command": "create-account", + "flags": [], + "extra": [], + "custom": true, + "preset": {} + }, + "x-core-call": "context.createAccount", + "x-source": "src/cli/engine/routes/login.ts", + "parameters": [], + "responses": { + "200": { + "description": "A session with `loginMethod: \"create\"`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sessionId": { + "type": "string", + "description": "Identifies this login. Every account-scoped call carries it, and the CLI stores the most recent one so commands can omit it." + }, + "username": { + "type": "string", + "description": "Absent for a light account, which has no username." + }, + "rootLoginId": { + "type": "string", + "description": "The account root, stable across appIds. Two sessions sharing it are the same account." + }, + "loginMethod": { + "anyOf": [ + { + "description": "\"password\"" + }, + { + "description": "\"pin\"" + }, + { + "description": "\"key\"" + }, + { + "description": "\"recovery\"" + }, + { + "description": "\"edge\"" + }, + { + "description": "\"create\"" + } + ], + "description": "How this session was established." + }, + "autoLogoutSeconds": { + "type": "number", + "description": "Idle time before the engine logs the account out. 0 disables it." + }, + "expiresAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "When auto-logout will fire, or null when it is disabled." + }, + "lastActivityAt": { + "type": "string", + "description": "Last call on this session, which is what auto-logout measures from." + }, + "createdAt": { + "type": "string", + "description": "When the login completed." + } + }, + "required": [ + "sessionId", + "rootLoginId", + "loginMethod", + "autoLogoutSeconds", + "expiresAt", + "lastActivityAt", + "createdAt" + ] + } + } + } + }, + "default": { + "description": "USERNAME_ERROR, CHALLENGE_REQUIRED, BAD_REQUEST, NETWORK_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "otp": { + "type": "string", + "description": "A current 2FA code." + }, + "otpKey": { + "type": "string", + "description": "The 2FA secret itself, instead of a code." + }, + "challengeId": { + "type": "string", + "description": "Supply after solving a CAPTCHA to retry the same request." + }, + "username": { + "type": "string", + "description": "The name to claim." + }, + "password": { + "type": "string", + "description": "The account password." + }, + "pin": { + "type": "string", + "description": "A device PIN to save." + } + } + } + } + } + } + } + }, + "/request-edge-login": { + "post": { + "operationId": "requestEdgeLogin", + "summary": "Start a QR login.", + "description": "**Core call:** `context.requestEdgeLogin`\n\n**Command line**\n\n```\nrequest-edge-login [--no-wait]\n```\n\nAsks the login server for a lobby another logged-in Edge device can approve. The returned `lobbyId` is what goes in the QR code.", + "tags": [ + "Login methods" + ], + "x-cli": { + "command": "request-edge-login", + "flags": [], + "extra": [ + { + "name": "no-wait", + "kind": "boolean", + "required": false, + "doc": "Print the lobby and exit instead of polling, so the QR can be displayed while `poll-edge-login` watches the same handle from another process." + } + ], + "custom": true, + "preset": {}, + "notes": "Prints the pending login, then polls every 2s for up to 5 minutes. On `done` it stores the session. With `--no-wait` it returns immediately and `poll-edge-login` takes over." + }, + "x-core-call": "context.requestEdgeLogin", + "x-source": "src/cli/engine/routes/login.ts", + "parameters": [], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "objectId": { + "type": "string", + "description": "Handle for the value the engine is holding. Pass it to the calls that consume it." + }, + "pendingId": { + "type": "string", + "description": "Same value as `objectId`, under the name the poll command takes." + }, + "kind": { + "type": "string", + "description": "What the handle refers to, which decides the calls that accept it." + }, + "expiresAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "When the lobby closes and the QR code stops working." + }, + "lobbyId": { + "type": "string", + "description": "Lobby the phone connects to." + }, + "uri": { + "type": "string", + "description": "The `edge://` URI to render as a QR code for the phone to scan." + }, + "state": { + "type": "string", + "description": "How far the login has got: `pending` before the phone scans, `started` once it has, and `done` when `session` is filled in." + }, + "username": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Account that approved the login, known once the phone has scanned." + }, + "session": { + "anyOf": [ + { + "description": "{ sessionId: string; username: string" + }, + { + "description": "undefined; rootLoginId: string; loginMethod: \"password\"" + }, + { + "description": "\"pin\"" + }, + { + "description": "\"key\"" + }, + { + "description": "\"recovery\"" + }, + { + "description": "\"edge\"" + }, + { + "description": "\"create\"; autoLogoutSeconds: number; expiresAt: string" + }, + { + "description": "null; lastActivityAt: string; createdAt: string; }" + }, + { + "type": "null" + } + ], + "description": "The session, null until `state` is `done`." + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Why the login failed, set only when `state` is `error`." + } + }, + "required": [ + "objectId", + "pendingId", + "kind", + "expiresAt", + "lobbyId", + "uri", + "state", + "username", + "session", + "error" + ] + } + } + } + }, + "default": { + "description": "NETWORK_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/pending-edge-login/{pendingId}": { + "get": { + "operationId": "pollEdgeLogin", + "summary": "Poll a pending QR login.", + "description": "**Core call:** _none — Engine state for an in-flight requestEdgeLogin; core exposes it as EdgePendingEdgeLogin properties._\n\n**Command line**\n\n```\npoll-edge-login \n```\n\nOnce `state` reaches `done` the engine has already created the session, so the response carries one ready to use.", + "tags": [ + "Login methods" + ], + "x-cli": { + "command": "poll-edge-login", + "positional": "pendingId", + "flags": [], + "extra": [], + "custom": true, + "preset": {} + }, + "x-core-call": null, + "x-core-note": "Engine state for an in-flight requestEdgeLogin; core exposes it as EdgePendingEdgeLogin properties.", + "x-source": "src/cli/engine/routes/login.ts", + "parameters": [ + { + "name": "pendingId", + "in": "path", + "required": true, + "description": "The `pendingId` returned when the QR login was requested.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "objectId": { + "type": "string", + "description": "Handle for the value the engine is holding. Pass it to the calls that consume it." + }, + "pendingId": { + "type": "string", + "description": "Same value as `objectId`, under the name the poll command takes." + }, + "kind": { + "type": "string", + "description": "What the handle refers to, which decides the calls that accept it." + }, + "expiresAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "When the lobby closes and the QR code stops working." + }, + "lobbyId": { + "type": "string", + "description": "Lobby the phone connects to." + }, + "uri": { + "type": "string", + "description": "The `edge://` URI to render as a QR code for the phone to scan." + }, + "state": { + "type": "string", + "description": "How far the login has got: `pending` before the phone scans, `started` once it has, and `done` when `session` is filled in." + }, + "username": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Account that approved the login, known once the phone has scanned." + }, + "session": { + "anyOf": [ + { + "description": "{ sessionId: string; username: string" + }, + { + "description": "undefined; rootLoginId: string; loginMethod: \"password\"" + }, + { + "description": "\"pin\"" + }, + { + "description": "\"key\"" + }, + { + "description": "\"recovery\"" + }, + { + "description": "\"edge\"" + }, + { + "description": "\"create\"; autoLogoutSeconds: number; expiresAt: string" + }, + { + "description": "null; lastActivityAt: string; createdAt: string; }" + }, + { + "type": "null" + } + ], + "description": "The session, null until `state` is `done`." + }, + "error": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Why the login failed, set only when `state` is `error`." + } + }, + "required": [ + "objectId", + "pendingId", + "kind", + "expiresAt", + "lobbyId", + "uri", + "state", + "username", + "session", + "error" + ] + } + } + } + }, + "default": { + "description": "PENDING_LOGIN_NOT_FOUND, OBJECT_EXPIRED", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/pending-edge-login/cancel-request/{pendingId}": { + "post": { + "operationId": "cancelEdgeLogin", + "summary": "Cancel a pending QR login.", + "description": "**Core call:** `EdgePendingEdgeLogin.cancelRequest`\n\n**Command line**\n\n```\ncancel-request \n```\n\n", + "tags": [ + "Login methods" + ], + "x-cli": { + "command": "cancel-request", + "positional": "pendingId", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "EdgePendingEdgeLogin.cancelRequest", + "x-source": "src/cli/engine/routes/login.ts", + "parameters": [ + { + "name": "pendingId", + "in": "path", + "required": true, + "description": "The `pendingId` returned when the QR login was requested.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No content." + }, + "default": { + "description": "PENDING_LOGIN_NOT_FOUND", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/engine/sessions": { + "get": { + "operationId": "engineSessions", + "summary": "List active sessions.", + "description": "**Core call:** _none — The session registry is an engine construct; core has no multi-account session concept._\n\n**Command line**\n\n```\nengine-sessions\n```\n\n", + "tags": [ + "Login methods" + ], + "x-cli": { + "command": "engine-sessions", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": null, + "x-core-note": "The session registry is an engine construct; core has no multi-account session concept.", + "x-source": "src/cli/engine/routes/login.ts", + "parameters": [], + "responses": { + "200": { + "description": "A bare array, not wrapped in a key.", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "anyOf": [ + { + "description": "{ sessionId: string; username: string" + }, + { + "description": "undefined; rootLoginId: string; loginMethod: \"password\"" + }, + { + "description": "\"pin\"" + }, + { + "description": "\"key\"" + }, + { + "description": "\"recovery\"" + }, + { + "description": "\"edge\"" + }, + { + "description": "\"create\"; autoLogoutSeconds: number; expiresAt: string" + }, + { + "description": "null; lastActivityAt: string; createdAt: string; }" + } + ] + } + } + } + } + }, + "default": { + "description": "Error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}": { + "get": { + "operationId": "accountInfo", + "summary": "Account and session summary.", + "description": "**Core call:** _none — Engine composite of the session record plus EdgeAccount properties._\n\n**Command line**\n\n```\naccount-info\n```\n\nSession fields are spread at the top level alongside the account's own properties — there is no nested `session` object.", + "tags": [ + "Session" + ], + "x-cli": { + "command": "account-info", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": null, + "x-core-note": "Engine composite of the session record plus EdgeAccount properties.", + "x-source": "src/cli/engine/routes/account.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "appId": { + "type": "string", + "description": "Application this session logged into." + }, + "created": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "When the account was created, null for accounts predating the field." + }, + "lastLogin": { + "type": "string", + "description": "The previous login, not this one." + }, + "loggedIn": { + "type": "boolean", + "description": "False once the account has been logged out; the session object outlives it briefly." + }, + "recoveryKey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Present only while recovery is configured." + }, + "otpEnabled": { + "type": "boolean", + "description": "2FA is on for this account." + }, + "otpResetPending": { + "type": "boolean", + "description": "True while somebody has a reset pending against this account." + }, + "canDuressLogin": { + "type": "boolean", + "description": "A duress PIN is configured, so this account can be opened in duress mode." + }, + "isDuressAccount": { + "type": "boolean", + "description": "True when this very session is the duress account rather than the real one." + }, + "edgeLogin": { + "type": "boolean", + "description": "This account was reached by QR login." + }, + "keyLogin": { + "type": "boolean", + "description": "This session was reached with a login key." + }, + "newAccount": { + "type": "boolean", + "description": "This session created the account rather than logging into an existing one." + }, + "passwordLogin": { + "type": "boolean", + "description": "This session was reached with a password." + }, + "pinLogin": { + "type": "boolean", + "description": "This session was reached with a PIN." + }, + "recoveryLogin": { + "type": "boolean", + "description": "This session was reached by answering recovery questions." + } + }, + "required": [ + "appId", + "created", + "lastLogin", + "loggedIn", + "recoveryKey", + "otpEnabled", + "otpResetPending", + "canDuressLogin", + "isDuressAccount", + "edgeLogin", + "keyLogin", + "newAccount", + "passwordLogin", + "pinLogin", + "recoveryLogin" + ] + } + } + } + }, + "default": { + "description": "Error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/logout": { + "post": { + "operationId": "logout", + "summary": "Log out.", + "description": "**Core call:** `account.logout`\n\n**Command line**\n\n```\nlogout\n```\n\nEnds the session and drops it from the engine. Any subscription scoped to this account or its wallets is closed with it.", + "tags": [ + "Session" + ], + "x-cli": { + "command": "logout", + "flags": [], + "extra": [], + "custom": true, + "preset": {}, + "notes": "Also clears the stored id from `session.json`." + }, + "x-core-call": "account.logout", + "x-source": "src/cli/engine/routes/account.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No content." + }, + "default": { + "description": "Error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/touch": { + "post": { + "operationId": "touchSession", + "summary": "Keepalive.", + "description": "**Core call:** _none — Engine auto-logout timer; core has no idle concept._\n\n**Command line**\n\n```\ntouch\n```\n\nResets the idle auto-logout timer without doing any other work.", + "tags": [ + "Session" + ], + "x-cli": { + "command": "touch", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": null, + "x-core-note": "Engine auto-logout timer; core has no idle concept.", + "x-source": "src/cli/engine/routes/account.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The session, with a refreshed `expiresAt`.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "sessionId": { + "type": "string", + "description": "Identifies this login. Every account-scoped call carries it, and the CLI stores the most recent one so commands can omit it." + }, + "username": { + "type": "string", + "description": "Absent for a light account, which has no username." + }, + "rootLoginId": { + "type": "string", + "description": "The account root, stable across appIds. Two sessions sharing it are the same account." + }, + "loginMethod": { + "anyOf": [ + { + "description": "\"password\"" + }, + { + "description": "\"pin\"" + }, + { + "description": "\"key\"" + }, + { + "description": "\"recovery\"" + }, + { + "description": "\"edge\"" + }, + { + "description": "\"create\"" + } + ], + "description": "How this session was established." + }, + "autoLogoutSeconds": { + "type": "number", + "description": "Idle time before the engine logs the account out. 0 disables it." + }, + "expiresAt": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "When auto-logout will fire, or null when it is disabled." + }, + "lastActivityAt": { + "type": "string", + "description": "Last call on this session, which is what auto-logout measures from." + }, + "createdAt": { + "type": "string", + "description": "When the login completed." + } + }, + "required": [ + "sessionId", + "rootLoginId", + "loginMethod", + "autoLogoutSeconds", + "expiresAt", + "lastActivityAt", + "createdAt" + ] + } + } + } + }, + "default": { + "description": "Error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/get-login-key": { + "get": { + "operationId": "getLoginKey", + "summary": "Read the account login key.", + "description": "**Core call:** `account.getLoginKey`\n\n**Command line**\n\n```\nget-login-key\n```\n\nThe key `login-with-key` takes. It grants full account access, so treat the output as secret.", + "tags": [ + "Session" + ], + "x-cli": { + "command": "get-login-key", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "account.getLoginKey", + "x-source": "src/cli/engine/routes/account.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "loginKey": { + "type": "string", + "description": "base58. Full account access — keep it safe." + } + }, + "required": [ + "loginKey" + ] + } + } + } + }, + "default": { + "description": "Error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/sync": { + "post": { + "operationId": "accountSync", + "summary": "Force an account data sync.", + "description": "**Core call:** `account.sync`\n\n**Command line**\n\n```\nsync\n```\n\nPushes and pulls the account repos immediately rather than waiting for the next scheduled sync.", + "tags": [ + "Session" + ], + "x-cli": { + "command": "sync", + "flags": [], + "extra": [], + "custom": false, + "preset": {}, + "notes": "Named `sync` for the account; the wallet one is `wallet-sync`." + }, + "x-core-call": "account.sync", + "x-source": "src/cli/engine/routes/account.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No content." + }, + "default": { + "description": "NETWORK_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/delete-remote-account": { + "post": { + "operationId": "deleteRemoteAccount", + "summary": "Permanently delete the remote account.", + "description": "**Core call:** `account.deleteRemoteAccount`\n\n**Command line**\n\n```\ndelete-remote-account --yes\n```\n\nIrreversible. The account is removed from the login server, and funds in its wallets are unrecoverable without the keys. The session is logged out afterwards.", + "tags": [ + "Session" + ], + "x-cli": { + "command": "delete-remote-account", + "flags": [], + "extra": [ + { + "name": "yes", + "kind": "boolean", + "required": true, + "doc": "Confirms intent. Without it the command refuses to run." + } + ], + "custom": true, + "preset": {} + }, + "x-core-call": "account.deleteRemoteAccount", + "x-source": "src/cli/engine/routes/account.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No content." + }, + "default": { + "description": "NETWORK_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/wait-for-all-wallets": { + "post": { + "operationId": "waitForAllWallets", + "summary": "Wait for every wallet to finish loading.", + "description": "**Core call:** `account.waitForAllWallets`\n\n**Command line**\n\n```\nwait-for-all-wallets\n```\n\nWallets load in the background after login, so a list taken straight afterwards can be short. This resolves once each active wallet has either loaded or failed — balances may still be syncing afterwards.", + "tags": [ + "Session" + ], + "x-cli": { + "command": "wait-for-all-wallets", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "account.waitForAllWallets", + "x-source": "src/cli/engine/routes/account.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No content." + }, + "default": { + "description": "Error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/currency-wallets": { + "get": { + "operationId": "currencyWallets", + "summary": "List the account's wallets.", + "description": "**Core call:** `account.currencyWallets`\n\n**Command line**\n\n```\ncurrency-wallets [--filter=active|archived|hidden|all]\n```\n\n", + "tags": [ + "Session" + ], + "x-cli": { + "command": "currency-wallets", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "account.currencyWallets", + "x-core-note": "Filtered by account.activeWalletIds / archivedWalletIds / hiddenWalletIds.", + "x-source": "src/cli/engine/routes/account.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + }, + { + "name": "filter", + "in": "query", + "required": false, + "description": "Which of the account’s wallet lists to read. Defaults to `active`.", + "schema": { + "anyOf": [ + { + "description": "\"active\"" + }, + { + "description": "\"archived\"" + }, + { + "description": "\"hidden\"" + }, + { + "description": "\"all\"" + } + ] + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "currencyWallets": { + "type": "array", + "items": { + "anyOf": [ + { + "description": "{ walletId: string; id: string; type: string; name: string" + }, + { + "description": "null; pluginId: string; currencyCode: string; fiatCurrencyCode: string; blockHeight: number; syncStatus: unknown; syncRatio: string" + }, + { + "description": "undefined; paused: boolean; imported: boolean" + }, + { + "description": "undefined; created: string" + }, + { + "description": "null; enabledTokenIds: string[]; detectedTokenIds: string[]; unactivatedTokenIds: string[]; }" + } + ] + }, + "description": "Every wallet in the account, including paused ones." + } + }, + "required": [ + "currencyWallets" + ] + } + } + } + }, + "default": { + "description": "Error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/create-currency-wallet": { + "post": { + "operationId": "createCurrencyWallet", + "summary": "Create a currency wallet.", + "description": "**Core call:** `account.createCurrencyWallet`\n\n**Command line**\n\n```\ncreate-currency-wallet --wallet-type= [--name=] [--import-text=]\n```\n\n", + "tags": [ + "Session" + ], + "x-cli": { + "command": "create-currency-wallet", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "account.createCurrencyWallet", + "x-source": "src/cli/engine/routes/account.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "walletId": { + "type": "string", + "description": "The full wallet id. Commands taking a wallet accept any unique prefix." + }, + "id": { + "type": "string", + "description": "Same value as `walletId`; core exposes both names." + }, + "type": { + "type": "string", + "description": "Key type, such as `wallet:bitcoin`." + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "User-assigned name, null until one is set." + }, + "pluginId": { + "type": "string", + "description": "Currency plugin backing this wallet." + }, + "currencyCode": { + "type": "string", + "description": "Ticker for the native asset." + }, + "fiatCurrencyCode": { + "type": "string", + "description": "Fiat the wallet reports value in, as `iso:USD`." + }, + "blockHeight": { + "type": "number", + "description": "Chain height this wallet has seen." + }, + "syncStatus": { + "description": "`EdgeWalletSyncStatus` from core." + }, + "syncRatio": { + "type": "string", + "description": "Sync progress as a percentage, for display." + }, + "paused": { + "type": "boolean", + "description": "True while the engine is not syncing this wallet." + }, + "imported": { + "type": "boolean", + "description": "True when the keys came from an import rather than being generated here." + }, + "created": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "When the wallet was created, null for wallets predating the field." + }, + "enabledTokenIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Tokens the user turned on." + }, + "detectedTokenIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Tokens found on-chain that are not enabled yet." + }, + "unactivatedTokenIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Enabled tokens still awaiting on-chain activation." + } + }, + "required": [ + "walletId", + "id", + "type", + "name", + "pluginId", + "currencyCode", + "fiatCurrencyCode", + "blockHeight", + "syncStatus", + "paused", + "created", + "enabledTokenIds", + "detectedTokenIds", + "unactivatedTokenIds" + ] + } + } + } + }, + "default": { + "description": "BAD_REQUEST", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "walletType": { + "type": "string", + "description": "From `currency-configs`, e.g. `wallet:bitcoin`." + }, + "name": { + "type": "string", + "description": "Display name." + }, + "importText": { + "type": "string", + "description": "Seed or key text to import instead of generating." + } + }, + "required": [ + "walletType" + ] + } + } + } + } + } + }, + "/account/{sessionId}/create-currency-wallets": { + "post": { + "operationId": "createCurrencyWallets", + "summary": "Create several wallets at once.", + "description": "**Core call:** `account.createCurrencyWallets`\n\n**Command line**\n\n```\ncreate-currency-wallets --create-wallets=''\n```\n\nPartial success is normal: each entry reports its own outcome, and one failure does not roll back the others.", + "tags": [ + "Session" + ], + "x-cli": { + "command": "create-currency-wallets", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "account.createCurrencyWallets", + "x-source": "src/cli/engine/routes/account.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": {}, + "description": "Mirrors core's EdgeResult[]: `{ ok, wallet }` or `{ ok: false, error }`." + } + }, + "required": [ + "results" + ] + } + } + } + }, + "default": { + "description": "BAD_REQUEST", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "createWallets": { + "type": "array", + "items": {}, + "description": "`EdgeCreateCurrencyWallet[]`: walletType, name, fiatCurrencyCode." + } + }, + "required": [ + "createWallets" + ] + } + } + } + } + } + }, + "/account/{sessionId}/local-settings": { + "get": { + "operationId": "localSettings", + "summary": "Local settings.", + "description": "**Core call:** _none — GUI code (src/util/localAccountSettings), reached through account.localDisklet._\n\n**Command line**\n\n```\nlocal-settings\n```\n\nDevice-local account settings, stored in `Settings.json` on `account.localDisklet`. They are not synced — a phone and a CLI keep separate copies unless they share an Edge data directory.", + "tags": [ + "Local settings" + ], + "x-cli": { + "command": "local-settings", + "flags": [], + "extra": [], + "custom": true, + "preset": {} + }, + "x-core-call": null, + "x-core-note": "GUI code (src/util/localAccountSettings), reached through account.localDisklet.", + "x-source": "src/cli/engine/routes/localSettings.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "spamFilterOn": { + "type": "boolean", + "description": "Hide spam transactions in `get-transactions` results. Defaults to `true`, matching the GUI. The filter hides rows; it never changes stored metadata." + } + }, + "required": [ + "spamFilterOn" + ] + } + } + } + }, + "default": { + "description": "Error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/change-local-settings": { + "post": { + "operationId": "changeLocalSettings", + "summary": "Change local settings.", + "description": "**Core call:** _none — GUI code (src/util/localAccountSettings)._\n\n**Command line**\n\n```\nlocal-settings --spam-filter-on=true|false\n```\n\nWrites device-local account settings. Every option is a field on the body; `spamFilterOn` is the only one today, and new options are added alongside it.", + "tags": [ + "Local settings" + ], + "x-cli": { + "command": "local-settings", + "flags": [], + "extra": [], + "custom": true, + "preset": {}, + "notes": "With no flag the command reads; with one it writes." + }, + "x-core-call": null, + "x-core-note": "GUI code (src/util/localAccountSettings).", + "x-source": "src/cli/engine/routes/localSettings.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "spamFilterOn": { + "type": "boolean", + "description": "Hide spam transactions in `get-transactions` results. Defaults to `true`, matching the GUI. The filter hides rows; it never changes stored metadata." + } + }, + "required": [ + "spamFilterOn" + ] + } + } + } + }, + "default": { + "description": "Error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "spamFilterOn": { + "type": "boolean", + "description": "Hide spam transactions in `get-transactions` results. Defaults to `true`, matching the GUI. The filter hides rows; it never changes stored metadata." + } + }, + "required": [ + "spamFilterOn" + ] + } + } + } + } + } + }, + "/account/{sessionId}/change-password": { + "post": { + "operationId": "changePassword", + "summary": "Set or change the password.", + "description": "**Core call:** `account.changePassword`\n\n**Command line**\n\n```\nchange-password --password=\n```\n\nThe login server enforces its own rules; `check-password-rules` scores a candidate first.", + "tags": [ + "Credentials" + ], + "x-cli": { + "command": "change-password", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "account.changePassword", + "x-source": "src/cli/engine/routes/credentials.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No content." + }, + "default": { + "description": "BAD_REQUEST, NETWORK_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "password": { + "type": "string", + "description": "The new password." + } + }, + "required": [ + "password" + ] + } + } + } + } + } + }, + "/account/{sessionId}/delete-password": { + "post": { + "operationId": "deletePassword", + "summary": "Remove password login.", + "description": "**Core call:** `account.deletePassword`\n\n**Command line**\n\n```\ndelete-password\n```\n\nThe account keeps its other login methods; only the password stops working.", + "tags": [ + "Credentials" + ], + "x-cli": { + "command": "delete-password", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "account.deletePassword", + "x-source": "src/cli/engine/routes/credentials.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No content." + }, + "default": { + "description": "BAD_REQUEST, NETWORK_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/check-password": { + "post": { + "operationId": "checkPassword", + "summary": "Verify a password.", + "description": "**Core call:** `account.checkPassword`\n\n**Command line**\n\n```\ncheck-password --password=\n```\n\nChecks without changing anything, which is how a caller gates a destructive action behind a re-entry prompt.", + "tags": [ + "Credentials" + ], + "x-cli": { + "command": "check-password", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "account.checkPassword", + "x-source": "src/cli/engine/routes/credentials.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ok": { + "type": "boolean", + "description": "False for a wrong password — not an error response." + } + }, + "required": [ + "ok" + ] + } + } + } + }, + "default": { + "description": "Error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "password": { + "type": "string", + "description": "The account password." + } + }, + "required": [ + "password" + ] + } + } + } + } + } + }, + "/account/{sessionId}/get-pin": { + "get": { + "operationId": "getPin", + "summary": "Read the account PIN.", + "description": "**Core call:** `account.getPin`\n\n**Command line**\n\n```\nget-pin\n```\n\nReturns the PIN itself, not a status flag, so treat the output as secret.", + "tags": [ + "Credentials" + ], + "x-cli": { + "command": "get-pin", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "account.getPin", + "x-source": "src/cli/engine/routes/credentials.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "pin": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Null when no PIN is set." + } + }, + "required": [ + "pin" + ] + } + } + } + }, + "default": { + "description": "Error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/change-pin": { + "post": { + "operationId": "changePin", + "summary": "Set or change the PIN.", + "description": "**Core call:** `account.changePin`\n\n**Command line**\n\n```\nchange-pin --pin= [--enable-login] [--for-duress-account]\n```\n\n", + "tags": [ + "Credentials" + ], + "x-cli": { + "command": "change-pin", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "account.changePin", + "x-source": "src/cli/engine/routes/credentials.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "pin2Key": { + "type": "string", + "description": "The new PIN login key core returns." + } + }, + "required": [ + "pin2Key" + ] + } + } + } + }, + "default": { + "description": "BAD_REQUEST", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "pin": { + "type": "string", + "description": "The new PIN." + }, + "enableLogin": { + "type": "boolean", + "description": "Allow logging in with this PIN on this device." + }, + "forDuressAccount": { + "type": "boolean", + "description": "Act on the duress account rather than the real one." + } + }, + "required": [ + "pin" + ] + } + } + } + } + } + }, + "/account/{sessionId}/delete-pin": { + "post": { + "operationId": "deletePin", + "summary": "Remove the PIN.", + "description": "**Core call:** `account.deletePin`\n\n**Command line**\n\n```\ndelete-pin\n```\n\nPIN login stops working on this device; other methods are untouched.", + "tags": [ + "Credentials" + ], + "x-cli": { + "command": "delete-pin", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "account.deletePin", + "x-source": "src/cli/engine/routes/credentials.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No content." + }, + "default": { + "description": "Error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/check-pin": { + "post": { + "operationId": "checkPin", + "summary": "Verify a PIN.", + "description": "**Core call:** `account.checkPin`\n\n**Command line**\n\n```\ncheck-pin --pin= [--for-duress-account]\n```\n\n", + "tags": [ + "Credentials" + ], + "x-cli": { + "command": "check-pin", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "account.checkPin", + "x-source": "src/cli/engine/routes/credentials.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ok": { + "type": "boolean", + "description": "False for a wrong PIN — not an error response." + } + }, + "required": [ + "ok" + ] + } + } + } + }, + "default": { + "description": "Error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "pin": { + "type": "string", + "description": "The device PIN, usually four digits." + }, + "forDuressAccount": { + "type": "boolean", + "description": "Act on the duress account rather than the real one." + } + }, + "required": [ + "pin" + ] + } + } + } + } + } + }, + "/account/{sessionId}/change-username": { + "post": { + "operationId": "changeUsername", + "summary": "Change the username.", + "description": "**Core call:** `account.changeUsername`\n\n**Command line**\n\n```\nchange-username --username= [--password=]\n```\n\nThe old name is released, so it becomes available to anyone else.", + "tags": [ + "Credentials" + ], + "x-cli": { + "command": "change-username", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "account.changeUsername", + "x-source": "src/cli/engine/routes/credentials.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No content." + }, + "default": { + "description": "USERNAME_ERROR, BAD_REQUEST, NETWORK_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "username": { + "type": "string", + "description": "The new username." + }, + "password": { + "type": "string", + "description": "Required by core when the account has a password." + } + }, + "required": [ + "username" + ] + } + } + } + } + } + }, + "/account/{sessionId}/change-recovery": { + "post": { + "operationId": "changeRecovery", + "summary": "Set recovery questions and answers.", + "description": "**Core call:** `account.changeRecovery`\n\n**Command line**\n\n```\nchange-recovery --question= … --answer= …\n```\n\nThe returned key is half of the credential: without it the answers alone cannot recover the account, so it has to be stored somewhere else.", + "tags": [ + "Credentials" + ], + "x-cli": { + "command": "change-recovery", + "flags": [ + { + "name": "question", + "maps": "questions", + "repeat": true + }, + { + "name": "answer", + "maps": "answers", + "repeat": true + } + ], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "account.changeRecovery", + "x-core-note": "Our surface drops the `2` from core's recovery2 naming; a future Recovery1 would be suffixed `V1`.", + "x-source": "src/cli/engine/routes/credentials.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "recoveryKey": { + "type": "string", + "description": "Store this out of band. `login-with-recovery` needs it alongside the answers." + } + }, + "required": [ + "recoveryKey" + ] + } + } + } + }, + "default": { + "description": "BAD_REQUEST", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "questions": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The questions to ask." + }, + "answers": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Same length and order as `questions`." + } + }, + "required": [ + "questions", + "answers" + ] + } + } + } + } + } + }, + "/account/{sessionId}/delete-recovery": { + "post": { + "operationId": "deleteRecovery", + "summary": "Disable recovery login.", + "description": "**Core call:** `account.deleteRecovery`\n\n**Command line**\n\n```\ndelete-recovery\n```\n\nThe existing recovery key stops working.", + "tags": [ + "Credentials" + ], + "x-cli": { + "command": "delete-recovery", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "account.deleteRecovery", + "x-source": "src/cli/engine/routes/credentials.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No content." + }, + "default": { + "description": "Error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/otp-key": { + "get": { + "operationId": "otpKey", + "summary": "Read the 2FA secret and reset state.", + "description": "**Core call:** `account.otpKey`\n\n**Command line**\n\n```\notp-key\n```\n\n", + "tags": [ + "Two-factor authentication" + ], + "x-cli": { + "command": "otp-key", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "account.otpKey", + "x-core-note": "Also carries account.otpResetDate.", + "x-source": "src/cli/engine/routes/otp.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "otpKey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Null when 2FA is off. The 2FA secret itself. Secret material — record it safely." + }, + "otpResetDate": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Set once somebody has requested a reset; cancel it with `cancel-otp-reset`." + } + }, + "required": [ + "otpKey", + "otpResetDate" + ] + } + } + } + }, + "default": { + "description": "Error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/enable-otp": { + "post": { + "operationId": "enableOtp", + "summary": "Enable 2FA.", + "description": "**Core call:** `account.enableOtp`\n\n**Command line**\n\n```\nenable-otp [--timeout=]\n```\n\nRecord the returned key before leaving the terminal: it is the only copy.", + "tags": [ + "Two-factor authentication" + ], + "x-cli": { + "command": "enable-otp", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "account.enableOtp", + "x-source": "src/cli/engine/routes/otp.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "otpKey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "The new secret. The 2FA secret itself. Secret material — record it safely." + } + }, + "required": [ + "otpKey" + ] + } + } + } + }, + "default": { + "description": "Error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "timeout": { + "type": "number", + "description": "How long a reset request must wait before it completes. Core supplies the default when omitted." + } + } + } + } + } + } + } + }, + "/account/{sessionId}/disable-otp": { + "post": { + "operationId": "disableOtp", + "summary": "Disable 2FA.", + "description": "**Core call:** `account.disableOtp`\n\n**Command line**\n\n```\ndisable-otp\n```\n\nLogins stop requiring a code immediately.", + "tags": [ + "Two-factor authentication" + ], + "x-cli": { + "command": "disable-otp", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "account.disableOtp", + "x-source": "src/cli/engine/routes/otp.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No content." + }, + "default": { + "description": "Error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/cancel-otp-reset": { + "post": { + "operationId": "cancelOtpReset", + "summary": "Cancel a pending 2FA reset.", + "description": "**Core call:** `account.cancelOtpReset`\n\n**Command line**\n\n```\ncancel-otp-reset\n```\n\nThe defence against somebody else requesting a reset on your account: as long as you cancel before the timer runs out, their reset never lands.", + "tags": [ + "Two-factor authentication" + ], + "x-cli": { + "command": "cancel-otp-reset", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "account.cancelOtpReset", + "x-source": "src/cli/engine/routes/otp.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No content." + }, + "default": { + "description": "Error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/repair-otp": { + "post": { + "operationId": "repairOtp", + "summary": "Re-point the account at a known 2FA secret.", + "description": "**Core call:** `account.repairOtp`\n\n**Command line**\n\n```\nrepair-otp --otp-key=\n```\n\nFor a device whose stored secret has drifted from the server's.", + "tags": [ + "Two-factor authentication" + ], + "x-cli": { + "command": "repair-otp", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "account.repairOtp", + "x-source": "src/cli/engine/routes/otp.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No content." + }, + "default": { + "description": "OTP_REQUIRED, BAD_REQUEST", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "otpKey": { + "type": "string", + "description": "The secret the account should use." + } + }, + "required": [ + "otpKey" + ] + } + } + } + } + } + }, + "/account/{sessionId}/pending-vouchers": { + "get": { + "operationId": "pendingVouchers", + "summary": "List pending 2FA vouchers.", + "description": "**Core call:** `account.pendingVouchers`\n\n**Command line**\n\n```\npending-vouchers\n```\n\nWhen 2FA blocks a login, the login server issues a voucher that an already-trusted device can approve or reject.", + "tags": [ + "Vouchers" + ], + "x-cli": { + "command": "pending-vouchers", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "account.pendingVouchers", + "x-source": "src/cli/engine/routes/vouchers.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "pendingVouchers": { + "type": "array", + "items": {}, + "description": "`EdgePendingVoucher[]`: voucherId, activates, created, deviceDescription, ipDescription." + } + }, + "required": [ + "pendingVouchers" + ] + } + } + } + }, + "default": { + "description": "Error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/approve-voucher": { + "post": { + "operationId": "approveVoucher", + "summary": "Approve a voucher.", + "description": "**Core call:** `account.approveVoucher`\n\n**Command line**\n\n```\napprove-voucher --voucher-id=\n```\n\nLets the waiting device finish logging in.", + "tags": [ + "Vouchers" + ], + "x-cli": { + "command": "approve-voucher", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "account.approveVoucher", + "x-source": "src/cli/engine/routes/vouchers.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No content." + }, + "default": { + "description": "BAD_REQUEST, NETWORK_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "voucherId": { + "type": "string", + "description": "From `pending-vouchers`, or an `OTP_REQUIRED` error’s `details.voucherId`." + } + }, + "required": [ + "voucherId" + ] + } + } + } + } + } + }, + "/account/{sessionId}/reject-voucher": { + "post": { + "operationId": "rejectVoucher", + "summary": "Reject a voucher.", + "description": "**Core call:** `account.rejectVoucher`\n\n**Command line**\n\n```\nreject-voucher --voucher-id=\n```\n\nDenies the waiting device. The login it was issued for cannot complete.", + "tags": [ + "Vouchers" + ], + "x-cli": { + "command": "reject-voucher", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "account.rejectVoucher", + "x-source": "src/cli/engine/routes/vouchers.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No content." + }, + "default": { + "description": "BAD_REQUEST, NETWORK_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "voucherId": { + "type": "string", + "description": "From `pending-vouchers`, or an `OTP_REQUIRED` error’s `details.voucherId`." + } + }, + "required": [ + "voucherId" + ] + } + } + } + } + } + }, + "/account/{sessionId}/fetch-lobby/{lobbyId}": { + "get": { + "operationId": "fetchLobby", + "summary": "Inspect a login request.", + "description": "**Core call:** `account.fetchLobby`\n\n**Command line**\n\n```\nfetch-lobby \n```\n\nThe other side of `request-edge-login`: shows who is asking, so a human can decide before approving.", + "tags": [ + "Approving a login" + ], + "x-cli": { + "command": "fetch-lobby", + "positional": "lobbyId", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "account.fetchLobby", + "x-source": "src/cli/engine/routes/lobby.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + }, + { + "name": "lobbyId", + "in": "path", + "required": true, + "description": "From the QR code, or an `edge://edge/` link.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "lobbyId": { + "type": "string", + "description": "The lobby that was fetched, echoed back." + }, + "loginRequest": { + "anyOf": [ + { + "description": "{ appId: string; displayName: string; displayImageDarkUrl: string" + }, + { + "description": "null; displayImageLightUrl: string" + }, + { + "description": "null; }" + }, + { + "type": "null" + } + ], + "description": "Null when the lobby carries no pending login request." + } + }, + "required": [ + "lobbyId", + "loginRequest" + ] + } + } + } + }, + "default": { + "description": "BAD_REQUEST, NETWORK_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/approve-login-request/{lobbyId}": { + "post": { + "operationId": "approveLoginRequest", + "summary": "Approve a login request.", + "description": "**Core call:** `EdgeLoginRequest.approve`\n\n**Command line**\n\n```\napprove-login-request \n```\n\nGrants the requesting device access to this account.", + "tags": [ + "Approving a login" + ], + "x-cli": { + "command": "approve-login-request", + "positional": "lobbyId", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "EdgeLoginRequest.approve", + "x-core-note": "Reached through account.fetchLobby(lobbyId).loginRequest.", + "x-source": "src/cli/engine/routes/lobby.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + }, + { + "name": "lobbyId", + "in": "path", + "required": true, + "description": "From the QR code, or an `edge://edge/` link.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ok": { + "type": "boolean", + "description": "Always true; a failure arrives as an error envelope." + } + }, + "required": [ + "ok" + ] + } + } + } + }, + "default": { + "description": "NO_LOGIN_REQUEST, BAD_REQUEST, NETWORK_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/all-keys": { + "get": { + "operationId": "allKeys", + "summary": "List every key in the account.", + "description": "**Core call:** `account.allKeys`\n\n**Command line**\n\n```\nall-keys\n```\n\nIncludes archived and deleted keys, unlike `currency-wallets`.", + "tags": [ + "Keys" + ], + "x-cli": { + "command": "all-keys", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "account.allKeys", + "x-source": "src/cli/engine/routes/keys.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "allKeys": { + "type": "array", + "items": {}, + "description": "`EdgeWalletInfoFull[]`: id, type, keys, archived, deleted, hidden, sortIndex." + } + }, + "required": [ + "allKeys" + ] + } + } + } + }, + "default": { + "description": "Error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/create-wallet": { + "post": { + "operationId": "createWallet", + "summary": "Create a wallet from raw key JSON.", + "description": "**Core call:** `account.createWallet`\n\n**Command line**\n\n```\ncreate-wallet --type= [--keys='']\n```\n\nThe import path. Use `create-currency-wallet` to make a fresh wallet with generated keys.", + "tags": [ + "Keys" + ], + "x-cli": { + "command": "create-wallet", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "account.createWallet", + "x-source": "src/cli/engine/routes/keys.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "walletId": { + "type": "string", + "description": "The new wallet. Its keys are already saved." + } + }, + "required": [ + "walletId" + ] + } + } + } + }, + "default": { + "description": "BAD_REQUEST", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Wallet type, e.g. `wallet:bitcoin`." + }, + "keys": { + "description": "Plugin key material. Omit to let core generate it." + } + }, + "required": [ + "type" + ] + } + } + } + } + } + }, + "/account/{sessionId}/get-wallet-info": { + "get": { + "operationId": "getWalletInfo", + "summary": "Read one wallet's key info.", + "description": "**Core call:** `account.getWalletInfo`\n\n**Command line**\n\n```\nget-wallet-info --id=\n```\n\n", + "tags": [ + "Keys" + ], + "x-cli": { + "command": "get-wallet-info", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "account.getWalletInfo", + "x-source": "src/cli/engine/routes/keys.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + }, + { + "name": "id", + "in": "query", + "required": true, + "description": "The key id, from `all-keys`. Base64, like a wallet id.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "`EdgeWalletInfoFull`, verbatim from core — including the `keys` object.", + "content": { + "application/json": { + "schema": {} + } + } + }, + "default": { + "description": "WALLET_NOT_FOUND, AMBIGUOUS_WALLET_ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/get-raw-private-key": { + "get": { + "operationId": "getRawPrivateKey", + "summary": "Read raw private key material.", + "description": "**Core call:** `account.getRawPrivateKey`\n\n**Command line**\n\n```\nget-raw-private-key --wallet-id=\n```\n\nSecret. Whatever the plugin stores — seed, mnemonic, xpriv.", + "tags": [ + "Keys" + ], + "x-cli": { + "command": "get-raw-private-key", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "account.getRawPrivateKey", + "x-source": "src/cli/engine/routes/keys.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + }, + { + "name": "walletId", + "in": "query", + "required": true, + "description": "The wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns `409 AMBIGUOUS_WALLET_ID` with `details.candidates`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The plugin's key object, at the top level.", + "content": { + "application/json": { + "schema": {} + } + } + }, + "default": { + "description": "WALLET_NOT_FOUND, AMBIGUOUS_WALLET_ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/get-raw-public-key": { + "get": { + "operationId": "getRawPublicKey", + "summary": "Read raw public key material.", + "description": "**Core call:** `account.getRawPublicKey`\n\n**Command line**\n\n```\nget-raw-public-key --wallet-id=\n```\n\n", + "tags": [ + "Keys" + ], + "x-cli": { + "command": "get-raw-public-key", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "account.getRawPublicKey", + "x-source": "src/cli/engine/routes/keys.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + }, + { + "name": "walletId", + "in": "query", + "required": true, + "description": "The wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns `409 AMBIGUOUS_WALLET_ID` with `details.candidates`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The plugin's public key object.", + "content": { + "application/json": { + "schema": {} + } + } + }, + "default": { + "description": "WALLET_NOT_FOUND, AMBIGUOUS_WALLET_ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/get-display-private-key": { + "get": { + "operationId": "getDisplayPrivateKey", + "summary": "Export the private key for display.", + "description": "**Core call:** `account.getDisplayPrivateKey`\n\n**Command line**\n\n```\nget-display-private-key --wallet-id=\n```\n\nSecret. The human-facing form — WIF, seed phrase, whatever the plugin shows on its export screen.", + "tags": [ + "Keys" + ], + "x-cli": { + "command": "get-display-private-key", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "account.getDisplayPrivateKey", + "x-source": "src/cli/engine/routes/keys.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + }, + { + "name": "walletId", + "in": "query", + "required": true, + "description": "The wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns `409 AMBIGUOUS_WALLET_ID` with `details.candidates`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "The displayable private key." + } + }, + "required": [ + "key" + ] + } + } + } + }, + "default": { + "description": "WALLET_NOT_FOUND, AMBIGUOUS_WALLET_ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/get-display-public-key": { + "get": { + "operationId": "getDisplayPublicKey", + "summary": "Export the public key for display.", + "description": "**Core call:** `account.getDisplayPublicKey`\n\n**Command line**\n\n```\nget-display-public-key --wallet-id=\n```\n\nThe xpub or equivalent — safe to share for watch-only use.", + "tags": [ + "Keys" + ], + "x-cli": { + "command": "get-display-public-key", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "account.getDisplayPublicKey", + "x-source": "src/cli/engine/routes/keys.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + }, + { + "name": "walletId", + "in": "query", + "required": true, + "description": "The wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns `409 AMBIGUOUS_WALLET_ID` with `details.candidates`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "The displayable public key." + } + }, + "required": [ + "key" + ] + } + } + } + }, + "default": { + "description": "WALLET_NOT_FOUND, AMBIGUOUS_WALLET_ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/list-splittable-wallet-types": { + "get": { + "operationId": "listSplittableWalletTypes", + "summary": "List chains a wallet can split into.", + "description": "**Core call:** `account.listSplittableWalletTypes`\n\n**Command line**\n\n```\nlist-splittable-wallet-types --wallet-id=\n```\n\nForked-chain support: which wallet types can be derived from these keys.", + "tags": [ + "Keys" + ], + "x-cli": { + "command": "list-splittable-wallet-types", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "account.listSplittableWalletTypes", + "x-source": "src/cli/engine/routes/keys.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + }, + { + "name": "walletId", + "in": "query", + "required": true, + "description": "The wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns `409 AMBIGUOUS_WALLET_ID` with `details.candidates`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "walletTypes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Types valid for `split`." + } + }, + "required": [ + "walletTypes" + ] + } + } + } + }, + "default": { + "description": "WALLET_NOT_FOUND, AMBIGUOUS_WALLET_ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/change-wallet-states": { + "post": { + "operationId": "changeWalletStates", + "summary": "Archive, delete, hide, or reorder wallets.", + "description": "**Core call:** `account.changeWalletStates`\n\n**Command line**\n\n```\nchange-wallet-states [--wallet-states=''] --wallet-id= [--archived=] [--deleted=] [--hidden=] [--sort-index=]\n```\n\nThe canonical backend for every wallet flag; there are no separate archive, unarchive or undelete verbs.", + "tags": [ + "Keys" + ], + "x-cli": { + "command": "change-wallet-states", + "flags": [], + "extra": [ + { + "name": "wallet-id", + "kind": "string", + "required": true, + "doc": "The wallet to change. The command makes it the key of a single-entry `walletStates` map." + }, + { + "name": "archived", + "kind": "boolstr", + "required": false, + "doc": "Hide from the active list." + }, + { + "name": "deleted", + "kind": "boolstr", + "required": false, + "doc": "Mark deleted." + }, + { + "name": "hidden", + "kind": "boolstr", + "required": false, + "doc": "Hide from the wallet picker." + }, + { + "name": "sort-index", + "kind": "string", + "required": false, + "doc": "Position in the wallet list." + } + ], + "custom": true, + "preset": {}, + "notes": "The command builds a single-wallet `walletStates` map from these flags, and needs at least one." + }, + "x-core-call": "account.changeWalletStates", + "x-source": "src/cli/engine/routes/keys.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No content." + }, + "default": { + "description": "BAD_REQUEST", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "walletStates": { + "anyOf": [ + { + "description": "{ [keys: string]: { archived: boolean" + }, + { + "description": "undefined; deleted: boolean" + }, + { + "description": "undefined; hidden: boolean" + }, + { + "description": "undefined; sortIndex: number" + }, + { + "description": "undefined; }; }" + } + ], + "description": "`EdgeWalletStates`: wallet ids to the flags being changed." + } + } + } + } + } + } + } + }, + "/account/{sessionId}/wallet": { + "get": { + "operationId": "walletInfo", + "summary": "Wallet detail.", + "description": "**Core call:** _none — Engine composite of EdgeCurrencyWallet properties plus its EdgeCurrencyConfig token map._\n\n**Command line**\n\n```\nwallet-info --wallet-id=\n```\n\n", + "tags": [ + "Wallet state" + ], + "x-cli": { + "command": "wallet-info", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": null, + "x-core-note": "Engine composite of EdgeCurrencyWallet properties plus its EdgeCurrencyConfig token map.", + "x-source": "src/cli/engine/routes/wallets.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + }, + { + "name": "walletId", + "in": "query", + "required": true, + "description": "The wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns `409 AMBIGUOUS_WALLET_ID` with `details.candidates`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Every WalletSummary field, plus denominations, walletSettings and allTokens.", + "content": { + "application/json": { + "schema": {} + } + } + }, + "default": { + "description": "Error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/wallet/rename-wallet": { + "post": { + "operationId": "renameWallet", + "summary": "Rename a wallet.", + "description": "**Core call:** `wallet.renameWallet`\n\n**Command line**\n\n```\nrename-wallet --wallet-id= --name=\n```\n\n", + "tags": [ + "Wallet state" + ], + "x-cli": { + "command": "rename-wallet", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "wallet.renameWallet", + "x-source": "src/cli/engine/routes/wallets.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No content." + }, + "default": { + "description": "BAD_REQUEST", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "walletId": { + "type": "string", + "description": "The wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns `409 AMBIGUOUS_WALLET_ID` with `details.candidates`." + }, + "name": { + "type": "string", + "description": "The new display name." + } + }, + "required": [ + "walletId", + "name" + ] + } + } + } + } + } + }, + "/account/{sessionId}/wallet/set-fiat-currency-code": { + "post": { + "operationId": "setFiatCurrencyCode", + "summary": "Change a wallet's fiat currency.", + "description": "**Core call:** `wallet.setFiatCurrencyCode`\n\n**Command line**\n\n```\nset-fiat-currency-code --wallet-id= --fiat-currency-code=\n```\n\nAffects how balances and history are priced, not the asset itself.", + "tags": [ + "Wallet state" + ], + "x-cli": { + "command": "set-fiat-currency-code", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "wallet.setFiatCurrencyCode", + "x-source": "src/cli/engine/routes/wallets.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No content." + }, + "default": { + "description": "BAD_REQUEST", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "walletId": { + "type": "string", + "description": "The wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns `409 AMBIGUOUS_WALLET_ID` with `details.candidates`." + }, + "fiatCurrencyCode": { + "type": "string", + "description": "e.g. `iso:EUR`." + } + }, + "required": [ + "walletId", + "fiatCurrencyCode" + ] + } + } + } + } + } + }, + "/account/{sessionId}/wallet/change-paused": { + "post": { + "operationId": "changePaused", + "summary": "Pause or resume a wallet engine.", + "description": "**Core call:** `wallet.changePaused`\n\n**Command line**\n\n```\nchange-paused --wallet-id= --paused=true|false\n```\n\nA paused wallet stops syncing, which is how a caller quiets a chain it does not currently care about.", + "tags": [ + "Wallet state" + ], + "x-cli": { + "command": "change-paused", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "wallet.changePaused", + "x-source": "src/cli/engine/routes/wallets.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No content." + }, + "default": { + "description": "BAD_REQUEST", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "walletId": { + "type": "string", + "description": "The wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns `409 AMBIGUOUS_WALLET_ID` with `details.candidates`." + }, + "paused": { + "type": "boolean", + "description": "True to stop syncing." + } + }, + "required": [ + "walletId", + "paused" + ] + } + } + } + } + } + }, + "/account/{sessionId}/wallet/sync": { + "post": { + "operationId": "walletSync", + "summary": "Nudge one wallet to sync.", + "description": "**Core call:** `wallet.sync`\n\n**Command line**\n\n```\nwallet-sync --wallet-id=\n```\n\n", + "tags": [ + "Wallet state" + ], + "x-cli": { + "command": "wallet-sync", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "wallet.sync", + "x-source": "src/cli/engine/routes/wallets.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No content." + }, + "default": { + "description": "Error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "walletId": { + "type": "string", + "description": "The wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns `409 AMBIGUOUS_WALLET_ID` with `details.candidates`." + } + }, + "required": [ + "walletId" + ] + } + } + } + } + } + }, + "/account/{sessionId}/wallet/resync-blockchain": { + "post": { + "operationId": "resyncBlockchain", + "summary": "Rescan the blockchain from scratch.", + "description": "**Core call:** `wallet.resyncBlockchain`\n\n**Command line**\n\n```\nresync-blockchain --wallet-id=\n```\n\nDrops cached chain state and re-scans. Expensive, and the wallet reports an incomplete balance until it finishes.", + "tags": [ + "Wallet state" + ], + "x-cli": { + "command": "resync-blockchain", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "wallet.resyncBlockchain", + "x-source": "src/cli/engine/routes/wallets.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No content." + }, + "default": { + "description": "Error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "walletId": { + "type": "string", + "description": "The wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns `409 AMBIGUOUS_WALLET_ID` with `details.candidates`." + } + }, + "required": [ + "walletId" + ] + } + } + } + } + } + }, + "/account/{sessionId}/wallet/split": { + "post": { + "operationId": "splitWallet", + "summary": "Split a wallet into another chain.", + "description": "**Core call:** `wallet.split`\n\n**Command line**\n\n```\nsplit --wallet-id= --split-wallets=''\n```\n\nForked-chain support: derive a wallet of a different type from the same keys. `list-splittable-wallet-types` says which are valid.", + "tags": [ + "Wallet state" + ], + "x-cli": { + "command": "split", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "wallet.split", + "x-source": "src/cli/engine/routes/wallets.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": {}, + "description": "Per-entry outcomes, like batch create." + } + }, + "required": [ + "results" + ] + } + } + } + }, + "default": { + "description": "BAD_REQUEST", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "walletId": { + "type": "string", + "description": "The wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns `409 AMBIGUOUS_WALLET_ID` with `details.candidates`." + }, + "splitWallets": { + "type": "array", + "items": {}, + "description": "`EdgeSplitCurrencyWallet[]`: walletType, name, fiatCurrencyCode." + } + }, + "required": [ + "walletId", + "splitWallets" + ] + } + } + } + } + } + }, + "/account/{sessionId}/wallet/dump-data": { + "get": { + "operationId": "dumpData", + "summary": "Dump wallet engine state.", + "description": "**Core call:** `wallet.dumpData`\n\n**Command line**\n\n```\ndump-data --wallet-id=\n```\n\nPlugin-defined debug output. Shape varies by plugin and can be very large.", + "tags": [ + "Wallet state" + ], + "x-cli": { + "command": "dump-data", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "wallet.dumpData", + "x-source": "src/cli/engine/routes/wallets.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + }, + { + "name": "walletId", + "in": "query", + "required": true, + "description": "The wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns `409 AMBIGUOUS_WALLET_ID` with `details.candidates`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "`EdgeDataDump`, straight from the plugin.", + "content": { + "application/json": { + "schema": {} + } + } + }, + "default": { + "description": "Error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/wallet/balance-map": { + "get": { + "operationId": "balanceMap", + "summary": "Balances for every asset in the wallet.", + "description": "**Core call:** `wallet.balanceMap`\n\n**Command line**\n\n```\nbalance-map --wallet-id= [--token-id=]\n```\n\nThe native currency plus every enabled token.", + "tags": [ + "Wallet state" + ], + "x-cli": { + "command": "balance-map", + "flags": [], + "extra": [ + { + "name": "token-id", + "kind": "string", + "required": false, + "doc": "Client-side filter; core has no single-balance accessor." + } + ], + "custom": true, + "preset": {} + }, + "x-core-call": "wallet.balanceMap", + "x-core-note": "Rendered as an array, with currencyCode and displayAmount added from the wallet's denominations.", + "x-source": "src/cli/engine/routes/wallets.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + }, + { + "name": "walletId", + "in": "query", + "required": true, + "description": "The wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns `409 AMBIGUOUS_WALLET_ID` with `details.candidates`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "balances": { + "type": "array", + "items": { + "anyOf": [ + { + "description": "{ tokenId: string" + }, + { + "description": "null; currencyCode: string; nativeAmount: string; displayAmount: string; }" + } + ] + }, + "description": "One entry per asset the wallet holds, native coin first." + } + }, + "required": [ + "balances" + ] + } + } + } + }, + "default": { + "description": "Error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/wallet/get-addresses": { + "get": { + "operationId": "getAddresses", + "summary": "Receive addresses.", + "description": "**Core call:** `wallet.getAddresses`\n\n**Command line**\n\n```\nget-addresses --wallet-id= [--token-id=] [--force-index=]\n```\n\n", + "tags": [ + "Wallet state" + ], + "x-cli": { + "command": "get-addresses", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "wallet.getAddresses", + "x-source": "src/cli/engine/routes/wallets.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + }, + { + "name": "walletId", + "in": "query", + "required": true, + "description": "The wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns `409 AMBIGUOUS_WALLET_ID` with `details.candidates`.", + "schema": { + "type": "string" + } + }, + { + "name": "tokenId", + "in": "query", + "required": false, + "description": "Defaults to the native asset.", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + { + "name": "forceIndex", + "in": "query", + "required": false, + "description": "Derive at a specific index.", + "schema": { + "type": "number" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "addresses": { + "type": "array", + "items": {}, + "description": "`EdgeAddress[]`: addressType, publicAddress, nativeBalance." + } + }, + "required": [ + "addresses" + ] + } + } + } + }, + "default": { + "description": "Error.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/wallet/tokens": { + "get": { + "operationId": "walletTokens", + "summary": "List a wallet's tokens.", + "description": "**Core call:** _none — Engine composite of the EdgeCurrencyConfig token maps plus wallet.enabledTokenIds and wallet.detectedTokenIds._\n\n**Command line**\n\n```\nwallet-tokens --wallet-id=\n```\n\n\"Enabled\" tokens are the ones the wallet syncs balances for; \"detected\" ones were seen on-chain but are not yet enabled.", + "tags": [ + "Tokens" + ], + "x-cli": { + "command": "wallet-tokens", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": null, + "x-core-note": "Engine composite of the EdgeCurrencyConfig token maps plus wallet.enabledTokenIds and wallet.detectedTokenIds.", + "x-source": "src/cli/engine/routes/tokens.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + }, + { + "name": "walletId", + "in": "query", + "required": true, + "description": "The wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns `409 AMBIGUOUS_WALLET_ID` with `details.candidates`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "allTokens": { + "description": "Built-in and custom together, keyed by tokenId. Large on EVM chains." + }, + "builtinTokens": { + "description": "`EdgeToken` by tokenId: everything the plugin ships with." + }, + "customTokens": { + "description": "`EdgeToken` by tokenId: tokens this account added by hand." + }, + "enabledTokenIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Which of the above the wallet is actually tracking." + }, + "detectedTokenIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Seen on-chain but not enabled, so their balances are not synced." + } + }, + "required": [ + "allTokens", + "builtinTokens", + "customTokens", + "enabledTokenIds", + "detectedTokenIds" + ] + } + } + } + }, + "default": { + "description": "WALLET_NOT_FOUND, AMBIGUOUS_WALLET_ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/wallet/change-enabled-token-ids": { + "post": { + "operationId": "changeEnabledTokenIds", + "summary": "Set the enabled token set.", + "description": "**Core call:** `wallet.changeEnabledTokenIds`\n\n**Command line**\n\n```\nchange-enabled-token-ids --wallet-id= --token-ids='' [--add=] [--remove=]\n```\n\nAbsolute: anything missing from `tokenIds` is disabled. Core has only this setter, so there is no add or remove call.", + "tags": [ + "Tokens" + ], + "x-cli": { + "command": "change-enabled-token-ids", + "flags": [], + "extra": [ + { + "name": "add", + "kind": "repeat", + "required": false, + "doc": "Read the current set, add this id, write it back." + }, + { + "name": "remove", + "kind": "repeat", + "required": false, + "doc": "Read the current set, drop this id, write it back." + } + ], + "custom": true, + "preset": {} + }, + "x-core-call": "wallet.changeEnabledTokenIds", + "x-source": "src/cli/engine/routes/tokens.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "enabledTokenIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The wallet’s enabled tokens after the change, not just what changed." + } + }, + "required": [ + "enabledTokenIds" + ] + } + } + } + }, + "default": { + "description": "BAD_REQUEST, WALLET_NOT_FOUND, AMBIGUOUS_WALLET_ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "walletId": { + "type": "string", + "description": "The wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns `409 AMBIGUOUS_WALLET_ID` with `details.candidates`." + }, + "tokenIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The complete desired set." + } + }, + "required": [ + "walletId", + "tokenIds" + ] + } + } + } + } + } + }, + "/account/{sessionId}/wallet/get-transactions": { + "get": { + "operationId": "getTransactions", + "summary": "List or export a wallet's transactions.", + "description": "**Core call:** `wallet.getTransactions`\n\n**Command line**\n\n```\nget-transactions --wallet-id= [--token-id=] [--limit=] [--offset=] [--start-date=] [--end-date=] [--search-string=] [--spam-threshold=] [--fiat=] [--export-format=] [--bitwave-account=] [--out=]\n```\n\nReads history, overlays the display metadata the GUI shows, fills historical fiat, and optionally formats the result — all on this one call.", + "tags": [ + "Transactions" + ], + "x-cli": { + "command": "get-transactions", + "flags": [ + { + "name": "bitwave-account", + "maps": "bitwaveAccountId", + "repeat": false + } + ], + "extra": [ + { + "name": "out", + "kind": "string", + "required": false, + "requiredWith": "exportFormat", + "doc": "Where to write the returned files. One format: the path. Several: a stem, plus .csv / .qbo / .bitwave.csv." + } + ], + "custom": true, + "preset": {} + }, + "x-core-call": "wallet.getTransactions", + "x-source": "src/cli/engine/routes/transactions.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + }, + { + "name": "walletId", + "in": "query", + "required": true, + "description": "The wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns `409 AMBIGUOUS_WALLET_ID` with `details.candidates`.", + "schema": { + "type": "string" + } + }, + { + "name": "tokenId", + "in": "query", + "required": false, + "description": "Defaults to the native asset.", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "description": "Omitting it returns every transaction from `offset` on.", + "schema": { + "type": "number" + } + }, + { + "name": "offset", + "in": "query", + "required": false, + "description": "Where to start. Defaults to 0.", + "schema": { + "type": "number" + } + }, + { + "name": "startDate", + "in": "query", + "required": false, + "description": "ISO-8601, or epoch milliseconds.", + "schema": { + "description": "Date" + } + }, + { + "name": "endDate", + "in": "query", + "required": false, + "description": "ISO-8601, or epoch milliseconds.", + "schema": { + "description": "Date" + } + }, + { + "name": "searchString", + "in": "query", + "required": false, + "description": "Matches payee, category, notes and txid.", + "schema": { + "type": "string" + } + }, + { + "name": "spamThreshold", + "in": "query", + "required": false, + "description": "Native-amount floor. Omitted, the account spam-filter setting applies; passing it always overrides.", + "schema": { + "type": "string" + } + }, + { + "name": "fiat", + "in": "query", + "required": false, + "description": "Three-letter ISO 4217 code. Defaults to the account defaultIsoFiat.", + "schema": { + "type": "string" + } + }, + { + "name": "exportFormat", + "in": "query", + "required": false, + "description": "Comma list of `csv`, `qbo`, `bitwave`.", + "schema": { + "type": "string" + } + }, + { + "name": "bitwaveAccountId", + "in": "query", + "required": false, + "description": "A 400 unless `exportFormat` includes `bitwave`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "`{ transactions, total, isoFiat }`, or `{ ok, isoFiat, total, files }` when exportFormat is set.", + "content": { + "application/json": { + "schema": {} + } + } + }, + "default": { + "description": "BAD_REQUEST, MISSING_BITWAVE_ACCOUNT_ID, WALLET_NOT_FOUND, AMBIGUOUS_WALLET_ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/wallet/get-num-transactions": { + "get": { + "operationId": "getNumTransactions", + "summary": "Count transactions in a wallet.", + "description": "**Core call:** `wallet.getNumTransactions`\n\n**Command line**\n\n```\nget-num-transactions --wallet-id= [--token-id=]\n```\n\nCheaper than listing when only the total matters.", + "tags": [ + "Transactions" + ], + "x-cli": { + "command": "get-num-transactions", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "wallet.getNumTransactions", + "x-source": "src/cli/engine/routes/transactions.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + }, + { + "name": "walletId", + "in": "query", + "required": true, + "description": "The wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns `409 AMBIGUOUS_WALLET_ID` with `details.candidates`.", + "schema": { + "type": "string" + } + }, + { + "name": "tokenId", + "in": "query", + "required": false, + "description": "Defaults to the native asset.", + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "numTransactions": { + "type": "number", + "description": "Every transaction the wallet knows of." + } + }, + "required": [ + "numTransactions" + ] + } + } + } + }, + "default": { + "description": "WALLET_NOT_FOUND, AMBIGUOUS_WALLET_ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/wallet/save-tx-metadata": { + "post": { + "operationId": "saveTxMetadata", + "summary": "Save transaction metadata.", + "description": "**Core call:** `wallet.saveTxMetadata`\n\n**Command line**\n\n```\nsave-tx-metadata --wallet-id= --txid= [--token-id=] --metadata=''\n```\n\nOne of only two routes that write transaction metadata to disk.", + "tags": [ + "Transactions" + ], + "x-cli": { + "command": "save-tx-metadata", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "wallet.saveTxMetadata", + "x-source": "src/cli/engine/routes/transactions.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No content." + }, + "default": { + "description": "BAD_REQUEST, WALLET_NOT_FOUND, AMBIGUOUS_WALLET_ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "walletId": { + "type": "string", + "description": "The wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns `409 AMBIGUOUS_WALLET_ID` with `details.candidates`." + }, + "txid": { + "type": "string", + "description": "Which transaction to tag." + }, + "tokenId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Defaults to the native asset." + }, + "metadata": { + "description": "`EdgeMetadataChange`: name, category, notes, exchangeAmount." + } + }, + "required": [ + "walletId", + "txid", + "metadata" + ] + } + } + } + } + } + }, + "/account/{sessionId}/wallet/save-tx-action": { + "post": { + "operationId": "saveTxAction", + "summary": "Save a transaction action.", + "description": "**Core call:** `wallet.saveTxAction`\n\n**Command line**\n\n```\nsave-tx-action --wallet-id= --txid= [--token-id=] --saved-action='' [--asset-action='']\n```\n\nRecords what a transaction *was* — a swap, a stake — beyond its metadata.", + "tags": [ + "Transactions" + ], + "x-cli": { + "command": "save-tx-action", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "wallet.saveTxAction", + "x-source": "src/cli/engine/routes/transactions.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "No content." + }, + "default": { + "description": "BAD_REQUEST, WALLET_NOT_FOUND, AMBIGUOUS_WALLET_ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "walletId": { + "type": "string", + "description": "The wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns `409 AMBIGUOUS_WALLET_ID` with `details.candidates`." + }, + "txid": { + "type": "string", + "description": "Which transaction to annotate." + }, + "tokenId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Defaults to the native asset." + }, + "savedAction": { + "description": "`EdgeTxAction` describing what happened." + }, + "assetAction": { + "description": "`EdgeAssetAction`." + } + }, + "required": [ + "walletId", + "txid", + "savedAction" + ] + } + } + } + } + } + }, + "/account/{sessionId}/object/{objectId}": { + "get": { + "operationId": "getObject", + "summary": "Inspect an object handle.", + "description": "**Core call:** _none — Engine handle store; core identifies these values by object reference._\n\n**Command line**\n\n```\nobject-get \n```\n\nWorks for every kind: transactions, pending logins, swap quotes.", + "tags": [ + "Object handles" + ], + "x-cli": { + "command": "object-get", + "positional": "objectId", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": null, + "x-core-note": "Engine handle store; core identifies these values by object reference.", + "x-source": "src/cli/engine/routes/objects.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + }, + { + "name": "objectId", + "in": "path", + "required": true, + "description": "An ephemeral object handle id.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The handle fields, plus a `value` holding the live core object.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "objectId": { + "type": "string", + "description": "Handle for the value the engine is holding. Pass it to the calls that consume it." + }, + "kind": { + "type": "string", + "description": "What the handle refers to, which decides the calls that accept it." + }, + "expiresAt": { + "type": "string", + "description": "When the engine drops the handle. Handles live 5 minutes." + }, + "sessionId": { + "type": "string", + "description": "Session that created the handle; only that session may use it." + }, + "walletId": { + "type": "string", + "description": "Wallet the handle is bound to, when it belongs to one." + } + }, + "required": [ + "objectId", + "kind", + "expiresAt" + ] + } + } + } + }, + "default": { + "description": "OBJECT_NOT_FOUND, OBJECT_EXPIRED, OBJECT_SESSION_MISMATCH", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/object/delete/{objectId}": { + "post": { + "operationId": "deleteObject", + "summary": "Release an object handle.", + "description": "**Core call:** _none — Engine handle store._\n\n**Command line**\n\n```\nobject-delete \n```\n\nRuns the handle's cleanup — closing a swap quote, cancelling a pending login — instead of waiting out the TTL.", + "tags": [ + "Object handles" + ], + "x-cli": { + "command": "object-delete", + "positional": "objectId", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": null, + "x-core-note": "Engine handle store.", + "x-source": "src/cli/engine/routes/objects.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + }, + { + "name": "objectId", + "in": "path", + "required": true, + "description": "An ephemeral object handle id.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ok": { + "type": "boolean", + "description": "Always true; a failure arrives as an error envelope." + }, + "objectId": { + "type": "string", + "description": "The handle this call consumed. It is now expired." + } + }, + "required": [ + "ok", + "objectId" + ] + } + } + } + }, + "default": { + "description": "OBJECT_NOT_FOUND, OBJECT_EXPIRED, OBJECT_SESSION_MISMATCH", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/wallet/get-max-spendable": { + "post": { + "operationId": "getMaxSpendable", + "summary": "Largest sendable amount.", + "description": "**Core call:** `wallet.getMaxSpendable`\n\n**Command line**\n\n```\nget-max-spendable --wallet-id= [--spend-info=''] [--to=] [--native-amount=] [--amount=] [--token-id=] [--metadata='']\n```\n\nWhat empties the wallet after fees. A destination is still required, since fees depend on it.", + "tags": [ + "Spending" + ], + "x-cli": { + "command": "get-max-spendable", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "wallet.getMaxSpendable", + "x-source": "src/cli/engine/routes/spend.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "nativeAmount": { + "type": "string", + "description": "The most this wallet can send." + } + }, + "required": [ + "nativeAmount" + ] + } + } + } + }, + "default": { + "description": "INSUFFICIENT_FUNDS, BAD_REQUEST, NETWORK_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "walletId": { + "type": "string", + "description": "The wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns `409 AMBIGUOUS_WALLET_ID` with `details.candidates`." + }, + "spendInfo": { + "description": "A full `EdgeSpendInfo`, used as-is when present." + }, + "to": { + "type": "string", + "description": "Address or BIP21 URI, run through `wallet.parseUri`." + }, + "nativeAmount": { + "type": "string", + "description": "How much, in native units." + }, + "amount": { + "type": "string", + "description": "Alias of `nativeAmount`." + }, + "tokenId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Defaults to the native asset." + }, + "metadata": { + "description": "Wins over anything parsed out of the URI." + } + }, + "required": [ + "walletId" + ] + } + } + } + } + } + }, + "/account/{sessionId}/wallet/spend": { + "post": { + "operationId": "spend", + "summary": "Send funds.", + "description": "**Core call:** _none — GUI composite: makeSpend, signTx, broadcastTx and saveTx together._\n\n**Command line**\n\n```\nspend --wallet-id= [--spend-info=''] [--to=] [--native-amount=] [--amount=] [--token-id=] [--metadata=''] [--use-max] [--dry-run] [--broadcast] [--save]\n```\n\n`makeSpend`, then `signTx`, then optionally `broadcastTx` and `saveTx`, in one request. `broadcast` and `save` both default to true, so a bare body with a destination and an amount moves real money. A completed spend leaves no handle behind.", + "tags": [ + "Spending" + ], + "x-cli": { + "command": "spend", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": null, + "x-core-note": "GUI composite: makeSpend, signTx, broadcastTx and saveTx together.", + "x-source": "src/cli/engine/routes/spend.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "`{ transaction }`, plus `saveError` when the broadcast succeeded but saving failed. With dryRun, a TransactionHandle instead.", + "content": { + "application/json": { + "schema": {} + } + } + }, + "default": { + "description": "INSUFFICIENT_FUNDS, DUST_SPEND, PENDING_FUNDS, SPEND_TO_SELF, NO_AMOUNT_SPECIFIED, BAD_REQUEST, NETWORK_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "walletId": { + "type": "string", + "description": "The wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns `409 AMBIGUOUS_WALLET_ID` with `details.candidates`." + }, + "spendInfo": { + "description": "A full `EdgeSpendInfo`, used as-is when present." + }, + "to": { + "type": "string", + "description": "Address or BIP21 URI, run through `wallet.parseUri`." + }, + "nativeAmount": { + "type": "string", + "description": "How much, in native units." + }, + "amount": { + "type": "string", + "description": "Alias of `nativeAmount`." + }, + "tokenId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Defaults to the native asset." + }, + "metadata": { + "description": "Wins over anything parsed out of the URI." + }, + "useMax": { + "type": "boolean", + "description": "Replace the first target's amount with the maximum." + }, + "dryRun": { + "type": "boolean", + "description": "Build only. Never signs or broadcasts." + }, + "broadcast": { + "type": "boolean", + "description": "Defaults to **true**." + }, + "save": { + "type": "boolean", + "description": "Defaults to **true**." + } + }, + "required": [ + "walletId" + ] + } + } + } + } + } + }, + "/account/{sessionId}/wallet/make-spend": { + "post": { + "operationId": "makeSpend", + "summary": "Build an unsigned transaction.", + "description": "**Core call:** `wallet.makeSpend`\n\n**Command line**\n\n```\nmake-spend --wallet-id= [--spend-info=''] [--to=] [--native-amount=] [--amount=] [--token-id=] [--metadata='']\n```\n\nFirst step of the staged workflow: nothing is signed and no funds move. Inspect `transaction.networkFee` on the result before signing.", + "tags": [ + "Spending" + ], + "x-cli": { + "command": "make-spend", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "wallet.makeSpend", + "x-source": "src/cli/engine/routes/spend.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "objectId": { + "type": "string", + "description": "Handle for the value the engine is holding. Pass it to the calls that consume it." + }, + "kind": { + "type": "string", + "description": "What the handle refers to, which decides the calls that accept it." + }, + "expiresAt": { + "type": "string", + "description": "When the engine drops the handle. Handles live 5 minutes." + }, + "sessionId": { + "type": "string", + "description": "Session that created the handle; only that session may use it." + }, + "walletId": { + "type": "string", + "description": "Wallet the handle is bound to, when it belongs to one." + }, + "transaction": { + "description": "`EdgeTransaction` as it stands after this step. Unsigned after `make-spend`, signed after `sign-tx`, and carrying a txid once broadcast." + } + }, + "required": [ + "objectId", + "kind", + "expiresAt", + "transaction" + ] + } + } + } + }, + "default": { + "description": "INSUFFICIENT_FUNDS, DUST_SPEND, NO_AMOUNT_SPECIFIED, BAD_REQUEST", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "walletId": { + "type": "string", + "description": "The wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns `409 AMBIGUOUS_WALLET_ID` with `details.candidates`." + }, + "spendInfo": { + "description": "A full `EdgeSpendInfo`, used as-is when present." + }, + "to": { + "type": "string", + "description": "Address or BIP21 URI, run through `wallet.parseUri`." + }, + "nativeAmount": { + "type": "string", + "description": "How much, in native units." + }, + "amount": { + "type": "string", + "description": "Alias of `nativeAmount`." + }, + "tokenId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Defaults to the native asset." + }, + "metadata": { + "description": "Wins over anything parsed out of the URI." + } + }, + "required": [ + "walletId" + ] + } + } + } + } + } + }, + "/account/{sessionId}/sign-tx/{objectId}": { + "post": { + "operationId": "signTx", + "summary": "Sign a staged transaction.", + "description": "**Core call:** `wallet.signTx`\n\n**Command line**\n\n```\nsign-tx \n```\n\nKeeps the same handle and pushes its expiry out another five minutes.", + "tags": [ + "Spending" + ], + "x-cli": { + "command": "sign-tx", + "positional": "objectId", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "wallet.signTx", + "x-source": "src/cli/engine/routes/spend.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + }, + { + "name": "objectId", + "in": "path", + "required": true, + "description": "From `make-spend`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "objectId": { + "type": "string", + "description": "Handle for the value the engine is holding. Pass it to the calls that consume it." + }, + "kind": { + "type": "string", + "description": "What the handle refers to, which decides the calls that accept it." + }, + "expiresAt": { + "type": "string", + "description": "When the engine drops the handle. Handles live 5 minutes." + }, + "sessionId": { + "type": "string", + "description": "Session that created the handle; only that session may use it." + }, + "walletId": { + "type": "string", + "description": "Wallet the handle is bound to, when it belongs to one." + }, + "transaction": { + "description": "`EdgeTransaction` as it stands after this step. Unsigned after `make-spend`, signed after `sign-tx`, and carrying a txid once broadcast." + } + }, + "required": [ + "objectId", + "kind", + "expiresAt", + "transaction" + ] + } + } + } + }, + "default": { + "description": "BAD_REQUEST", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/broadcast-tx/{objectId}": { + "post": { + "operationId": "broadcastTx", + "summary": "Broadcast a signed transaction.", + "description": "**Core call:** `wallet.broadcastTx`\n\n**Command line**\n\n```\nbroadcast-tx \n```\n\nThe irreversible step: once this returns, the funds have left the wallet.", + "tags": [ + "Spending" + ], + "x-cli": { + "command": "broadcast-tx", + "positional": "objectId", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "wallet.broadcastTx", + "x-source": "src/cli/engine/routes/spend.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + }, + { + "name": "objectId", + "in": "path", + "required": true, + "description": "From `sign-tx`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The handle survives, so `save-tx` can still run.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "objectId": { + "type": "string", + "description": "Handle for the value the engine is holding. Pass it to the calls that consume it." + }, + "kind": { + "type": "string", + "description": "What the handle refers to, which decides the calls that accept it." + }, + "expiresAt": { + "type": "string", + "description": "When the engine drops the handle. Handles live 5 minutes." + }, + "sessionId": { + "type": "string", + "description": "Session that created the handle; only that session may use it." + }, + "walletId": { + "type": "string", + "description": "Wallet the handle is bound to, when it belongs to one." + }, + "transaction": { + "description": "`EdgeTransaction` as it stands after this step. Unsigned after `make-spend`, signed after `sign-tx`, and carrying a txid once broadcast." + } + }, + "required": [ + "objectId", + "kind", + "expiresAt", + "transaction" + ] + } + } + } + }, + "default": { + "description": "BAD_REQUEST, NETWORK_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/save-tx/{objectId}": { + "post": { + "operationId": "saveTx", + "summary": "Record a transaction and release its handle.", + "description": "**Core call:** `wallet.saveTx`\n\n**Command line**\n\n```\nsave-tx \n```\n\nFinal step. The handle is gone afterwards, so a second call is a 404.", + "tags": [ + "Spending" + ], + "x-cli": { + "command": "save-tx", + "positional": "objectId", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "wallet.saveTx", + "x-source": "src/cli/engine/routes/spend.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + }, + { + "name": "objectId", + "in": "path", + "required": true, + "description": "The handle to persist and release.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ok": { + "type": "boolean", + "description": "Always true; a failure arrives as an error envelope." + }, + "objectId": { + "type": "string", + "description": "The handle this call consumed. It is now expired." + } + }, + "required": [ + "ok", + "objectId" + ] + } + } + } + }, + "default": { + "description": "BAD_REQUEST", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/wallet/accelerate": { + "post": { + "operationId": "accelerate", + "summary": "Fee-bump a pending transaction.", + "description": "**Core call:** `wallet.accelerate`\n\n**Command line**\n\n```\naccelerate --wallet-id= [--object-id=] [--transaction='']\n```\n\nReplace-by-fee, where the plugin supports it. Returns a new unsigned transaction to sign and broadcast.", + "tags": [ + "Spending" + ], + "x-cli": { + "command": "accelerate", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "wallet.accelerate", + "x-source": "src/cli/engine/routes/spend.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Given objectId the same handle is updated; given a transaction a new one is created.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "objectId": { + "type": "string", + "description": "Handle for the value the engine is holding. Pass it to the calls that consume it." + }, + "kind": { + "type": "string", + "description": "What the handle refers to, which decides the calls that accept it." + }, + "expiresAt": { + "type": "string", + "description": "When the engine drops the handle. Handles live 5 minutes." + }, + "sessionId": { + "type": "string", + "description": "Session that created the handle; only that session may use it." + }, + "walletId": { + "type": "string", + "description": "Wallet the handle is bound to, when it belongs to one." + }, + "transaction": { + "description": "`EdgeTransaction` as it stands after this step. Unsigned after `make-spend`, signed after `sign-tx`, and carrying a txid once broadcast." + } + }, + "required": [ + "objectId", + "kind", + "expiresAt", + "transaction" + ] + } + } + } + }, + "default": { + "description": "BAD_REQUEST", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "walletId": { + "type": "string", + "description": "The wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns `409 AMBIGUOUS_WALLET_ID` with `details.candidates`." + }, + "objectId": { + "type": "string", + "description": "Handle of the transaction to bump." + }, + "transaction": { + "description": "Or the transaction itself." + } + }, + "required": [ + "walletId" + ] + } + } + } + } + } + }, + "/account/{sessionId}/wallet/sweep-private-keys": { + "post": { + "operationId": "sweepPrivateKeys", + "summary": "Sweep private keys into this wallet.", + "description": "**Core call:** `wallet.sweepPrivateKeys`\n\n**Command line**\n\n```\nsweep-private-keys --wallet-id= --spend-info=''\n```\n\nBuilds a transaction moving everything from an external key. Returns an unsigned handle: sign, broadcast and save it like any staged spend.", + "tags": [ + "Spending" + ], + "x-cli": { + "command": "sweep-private-keys", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "wallet.sweepPrivateKeys", + "x-source": "src/cli/engine/routes/spend.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "objectId": { + "type": "string", + "description": "Handle for the value the engine is holding. Pass it to the calls that consume it." + }, + "kind": { + "type": "string", + "description": "What the handle refers to, which decides the calls that accept it." + }, + "expiresAt": { + "type": "string", + "description": "When the engine drops the handle. Handles live 5 minutes." + }, + "sessionId": { + "type": "string", + "description": "Session that created the handle; only that session may use it." + }, + "walletId": { + "type": "string", + "description": "Wallet the handle is bound to, when it belongs to one." + }, + "transaction": { + "description": "`EdgeTransaction` as it stands after this step. Unsigned after `make-spend`, signed after `sign-tx`, and carrying a txid once broadcast." + } + }, + "required": [ + "objectId", + "kind", + "expiresAt", + "transaction" + ] + } + } + } + }, + "default": { + "description": "BAD_REQUEST, INSUFFICIENT_FUNDS, NETWORK_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "walletId": { + "type": "string", + "description": "The wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns `409 AMBIGUOUS_WALLET_ID` with `details.candidates`." + }, + "spendInfo": { + "description": "A full `EdgeSpendInfo`, with the keys to sweep in `privateKeys`." + } + }, + "required": [ + "walletId", + "spendInfo" + ] + } + } + } + } + } + }, + "/account/{sessionId}/wallet/sign-bytes": { + "post": { + "operationId": "signBytes", + "summary": "Sign arbitrary bytes.", + "description": "**Core call:** `wallet.signBytes`\n\n**Command line**\n\n```\nsign-bytes --wallet-id= [--bytes=] [--other-params='']\n```\n\nMessage signing and proof-of-ownership, for plugins that support it.", + "tags": [ + "Spending" + ], + "x-cli": { + "command": "sign-bytes", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "wallet.signBytes", + "x-source": "src/cli/engine/routes/spend.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "signature": { + "type": "string", + "description": "Base64." + } + }, + "required": [ + "signature" + ] + } + } + } + }, + "default": { + "description": "BAD_REQUEST", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "walletId": { + "type": "string", + "description": "The wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns `409 AMBIGUOUS_WALLET_ID` with `details.candidates`." + }, + "bytes": { + "type": "string", + "description": "Base64. Defaults to empty when absent." + }, + "otherParams": { + "description": "Plugin-specific options. Bitcoin needs `{ publicAddress }`; other plugins take nothing, or refuse the call entirely." + } + }, + "required": [ + "walletId" + ] + } + } + } + } + } + }, + "/account/{sessionId}/wallet/get-payment-protocol-info": { + "get": { + "operationId": "getPaymentProtocolInfo", + "summary": "Fetch a BIP70 payment request.", + "description": "**Core call:** `wallet.getPaymentProtocolInfo`\n\n**Command line**\n\n```\nget-payment-protocol-info --wallet-id= --payment-protocol-url=\n```\n\nFeed `spendTargets` from the result into `make-spend` to pay it.", + "tags": [ + "Spending" + ], + "x-cli": { + "command": "get-payment-protocol-info", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "wallet.getPaymentProtocolInfo", + "x-source": "src/cli/engine/routes/spend.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + }, + { + "name": "walletId", + "in": "query", + "required": true, + "description": "The wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns `409 AMBIGUOUS_WALLET_ID` with `details.candidates`.", + "schema": { + "type": "string" + } + }, + { + "name": "paymentProtocolUrl", + "in": "query", + "required": true, + "description": "The payment-request URL.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "`EdgePaymentProtocolInfo`: domain, memo, merchant, nativeAmount, spendTargets.", + "content": { + "application/json": { + "schema": {} + } + } + }, + "default": { + "description": "BAD_REQUEST, NETWORK_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/fetch-swap-quotes": { + "post": { + "operationId": "fetchSwapQuotes", + "summary": "Fetch swap quotes.", + "description": "**Core call:** `account.fetchSwapQuotes`\n\n**Command line**\n\n```\nfetch-swap-quotes --from-wallet-id= --to-wallet-id= --native-amount= [--from-token-id=] [--to-token-id=] [--quote-for=] [--plugin-id=]\n```\n\nPolls every enabled swap plugin and parks each result under its own `swap_` handle with a 5 minute TTL.", + "tags": [ + "Swap quotes" + ], + "x-cli": { + "command": "fetch-swap-quotes", + "flags": [ + { + "name": "plugin-id", + "maps": "preferPluginId", + "repeat": false + } + ], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "account.fetchSwapQuotes", + "x-source": "src/cli/engine/routes/swap.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "quoteCount": { + "type": "number", + "description": "How many plugins answered." + }, + "quotes": { + "type": "array", + "items": { + "anyOf": [ + { + "description": "{ objectId: string; kind: string; expiresAt: string; pluginId: string; isEstimate: boolean; canBePartial: boolean" + }, + { + "description": "null; maxFulfillmentSeconds: number" + }, + { + "description": "null; minReceiveAmount: string" + }, + { + "description": "null; fromNativeAmount: string; toNativeAmount: string; networkFee: { nativeAmount: string; tokenId: string" + }, + { + "description": "null; }; quoteExpirationDate: string" + }, + { + "description": "null; swapInfo: { pluginId: string; displayName: string; supportEmail: string; isDex: boolean" + }, + { + "description": "null; }; request: { fromTokenId: string" + }, + { + "description": "null; toTokenId: string" + }, + { + "description": "null; nativeAmount: string; quoteFor: \"to\"" + }, + { + "description": "\"from\"" + }, + { + "description": "\"max\"; fromWalletId: string; toWalletId: string; }; }" + } + ] + }, + "description": "One quote per plugin that answered, each already parked under its own handle. Plugins that failed or had nothing to offer are simply absent." + } + }, + "required": [ + "quoteCount", + "quotes" + ] + } + } + } + }, + "default": { + "description": "BAD_REQUEST, SWAP_BELOW_LIMIT, SWAP_ABOVE_LIMIT, SWAP_CURRENCY, SWAP_PERMISSION, SWAP_ADDRESS, SAME_CURRENCY, INSUFFICIENT_FUNDS, WALLET_NOT_FOUND, NETWORK_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "fromWalletId": { + "type": "string", + "description": "Source wallet. Accepts a unique prefix." + }, + "toWalletId": { + "type": "string", + "description": "Destination wallet." + }, + "nativeAmount": { + "type": "string", + "description": "How much, in native units." + }, + "fromTokenId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Defaults to the native asset." + }, + "toTokenId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Defaults to the native asset." + }, + "quoteFor": { + "type": "string", + "description": "`from` spends this much of the source, `to` receives this much at the destination, `max` sends everything. Defaults to `from`." + }, + "preferPluginId": { + "type": "string", + "description": "Restrict to one exchange." + } + }, + "required": [ + "fromWalletId", + "toWalletId", + "nativeAmount" + ] + } + } + } + } + } + }, + "/account/{sessionId}/swap-quote/{objectId}": { + "get": { + "operationId": "getSwapQuote", + "summary": "Re-read a quote.", + "description": "**Core call:** _none — Engine handle store; the quote is a live EdgeSwapQuote held server-side._\n\n**Command line**\n\n```\nswap-quote-get \n```\n\n", + "tags": [ + "Swap quotes" + ], + "x-cli": { + "command": "swap-quote-get", + "positional": "objectId", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": null, + "x-core-note": "Engine handle store; the quote is a live EdgeSwapQuote held server-side.", + "x-source": "src/cli/engine/routes/swap.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + }, + { + "name": "objectId", + "in": "path", + "required": true, + "description": "An ephemeral object handle id.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "objectId": { + "type": "string", + "description": "Handle for the value the engine is holding. Pass it to the calls that consume it." + }, + "kind": { + "type": "string", + "description": "What the handle refers to, which decides the calls that accept it." + }, + "expiresAt": { + "type": "string", + "description": "When the engine drops the handle. Handles live 5 minutes." + }, + "pluginId": { + "type": "string", + "description": "Swap provider that produced this quote." + }, + "isEstimate": { + "type": "boolean", + "description": "True when the provider may settle at a different rate than quoted." + }, + "canBePartial": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "True when the provider may fill only part of the order. Null when it does not say." + }, + "maxFulfillmentSeconds": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "description": "Longest the provider expects a partial fill to take." + }, + "minReceiveAmount": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Least the provider guarantees to deliver, in the destination’s native units." + }, + "fromNativeAmount": { + "type": "string", + "description": "Amount leaving the source wallet." + }, + "toNativeAmount": { + "type": "string", + "description": "Amount arriving in the destination wallet." + }, + "networkFee": { + "anyOf": [ + { + "description": "{ nativeAmount: string; tokenId: string" + }, + { + "description": "null; }" + } + ], + "description": "On-chain fee for the sending transaction. It is not the provider’s own spread, which is already in the rate." + }, + "quoteExpirationDate": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "When the provider stops honouring the rate. Null when it does not expire." + }, + "swapInfo": { + "anyOf": [ + { + "description": "{ pluginId: string; displayName: string; supportEmail: string; isDex: boolean" + }, + { + "description": "null; }" + } + ], + "description": "`EdgeSwapInfo`: how to name the provider and where to send complaints." + }, + "request": { + "anyOf": [ + { + "description": "{ fromTokenId: string" + }, + { + "description": "null; toTokenId: string" + }, + { + "description": "null; nativeAmount: string; quoteFor: \"to\"" + }, + { + "description": "\"from\"" + }, + { + "description": "\"max\"; fromWalletId: string; toWalletId: string; }" + } + ], + "description": "The `EdgeSwapRequest` this quote answers, echoed back so quotes from different plugins can be compared without tracking what was asked." + } + }, + "required": [ + "objectId", + "kind", + "expiresAt", + "pluginId", + "isEstimate", + "canBePartial", + "maxFulfillmentSeconds", + "minReceiveAmount", + "fromNativeAmount", + "toNativeAmount", + "networkFee", + "quoteExpirationDate", + "swapInfo", + "request" + ] + } + } + } + }, + "default": { + "description": "OBJECT_NOT_FOUND, OBJECT_EXPIRED, OBJECT_KIND_MISMATCH, OBJECT_SESSION_MISMATCH", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/swap-quote/approve/{objectId}": { + "post": { + "operationId": "approveSwapQuote", + "summary": "Execute a quote.", + "description": "**Core call:** `EdgeSwapQuote.approve`\n\n**Command line**\n\n```\napprove-swap-quote \n```\n\nMoves funds. The handle is released afterwards whether or not the response is read, so record `orderId` from it.", + "tags": [ + "Swap quotes" + ], + "x-cli": { + "command": "approve-swap-quote", + "positional": "objectId", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "EdgeSwapQuote.approve", + "x-source": "src/cli/engine/routes/swap.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + }, + { + "name": "objectId", + "in": "path", + "required": true, + "description": "An ephemeral object handle id.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ok": { + "description": "True once the swap is submitted and the send broadcast." + }, + "objectId": { + "type": "string", + "description": "The handle that was consumed." + }, + "orderId": { + "description": "The exchange's order reference, when it gives one." + }, + "destinationAddress": { + "description": "Address the funds were sent to, when the exchange reports one." + }, + "transaction": { + "description": "The on-chain send to the exchange." + } + }, + "required": [ + "ok", + "objectId", + "orderId", + "destinationAddress", + "transaction" + ] + } + } + } + }, + "default": { + "description": "OBJECT_NOT_FOUND, OBJECT_EXPIRED, OBJECT_KIND_MISMATCH, OBJECT_SESSION_MISMATCH, INSUFFICIENT_FUNDS, NETWORK_ERROR", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/swap-quote/close/{objectId}": { + "post": { + "operationId": "closeSwapQuote", + "summary": "Discard a quote.", + "description": "**Core call:** `EdgeSwapQuote.close`\n\n**Command line**\n\n```\nclose-swap-quote \n```\n\nCloses the plugin object without executing, freeing whatever the exchange was holding.", + "tags": [ + "Swap quotes" + ], + "x-cli": { + "command": "close-swap-quote", + "positional": "objectId", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "EdgeSwapQuote.close", + "x-source": "src/cli/engine/routes/swap.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + }, + { + "name": "objectId", + "in": "path", + "required": true, + "description": "An ephemeral object handle id.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success.", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ok": { + "type": "boolean", + "description": "Always true; a failure arrives as an error envelope." + }, + "objectId": { + "type": "string", + "description": "The handle this call consumed. It is now expired." + } + }, + "required": [ + "ok", + "objectId" + ] + } + } + } + }, + "default": { + "description": "OBJECT_NOT_FOUND, OBJECT_EXPIRED, OBJECT_KIND_MISMATCH, OBJECT_SESSION_MISMATCH", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, + "/account/{sessionId}/wallet/parse-uri": { + "post": { + "operationId": "parseUri", + "summary": "Parse a payment URI or address.", + "description": "**Core call:** `wallet.parseUri`\n\n**Command line**\n\n```\nparse-uri --wallet-id= --uri= [--currency-code=]\n```\n\nWhat the GUI address tile does when you paste or scan something.", + "tags": [ + "URIs" + ], + "x-cli": { + "command": "parse-uri", + "flags": [], + "extra": [], + "custom": false, + "preset": {} + }, + "x-core-call": "wallet.parseUri", + "x-source": "src/cli/engine/routes/uri.ts", + "parameters": [ + { + "name": "sessionId", + "in": "path", + "required": true, + "description": "From a successful login. The CLI supplies this from `session.json`, `--session`, or `EDGE_CLI_SESSION`.", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "`EdgeParsedUri`: publicAddress, nativeAmount, currencyCode, metadata, paymentProtocolUrl, …", + "content": { + "application/json": { + "schema": {} + } + } + }, + "default": { + "description": "BAD_REQUEST, WALLET_NOT_FOUND, AMBIGUOUS_WALLET_ID", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "walletId": { + "type": "string", + "description": "The wallet to act on. A full wallet id, or any unique prefix of one. An ambiguous prefix returns `409 AMBIGUOUS_WALLET_ID` with `details.candidates`." + }, + "uri": { + "type": "string", + "description": "A payment URI or a bare address." + }, + "currencyCode": { + "type": "string", + "description": "Disambiguates on chains that carry several assets." + } + }, + "required": [ + "walletId", + "uri" + ] + } + } + } + } + } + }, + "/account/{sessionId}/wallet/encode-uri": { + "post": { + "operationId": "encodeUri", + "summary": "Build a payment URI.", + "description": "**Core call:** `wallet.encodeUri`\n\n**Command line**\n\n```\nencode-uri --wallet-id= --public-address= [--native-amount=] [--label=