Compare commits
33 commits
main
...
feature/in
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de7ef423dd | ||
|
|
9f03b9b34d | ||
|
|
f76584a50e | ||
|
|
be9f34fcc2 | ||
|
|
aae968b1c2 | ||
|
|
7d91496132 | ||
|
|
662c118000 | ||
|
|
40aa19391d | ||
|
|
b49225b0a5 | ||
|
|
9d4a005a1b | ||
|
|
41d0d3d866 | ||
|
|
3c8091d8e4 | ||
|
|
1cbe6af12e | ||
|
|
1c15e8f150 | ||
|
|
3ed29dc179 | ||
|
|
055ae2bdcd | ||
|
|
b7245ff268 | ||
|
|
a96cd00b71 | ||
|
|
4ec18ab118 | ||
|
|
28a089c692 | ||
|
|
c6d8b20607 | ||
|
|
776b027d84 | ||
|
|
64a6bd97a8 | ||
|
|
be6b4515d8 | ||
|
|
56f250e851 | ||
|
|
150bcf08c1 | ||
|
|
c0787c9d61 | ||
|
|
862c2bfc15 | ||
|
|
bf94135220 | ||
|
|
027233cfd2 | ||
|
|
2690e24312 | ||
|
|
0cb9132983 | ||
|
|
d23efda95b |
33 changed files with 808 additions and 27 deletions
110
.github/workflows/ci.yml
vendored
110
.github/workflows/ci.yml
vendored
|
|
@ -9,6 +9,8 @@ on:
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ${{ matrix.os }}
|
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:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
|
|
@ -24,10 +26,96 @@ jobs:
|
||||||
with:
|
with:
|
||||||
path: ~/.cargo/registry
|
path: ~/.cargo/registry
|
||||||
key: ${{ runner.os }}-cargo-registry-${{ matrix.rust }}
|
key: ${{ runner.os }}-cargo-registry-${{ matrix.rust }}
|
||||||
- name: Build
|
|
||||||
run: cargo build --workspace --verbose
|
- name: Install system packages (Ubuntu)
|
||||||
- name: Run Rust tests
|
if: matrix.os == 'ubuntu-latest'
|
||||||
run: cargo test --workspace --verbose
|
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
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
|
|
@ -40,3 +128,17 @@ jobs:
|
||||||
npm ci || true
|
npm ci || true
|
||||||
npm test
|
npm test
|
||||||
shell: bash
|
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
10
Cargo.toml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
[workspace]
|
||||||
|
members = [
|
||||||
|
"core",
|
||||||
|
"bindings",
|
||||||
|
"tools/cli"
|
||||||
|
]
|
||||||
|
|
||||||
|
[workspace.metadata]
|
||||||
|
# Project metadata
|
||||||
|
name = "cryptec-workspace"
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
# CrypteCipher
|
# 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)
|
Secure privacy-first multi-currency wallet (scaffold)
|
||||||
|
|
||||||
Overview
|
Overview
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,8 @@ bip32 = "0.3"
|
||||||
# BDK for wallet abstractions (UTXO management, PSBT, descriptors)
|
# BDK for wallet abstractions (UTXO management, PSBT, descriptors)
|
||||||
bdk = { version = "0.27", features = ["keys-bip39", "electrum", "sqlite"] }
|
bdk = { version = "0.27", features = ["keys-bip39", "electrum", "sqlite"] }
|
||||||
|
|
||||||
# Rust-Lightning (lightning protocol) - use as optional or for in-process nodes
|
# Rust-Lightning (lightning protocol) - add as an optional dependency when needed
|
||||||
lightning = "0.0.124"
|
# lightning = { version = "0.0.124", optional = true } # disabled because some versions may be yanked
|
||||||
|
|
||||||
# Crypto / storage / tools
|
# Crypto / storage / tools
|
||||||
argon2 = "0.4"
|
argon2 = "0.4"
|
||||||
|
|
@ -36,7 +36,17 @@ keyring = "1.0"
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
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)
|
# Monero (RPC client)
|
||||||
reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"] }
|
reqwest = { version = "0.11", features = ["blocking", "json", "rustls-tls"] }
|
||||||
|
|
|
||||||
5
core/build.rs
Normal file
5
core/build.rs
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use aes_gcm::{Aes256Gcm, Key, Nonce}; // Or `AesGcm`
|
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 argon2::{Argon2, password_hash::{SaltString, PasswordHasher, PasswordVerifier}, PasswordHash, PasswordHasher as _};
|
||||||
use rand::rngs::OsRng;
|
use rand::rngs::OsRng;
|
||||||
use rand::RngCore;
|
use rand::RngCore;
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,9 @@ pub use swap_manager::SwapOffer;
|
||||||
pub use swap_sim::SwapSimulation;
|
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};
|
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
|
/// WalletCore: minimal demonstrative API
|
||||||
pub struct WalletCore {
|
pub struct WalletCore {
|
||||||
// In a real implementation, secrets should be stored in a secure enclave or encrypted storage
|
// In a real implementation, secrets should be stored in a secure enclave or encrypted storage
|
||||||
|
|
|
||||||
44
core/src/uniffi.rs
Normal file
44
core/src/uniffi.rs
Normal 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")
|
||||||
|
}
|
||||||
|
|
@ -2,11 +2,13 @@ use bdk::Wallet;
|
||||||
use bdk::database::{MemoryDatabase, SqliteDatabase};
|
use bdk::database::{MemoryDatabase, SqliteDatabase};
|
||||||
use bdk::wallet::AddressIndex;
|
use bdk::wallet::AddressIndex;
|
||||||
use bdk::blockchain::{noop_progress::NoopProgress, ElectrumBlockchain, ElectrumBlockchainConfig};
|
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 bdk::descriptor::DescriptorTemplateOut;
|
||||||
|
use bitcoin::util::bip32::ExtendedPubKey;
|
||||||
use bitcoin::Network;
|
use bitcoin::Network;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use bitcoin::consensus::encode as consensus_encode;
|
||||||
|
|
||||||
/// Minimal BDK wallet scaffold with Electrum support (in-memory)
|
/// Minimal BDK wallet scaffold with Electrum support (in-memory)
|
||||||
pub struct BdkWallet {
|
pub struct BdkWallet {
|
||||||
|
|
@ -16,14 +18,14 @@ pub struct BdkWallet {
|
||||||
impl BdkWallet {
|
impl BdkWallet {
|
||||||
/// Create a new BDK wallet from a BIP39 mnemonic (and passphrase) for the specified network
|
/// 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> {
|
pub fn new_from_mnemonic(phrase: &str, passphrase: &str, network: Network) -> Result<Self> {
|
||||||
// Use BDK's bip39 helper to create descriptor templates
|
// Derive master xprv using our local bip39 helper and build simple `wpkh(<xpub>/0/*)` descriptors.
|
||||||
let mnemonic = BdkMnemonic::from_str(phrase).map_err(|e| anyhow::anyhow!(e.to_string()))?;
|
let local_m = LocalMnemonic::from_phrase(phrase).map_err(|e| anyhow::anyhow!(e.to_string()))?;
|
||||||
let xkey: DescriptorTemplateOut = bdk::keys::bip39::translate_mnemonic(&mnemonic, passphrase, network)
|
let xprv = local_m.to_master_xprv(passphrase, network);
|
||||||
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
|
// neuter to xpub for descriptor
|
||||||
|
let secp = secp256k1::Secp256k1::new();
|
||||||
// For demo, use the first "external" descriptor for a single-key wallet
|
let xpub = ExtendedPubKey::from_private(&secp, &xprv);
|
||||||
let descriptor = xkey.external.clone();
|
let descriptor = format!("wpkh({}/0/*)", xpub.to_string());
|
||||||
let change_descriptor = xkey.internal.clone();
|
let change_descriptor = format!("wpkh({}/1/*)", xpub.to_string());
|
||||||
|
|
||||||
let wallet = Wallet::new_offline(&descriptor, Some(&change_descriptor), network, MemoryDatabase::default())?;
|
let wallet = Wallet::new_offline(&descriptor, Some(&change_descriptor), network, MemoryDatabase::default())?;
|
||||||
Ok(Self { wallet })
|
Ok(Self { wallet })
|
||||||
|
|
@ -49,7 +51,8 @@ impl BdkWallet {
|
||||||
let mut builder = bdk::TxBuilder::new();
|
let mut builder = bdk::TxBuilder::new();
|
||||||
builder = builder.add_recipient(addr.script_pubkey(), satoshis);
|
builder = builder.add_recipient(addr.script_pubkey(), satoshis);
|
||||||
let (psbt, _details) = self.wallet.build_tx(builder)?;
|
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)
|
Ok(bs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -58,7 +61,8 @@ impl BdkWallet {
|
||||||
let raw = base64::decode(psbt_b64)?;
|
let raw = base64::decode(psbt_b64)?;
|
||||||
let mut psbt: bitcoin::util::psbt::PartiallySignedTransaction = bitcoin::consensus::deserialize(&raw)?;
|
let mut psbt: bitcoin::util::psbt::PartiallySignedTransaction = bitcoin::consensus::deserialize(&raw)?;
|
||||||
self.wallet.sign(&mut psbt, bdk::SignOptions::default())?;
|
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)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,12 +82,13 @@ impl PersistentBdkWallet {
|
||||||
/// Create or open a persistent wallet using a SQLite DB file at `db_path`.
|
/// 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`.
|
/// `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> {
|
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()))?;
|
// derive master xprv & descriptors just like in BdkWallet
|
||||||
let xkey: DescriptorTemplateOut = bdk::keys::bip39::translate_mnemonic(&mnemonic, passphrase, network)
|
let local_m = LocalMnemonic::from_phrase(phrase).map_err(|e| anyhow::anyhow!(e.to_string()))?;
|
||||||
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
|
let xprv = local_m.to_master_xprv(passphrase, network);
|
||||||
|
let secp = secp256k1::Secp256k1::new();
|
||||||
let descriptor = xkey.external.clone();
|
let xpub = ExtendedPubKey::from_private(&secp, &xprv);
|
||||||
let change_descriptor = xkey.internal.clone();
|
let descriptor = format!("wpkh({}/0/*)", xpub.to_string());
|
||||||
|
let change_descriptor = format!("wpkh({}/1/*)", xpub.to_string());
|
||||||
|
|
||||||
// Ensure directory exists
|
// Ensure directory exists
|
||||||
let p = Path::new(db_path);
|
let p = Path::new(db_path);
|
||||||
|
|
@ -116,7 +121,8 @@ impl PersistentBdkWallet {
|
||||||
let mut builder = bdk::TxBuilder::new();
|
let mut builder = bdk::TxBuilder::new();
|
||||||
builder = builder.add_recipient(addr.script_pubkey(), satoshis);
|
builder = builder.add_recipient(addr.script_pubkey(), satoshis);
|
||||||
let (psbt, _details) = self.wallet.build_tx(builder)?;
|
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)
|
Ok(bs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -124,7 +130,8 @@ impl PersistentBdkWallet {
|
||||||
let raw = base64::decode(psbt_b64)?;
|
let raw = base64::decode(psbt_b64)?;
|
||||||
let mut psbt: bitcoin::util::psbt::PartiallySignedTransaction = bitcoin::consensus::deserialize(&raw)?;
|
let mut psbt: bitcoin::util::psbt::PartiallySignedTransaction = bitcoin::consensus::deserialize(&raw)?;
|
||||||
self.wallet.sign(&mut psbt, bdk::SignOptions::default())?;
|
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)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -195,6 +202,7 @@ impl PersistentBdkWallet {
|
||||||
let _ = crate::keystore::secure_delete(&tmp_path);
|
let _ = crate::keystore::secure_delete(&tmp_path);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Temporary decrypted DB handle. When dropped, it securely deletes the underlying file.
|
/// Temporary decrypted DB handle. When dropped, it securely deletes the underlying file.
|
||||||
pub struct DecryptedDb {
|
pub struct DecryptedDb {
|
||||||
|
|
|
||||||
20
core/tests/uniffi_bridge_tests.rs
Normal file
20
core/tests/uniffi_bridge_tests.rs
Normal 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
15
core/uniffi/README.md
Normal 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.
|
||||||
10
core/uniffi/bindings/android/Cryptec.java
Normal file
10
core/uniffi/bindings/android/Cryptec.java
Normal 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 ""; }
|
||||||
|
}
|
||||||
15
core/uniffi/bindings/ios/Cryptec.h
Normal file
15
core/uniffi/bindings/ios/Cryptec.h
Normal 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
8
core/uniffi/cryptec.udl
Normal 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
27
electron/main.js
Normal 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
13
electron/package.json
Normal 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
56
electron/preload.js
Normal 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)
|
||||||
|
});
|
||||||
39
electron/renderer/index.html
Normal file
39
electron/renderer/index.html
Normal 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>
|
||||||
21
electron/renderer/renderer.js
Normal file
21
electron/renderer/renderer.js
Normal 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;
|
||||||
|
});
|
||||||
12
electron/test/electron-smoke.js
Normal file
12
electron/test/electron-smoke.js
Normal 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);
|
||||||
|
}
|
||||||
17
mobile/__tests__/nativeBridge.test.ts
Normal file
17
mobile/__tests__/nativeBridge.test.ts
Normal 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();
|
||||||
|
});
|
||||||
35
mobile/__tests__/nativeModuleIntegration.test.ts
Normal file
35
mobile/__tests__/nativeModuleIntegration.test.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
39
mobile/android/src/main/java/com/cryptec/CryptecModule.java
Normal file
39
mobile/android/src/main/java/com/cryptec/CryptecModule.java
Normal 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("");
|
||||||
|
}
|
||||||
|
}
|
||||||
24
mobile/android/src/main/java/com/cryptec/CryptecPackage.java
Normal file
24
mobile/android/src/main/java/com/cryptec/CryptecPackage.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
10
mobile/ios/CryptecModule.m
Normal file
10
mobile/ios/CryptecModule.m
Normal 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
8
mobile/jest.config.js
Normal 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
34
mobile/package.json
Normal 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
18
mobile/src/App.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
55
mobile/src/nativeBridge.ts
Normal file
55
mobile/src/nativeBridge.ts
Normal 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
|
||||||
|
};
|
||||||
41
mobile/src/screens/Onboarding.tsx
Normal file
41
mobile/src/screens/Onboarding.tsx
Normal 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 }
|
||||||
|
});
|
||||||
45
mobile/src/screens/WalletHome.tsx
Normal file
45
mobile/src/screens/WalletHome.tsx
Normal 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
14
mobile/tsconfig.json
Normal 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
17
scripts/uniffi_codegen.sh
Executable 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
|
||||||
Loading…
Add table
Reference in a new issue