From 5401f5f409a4dbee3863557f8309d4a44117c960 Mon Sep 17 00:00:00 2001 From: Derek Guenther Date: Tue, 23 Apr 2024 16:45:10 -0400 Subject: [PATCH 1/3] Add SQLite, account creation, and account fetching (#16) --- Cargo.lock | 4 +- package-lock.json | 67 ++++++++++++++++++ packages/data-facade/src/facade.ts | 5 +- packages/data-facade/src/types.ts | 2 +- packages/mobile-app/app/(tabs)/transact.tsx | 31 ++++---- packages/mobile-app/app/_layout.tsx | 44 +++++++++--- packages/mobile-app/data/accounts/handlers.ts | 50 ------------- packages/mobile-app/data/accounts/types.ts | 9 --- packages/mobile-app/data/facades/README.md | 5 ++ .../data/facades/accounts/demoHandlers.ts | 38 ++++++++++ .../data/facades/accounts/handlers.ts | 20 ++++++ .../mobile-app/data/facades/accounts/types.ts | 12 ++++ .../data/facades/app/demoHandlers.ts | 9 +++ .../mobile-app/data/facades/app/handlers.ts | 13 ++++ packages/mobile-app/data/facades/app/types.ts | 5 ++ packages/mobile-app/data/facades/index.ts | 18 +++++ packages/mobile-app/data/index.ts | 7 -- packages/mobile-app/data/wallet/db.ts | 70 +++++++++++++++++++ packages/mobile-app/data/wallet/index.ts | 56 +++++++++++++++ packages/mobile-app/index.js | 7 +- packages/mobile-app/package.json | 5 +- 21 files changed, 378 insertions(+), 99 deletions(-) delete mode 100644 packages/mobile-app/data/accounts/handlers.ts delete mode 100644 packages/mobile-app/data/accounts/types.ts create mode 100644 packages/mobile-app/data/facades/README.md create mode 100644 packages/mobile-app/data/facades/accounts/demoHandlers.ts create mode 100644 packages/mobile-app/data/facades/accounts/handlers.ts create mode 100644 packages/mobile-app/data/facades/accounts/types.ts create mode 100644 packages/mobile-app/data/facades/app/demoHandlers.ts create mode 100644 packages/mobile-app/data/facades/app/handlers.ts create mode 100644 packages/mobile-app/data/facades/app/types.ts create mode 100644 packages/mobile-app/data/facades/index.ts delete mode 100644 packages/mobile-app/data/index.ts create mode 100644 packages/mobile-app/data/wallet/db.ts create mode 100644 packages/mobile-app/data/wallet/index.ts diff --git a/Cargo.lock b/Cargo.lock index 76adb0d..7664eaf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1799,9 +1799,9 @@ checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" [[package]] name = "num" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af" +checksum = "3135b08af27d103b0a51f2ae0f8632117b7b185ccf931445affa8df530576a41" dependencies = [ "num-bigint", "num-complex", diff --git a/package-lock.json b/package-lock.json index 63a5936..151fd67 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4527,6 +4527,18 @@ "resolved": "https://registry.npmjs.org/@expo/vector-icons/-/vector-icons-14.0.0.tgz", "integrity": "sha512-5orm59pdnBQlovhU9k4DbjMUZBHNlku7IRgFY56f7pcaaCnXq9yaLJoOQl9sMwNdFzf4gnkTyHmR5uN10mI9rA==" }, + "node_modules/@expo/websql": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@expo/websql/-/websql-1.0.1.tgz", + "integrity": "sha512-H9/t1V7XXyKC343FJz/LwaVBfDhs6IqhDtSYWpt8LNSQDVjf5NvVJLc5wp+KCpRidZx8+0+YeHJN45HOXmqjFA==", + "dependencies": { + "argsarray": "^0.0.1", + "immediate": "^3.2.2", + "noop-fn": "^1.0.0", + "pouchdb-collections": "^1.0.1", + "tiny-queue": "^0.2.1" + } + }, "node_modules/@expo/xcpretty": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@expo/xcpretty/-/xcpretty-4.3.1.tgz", @@ -10367,6 +10379,11 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, + "node_modules/argsarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/argsarray/-/argsarray-0.0.1.tgz", + "integrity": "sha512-u96dg2GcAKtpTrBdDoFIM7PjcBA+6rSP0OR94MOReNRyUECL6MtQt5XXmRr4qrftYaef9+l5hcpO5te7sML1Cg==" + }, "node_modules/array-buffer-byte-length": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", @@ -14188,6 +14205,17 @@ "expo": "*" } }, + "node_modules/expo-sqlite": { + "version": "13.4.0", + "resolved": "https://registry.npmjs.org/expo-sqlite/-/expo-sqlite-13.4.0.tgz", + "integrity": "sha512-5f7d2EDM+pgerM33KndtX4gWw2nuVaXY68nnqx7PhkiYeyEmeNfZ29bIFtpBzNb/L5l0/DTtRxuSqftxbknFtw==", + "dependencies": { + "@expo/websql": "^1.0.1" + }, + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-status-bar": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-1.11.1.tgz", @@ -17410,6 +17438,27 @@ "node": ">=6" } }, + "node_modules/kysely": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.27.3.tgz", + "integrity": "sha512-lG03Ru+XyOJFsjH3OMY6R/9U38IjDPfnOfDgO3ynhbDr+Dz8fak+X6L62vqu3iybQnj+lG84OttBuU9KY3L9kA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/kysely-expo": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/kysely-expo/-/kysely-expo-1.0.0-beta.2.tgz", + "integrity": "sha512-9un9Y9ghrj7K/DCjcIgALrtFwRkLWm6oAJ1z/CHQ7vhIHS+RfKkKfgCvfWOEczGnI/2mJAPGLYaBwZjAvYyCGw==", + "dependencies": { + "expo-sqlite": "^13.2.2", + "kysely": "^0.27.0" + }, + "peerDependencies": { + "expo-sqlite": "^13.2.1", + "kysely": "^0.27.0" + } + }, "node_modules/level-concat-iterator": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/level-concat-iterator/-/level-concat-iterator-2.0.1.tgz", @@ -19018,6 +19067,11 @@ "url": "https://github.com/sponsors/antelle" } }, + "node_modules/noop-fn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/noop-fn/-/noop-fn-1.0.0.tgz", + "integrity": "sha512-pQ8vODlgXt2e7A3mIbFDlizkr46r75V+BJxVAyat8Jl7YmI513gG5cfyRL0FedKraoZ+VAouI1h4/IWpus5pcQ==" + }, "node_modules/nopt": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", @@ -19767,6 +19821,11 @@ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" }, + "node_modules/pouchdb-collections": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pouchdb-collections/-/pouchdb-collections-1.0.1.tgz", + "integrity": "sha512-31db6JRg4+4D5Yzc2nqsRqsA2oOkZS8DpFav3jf/qVNBxusKa2ClkEIZ2bJNpaDbMfWtnuSq59p6Bn+CipPMdg==" + }, "node_modules/prebuild-install": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", @@ -22160,6 +22219,11 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/tiny-queue": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tiny-queue/-/tiny-queue-0.2.1.tgz", + "integrity": "sha512-EijGsv7kzd9I9g0ByCl6h42BWNGUZrlCSejfrb3AKeHC33SGbASu1VDf5O3rRiiUOhAC9CHdZxFPbZu0HmR70A==" + }, "node_modules/tmp": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", @@ -23294,8 +23358,11 @@ "expo-constants": "~15.4.5", "expo-linking": "~6.2.2", "expo-router": "~3.4.8", + "expo-sqlite": "~13.4.0", "expo-status-bar": "~1.11.1", "ironfish-native-module": "*", + "kysely": "^0.27.3", + "kysely-expo": "^1.0.0-beta.2", "process": "^0.11.10", "react": "18.2.0", "react-native": "0.73.6", diff --git a/packages/data-facade/src/facade.ts b/packages/data-facade/src/facade.ts index 3c31e25..24ef4ff 100644 --- a/packages/data-facade/src/facade.ts +++ b/packages/data-facade/src/facade.ts @@ -1,5 +1,5 @@ import { ZodTypeAny, z } from "zod"; -import { useQuery, useMutation } from "@tanstack/react-query"; +import { useQuery, useMutation, UseMutationOptions } from "@tanstack/react-query"; import { buildQueryKey } from "./utils"; import type { ResolverFunc, @@ -32,13 +32,14 @@ function handlerQueryBuilder( function buildUseMutation() { return (resolver: TResolver) => ({ - useMutation: () => { + useMutation: (opts?: UseMutationOptions>, Error, unknown, unknown>) => { return useMutation< Awaited>, Error, unknown, unknown >({ + ...opts, mutationFn: resolver, }); }, diff --git a/packages/data-facade/src/types.ts b/packages/data-facade/src/types.ts index 98ce6dd..fa27bd6 100644 --- a/packages/data-facade/src/types.ts +++ b/packages/data-facade/src/types.ts @@ -35,7 +35,7 @@ export type HandlerQueryBuilderReturn = ( type UseMutationType< TResolver extends ResolverFunc, TReturn = Awaited>, -> = (opts?: UseMutationOptions) => UseMutationResult; +> = (opts?: UseMutationOptions) => UseMutationResult; export type HandlerMutationBuilderReturn = () => { diff --git a/packages/mobile-app/app/(tabs)/transact.tsx b/packages/mobile-app/app/(tabs)/transact.tsx index ca59b50..b8c7b20 100644 --- a/packages/mobile-app/app/(tabs)/transact.tsx +++ b/packages/mobile-app/app/(tabs)/transact.tsx @@ -1,33 +1,34 @@ import { View, Text } from "react-native"; -import { useFacade } from "../../data"; +import { useFacade } from "../../data/facades"; import { Button } from "@ironfish/ui"; -import { useState } from "react"; +import { useQueryClient } from "@tanstack/react-query"; export default function Transact() { - const [facadeResult, setFacadeResult] = useState([""]); const facade = useFacade(); + const qc = useQueryClient() - const getAccountsResult = facade.getAccounts.useQuery(123); - const getAccountsWithZodResult = facade.getAccountsWithZod.useQuery({ - limit: 2, + const getAccountsResult = facade.getAccounts.useQuery(); + const createAccount = facade.createAccount.useMutation({ + onSuccess: () => { + qc.invalidateQueries({ + queryKey: ['getAccounts'] + }) + } }); - const getAllAccountsResult = facade.getAllAccounts.useQuery(); - const createAccount = facade.createAccount.useMutation(); return ( Accounts - {JSON.stringify(getAccountsResult.data)} - {JSON.stringify(getAccountsWithZodResult.data)} - {JSON.stringify(getAllAccountsResult.data)} - Mutation: {facadeResult} + {(getAccountsResult.data ?? []).map((account) => ( + {account.name} + ))} ); diff --git a/packages/mobile-app/app/_layout.tsx b/packages/mobile-app/app/_layout.tsx index d64ac1d..ebd632f 100644 --- a/packages/mobile-app/app/_layout.tsx +++ b/packages/mobile-app/app/_layout.tsx @@ -1,22 +1,48 @@ import { Stack } from "expo-router"; +import { Text } from 'react-native'; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { FacadeProvider } from "../data"; +import { FacadeProvider, useFacade } from "../data/facades"; +import React, { useEffect } from "react"; const queryClient = new QueryClient(); +function DatabaseLoader({ loading, children }: { loading: React.ReactNode, children?: React.ReactNode }) { + const facade = useFacade(); + const [status, setStatus] = React.useState("loading"); + const loadDatabases = facade.loadDatabases.useMutation(); + + useEffect(() => { + const fn = async () => { + const result = await loadDatabases.mutateAsync(undefined); + setStatus(result) + } + fn() + }, []) + + if (status === "loading") { + return loading; + } else if (status === 'loaded') { + return children; + } else { + throw new Error(`Unknown status ${status}`); + } +} + export default function Layout() { return ( - - - + Loading databases...}> + + + + ); diff --git a/packages/mobile-app/data/accounts/handlers.ts b/packages/mobile-app/data/accounts/handlers.ts deleted file mode 100644 index 3526213..0000000 --- a/packages/mobile-app/data/accounts/handlers.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { f } from "data-facade"; -import { z } from "zod"; -import { AccountsMethods } from "./types"; - -const accounts = ["alice", "bob", "carol"]; - -async function getAccounts(limit: number) { - await new Promise((resolve) => setTimeout(resolve, 1000)); - return accounts.slice(0, limit); -} - -export const accountsHandlers = f.facade({ - getAccounts: f.handler.query(async (count: number) => { - const accounts = await getAccounts(count ?? 1); - console.log("getAccounts", accounts); - return accounts; - }), - getAllAccounts: f.handler.query(async () => { - const accounts = await getAccounts(1); - console.log("getAllAccounts", accounts); - return accounts; - }), - getAccountsWithZod: f.handler - .input( - z.object({ - limit: z.number(), - }), - ) - .query(async ({ limit }) => { - const accounts = await getAccounts(limit); - console.log("getAccountsWithZod", accounts); - return accounts; - }), - createAccount: f.handler.mutation(async (account: string) => { - console.log("createAccount", account); - accounts.push(account); - return accounts; - }), - createAccountWithZod: f.handler - .input( - z.object({ - account: z.string(), - }), - ) - .mutation(async ({ account }) => { - console.log("createAccountWithZod", account); - accounts.push(account); - return accounts; - }), -}); diff --git a/packages/mobile-app/data/accounts/types.ts b/packages/mobile-app/data/accounts/types.ts deleted file mode 100644 index f294078..0000000 --- a/packages/mobile-app/data/accounts/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Query, Mutation } from "data-facade"; - -export type AccountsMethods = { - getAccounts: Query<(count: number) => string[]>; - getAllAccounts: Query<() => string[]>; - getAccountsWithZod: Query<(args: { limit: number }) => string[]>; - createAccount: Mutation<(account: string) => string[]>; - createAccountWithZod: Mutation<(args: { account: string }) => string[]>; -}; diff --git a/packages/mobile-app/data/facades/README.md b/packages/mobile-app/data/facades/README.md new file mode 100644 index 0000000..f9e84b3 --- /dev/null +++ b/packages/mobile-app/data/facades/README.md @@ -0,0 +1,5 @@ +# facades + +Facades use the `data-facade` package to provide a `tanstack/react-query` interface between the wallet code in the `data` folder and the Expo frontend in the `app` folder. + +The facade routes are intended to be similar to the [Iron Fish RPC routes](https://github.com/iron-fish/ironfish/tree/master/ironfish/src/rpc/routes). Our goal is that in the future, we could implement a facade that connects directly to an Iron Fish node with minimal changes to the frontend code. \ No newline at end of file diff --git a/packages/mobile-app/data/facades/accounts/demoHandlers.ts b/packages/mobile-app/data/facades/accounts/demoHandlers.ts new file mode 100644 index 0000000..e9808f6 --- /dev/null +++ b/packages/mobile-app/data/facades/accounts/demoHandlers.ts @@ -0,0 +1,38 @@ +import { f } from "data-facade"; +import { z } from "zod"; +import { AccountsMethods } from "./types"; + +let ACCOUNTS = [ + { id: 0, name: "alice", viewOnlyAccount: "alice" }, + { id: 1, name: "bob", viewOnlyAccount: "bob"}, + { id: 2, name: "carol", viewOnlyAccount: "carol"} +]; + +async function getAccounts(limit: number) { + await new Promise((resolve) => setTimeout(resolve, 1000)); + return ACCOUNTS.slice(0, limit); +} + +export const accountsHandlers = f.facade({ + getAccounts: f.handler.query(async () => { + const accounts = await getAccounts(ACCOUNTS.length); + console.log("getAccounts", accounts); + return accounts; + }), + createAccount: f.handler + .input( + z.object({ + name: z.string(), + }), + ) + .mutation(async ({ name }) => { + const existingId = ACCOUNTS.at(-1)?.id + if (existingId === undefined) { + throw new Error("No accounts found"); + } + const account = { id: existingId + 1, name, viewOnlyAccount: name } + console.log("createAccount", account); + ACCOUNTS.push(account); + return account; + }), +}); diff --git a/packages/mobile-app/data/facades/accounts/handlers.ts b/packages/mobile-app/data/facades/accounts/handlers.ts new file mode 100644 index 0000000..80d3bd2 --- /dev/null +++ b/packages/mobile-app/data/facades/accounts/handlers.ts @@ -0,0 +1,20 @@ +import { f } from "data-facade"; +import { z } from "zod"; +import { AccountsMethods } from "./types"; +import { wallet } from "../../wallet"; + +export const accountsHandlers = f.facade({ + getAccounts: f.handler.query(async () => { + return await wallet.getAccounts(); + }), + createAccount: f.handler + .input( + z.object({ + name: z.string(), + }), + ) + .mutation(async ({ name }) => { + const account = await wallet.createAccount(name); + return account; + }), +}); diff --git a/packages/mobile-app/data/facades/accounts/types.ts b/packages/mobile-app/data/facades/accounts/types.ts new file mode 100644 index 0000000..54776ef --- /dev/null +++ b/packages/mobile-app/data/facades/accounts/types.ts @@ -0,0 +1,12 @@ +import { Query, Mutation } from "data-facade"; + +export type Account = { + id: number; + name: string; + viewOnlyAccount: string; +} + +export type AccountsMethods = { + getAccounts: Query<() => Account[]>; + createAccount: Mutation<(args: { name: string }) => Account>; +}; diff --git a/packages/mobile-app/data/facades/app/demoHandlers.ts b/packages/mobile-app/data/facades/app/demoHandlers.ts new file mode 100644 index 0000000..ea3e840 --- /dev/null +++ b/packages/mobile-app/data/facades/app/demoHandlers.ts @@ -0,0 +1,9 @@ +import { f } from "data-facade"; +import { AppMethods } from "./types"; + +export const appHandlers = f.facade({ + loadDatabases: f.handler.mutation(async () => { + console.log("loadDatabases"); + return 'loaded' + }), +}); diff --git a/packages/mobile-app/data/facades/app/handlers.ts b/packages/mobile-app/data/facades/app/handlers.ts new file mode 100644 index 0000000..423dde7 --- /dev/null +++ b/packages/mobile-app/data/facades/app/handlers.ts @@ -0,0 +1,13 @@ +import { f } from "data-facade"; +import { AppMethods } from "./types"; + +import { wallet } from "../../wallet"; + +export const appHandlers = f.facade({ + loadDatabases: f.handler.mutation(async () => { + if (wallet.state.type !== 'STARTED') { + await wallet.start(); + } + return 'loaded' + }), +}); diff --git a/packages/mobile-app/data/facades/app/types.ts b/packages/mobile-app/data/facades/app/types.ts new file mode 100644 index 0000000..46dda1f --- /dev/null +++ b/packages/mobile-app/data/facades/app/types.ts @@ -0,0 +1,5 @@ +import { Query, Mutation } from "data-facade"; + +export type AppMethods = { + loadDatabases: Mutation<() => string>; +}; diff --git a/packages/mobile-app/data/facades/index.ts b/packages/mobile-app/data/facades/index.ts new file mode 100644 index 0000000..a740cd8 --- /dev/null +++ b/packages/mobile-app/data/facades/index.ts @@ -0,0 +1,18 @@ +import { accountsHandlers as accountsDemoHandlers } from "./accounts/demoHandlers"; +import { accountsHandlers } from "./accounts/handlers"; +import { appHandlers as appDemoHandlers } from "./app/demoHandlers"; +import { appHandlers } from "./app/handlers"; +import { createFacadeContext } from "data-facade"; + +const DEMO = true; + +export const facadeContext = createFacadeContext(DEMO ? { + ...accountsDemoHandlers, + ...appDemoHandlers, +} : { + ...accountsHandlers, + ...appHandlers, +}); + +export const FacadeProvider = facadeContext.Provider; +export const useFacade = facadeContext.useFacade; diff --git a/packages/mobile-app/data/index.ts b/packages/mobile-app/data/index.ts deleted file mode 100644 index 34d0689..0000000 --- a/packages/mobile-app/data/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { createFacadeContext } from "data-facade"; -import { accountsHandlers } from "./accounts/handlers"; - -const facadeContext = createFacadeContext(accountsHandlers); - -export const FacadeProvider = facadeContext.Provider; -export const useFacade = facadeContext.useFacade; diff --git a/packages/mobile-app/data/wallet/db.ts b/packages/mobile-app/data/wallet/db.ts new file mode 100644 index 0000000..49e772c --- /dev/null +++ b/packages/mobile-app/data/wallet/db.ts @@ -0,0 +1,70 @@ +import { Kysely, Generated, Migrator } from "kysely"; +import { ExpoDialect, ExpoMigrationProvider, SQLiteType } from "kysely-expo"; + +interface AccountsTable { + id: Generated; + name: string; + viewOnlyAccount: string; +} + +interface Database { + accounts: AccountsTable; +} + +export class WalletDb { + db: Kysely; + + constructor(db: Kysely) { + this.db = db + } + + static async init() { + const db = new Kysely({ + dialect: new ExpoDialect({ + database: "wallet.db", + }), + }) + + const migrator = new Migrator({ + db: db, + provider: new ExpoMigrationProvider({ + migrations: { + "createAccounts": { + up: async (db: Kysely) => { + console.log("running createAccounts migration"); + await db.schema + .createTable("accounts") + .addColumn("id", "integer", (col) => + col.primaryKey().autoIncrement() + ) + .addColumn("name", SQLiteType.String, (col) => col.notNull()) + .addColumn("viewOnlyAccount", SQLiteType.String, (col) => col.notNull()) + .execute(); + }, + }, + }, + }), + }); + + await migrator.migrateToLatest(); + + return new WalletDb(db); + } + + async createAccount(name: string, viewOnlyAccount: string) { + const result = await this.db.insertInto("accounts").values({ + name: name, + viewOnlyAccount: viewOnlyAccount, + }).executeTakeFirst(); + + return { + id: Number(result.insertId), + name: name, + viewOnlyAccount: viewOnlyAccount, + }; + } + + async getAccounts() { + return await this.db.selectFrom("accounts").selectAll('accounts').execute(); + } +} diff --git a/packages/mobile-app/data/wallet/index.ts b/packages/mobile-app/data/wallet/index.ts new file mode 100644 index 0000000..d6fb071 --- /dev/null +++ b/packages/mobile-app/data/wallet/index.ts @@ -0,0 +1,56 @@ +import { generateKey } from "ironfish-native-module"; +import { WalletDb } from "./db"; +import { AccountFormat, encodeAccount } from "@ironfish/sdk"; + +class Wallet { + state: { type: 'STOPPED' } | { type: 'LOADING' } | { type: 'STARTED', db: WalletDb } = { type: 'STOPPED' }; + + async start() { + if (this.state.type !== 'STOPPED') { + throw new Error('Wallet is not stopped'); + } + + this.state = { type: 'LOADING' }; + + const db = await WalletDb.init(); + + this.state = { type: 'STARTED', db }; + } + + async stop() { + this.state = { type: 'STOPPED' }; + } + + async createAccount(name: string) { + if (this.state.type !== 'STARTED') { + throw new Error('Wallet is not started'); + } + + const key = generateKey(); + + const viewOnlyAccount = encodeAccount({ + // TODO: support account birthdays on new accounts + createdAt: null, + spendingKey: null, + incomingViewKey: key.incomingViewKey, + outgoingViewKey: key.outgoingViewKey, + proofAuthorizingKey: key.proofAuthorizingKey, + publicAddress: key.publicAddress, + version: 4, + viewKey: key.viewKey, + name, + }, AccountFormat.Base64Json) + + return await this.state.db.createAccount(name, viewOnlyAccount) + } + + async getAccounts() { + if (this.state.type !== 'STARTED') { + throw new Error('Wallet is not started'); + } + + return this.state.db.getAccounts(); + } +} + +export const wallet = new Wallet(); \ No newline at end of file diff --git a/packages/mobile-app/index.js b/packages/mobile-app/index.js index 5fd059f..8f88382 100644 --- a/packages/mobile-app/index.js +++ b/packages/mobile-app/index.js @@ -1,4 +1,5 @@ -import { registerRootComponent } from 'expo'; -import App from './App'; +import 'expo-router/entry' +// import { registerRootComponent } from 'expo'; +// import App from './App'; -registerRootComponent(App); +// registerRootComponent(App); diff --git a/packages/mobile-app/package.json b/packages/mobile-app/package.json index 1b88cac..79d1537 100644 --- a/packages/mobile-app/package.json +++ b/packages/mobile-app/package.json @@ -1,7 +1,7 @@ { "name": "mobile-app", "version": "0.0.1", - "main": "expo-router/entry", + "main": "index.js", "scripts": { "start": "expo start", "android": "expo run:android", @@ -16,8 +16,11 @@ "expo-constants": "~15.4.5", "expo-linking": "~6.2.2", "expo-router": "~3.4.8", + "expo-sqlite": "~13.4.0", "expo-status-bar": "~1.11.1", "ironfish-native-module": "*", + "kysely": "^0.27.3", + "kysely-expo": "^1.0.0-beta.2", "process": "^0.11.10", "react": "18.2.0", "react-native": "0.73.6", From 63510313ddcaeabdfadd1a805d77922dde4530cb Mon Sep 17 00:00:00 2001 From: Derek Guenther Date: Wed, 24 Apr 2024 15:03:04 -0400 Subject: [PATCH 2/3] 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 3/3] 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,