Compare commits
No commits in common. "de7ef423dd681d8c29083dc2bea7bb8913b440ef" and "41d0d3d8668d01550677ab96f3877a08f5ed795c" have entirely different histories.
de7ef423dd
...
41d0d3d866
29 changed files with 0 additions and 667 deletions
18
.github/workflows/ci.yml
vendored
18
.github/workflows/ci.yml
vendored
|
|
@ -74,10 +74,6 @@ jobs:
|
|||
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
|
||||
|
|
@ -128,17 +124,3 @@ jobs:
|
|||
npm ci || true
|
||||
npm test
|
||||
shell: bash
|
||||
|
||||
- name: Run mobile unit tests (React Native)
|
||||
working-directory: ./mobile
|
||||
run: |
|
||||
npm ci || true
|
||||
npm test
|
||||
shell: bash
|
||||
|
||||
- name: Run Electron smoke tests (desktop)
|
||||
working-directory: ./electron
|
||||
run: |
|
||||
npm ci || true
|
||||
npm test
|
||||
shell: bash
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
# 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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -30,9 +30,6 @@ pub use swap_manager::SwapOffer;
|
|||
pub use swap_sim::SwapSimulation;
|
||||
pub use wallet_manager::{register_decrypted_db, close_decrypted_db, register_inmemory_db, close_inmemory_db, close_all, list_handles};
|
||||
|
||||
mod uniffi;
|
||||
pub use uniffi::{generate_mnemonic, first_receive_address, create_psbt, sign_psbt};
|
||||
|
||||
/// WalletCore: minimal demonstrative API
|
||||
pub struct WalletCore {
|
||||
// In a real implementation, secrets should be stored in a secure enclave or encrypted storage
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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