Compare commits
No commits in common. "feature/initial-scaffold" and "main" have entirely different histories.
feature/in
...
main
33 changed files with 27 additions and 808 deletions
110
.github/workflows/ci.yml
vendored
110
.github/workflows/ci.yml
vendored
|
|
@ -9,8 +9,6 @@ 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]
|
||||||
|
|
@ -26,96 +24,10 @@ 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
|
||||||
- name: Install system packages (Ubuntu)
|
run: cargo build --workspace --verbose
|
||||||
if: matrix.os == 'ubuntu-latest'
|
- name: Run Rust tests
|
||||||
run: |
|
run: cargo test --workspace --verbose
|
||||||
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
|
||||||
|
|
@ -128,17 +40,3 @@ 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
10
Cargo.toml
|
|
@ -1,10 +0,0 @@
|
||||||
[workspace]
|
|
||||||
members = [
|
|
||||||
"core",
|
|
||||||
"bindings",
|
|
||||||
"tools/cli"
|
|
||||||
]
|
|
||||||
|
|
||||||
[workspace.metadata]
|
|
||||||
# Project metadata
|
|
||||||
name = "cryptec-workspace"
|
|
||||||
|
|
@ -1,11 +1,5 @@
|
||||||
# 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) - add as an optional dependency when needed
|
# Rust-Lightning (lightning protocol) - use as optional or for in-process nodes
|
||||||
# lightning = { version = "0.0.124", optional = true } # disabled because some versions may be yanked
|
lightning = "0.0.124"
|
||||||
|
|
||||||
# Crypto / storage / tools
|
# Crypto / storage / tools
|
||||||
argon2 = "0.4"
|
argon2 = "0.4"
|
||||||
|
|
@ -36,17 +36,7 @@ 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 = { version = "1.3", optional = true }
|
hidapi = "1.3"
|
||||||
|
|
||||||
[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"] }
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
// 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;
|
use aes_gcm::aead::{Aead, NewAead};
|
||||||
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,9 +30,6 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
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,13 +2,11 @@ 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 crate::bip39::Mnemonic as LocalMnemonic;
|
use bdk::keys::bip39::{Mnemonic as BdkMnemonic};
|
||||||
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 {
|
||||||
|
|
@ -18,14 +16,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> {
|
||||||
// Derive master xprv using our local bip39 helper and build simple `wpkh(<xpub>/0/*)` descriptors.
|
// Use BDK's bip39 helper to create descriptor templates
|
||||||
let local_m = LocalMnemonic::from_phrase(phrase).map_err(|e| anyhow::anyhow!(e.to_string()))?;
|
let mnemonic = BdkMnemonic::from_str(phrase).map_err(|e| anyhow::anyhow!(e.to_string()))?;
|
||||||
let xprv = local_m.to_master_xprv(passphrase, network);
|
let xkey: DescriptorTemplateOut = bdk::keys::bip39::translate_mnemonic(&mnemonic, passphrase, network)
|
||||||
// neuter to xpub for descriptor
|
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
|
||||||
let secp = secp256k1::Secp256k1::new();
|
|
||||||
let xpub = ExtendedPubKey::from_private(&secp, &xprv);
|
// For demo, use the first "external" descriptor for a single-key wallet
|
||||||
let descriptor = format!("wpkh({}/0/*)", xpub.to_string());
|
let descriptor = xkey.external.clone();
|
||||||
let change_descriptor = format!("wpkh({}/1/*)", xpub.to_string());
|
let change_descriptor = xkey.internal.clone();
|
||||||
|
|
||||||
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 })
|
||||||
|
|
@ -51,8 +49,7 @@ 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 serialized = consensus_encode::serialize(&psbt);
|
let bs = base64::encode(&psbt.serialize());
|
||||||
let bs = base64::encode(&serialized);
|
|
||||||
Ok(bs)
|
Ok(bs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,8 +58,7 @@ 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 serialized = consensus_encode::serialize(&psbt);
|
let out = base64::encode(&psbt.serialize());
|
||||||
let out = base64::encode(&serialized);
|
|
||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -82,13 +78,12 @@ 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> {
|
||||||
// derive master xprv & descriptors just like in BdkWallet
|
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()))?;
|
||||||
let secp = secp256k1::Secp256k1::new();
|
|
||||||
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());
|
|
||||||
|
|
||||||
// Ensure directory exists
|
// Ensure directory exists
|
||||||
let p = Path::new(db_path);
|
let p = Path::new(db_path);
|
||||||
|
|
@ -121,8 +116,7 @@ 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 serialized = consensus_encode::serialize(&psbt);
|
let bs = base64::encode(&psbt.serialize());
|
||||||
let bs = base64::encode(&serialized);
|
|
||||||
Ok(bs)
|
Ok(bs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,8 +124,7 @@ 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 serialized = consensus_encode::serialize(&psbt);
|
let out = base64::encode(&psbt.serialize());
|
||||||
let out = base64::encode(&serialized);
|
|
||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -202,7 +195,6 @@ 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 {
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
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.
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
// 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 ""; }
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
// 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
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
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);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
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();
|
|
||||||
});
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
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)
|
|
||||||
});
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
<!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>
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
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;
|
|
||||||
});
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
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();
|
|
||||||
});
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
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("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
#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
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
preset: 'react-native',
|
|
||||||
transform: {
|
|
||||||
'^.+\\.tsx?$': 'ts-jest'
|
|
||||||
},
|
|
||||||
testEnvironment: 'node',
|
|
||||||
setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect']
|
|
||||||
};
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
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
|
|
||||||
};
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
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 }
|
|
||||||
});
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
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 }
|
|
||||||
});
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
{
|
|
||||||
"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"]
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
#!/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