Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
20714dc
Lift ESLint exemptions and fix lint drift
paullinator Aug 8, 2026
0ccdf61
Serialize DeviceSettings init and writes
paullinator Aug 8, 2026
73289d9
fixup! Serialize DeviceSettings init and writes
paullinator Sep 25, 2026
5187816
Split env.json into config and keys files
paullinator Aug 14, 2026
7e65d27
fixup! Split env.json into config and keys files
paullinator Sep 17, 2026
b2359cf
fixup! Split env.json into config and keys files
paullinator Sep 17, 2026
f0b78ac
fixup! Split env.json into config and keys files
paullinator Sep 17, 2026
08aeea8
fixup! Split env.json into config and keys files
paullinator Sep 17, 2026
0714d5a
fixup! Split env.json into config and keys files
paullinator Sep 21, 2026
8437049
fixup! Split env.json into config and keys files
paullinator Sep 22, 2026
6e0d08b
fixup! Split env.json into config and keys files
paullinator Sep 22, 2026
9446f2a
fixup! Split env.json into config and keys files
paullinator Sep 22, 2026
dd8bd15
fixup! Split env.json into config and keys files
paullinator Sep 25, 2026
51be34c
fixup! Split env.json into config and keys files
paullinator Sep 25, 2026
6b02ae4
Fetch remote secrets via infoRollup appKeys
paullinator Aug 14, 2026
9bb57ec
fixup! Fetch remote secrets via infoRollup appKeys
paullinator Sep 17, 2026
1ea7d3f
fixup! Fetch remote secrets via infoRollup appKeys
paullinator Sep 17, 2026
057456a
fixup! Fetch remote secrets via infoRollup appKeys
paullinator Sep 21, 2026
2da4861
fixup! Fetch remote secrets via infoRollup appKeys
paullinator Sep 22, 2026
beb56d8
fixup! Fetch remote secrets via infoRollup appKeys
paullinator Sep 25, 2026
6487f2d
fixup! Fetch remote secrets via infoRollup appKeys
paullinator Sep 25, 2026
8b90454
Add native Edge API HMAC signer with edgeKey.json
paullinator Aug 18, 2026
a39fb8a
fixup! Add native Edge API HMAC signer with edgeKey.json
paullinator Sep 17, 2026
f3f4e9c
fixup! Add native Edge API HMAC signer with edgeKey.json
paullinator Sep 17, 2026
0e39c01
fixup! Add native Edge API HMAC signer with edgeKey.json
paullinator Sep 17, 2026
6b23a1a
fixup! Add native Edge API HMAC signer with edgeKey.json
paullinator Sep 17, 2026
a39c300
fixup! Add native Edge API HMAC signer with edgeKey.json
paullinator Sep 25, 2026
4418eeb
Log keys tier and signer status on launch
paullinator Aug 27, 2026
4f0896a
fixup! Log keys tier and signer status on launch
paullinator Sep 17, 2026
0776501
Report plugins that failed to load after login
paullinator Aug 31, 2026
78d8225
fixup! Report plugins that failed to load after login
paullinator Sep 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .cursorignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
env.json
config.json
keys.json
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand All @@ -17,6 +21,7 @@ IDEWorkspaceChecks.plist
android-release.bundle.map
ios-release.bundle.map
keystores/
/.edgeApiSigner.stamp

# Debugging
overrideTheme.json
Expand All @@ -35,6 +40,13 @@ 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
/vendor/*.tgz
/vendor/edge-core-js-*.tgz
/*.tgz

# Checkpoint jsons
/android/app/src/main/assets/saplingtree/
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@
- added: Exchange deep links (`edge://exchange/buy|sell|swap`) that open the flow with the asset pre-selected
- added: Promo attribution for buy, sell and swap flows opened from deep links and promo cards
- added: WalletConnect Bitcoin (bip122) message signing for proof of ownership with existing BTC wallets
- 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: 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
- changed: Standardize wallet list automation test IDs to use period separators.
- changed: Lock the send confirmation slider for the rest of the scene once a broadcast has been attempted, whether the broadcast reported success or failure, and replace the generic failure card with a message that the transaction may have gone through, pointing at the block explorer or confirmation email before trying again.
- changed: Prevent sending to the same wallet's own address for EVM assets.
- changed: (ARRR) Pirate Chain wallets run on `react-native-pirate-wallet` 0.3.4, replacing `react-native-piratechain`
- changed: Balance-effect checks and the login FIO refresh wait for engine readiness on cache-emitted wallets
- changed: Opening any wallet-scoped scene asks the core to prioritize that wallet's engine startup in the post-login queue.
- 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)
- fixed: A send whose funds are not spendable yet says so instead of reporting a network error
- fixed: Auto-login starting two competing accounts when both `YOLO_PASSWORD` and `YOLO_PIN` are set, and attempting a login when either is set to an empty string.
- fixed: Auto-login never running on a device with no accounts, since the welcome carousel took priority over the login scene that owns it.
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
65 changes: 65 additions & 0 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")
}
27 changes: 27 additions & 0 deletions android/app/src/main/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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})
95 changes: 95 additions & 0 deletions android/app/src/main/cpp/edge_api_signer_jni.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#include <jni.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

#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;
}
82 changes: 82 additions & 0 deletions android/app/src/main/java/co/edgesecure/app/EdgeApiSignerModule.kt
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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<NativeModule> =
if (EdgeApiSignerModule.libraryLoaded) listOf(EdgeApiSignerModule(reactContext))
else emptyList()

override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> =
emptyList()
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class MainApplication :
// packages.add(new MyReactNativePackage());
val packages = PackageList(this).packages
packages.add(EdgeAttestationPackage())
packages.add(EdgeApiSignerPackage())
return packages
}

Expand Down
Loading
Loading