diff --git a/mobile/__tests__/nativeBridge.test.ts b/mobile/__tests__/nativeBridge.test.ts
new file mode 100644
index 0000000..0ca3f50
--- /dev/null
+++ b/mobile/__tests__/nativeBridge.test.ts
@@ -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();
+});
diff --git a/mobile/jest.config.js b/mobile/jest.config.js
new file mode 100644
index 0000000..14c7903
--- /dev/null
+++ b/mobile/jest.config.js
@@ -0,0 +1,8 @@
+module.exports = {
+ preset: 'react-native',
+ transform: {
+ '^.+\\.tsx?$': 'ts-jest'
+ },
+ testEnvironment: 'node',
+ setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect']
+};
diff --git a/mobile/package.json b/mobile/package.json
new file mode 100644
index 0000000..23575bb
--- /dev/null
+++ b/mobile/package.json
@@ -0,0 +1,33 @@
+{
+ "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"
+ },
+ "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"
+ }
+}
diff --git a/mobile/src/App.tsx b/mobile/src/App.tsx
new file mode 100644
index 0000000..0aba90d
--- /dev/null
+++ b/mobile/src/App.tsx
@@ -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 (
+
+
+
+
+
+
+ );
+}
diff --git a/mobile/src/nativeBridge.ts b/mobile/src/nativeBridge.ts
new file mode 100644
index 0000000..ded2eb4
--- /dev/null
+++ b/mobile/src/nativeBridge.ts
@@ -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 {
+ 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 {
+ 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 {
+ 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
+};
diff --git a/mobile/src/screens/Onboarding.tsx b/mobile/src/screens/Onboarding.tsx
new file mode 100644
index 0000000..d596db0
--- /dev/null
+++ b/mobile/src/screens/Onboarding.tsx
@@ -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 (
+
+ CrypteCipher — Onboarding
+
+ — or —
+
+
+
+ );
+}
+
+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 }
+});
diff --git a/mobile/src/screens/WalletHome.tsx b/mobile/src/screens/WalletHome.tsx
new file mode 100644
index 0000000..9d8663f
--- /dev/null
+++ b/mobile/src/screens/WalletHome.tsx
@@ -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 (
+
+ Wallet
+ Receive address (first):
+ {address}
+
+ Send (mock)
+
+
+
+
+
+
+ );
+}
+
+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 }
+});
diff --git a/mobile/tsconfig.json b/mobile/tsconfig.json
new file mode 100644
index 0000000..2f14106
--- /dev/null
+++ b/mobile/tsconfig.json
@@ -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"]
+}