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 +