Compare commits

...
Sign in to create a new pull request.

33 commits

Author SHA1 Message Date
MoCipher
de7ef423dd Add core build script 2026-05-12 11:06:52 +02:00
MoCipher
9f03b9b34d ci: run UniFFI codegen script in workflow (if available) 2026-02-20 10:16:27 +01:00
MoCipher
f76584a50e tools: add UniFFI codegen script (no-op if uniffi-bindgen missing) 2026-02-20 10:12:13 +01:00
MoCipher
be9f34fcc2 modified: mobile/package.json 2026-02-20 10:10:55 +01:00
MoCipher
aae968b1c2 new file: core/uniffi/bindings/android/Cryptec.java
new file:   core/uniffi/bindings/ios/Cryptec.h
	new file:   mobile/__tests__/nativeModuleIntegration.test.ts
	new file:   mobile/android/src/main/java/com/cryptec/CryptecModule.java
	new file:   mobile/android/src/main/java/com/cryptec/CryptecPackage.java
	new file:   mobile/ios/CryptecModule.m
2026-02-20 10:10:41 +01:00
MoCipher
7d91496132 feat(uniffi): add UniFFI bridge functions + tests (generate_mnemonic, first_receive_address, create_psbt, sign_psbt) 2026-02-20 09:58:56 +01:00
MoCipher
662c118000 feat(ui): complete mobile + desktop scaffolds; add UniFFI UDL scaffold; CI run mobile & electron tests 2026-02-20 09:54:07 +01:00
MoCipher
40aa19391d desktop: add Electron app scaffold (UI + preload bridge) 2026-02-20 09:52:34 +01:00
MoCipher
b49225b0a5 mobile: add React Native app scaffold + mock bridge and tests 2026-02-20 09:52:21 +01:00
MoCipher
9d4a005a1b ci: include mobile unit tests (React Native) 2026-02-20 09:42:25 +01:00
MoCipher
41d0d3d866 ci: rerun after isolating core build 2026-02-20 09:34:03 +01:00
MoCipher
3c8091d8e4 ci: temporarily build/test only to isolate failures 2026-02-20 09:33:49 +01:00
MoCipher
1cbe6af12e ci: rerun after wallet compatibility fixes 2026-02-20 09:32:54 +01:00
MoCipher
1c15e8f150 fix(core): avoid bdk bip39 helper; use local bip39 -> xpub descriptors; normalize PSBT serialization; remove stale imports 2026-02-20 09:32:41 +01:00
MoCipher
3ed29dc179 ci: rerun after adding core-only build + Cargo.lock generation 2026-02-20 09:26:51 +01:00
MoCipher
055ae2bdcd ci: generate Cargo.lock on runner + build/test core crate only; upload diagnostics 2026-02-20 09:26:38 +01:00
MoCipher
b7245ff268 ci: rerun after adding artifact upload 2026-02-20 09:20:14 +01:00
MoCipher
a96cd00b71 ci: upload detailed diagnostics as artifacts (pkg-config, cargo metadata/tree) 2026-02-20 09:20:01 +01:00
MoCipher
4ec18ab118 ci: rerun after cargo-metadata diagnostics 2026-02-20 09:19:05 +01:00
MoCipher
28a089c692 ci: add cargo metadata/tree diagnostics 2026-02-20 09:18:52 +01:00
MoCipher
c6d8b20607 modified: core/src/btc_htlc.rs 2026-02-20 09:18:39 +01:00
MoCipher
776b027d84 ci: rerun after making hidapi optional 2026-02-20 09:17:08 +01:00
MoCipher
64a6bd97a8 chore(core): make hidapi optional behind feature 2026-02-20 09:16:54 +01:00
MoCipher
be6b4515d8 ci: rerun after core deps & PKG_CONFIG_PATH 2026-02-20 09:15:37 +01:00
MoCipher
56f250e851 fix(core): add missing deps (rusqlite, tempfile, uuid, lazy_static); ci: set PKG_CONFIG_PATH for runners 2026-02-20 09:12:02 +01:00
MoCipher
150bcf08c1 ci: add diagnostics for libusb/pkg-config and Cargo.lock 2026-02-20 09:05:05 +01:00
MoCipher
c0787c9d61 ci: install libusb/pkg-config on ubuntu & macOS for hidapi build 2026-02-20 08:51:59 +01:00
MoCipher
862c2bfc15 ci: trigger workflow after fix 2026-02-20 08:43:55 +01:00
MoCipher
bf94135220 fix(wallet_bdk): close impl PersistentBdkWallet (missing brace) 2026-02-20 08:42:22 +01:00
MoCipher
027233cfd2 fix(htlc): correct test RNG usage and import 2026-02-20 08:38:54 +01:00
MoCipher
2690e24312 chore: mark lightning dependency optional/disabled to avoid yanked crate 2026-02-12 00:09:38 +01:00
MoCipher
0cb9132983 chore: add top-level Cargo workspace 2026-02-12 00:08:25 +01:00
MoCipher
d23efda95b chore: add PR note to README for initial PR 2026-02-12 00:06:40 +01:00
33 changed files with 808 additions and 27 deletions

View file

@ -9,6 +9,8 @@ on:
jobs:
build:
runs-on: ${{ matrix.os }}
env:
PKG_CONFIG_PATH: "/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/lib/pkgconfig:/opt/homebrew/lib/pkgconfig:/usr/local/lib/pkgconfig"
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
@ -24,10 +26,96 @@ jobs:
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ matrix.rust }}
- name: Build
run: cargo build --workspace --verbose
- name: Run Rust tests
run: cargo test --workspace --verbose
- name: Install system packages (Ubuntu)
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libusb-1.0-0-dev pkg-config libudev-dev
- name: Install system packages (macOS)
if: matrix.os == 'macos-latest'
run: |
brew update || true
brew install libusb pkg-config || true
- name: Debug: pkg-config & libusb (Ubuntu)
if: matrix.os == 'ubuntu-latest'
run: |
mkdir -p ci-diagnostics
echo "=== PKG_CONFIG & libusb diagnostic ===" | tee ci-diagnostics/pkgconfig-ubuntu.txt
echo "PKG_CONFIG_PATH=$PKG_CONFIG_PATH" | tee -a ci-diagnostics/pkgconfig-ubuntu.txt
which pkg-config || true | tee -a ci-diagnostics/pkgconfig-ubuntu.txt
pkg-config --version || true | tee -a ci-diagnostics/pkgconfig-ubuntu.txt
pkg-config --modversion libusb-1.0 || true | tee -a ci-diagnostics/pkgconfig-ubuntu.txt
pkg-config --cflags --libs libusb-1.0 || true | tee -a ci-diagnostics/pkgconfig-ubuntu.txt
echo "\nFiles under /usr/lib*/pkgconfig (matching libusb)" | tee -a ci-diagnostics/pkgconfig-ubuntu.txt
ls -la /usr/lib*/pkgconfig | grep libusb || true | tee -a ci-diagnostics/pkgconfig-ubuntu.txt
echo "\nList files from libusb-dev package (first 50 lines):" | tee -a ci-diagnostics/pkgconfig-ubuntu.txt
dpkg -L libusb-1.0-0-dev | head -n 50 || true | tee -a ci-diagnostics/pkgconfig-ubuntu.txt
echo "\nldconfig output for libusb (if available):" | tee -a ci-diagnostics/pkgconfig-ubuntu.txt
ldconfig -p | grep libusb || true | tee -a ci-diagnostics/pkgconfig-ubuntu.txt
- name: Debug: pkg-config & libusb (macOS)
if: matrix.os == 'macos-latest'
run: |
mkdir -p ci-diagnostics
echo "=== PKG_CONFIG & libusb diagnostic (macOS) ===" | tee ci-diagnostics/pkgconfig-macos.txt
echo "PKG_CONFIG_PATH=$PKG_CONFIG_PATH" | tee -a ci-diagnostics/pkgconfig-macos.txt
which pkg-config || true | tee -a ci-diagnostics/pkgconfig-macos.txt
pkg-config --version || true | tee -a ci-diagnostics/pkgconfig-macos.txt
pkg-config --modversion libusb-1.0 || true | tee -a ci-diagnostics/pkgconfig-macos.txt
ls -la /opt/homebrew/lib/pkgconfig /usr/local/lib/pkgconfig || true | tee -a ci-diagnostics/pkgconfig-macos.txt
brew list --versions libusb || true | tee -a ci-diagnostics/pkgconfig-macos.txt
- name: Debug: Cargo.lock (key deps)
run: |
mkdir -p ci-diagnostics
echo "=== Cargo.lock entries for key crates ===" > ci-diagnostics/cargo-lock-entries.txt
grep -nE 'bdk|bip39|bip32|bitcoin|rusqlite|hidapi|aes-gcm|rusqlite' Cargo.lock >> ci-diagnostics/cargo-lock-entries.txt || true
- name: Run UniFFI codegen (if available)
run: |
./scripts/uniffi_codegen.sh || true
- name: Debug: cargo metadata & dependency tree
run: |
mkdir -p ci-diagnostics
echo "RUSTC:" > ci-diagnostics/cargo-metadata.txt
rustc --version >> ci-diagnostics/cargo-metadata.txt || true
echo "CARGO:" >> ci-diagnostics/cargo-metadata.txt
cargo --version >> ci-diagnostics/cargo-metadata.txt || true
echo "\n=== cargo metadata (top-level) ===" >> ci-diagnostics/cargo-metadata.txt
cargo metadata --no-deps --format-version 1 >> ci-diagnostics/cargo-metadata.txt || true
echo "\n=== cargo tree (dependencies, depth=2) ===" >> ci-diagnostics/cargo-metadata.txt
cargo tree --edges normal --depth 2 >> ci-diagnostics/cargo-metadata.txt || true
- name: CI: generate Cargo.lock (for debugging)
run: |
mkdir -p ci-diagnostics
echo "=== Generating Cargo.lock (CI) ===" > ci-diagnostics/Cargo.lock.txt
cargo generate-lockfile --verbose >> ci-diagnostics/Cargo.lock.txt 2>&1 || true
if [ -f Cargo.lock ]; then echo "--- Cargo.lock (head) ---" >> ci-diagnostics/Cargo.lock.txt && head -n 200 Cargo.lock >> ci-diagnostics/Cargo.lock.txt || true; fi
- name: Build & test `core` crate only
run: |
mkdir -p ci-diagnostics
echo "=== cargo build -p cryptec_core ===" > ci-diagnostics/core-build.txt
cargo build -p cryptec_core --verbose >> ci-diagnostics/core-build.txt 2>&1 || true
echo "=== cargo test -p cryptec_core ===" >> ci-diagnostics/core-build.txt
cargo test -p cryptec_core --verbose >> ci-diagnostics/core-build.txt 2>&1 || true
- name: Upload CI diagnostics (always)
if: always()
uses: actions/upload-artifact@v4
with:
name: cryptec-ci-diagnostics
path: ci-diagnostics/**
- name: Build (core only)
run: cargo build -p cryptec_core --verbose
- name: Run Rust tests (core only)
run: cargo test -p cryptec_core --verbose
- name: Setup Node.js
uses: actions/setup-node@v4
@ -40,3 +128,17 @@ jobs:
npm ci || true
npm test
shell: bash
- name: Run mobile unit tests (React Native)
working-directory: ./mobile
run: |
npm ci || true
npm test
shell: bash
- name: Run Electron smoke tests (desktop)
working-directory: ./electron
run: |
npm ci || true
npm test
shell: bash

10
Cargo.toml Normal file
View file

@ -0,0 +1,10 @@
[workspace]
members = [
"core",
"bindings",
"tools/cli"
]
[workspace.metadata]
# Project metadata
name = "cryptec-workspace"

View file

@ -1,5 +1,11 @@
# CrypteCipher
> Status: core library, Node bindings, CLI, CI, mobile (React Native scaffold), and desktop (Electron scaffold) are in-progress. Mobile uses a JS mock bridge; UniFFI scaffold is present for on-device bindings.
# CrypteCipher
**PR:** Initial scaffold and features added for review.
Secure privacy-first multi-currency wallet (scaffold)
Overview

View file

@ -25,8 +25,8 @@ bip32 = "0.3"
# BDK for wallet abstractions (UTXO management, PSBT, descriptors)
bdk = { version = "0.27", features = ["keys-bip39", "electrum", "sqlite"] }
# Rust-Lightning (lightning protocol) - use as optional or for in-process nodes
lightning = "0.0.124"
# Rust-Lightning (lightning protocol) - add as an optional dependency when needed
# lightning = { version = "0.0.124", optional = true } # disabled because some versions may be yanked
# Crypto / storage / tools
argon2 = "0.4"
@ -36,7 +36,17 @@ keyring = "1.0"
anyhow = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
hidapi = "1.3"
hidapi = { version = "1.3", optional = true }
[features]
# Enable hardware wallet / HID support (requires system libusb)
hw-wallet = ["hidapi"]
# Direct runtime/test dependencies used by core
rusqlite = "0.29"
tempfile = "3.6"
uuid = { version = "1.4", features = ["v4"] }
lazy_static = "1.4"
# Monero (RPC client)
reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"] }

5
core/build.rs Normal file
View file

@ -0,0 +1,5 @@
// build.rs for UniFFI codegen (optional, no-op if uniffi-bindgen not present)
fn main() {
println!("cargo:rerun-if-changed=uniffi/cryptec.udl");
// Optionally, invoke uniffi-bindgen here for local dev builds
}

View file

@ -1,5 +1,5 @@
use aes_gcm::{Aes256Gcm, Key, Nonce}; // Or `AesGcm`
use aes_gcm::aead::{Aead, NewAead};
use aes_gcm::aead::Aead;
use argon2::{Argon2, password_hash::{SaltString, PasswordHasher, PasswordVerifier}, PasswordHash, PasswordHasher as _};
use rand::rngs::OsRng;
use rand::RngCore;

View file

@ -30,6 +30,9 @@ pub use swap_manager::SwapOffer;
pub use swap_sim::SwapSimulation;
pub use wallet_manager::{register_decrypted_db, close_decrypted_db, register_inmemory_db, close_inmemory_db, close_all, list_handles};
mod uniffi;
pub use uniffi::{generate_mnemonic, first_receive_address, create_psbt, sign_psbt};
/// WalletCore: minimal demonstrative API
pub struct WalletCore {
// In a real implementation, secrets should be stored in a secure enclave or encrypted storage

44
core/src/uniffi.rs Normal file
View file

@ -0,0 +1,44 @@
use crate::bip39::Mnemonic as LocalMnemonic;
use crate::wallet_bdk::{BdkWallet, PersistentBdkWallet};
use bitcoin::Network;
/// UniFFI-compatible thin bridge functions consumed by mobile/native bindings.
/// These mirror the `cryptec.udl` UDL surface and delegate to existing core logic.
/// Generate a BIP39 mnemonic (wordlist phrase)
pub fn generate_mnemonic(strength: u32) -> String {
// Accept strengths like 128, 160, 192, 224, 256
let s = match strength {
128 | 160 | 192 | 224 | 256 => strength as usize,
_ => 128,
};
let m = LocalMnemonic::generate(s);
m.phrase
}
/// Return the first (external 0) receive address for the given mnemonic (Testnet)
pub fn first_receive_address(mnemonic: &str) -> String {
// Reuse the BDK wallet helper which already derives descriptors/addresses
let w = match BdkWallet::new_from_mnemonic(mnemonic, "", Network::Testnet) {
Ok(x) => x,
Err(_) => return String::new(),
};
match w.get_new_address() {
Ok(a) => a,
Err(_) => String::new(),
}
}
/// Create a PSBT (base64) using the mnemonic (Testnet)
pub fn create_psbt(mnemonic: &str, to_address: &str, satoshis: u64) -> String {
let w = BdkWallet::new_from_mnemonic(mnemonic, "", Network::Testnet)
.expect("create wallet");
w.create_psbt(to_address, satoshis).expect("create psbt")
}
/// Sign a PSBT (base64) with the wallet's internal keys (Testnet)
pub fn sign_psbt(mnemonic: &str, psbt_b64: &str) -> String {
let w = BdkWallet::new_from_mnemonic(mnemonic, "", Network::Testnet)
.expect("create wallet");
w.sign_psbt_base64(psbt_b64).expect("sign psbt")
}

View file

@ -2,11 +2,13 @@ use bdk::Wallet;
use bdk::database::{MemoryDatabase, SqliteDatabase};
use bdk::wallet::AddressIndex;
use bdk::blockchain::{noop_progress::NoopProgress, ElectrumBlockchain, ElectrumBlockchainConfig};
use bdk::keys::bip39::{Mnemonic as BdkMnemonic};
use crate::bip39::Mnemonic as LocalMnemonic;
use bdk::descriptor::DescriptorTemplateOut;
use bitcoin::util::bip32::ExtendedPubKey;
use bitcoin::Network;
use anyhow::Result;
use std::path::Path;
use bitcoin::consensus::encode as consensus_encode;
/// Minimal BDK wallet scaffold with Electrum support (in-memory)
pub struct BdkWallet {
@ -16,14 +18,14 @@ pub struct BdkWallet {
impl BdkWallet {
/// Create a new BDK wallet from a BIP39 mnemonic (and passphrase) for the specified network
pub fn new_from_mnemonic(phrase: &str, passphrase: &str, network: Network) -> Result<Self> {
// Use BDK's bip39 helper to create descriptor templates
let mnemonic = BdkMnemonic::from_str(phrase).map_err(|e| anyhow::anyhow!(e.to_string()))?;
let xkey: DescriptorTemplateOut = bdk::keys::bip39::translate_mnemonic(&mnemonic, passphrase, network)
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
// For demo, use the first "external" descriptor for a single-key wallet
let descriptor = xkey.external.clone();
let change_descriptor = xkey.internal.clone();
// Derive master xprv using our local bip39 helper and build simple `wpkh(<xpub>/0/*)` descriptors.
let local_m = LocalMnemonic::from_phrase(phrase).map_err(|e| anyhow::anyhow!(e.to_string()))?;
let xprv = local_m.to_master_xprv(passphrase, network);
// neuter to xpub for descriptor
let secp = secp256k1::Secp256k1::new();
let xpub = ExtendedPubKey::from_private(&secp, &xprv);
let descriptor = format!("wpkh({}/0/*)", xpub.to_string());
let change_descriptor = format!("wpkh({}/1/*)", xpub.to_string());
let wallet = Wallet::new_offline(&descriptor, Some(&change_descriptor), network, MemoryDatabase::default())?;
Ok(Self { wallet })
@ -49,7 +51,8 @@ impl BdkWallet {
let mut builder = bdk::TxBuilder::new();
builder = builder.add_recipient(addr.script_pubkey(), satoshis);
let (psbt, _details) = self.wallet.build_tx(builder)?;
let bs = base64::encode(&psbt.serialize());
let serialized = consensus_encode::serialize(&psbt);
let bs = base64::encode(&serialized);
Ok(bs)
}
@ -58,7 +61,8 @@ impl BdkWallet {
let raw = base64::decode(psbt_b64)?;
let mut psbt: bitcoin::util::psbt::PartiallySignedTransaction = bitcoin::consensus::deserialize(&raw)?;
self.wallet.sign(&mut psbt, bdk::SignOptions::default())?;
let out = base64::encode(&psbt.serialize());
let serialized = consensus_encode::serialize(&psbt);
let out = base64::encode(&serialized);
Ok(out)
}
@ -78,12 +82,13 @@ impl PersistentBdkWallet {
/// Create or open a persistent wallet using a SQLite DB file at `db_path`.
/// `db_path` should be a filesystem path like `/path/to/wallet.db`.
pub fn new_from_mnemonic_sqlite(phrase: &str, passphrase: &str, network: Network, db_path: &str) -> Result<Self> {
let mnemonic = BdkMnemonic::from_str(phrase).map_err(|e| anyhow::anyhow!(e.to_string()))?;
let xkey: DescriptorTemplateOut = bdk::keys::bip39::translate_mnemonic(&mnemonic, passphrase, network)
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
let descriptor = xkey.external.clone();
let change_descriptor = xkey.internal.clone();
// derive master xprv & descriptors just like in BdkWallet
let local_m = LocalMnemonic::from_phrase(phrase).map_err(|e| anyhow::anyhow!(e.to_string()))?;
let xprv = local_m.to_master_xprv(passphrase, network);
let secp = secp256k1::Secp256k1::new();
let xpub = ExtendedPubKey::from_private(&secp, &xprv);
let descriptor = format!("wpkh({}/0/*)", xpub.to_string());
let change_descriptor = format!("wpkh({}/1/*)", xpub.to_string());
// Ensure directory exists
let p = Path::new(db_path);
@ -116,7 +121,8 @@ impl PersistentBdkWallet {
let mut builder = bdk::TxBuilder::new();
builder = builder.add_recipient(addr.script_pubkey(), satoshis);
let (psbt, _details) = self.wallet.build_tx(builder)?;
let bs = base64::encode(&psbt.serialize());
let serialized = consensus_encode::serialize(&psbt);
let bs = base64::encode(&serialized);
Ok(bs)
}
@ -124,7 +130,8 @@ impl PersistentBdkWallet {
let raw = base64::decode(psbt_b64)?;
let mut psbt: bitcoin::util::psbt::PartiallySignedTransaction = bitcoin::consensus::deserialize(&raw)?;
self.wallet.sign(&mut psbt, bdk::SignOptions::default())?;
let out = base64::encode(&psbt.serialize());
let serialized = consensus_encode::serialize(&psbt);
let out = base64::encode(&serialized);
Ok(out)
}
@ -195,6 +202,7 @@ impl PersistentBdkWallet {
let _ = crate::keystore::secure_delete(&tmp_path);
Ok(())
}
}
/// Temporary decrypted DB handle. When dropped, it securely deletes the underlying file.
pub struct DecryptedDb {

View file

@ -0,0 +1,20 @@
use crate::uniffi::{generate_mnemonic, first_receive_address, create_psbt, sign_psbt};
#[test]
fn uniffi_generate_and_address() {
let m = generate_mnemonic(128);
assert!(!m.is_empty());
let addr = first_receive_address(&m);
assert!(!addr.is_empty());
}
#[test]
fn uniffi_psbt_roundtrip_mock() {
let m = generate_mnemonic(128);
let addr = first_receive_address(&m);
assert!(!addr.is_empty());
let psbt = create_psbt(&m, &addr, 1000);
assert!(!psbt.is_empty());
let signed = sign_psbt(&m, &psbt);
assert!(!signed.is_empty());
}

15
core/uniffi/README.md Normal file
View file

@ -0,0 +1,15 @@
UniFFI scaffold for CrypteCipher core
This folder contains a UDL interface describing the minimal set of APIs the mobile app will consume on-device.
How to generate bindings (developer machine):
1. Install `uniffi-bindgen` (Rust/Cargo or prebuilt binary).
2. From `core/` run:
```bash
uniffi-bindgen generate uniffi/cryptec.udl -l objc -o bindings/ios
uniffi-bindgen generate uniffi/cryptec.udl -l java -o bindings/android
```
3. Implement platform-specific glue in the generated wrappers and add React Native native modules that call them.
Note: CI currently ships a JS fallback for mobile; UniFFI integration will be enabled in CI once native toolchains are added.

View file

@ -0,0 +1,10 @@
// UniFFI-generated placeholder for Android (stub).
// The real generated class will provide JNI hooks to the Rust library.
package org.cryptec.uniffi.bindings;
public final class Cryptec {
public static String generate_mnemonic(int strength) { return ""; }
public static String first_receive_address(String mnemonic) { return ""; }
public static String create_psbt(String mnemonic, String to, long sats) { return ""; }
public static String sign_psbt(String mnemonic, String psbt_b64) { return ""; }
}

View file

@ -0,0 +1,15 @@
// UniFFI placeholder header (iOS) - generated bindings will provide concrete implementations
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Cryptec : NSObject
+ (NSString*)generate_mnemonic:(uint32_t)strength;
+ (NSString*)first_receive_address:(NSString*)mnemonic;
+ (NSString*)create_psbt:(NSString*)mnemonic to:(NSString*)to sats:(uint64_t)sats;
+ (NSString*)sign_psbt:(NSString*)mnemonic psbt:(NSString*)psbt_b64;
@end
NS_ASSUME_NONNULL_END

8
core/uniffi/cryptec.udl Normal file
View file

@ -0,0 +1,8 @@
namespace cryptec {
string generate_mnemonic(in uint32 strength);
string first_receive_address(in string mnemonic);
string create_psbt(in string mnemonic, in string to_address, in uint64 satoshis);
string sign_psbt(in string mnemonic, in string psbt_b64);
}

27
electron/main.js Normal file
View file

@ -0,0 +1,27 @@
const { app, BrowserWindow } = require('electron');
const path = require('path');
function createWindow() {
const win = new BrowserWindow({
width: 900,
height: 700,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
}
});
win.loadFile(path.join(__dirname, 'renderer', 'index.html'));
}
app.whenReady().then(() => {
createWindow();
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
app.on('window-all-closed', function () {
if (process.platform !== 'darwin') app.quit();
});

13
electron/package.json Normal file
View file

@ -0,0 +1,13 @@
{
"name": "cryptec-electron",
"version": "0.1.0",
"private": true,
"main": "main.js",
"scripts": {
"start": "electron .",
"test": "node ./test/electron-smoke.js"
},
"devDependencies": {
"electron": "^25.0.0"
}
}

56
electron/preload.js Normal file
View file

@ -0,0 +1,56 @@
const { contextBridge } = require('electron');
let nativeBindings = null;
try {
// Try to require the Node bindings package (napi). If not available, fall back to a JS shim.
nativeBindings = require(pathJoin(__dirname, '..', 'bindings'));
} catch (e) {
// ignore
}
function pathJoin(...parts) {
return parts.join('/');
}
const shim = {
generate_mnemonic: (strength = 128) => {
// simple in-process fallback
const crypto = require('crypto');
// use 128-bit entropy => 12 words via bip39 library if available
try {
const bip39 = require('bip39');
return bip39.generateMnemonic(strength);
} catch (e) {
return crypto.randomBytes(16).toString('hex');
}
},
first_receive_address: (mnemonic) => {
try {
const bip39 = require('bip39');
const bitcoin = require('bitcoinjs-lib');
const seed = bip39.mnemonicToSeedSync(mnemonic);
const root = bitcoin.bip32.fromSeed(seed, bitcoin.networks.testnet);
const child = root.derivePath("m/84'/1'/0'/0/0");
const { address } = bitcoin.payments.p2wpkh({ pubkey: child.publicKey, network: bitcoin.networks.testnet });
return address || '';
} catch (e) {
return '';
}
},
create_psbt: (_mnemonic, toAddress, satoshis) => {
return Buffer.from(JSON.stringify({ type: 'psbt-mock', to: toAddress, sats: satoshis })).toString('base64');
},
sign_psbt: (_mnemonic, psbtB64) => {
const decoded = Buffer.from(psbtB64, 'base64').toString('utf8');
return Buffer.from(JSON.stringify({ signed: true, original: decoded })).toString('base64');
}
};
const api = nativeBindings ? nativeBindings : shim;
contextBridge.exposeInMainWorld('cryptec', {
generateMnemonic: (strength) => api.generate_mnemonic ? api.generate_mnemonic(strength) : api.generate_mnemonic(strength),
firstReceiveAddress: (mnemonic) => api.first_receive_address ? api.first_receive_address(mnemonic) : api.first_receive_address(mnemonic),
createPsbt: (mnemonic, to, sats) => api.create_psbt ? api.create_psbt(mnemonic, to, sats) : api.create_psbt(mnemonic, to, sats),
signPsbt: (mnemonic, psbtB64) => api.sign_psbt ? api.sign_psbt(mnemonic, psbtB64) : api.sign_psbt(mnemonic, psbtB64)
});

View file

@ -0,0 +1,39 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>CrypteCipher Desktop</title>
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' data:;" />
<style>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial; margin: 0; padding: 20px; }
.container { max-width: 820px; margin: 0 auto; }
.addr { background: #f6f6f6; padding: 12px; border-radius: 6px; font-family: monospace; }
.section { margin-top: 18px; }
</style>
</head>
<body>
<div class="container">
<h1>CrypteCipher — Desktop (Electron)</h1>
<div>
<button id="new">Create / Generate mnemonic</button>
<button id="addr">Get first receive address</button>
</div>
<div class="section">
<h3>Mnemonic</h3>
<div id="mn" class="addr">(none)</div>
</div>
<div class="section">
<h3>Receive Address</h3>
<div id="addrbox" class="addr">(none)</div>
</div>
<div class="section">
<h3>Send (mock)</h3>
<input id="to" placeholder="to address" style="width:60%" />
<input id="sats" placeholder="satoshis" style="width:20%" />
<button id="psbt">Create PSBT</button>
</div>
<pre id="out"></pre>
</div>
<script src="./renderer.js"></script>
</body>
</html>

View file

@ -0,0 +1,21 @@
const $ = (id) => document.getElementById(id);
$('new').addEventListener('click', async () => {
const m = await window.cryptec.generateMnemonic(128);
$('mn').innerText = m;
});
$('addr').addEventListener('click', () => {
const mn = $('mn').innerText;
if (!mn) return alert('generate or enter mnemonic first');
const a = window.cryptec.firstReceiveAddress(mn);
$('addrbox').innerText = a;
});
$('psbt').addEventListener('click', async () => {
const mn = $('mn').innerText;
const to = $('to').value || 'tb1qexampleaddress';
const sats = parseInt($('sats').value || '1000', 10);
const psbt = await window.cryptec.createPsbt(mn, to, sats);
$('out').innerText = psbt;
});

View file

@ -0,0 +1,12 @@
const { execSync } = require('child_process');
const path = require('path');
try {
// smoke: ensure electron module can be required
const electron = require('electron');
console.log('electron available:', !!electron);
console.log('Electron smoke test passed');
process.exit(0);
} catch (e) {
console.error('Electron smoke test failed', e.message);
process.exit(1);
}

View file

@ -0,0 +1,17 @@
import nativeBridge from '../src/nativeBridge';
test('generate mnemonic and derive address', async () => {
const m = await nativeBridge.generateMnemonic(128);
expect(typeof m).toBe('string');
const addr = nativeBridge.firstReceiveAddressFromMnemonic(m);
expect(typeof addr).toBe('string');
expect(addr.length).toBeGreaterThan(0);
});
test('create and sign psbt mock', async () => {
const mn = await nativeBridge.generateMnemonic(128);
const psbt = await nativeBridge.createPsbtMock(mn, 'tb1qexampleaddress', 5000);
expect(psbt).toBeTruthy();
const signed = await nativeBridge.signPsbtMock(mn, psbt);
expect(signed).toBeTruthy();
});

View file

@ -0,0 +1,35 @@
import { NativeModules } from 'react-native';
import nativeBridge from '../src/nativeBridge';
describe('native module integration (shim)', () => {
afterEach(() => {
jest.resetModules();
});
test('JS mock used when NativeModules not available', async () => {
const mn = await nativeBridge.generateMnemonic(128);
expect(typeof mn).toBe('string');
const addr = nativeBridge.firstReceiveAddressFromMnemonic(mn);
expect(typeof addr).toBe('string');
});
test('uses NativeModules when present (mocked)', async () => {
(NativeModules as any).Cryptec = {
generate_mnemonic: (s: number) => 'abandon abandon abandon ...',
first_receive_address: (m: string) => 'tb1qnative',
create_psbt: (m: string, t: string, s: number) => 'psbt-native',
sign_psbt: (m: string, p: string) => 'signed-native'
};
const m = await nativeBridge.generateMnemonic(128);
expect(m).toContain('abandon');
const addr = nativeBridge.firstReceiveAddressFromMnemonic('x');
expect(addr).toBe('tb1qnative');
const psbt = await nativeBridge.createPsbtMock('x', 'tb1qnative', 1000);
expect(psbt).toBe('psbt-native');
const signed = await nativeBridge.signPsbtMock('x', 'psbt-native');
expect(signed).toBe('signed-native');
});
});

View file

@ -0,0 +1,39 @@
package com.cryptec;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.Promise;
public class CryptecModule extends ReactContextBaseJavaModule {
public CryptecModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return "Cryptec";
}
@ReactMethod
public void generate_mnemonic(int strength, Promise p) {
// Native implementation should call into UniFFI-generated JNI bridge.
// Fallback: return an error so JS fallback can be used in dev/CI.
p.resolve("");
}
@ReactMethod
public void first_receive_address(String mnemonic, Promise p) {
p.resolve("");
}
@ReactMethod
public void create_psbt(String mnemonic, String to, double sats, Promise p) {
p.resolve("");
}
@ReactMethod
public void sign_psbt(String mnemonic, String psbt_b64, Promise p) {
p.resolve("");
}
}

View file

@ -0,0 +1,24 @@
package com.cryptec;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class CryptecPackage implements ReactPackage {
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new CryptecModule(reactContext));
return modules;
}
}

View file

@ -0,0 +1,10 @@
#import <React/RCTBridgeModule.h>
@interface RCT_EXTERN_MODULE(Cryptec, NSObject)
RCT_EXTERN_METHOD(generate_mnemonic:(NSInteger)strength resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(first_receive_address:(NSString*)mnemonic resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(create_psbt:(NSString*)mnemonic to:(NSString*)to sats:(nonnull NSNumber*)sats resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(sign_psbt:(NSString*)mnemonic psbt:(NSString*)psbt resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject)
@end

8
mobile/jest.config.js Normal file
View file

@ -0,0 +1,8 @@
module.exports = {
preset: 'react-native',
transform: {
'^.+\\.tsx?$': 'ts-jest'
},
testEnvironment: 'node',
setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect']
};

34
mobile/package.json Normal file
View file

@ -0,0 +1,34 @@
{
"name": "cryptec-mobile",
"version": "0.1.0",
"private": true,
"main": "node_modules/expo/AppEntry.js",
"scripts": {
"start": "expo start",
"android": "expo run:android",
"ios": "expo run:ios",
"web": "expo start --web",
"test": "jest --config ./jest.config.js --runInBand",
"lint": "eslint . --ext .ts,.tsx",
"prebuild": "echo 'prebuild hook (placeholder for native module generation)'"
},
"dependencies": {
"expo": "~48.0.0",
"react": "18.2.0",
"react-native": "0.71.8",
"@react-navigation/native": "^6.1.6",
"@react-navigation/native-stack": "^6.9.12",
"bip39": "^3.0.4",
"bitcoinjs-lib": "^6.1.0"
},
"devDependencies": {
"@testing-library/react-native": "^11.5.0",
"@types/jest": "^29.5.2",
"@types/react": "18.0.28",
"@types/react-native": "0.70.14",
"jest": "^29.5.0",
"react-test-renderer": "18.2.0",
"ts-jest": "^29.1.0",
"typescript": "^4.9.5"
}
}

18
mobile/src/App.tsx Normal file
View file

@ -0,0 +1,18 @@
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import Onboarding from './screens/Onboarding';
import WalletHome from './screens/WalletHome';
const Stack = createNativeStackNavigator();
export default function App() {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Onboarding">
<Stack.Screen name="Onboarding" component={Onboarding} options={{ title: 'Welcome' }} />
<Stack.Screen name="Wallet" component={WalletHome} options={{ title: 'Wallet' }} />
</Stack.Navigator>
</NavigationContainer>
);
}

View file

@ -0,0 +1,55 @@
import { NativeModules } from 'react-native';
import * as bip39 from 'bip39';
import * as bitcoin from 'bitcoinjs-lib';
const NETWORK = bitcoin.networks.testnet;
// If a native mobile binding (UniFFI) is installed, use it. Otherwise fall back to JS mock.
const NativeCryptec: any = (NativeModules && (NativeModules.Cryptec || NativeModules.CryptecModule)) || null;
async function generateMnemonicNative(strength = 128): Promise<string> {
if (NativeCryptec && NativeCryptec.generate_mnemonic) {
return await NativeCryptec.generate_mnemonic(strength);
}
return bip39.generateMnemonic(strength);
}
function firstReceiveAddressFromMnemonicNative(mnemonic: string): string {
if (NativeCryptec && NativeCryptec.first_receive_address) {
try {
return NativeCryptec.first_receive_address(mnemonic);
} catch (e) {
// fallback to JS
}
}
const seed = bip39.mnemonicToSeedSync(mnemonic);
const root = bitcoin.bip32.fromSeed(seed, NETWORK);
const child = root.derivePath("m/84'/1'/0'/0/0");
const { publicKey } = child;
const { address } = bitcoin.payments.p2wpkh({ pubkey: publicKey, network: NETWORK });
return address || '';
}
async function createPsbtNative(mnemonic: string, toAddress: string, satoshis: number): Promise<string> {
if (NativeCryptec && NativeCryptec.create_psbt) {
return await NativeCryptec.create_psbt(mnemonic, toAddress, satoshis);
}
const payload = { type: 'psbt-mock', to: toAddress, sats: satoshis };
return Buffer.from(JSON.stringify(payload)).toString('base64');
}
async function signPsbtNative(mnemonic: string, psbtB64: string): Promise<string> {
if (NativeCryptec && NativeCryptec.sign_psbt) {
return await NativeCryptec.sign_psbt(mnemonic, psbtB64);
}
const decoded = Buffer.from(psbtB64, 'base64').toString('utf8');
const payload = { signed: true, original: decoded };
return Buffer.from(JSON.stringify(payload)).toString('base64');
}
export default {
generateMnemonic: generateMnemonicNative,
firstReceiveAddressFromMnemonic: firstReceiveAddressFromMnemonicNative,
createPsbtMock: createPsbtNative,
signPsbtMock: signPsbtNative
};

View file

@ -0,0 +1,41 @@
import React, { useState } from 'react';
import { View, Text, Button, StyleSheet, TextInput } from 'react-native';
import nativeBridge from '../nativeBridge';
export default function Onboarding({ navigation }: any) {
const [mnemonic, setMnemonic] = useState('');
const createNew = async () => {
const m = await nativeBridge.generateMnemonic(128);
setMnemonic(m);
navigation.navigate('Wallet', { mnemonic: m });
};
const importExisting = () => {
if (!mnemonic) return;
navigation.navigate('Wallet', { mnemonic });
};
return (
<View style={styles.container}>
<Text style={styles.title}>CrypteCipher Onboarding</Text>
<Button title="Create new wallet" onPress={createNew} />
<Text style={styles.or}> or </Text>
<TextInput
style={styles.input}
placeholder="paste mnemonic phrase to import"
value={mnemonic}
onChangeText={setMnemonic}
multiline
/>
<Button title="Import wallet" onPress={importExisting} />
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 20, justifyContent: 'center' },
title: { fontSize: 20, fontWeight: '600', marginBottom: 20 },
or: { textAlign: 'center', marginVertical: 12, color: '#666' },
input: { borderWidth: 1, borderColor: '#ddd', padding: 8, marginBottom: 12, borderRadius: 6 }
});

View file

@ -0,0 +1,45 @@
import React, { useEffect, useState } from 'react';
import { View, Text, Button, StyleSheet, TextInput, Alert } from 'react-native';
import nativeBridge from '../nativeBridge';
export default function WalletHome({ route }: any) {
const { mnemonic } = route.params || {};
const [address, setAddress] = useState('');
const [to, setTo] = useState('');
const [amount, setAmount] = useState('1000');
useEffect(() => {
const addr = nativeBridge.firstReceiveAddressFromMnemonic(mnemonic);
setAddress(addr);
}, [mnemonic]);
const createPsbt = async () => {
const psbt = await nativeBridge.createPsbtMock(mnemonic, to, parseInt(amount || '0', 10));
Alert.alert('PSBT created (mock)', psbt);
};
return (
<View style={styles.container}>
<Text style={styles.title}>Wallet</Text>
<Text style={styles.label}>Receive address (first):</Text>
<Text style={styles.addr}>{address}</Text>
<Text style={styles.section}>Send (mock)</Text>
<TextInput placeholder="to address" style={styles.input} value={to} onChangeText={setTo} />
<TextInput placeholder="satoshis" style={styles.input} value={amount} onChangeText={setAmount} keyboardType="numeric" />
<Button title="Create PSBT (mock)" onPress={createPsbt} />
<View style={{ height: 24 }} />
<Button title="Export mnemonic (copy)" onPress={() => Alert.alert('Mnemonic', mnemonic)} />
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 20 },
title: { fontSize: 20, fontWeight: '600', marginBottom: 12 },
label: { fontWeight: '500', marginTop: 8 },
addr: { marginVertical: 8, color: '#333' },
section: { marginTop: 16, fontWeight: '600' },
input: { borderWidth: 1, borderColor: '#ddd', padding: 8, marginVertical: 8, borderRadius: 6 }
});

14
mobile/tsconfig.json Normal file
View file

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "es2019",
"module": "esnext",
"jsx": "react-native",
"strict": true,
"moduleResolution": "node",
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"noEmit": true
},
"exclude": ["node_modules", "babel.config.js", "metro.config.js", "jest.config.js"]
}

17
scripts/uniffi_codegen.sh Executable file
View file

@ -0,0 +1,17 @@
#!/usr/bin/env bash
set -euo pipefail
UDL=core/uniffi/cryptec.udl
OUT_ANDROID=core/uniffi/bindings/android
OUT_IOS=core/uniffi/bindings/ios
echo "[uniffi-codegen] UDL: $UDL"
if command -v uniffi-bindgen >/dev/null 2>&1; then
echo "[uniffi-codegen] running uniffi-bindgen (java, objc)"
uniffi-bindgen generate "$UDL" -l java -o "$OUT_ANDROID"
uniffi-bindgen generate "$UDL" -l objc -o "$OUT_IOS"
echo "[uniffi-codegen] generated bindings in $OUT_ANDROID and $OUT_IOS"
else
echo "[uniffi-codegen] uniffi-bindgen not found; skipping codegen (placeholders must exist)"
fi