From 63510313ddcaeabdfadd1a805d77922dde4530cb Mon Sep 17 00:00:00 2001 From: Derek Guenther Date: Wed, 24 Apr 2024 15:03:04 -0400 Subject: [PATCH 1/2] Demo using secure store with exportAccount (#17) --- package-lock.json | 9 ++++++ packages/mobile-app/app.json | 3 +- packages/mobile-app/app/(tabs)/transact.tsx | 19 +++++++++--- .../data/facades/accounts/demoHandlers.ts | 13 +++++++++ .../data/facades/accounts/handlers.ts | 10 +++++++ .../mobile-app/data/facades/accounts/types.ts | 1 + packages/mobile-app/data/wallet/db.ts | 6 +++- packages/mobile-app/data/wallet/index.ts | 29 +++++++++++++++++-- packages/mobile-app/package.json | 3 +- 9 files changed, 84 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 151fd67..e53f2ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14194,6 +14194,14 @@ } } }, + "node_modules/expo-secure-store": { + "version": "12.8.1", + "resolved": "https://registry.npmjs.org/expo-secure-store/-/expo-secure-store-12.8.1.tgz", + "integrity": "sha512-Ju3jmkHby4w7rIzdYAt9kQyQ7HhHJ0qRaiQOInknhOLIltftHjEgF4I1UmzKc7P5RCfGNmVbEH729Pncp/sHXQ==", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-splash-screen": { "version": "0.26.4", "resolved": "https://registry.npmjs.org/expo-splash-screen/-/expo-splash-screen-0.26.4.tgz", @@ -23358,6 +23366,7 @@ "expo-constants": "~15.4.5", "expo-linking": "~6.2.2", "expo-router": "~3.4.8", + "expo-secure-store": "~12.8.1", "expo-sqlite": "~13.4.0", "expo-status-bar": "~1.11.1", "ironfish-native-module": "*", diff --git a/packages/mobile-app/app.json b/packages/mobile-app/app.json index 23d1ea1..e217c02 100644 --- a/packages/mobile-app/app.json +++ b/packages/mobile-app/app.json @@ -27,7 +27,8 @@ "package": "com.ironfish.mobileapp" }, "plugins": [ - "expo-router" + "expo-router", + "expo-secure-store" ] } } diff --git a/packages/mobile-app/app/(tabs)/transact.tsx b/packages/mobile-app/app/(tabs)/transact.tsx index b8c7b20..ce61f09 100644 --- a/packages/mobile-app/app/(tabs)/transact.tsx +++ b/packages/mobile-app/app/(tabs)/transact.tsx @@ -1,4 +1,4 @@ -import { View, Text } from "react-native"; +import { View, Text, ScrollView, TextInput } from "react-native"; import { useFacade } from "../../data/facades"; import { Button } from "@ironfish/ui"; import { useQueryClient } from "@tanstack/react-query"; @@ -15,12 +15,23 @@ export default function Transact() { }) } }); + const exportAccount = facade.exportAccount.useMutation(); return ( - + Accounts {(getAccountsResult.data ?? []).map((account) => ( - {account.name} + + {account.name} + + ))} - + ); } diff --git a/packages/mobile-app/data/facades/accounts/demoHandlers.ts b/packages/mobile-app/data/facades/accounts/demoHandlers.ts index e9808f6..5d1323c 100644 --- a/packages/mobile-app/data/facades/accounts/demoHandlers.ts +++ b/packages/mobile-app/data/facades/accounts/demoHandlers.ts @@ -35,4 +35,17 @@ export const accountsHandlers = f.facade({ ACCOUNTS.push(account); return account; }), + exportAccount: f.handler + .input( + z.object({ + name: z.string(), + }), + ) + .mutation(async ({ name }) => { + const account = ACCOUNTS.find((a) => a.name === name) + if (account === undefined) { + throw new Error("No accounts found"); + } + return JSON.stringify(account); + }), }); diff --git a/packages/mobile-app/data/facades/accounts/handlers.ts b/packages/mobile-app/data/facades/accounts/handlers.ts index 80d3bd2..023ea4a 100644 --- a/packages/mobile-app/data/facades/accounts/handlers.ts +++ b/packages/mobile-app/data/facades/accounts/handlers.ts @@ -17,4 +17,14 @@ export const accountsHandlers = f.facade({ const account = await wallet.createAccount(name); return account; }), + exportAccount: f.handler + .input( + z.object({ + name: z.string(), + }), + ) + .mutation(async ({ name }) => { + const account = await wallet.exportAccount(name); + return account; + }), }); diff --git a/packages/mobile-app/data/facades/accounts/types.ts b/packages/mobile-app/data/facades/accounts/types.ts index 54776ef..08bb4af 100644 --- a/packages/mobile-app/data/facades/accounts/types.ts +++ b/packages/mobile-app/data/facades/accounts/types.ts @@ -9,4 +9,5 @@ export type Account = { export type AccountsMethods = { getAccounts: Query<() => Account[]>; createAccount: Mutation<(args: { name: string }) => Account>; + exportAccount: Mutation<(args: { name: string }) => string>; }; diff --git a/packages/mobile-app/data/wallet/db.ts b/packages/mobile-app/data/wallet/db.ts index 49e772c..bef7dbf 100644 --- a/packages/mobile-app/data/wallet/db.ts +++ b/packages/mobile-app/data/wallet/db.ts @@ -65,6 +65,10 @@ export class WalletDb { } async getAccounts() { - return await this.db.selectFrom("accounts").selectAll('accounts').execute(); + return await this.db.selectFrom("accounts").selectAll().execute(); + } + + async getAccount(name: string) { + return await this.db.selectFrom("accounts").selectAll().where('accounts.name', '==', name).executeTakeFirst(); } } diff --git a/packages/mobile-app/data/wallet/index.ts b/packages/mobile-app/data/wallet/index.ts index d6fb071..453a3bc 100644 --- a/packages/mobile-app/data/wallet/index.ts +++ b/packages/mobile-app/data/wallet/index.ts @@ -1,6 +1,7 @@ import { generateKey } from "ironfish-native-module"; import { WalletDb } from "./db"; -import { AccountFormat, encodeAccount } from "@ironfish/sdk"; +import { AccountFormat, decodeAccount, encodeAccount } from "@ironfish/sdk"; +import * as SecureStore from 'expo-secure-store' class Wallet { state: { type: 'STOPPED' } | { type: 'LOADING' } | { type: 'STARTED', db: WalletDb } = { type: 'STOPPED' }; @@ -41,7 +42,12 @@ class Wallet { name, }, AccountFormat.Base64Json) - return await this.state.db.createAccount(name, viewOnlyAccount) + const newAccount = await this.state.db.createAccount(name, viewOnlyAccount) + await SecureStore.setItemAsync(key.publicAddress, key.spendingKey, { + keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY, + requireAuthentication: false, + }); + return newAccount; } async getAccounts() { @@ -51,6 +57,25 @@ class Wallet { return this.state.db.getAccounts(); } + + async exportAccount(name: string) { + if (this.state.type !== 'STARTED') { + throw new Error('Wallet is not started'); + } + + const account = await this.state.db.getAccount(name); + if (account == null) { + throw new Error(`No account found with name ${name}`) + } + + const decodedAccount = decodeAccount(account.viewOnlyAccount); + decodedAccount.name = name; + decodedAccount.spendingKey = await SecureStore.getItemAsync(decodedAccount.publicAddress, { + keychainAccessible: SecureStore.WHEN_UNLOCKED_THIS_DEVICE_ONLY, + requireAuthentication: false, + }) + return encodeAccount(decodedAccount, AccountFormat.JSON) + } } export const wallet = new Wallet(); \ No newline at end of file diff --git a/packages/mobile-app/package.json b/packages/mobile-app/package.json index 79d1537..14c9c04 100644 --- a/packages/mobile-app/package.json +++ b/packages/mobile-app/package.json @@ -26,7 +26,8 @@ "react-native": "0.73.6", "react-native-safe-area-context": "4.8.2", "react-native-screens": "~3.29.0", - "zod": "^3.22.4" + "zod": "^3.22.4", + "expo-secure-store": "~12.8.1" }, "devDependencies": { "@babel/core": "^7.20.0", From 81765cc34602a93c82656fc5a16316dcdf0f59ad Mon Sep 17 00:00:00 2001 From: Derek Guenther Date: Wed, 24 Apr 2024 15:03:24 -0400 Subject: [PATCH 2/2] Add spendingKeyToWords to ironfish-native-module (#18) --- .../IronfishNativeModule.kt | 27 +++++++++++++++++++ .../ios/IronfishNativeModule.swift | 5 ++++ .../rust_lib/src/lib.rs | 13 +++++++++ packages/ironfish-native-module/src/index.ts | 7 +++++ .../mobile-app/shims/ironfish-rust-nodejs.js | 1 + 5 files changed, 53 insertions(+) diff --git a/packages/ironfish-native-module/android/src/main/java/expo/modules/ironfishnativemodule/IronfishNativeModule.kt b/packages/ironfish-native-module/android/src/main/java/expo/modules/ironfishnativemodule/IronfishNativeModule.kt index 10cbd4e..4dca9cc 100644 --- a/packages/ironfish-native-module/android/src/main/java/expo/modules/ironfishnativemodule/IronfishNativeModule.kt +++ b/packages/ironfish-native-module/android/src/main/java/expo/modules/ironfishnativemodule/IronfishNativeModule.kt @@ -41,6 +41,24 @@ class IronfishNativeModule : Module() { ) } + Function("spendingKeyToWords") { privateKey: String, languageCode: Long -> + try { + return spendingKeyToWords(privateKey: privateKey, languageCode: languageCode) + } catch (error: Exception) { + error.printStackTrace() + throw error + } + } + + Function("wordsToSpendingKey") { words: String, languageCode: Long -> + try { + return wordsToSpendingKey(words: words, languageCode: languageCode) + } catch (error: Exception) { + error.printStackTrace() + throw error + } + } + AsyncFunction("generateKeyFromPrivateKey") { privateKey: String -> val k = uniffi.rust_lib.generateKeyFromPrivateKey(privateKey) @@ -53,5 +71,14 @@ class IronfishNativeModule : Module() { k.proofAuthorizingKey ) } + + Function("isValidPublicAddress") { hexAddress: String -> + try { + uniffi.rust_lib.isValidPublicAddress(hexAddress: hexAddress) + } catch (error: Exception) { + error.printStackTrace() + throw error + } + } } } diff --git a/packages/ironfish-native-module/ios/IronfishNativeModule.swift b/packages/ironfish-native-module/ios/IronfishNativeModule.swift index f9affd7..82e1dea 100644 --- a/packages/ironfish-native-module/ios/IronfishNativeModule.swift +++ b/packages/ironfish-native-module/ios/IronfishNativeModule.swift @@ -43,6 +43,11 @@ public class IronfishNativeModule: Module { ) } + Function("spendingKeyToWords") { (privateKey: String, languageCode: Int32) throws -> String in + let phrase = try spendingKeyToWords(privateKey: privateKey, languageCode: languageCode) + return phrase + } + Function("wordsToSpendingKey") { (words: String, languageCode: Int32) throws -> String in let k = try wordsToSpendingKey(words: words, languageCode: languageCode) return k diff --git a/packages/ironfish-native-module/rust_lib/src/lib.rs b/packages/ironfish-native-module/rust_lib/src/lib.rs index 6dd9e2c..5593807 100644 --- a/packages/ironfish-native-module/rust_lib/src/lib.rs +++ b/packages/ironfish-native-module/rust_lib/src/lib.rs @@ -66,6 +66,19 @@ fn generate_key() -> Key { } } +#[uniffi::export] +pub fn spending_key_to_words( + private_key: String, + language_code: i32, +) -> Result { + let key = SaplingKey::from_hex(&private_key).map_err(|e| EnumError::Error { msg: e.to_string() })?; + let language_code_enum: LanguageCode = LanguageCode::from_i32(language_code).ok_or_else(|| EnumError::Error { msg: "Invalid language code".to_string() })?; + let language = Language::from(language_code_enum); + + let mnemonic = key.to_words(language).map_err(|e| EnumError::Error { msg: e.to_string() })?; + Ok(mnemonic.into_phrase()) +} + #[uniffi::export] pub fn words_to_spending_key( words: String, diff --git a/packages/ironfish-native-module/src/index.ts b/packages/ironfish-native-module/src/index.ts index f075550..7d553fa 100644 --- a/packages/ironfish-native-module/src/index.ts +++ b/packages/ironfish-native-module/src/index.ts @@ -15,6 +15,13 @@ export function generateKey(): Key { return IronfishNativeModule.generateKey(); } +export function spendingKeyToWords( + privateKey: string, + languageCode: number, +): string { + return IronfishNativeModule.spendingKeyToWords(privateKey, languageCode); +} + export function wordsToSpendingKey( words: string, languageCode: number, diff --git a/packages/mobile-app/shims/ironfish-rust-nodejs.js b/packages/mobile-app/shims/ironfish-rust-nodejs.js index e11ba89..84691b7 100644 --- a/packages/mobile-app/shims/ironfish-rust-nodejs.js +++ b/packages/mobile-app/shims/ironfish-rust-nodejs.js @@ -33,6 +33,7 @@ const mockIronfishRustNodejs = { throw new Error(message); } }), + spendingKeyToWords: IronfishNativeModule.spendingKeyToWords, wordsToSpendingKey: IronfishNativeModule.wordsToSpendingKey, generateKeyFromPrivateKey: IronfishNativeModule.generateKeyFromPrivateKey, isValidPublicAddress: IronfishNativeModule.isValidPublicAddress,