diff --git a/components/Balance.tsx b/components/Balance.tsx index 5ec3ba0..aac0a42 100644 --- a/components/Balance.tsx +++ b/components/Balance.tsx @@ -1,12 +1,11 @@ import clsx from 'clsx' import { tools } from 'nanocurrency-web' import { FC } from 'react' -import useSWR from 'swr' -import { useAccount } from '../lib/context/accountContext' +import { useCurrentAccount } from '../lib/context/accountContext' import { usePreferences } from '../lib/context/preferencesContext' import { ShowCurrencyPreference } from '../lib/db/types' -import fetcher from '../lib/fetcher' +import useXnoPrice from '../lib/hooks/useXnoPrice' export interface Props { className?: string @@ -20,13 +19,9 @@ const nextShowCurrency = (curr: ShowCurrencyPreference | undefined) => : ShowCurrencyPreference.Both const Balance: FC = ({ className }) => { - const { data: xnoPrice } = useSWR<{ - price: number - }>('https://nano.to/price?json=true', { - fetcher, - }) + const { xnoPrice } = useXnoPrice() - const account = useAccount() + const account = useCurrentAccount() const { preferences: { showCurrencyDash }, @@ -65,7 +60,7 @@ const Balance: FC = ({ className }) => { {showFiatBalance && (

- $ {(Number(xnoBalance) * xnoPrice.price).toFixed(2)} + $ {(Number(xnoBalance) * xnoPrice).toFixed(2)}

)} diff --git a/components/Layout.tsx b/components/Layout.tsx index 4cc79e8..df9278b 100644 --- a/components/Layout.tsx +++ b/components/Layout.tsx @@ -5,6 +5,8 @@ import { FC } from 'react' import { usePreferences } from '../lib/context/preferencesContext' import useListenToColorMedia from '../lib/hooks/useListenToColorMedia' +import useProtectedRoutes from '../lib/hooks/useProtectedRoutes' +import useSetupDb from '../lib/hooks/useSetupDb' import Balance from './Balance' import BottomMenu from './BottomMenu' import PreferencesMenu from './PreferencesMenu' diff --git a/components/RecentTransactions.tsx b/components/RecentTransactions.tsx index f99d7ce..9ee7bca 100644 --- a/components/RecentTransactions.tsx +++ b/components/RecentTransactions.tsx @@ -2,8 +2,6 @@ import { DownloadIcon, UploadIcon } from '@heroicons/react/solid' import clsx from 'clsx' import { tools } from 'nanocurrency-web' import { FC, useCallback, useMemo } from 'react' -import useSwr from 'swr' -import useSwrInfinite from 'swr/infinite' import { useAccount } from '../lib/context/accountContext' import fetcher from '../lib/fetcher' @@ -32,125 +30,25 @@ const mockAddressBook: Record = { const RecentTransactions: FC = ({ className }) => { const account = useAccount() - const params = useCallback( - (cursor: string | undefined) => ({ - method: 'POST', - headers: [['Content-Type', 'application/json']], - body: JSON.stringify({ - action: 'account_history', - account: account?.address, - count: 20, - head: cursor, - }), - }), - [account] - ) - - const { data: historyPages, setSize } = - useSwrInfinite( - (_, prevHistory) => - account === undefined - ? null - : prevHistory === null - ? 'no-cursor' - : prevHistory.previous ?? null, - (cursor: 'no-cursor' | string) => - fetcher( - 'https://mynano.ninja/api/node', - params(cursor === 'no-cursor' ? undefined : cursor) - ) - ) - - const paramsPending = useMemo( - () => ({ - action: 'accounts_pending', - accounts: [account?.address], - count: '20', - }), - [account] - ) - const { data: pendingTxnHashes } = useSwr( - account !== undefined ? account.address : null, - () => - fetcher('https://mynano.ninja/api/node', { - method: 'POST', - headers: [['Content-Type', 'application/json']], - body: JSON.stringify(paramsPending), - }) - ) - const formattedPendingHashes = useMemo( - () => - Object.values(pendingTxnHashes?.blocks ?? {}).flatMap(hashes => hashes), - [pendingTxnHashes] - ) - - const paramsPendingInfo = useCallback( - (hashes: string[]) => ({ - action: 'blocks_info', - json_block: 'true', - hashes, - }), - [] - ) - const { data: pendingTxns } = useSwr( - [formattedPendingHashes.length > 0 ? formattedPendingHashes : null], - hashes => - fetcher('https://mynano.ninja/api/node', { - method: 'POST', - headers: [['Content-Type', 'application/json']], - body: JSON.stringify(paramsPendingInfo(hashes)), - }) - ) - - const hasMoreTxns = historyPages?.at(-1)?.previous !== undefined - - const txns = useMemo( - () => - historyPages?.flatMap(({ history }) => - (history !== '' ? history ?? [] : []).map(txn => { - return { - send: txn.type === 'send', - account: txn.account, - hash: txn.hash, - amount: txn.amount, - timestamp: Number(txn.local_timestamp), - receivable: false, - } - }) - ) ?? [], - [historyPages] - ) - - const mappedPendingTxns = useMemo( - () => - (pendingTxns === undefined - ? [] - : Object.entries(pendingTxns.blocks ?? []) - ).map(([hash, block]) => ({ - send: block.subtype !== 'send', - account: block.block_account, - hash, - amount: block.amount, - timestamp: Number(block.local_timestamp), - receivable: true, - })), - [pendingTxns] - ) const receiveNano = useReceiveNano() - if (historyPages === undefined || account === undefined) return null - - const hasPendingTxns = (mappedPendingTxns ?? []).length > 0 - const hasTxns = (txns ?? []).length > 0 - return (
- {hasPendingTxns && ( + {false && (

pending

    - {mappedPendingTxns.map(txn => ( + {[ + { + hash: 'string', + send: 'string', + receivable: true, + amount: '0', + account: '', + timestamp: '', + }, + ].map(txn => (
  1. = ({ className }) => { day: '2-digit', month: '2-digit', year: '2-digit', - }).format(txn.timestamp * 1000)}{' '} + }).format(Number(txn.timestamp) * 1000)}{' '} -{' '} {mockAddressBook[txn.account]?.displayName ?? ( {txn.account} @@ -206,14 +104,23 @@ const RecentTransactions: FC = ({ className }) => {
)} - {hasPendingTxns && hasTxns &&
} - {hasTxns && ( + {false && false &&
} + {false && (

recent transactions

    - {txns.map(txn => ( + {[ + { + hash: 'string', + send: 'string', + receivable: true, + amount: '0', + account: '', + timestamp: '', + }, + ].map(txn => (
  1. = ({ className }) => { day: '2-digit', month: '2-digit', year: '2-digit', - }).format(txn.timestamp * 1000)}{' '} + }).format(Number(txn.timestamp) * 1000)}{' '} -{' '} {mockAddressBook[txn.account]?.displayName ?? ( {txn.account} @@ -266,7 +173,7 @@ const RecentTransactions: FC = ({ className }) => {
)} - {!hasPendingTxns && !hasTxns && ( + {!false && !false && (

no transactions yet...

@@ -276,10 +183,10 @@ const RecentTransactions: FC = ({ className }) => {

)} - {hasMoreTxns && ( + {false && ( diff --git a/lib/computeWorkAsync.ts b/lib/computeWorkAsync.ts index 0dcfcc9..edd9d8e 100644 --- a/lib/computeWorkAsync.ts +++ b/lib/computeWorkAsync.ts @@ -1,4 +1,7 @@ -const computeWorkAsync = (frontier: string, workerCount = 4) => { +const computeWorkAsync = ( + frontier: string, + { send, workerCount = 4 }: { send: boolean; workerCount?: number } +) => { const workers: Worker[] = [] const cleanup = () => { @@ -7,18 +10,24 @@ const computeWorkAsync = (frontier: string, workerCount = 4) => { } const abortController = new AbortController() - const onlineWorkPromise = fetch(`/api/computeWork?frontier=${frontier}`, { - signal: abortController.signal, - }).then(async res => { - if (res.status !== 200) throw new Error() - else { - const { work } = await res.json() - return work as string - } - }) + const onlineWorkPromise = + process.env.NODE_ENV === 'production' + ? fetch(`/api/computeWork?frontier=${frontier}`, { + signal: abortController.signal, + }).then(async res => { + if (res.status !== 200) throw new Error() + else { + const { work } = await res.json() + return work as string + } + }) + : Promise.reject( + 'not in production, so not generating work on the server' + ) const offlineWorkPromise = new Promise((res, rej) => { - const maxWorkers = navigator.hardwareConcurrency ?? workerCount + const maxWorkers = + Math.floor(navigator.hardwareConcurrency / 4) ?? workerCount const createWorker = (id: number) => { const worker = new Worker(new URL('./workComputer.ts', import.meta.url)) @@ -32,7 +41,7 @@ const computeWorkAsync = (frontier: string, workerCount = 4) => { rej(work) } - worker.postMessage({ frontier, id }) + worker.postMessage({ frontier, id, send }) return worker } diff --git a/lib/context/accountContext.tsx b/lib/context/accountContext.tsx index 009fc6f..f955b1c 100644 --- a/lib/context/accountContext.tsx +++ b/lib/context/accountContext.tsx @@ -7,17 +7,35 @@ import { useState, } from 'react' +import computeWorkAsync from '../computeWorkAsync' +import { addPrecomputedWork, getAllAccounts, putAccount } from '../db/accounts' import { AccountInfoCache } from '../types' +import fetchAccountInfo from '../xno/fetchAccountInfo' export interface AccountContextValue { currAccount: AccountInfoCache | undefined - accounts: { [key: number]: AccountInfoCache } | undefined + accounts: { [index: number]: AccountInfoCache } | undefined setAccount: (info: AccountInfoCache) => void removeAccount: (index: number) => void + setCurrAccountIndex: (index: number) => void } const accountContext = createContext(undefined) +const refreshAccountFromNetwork = async (account: AccountInfoCache) => { + const infoResponse = await fetchAccountInfo(account.address) + + const freshAccountInfo = { + ...account, + frontier: 'error' in infoResponse ? null : infoResponse.confirmed_frontier, + representative: + 'error' in infoResponse ? null : infoResponse.confirmed_representative, + balance: 'error' in infoResponse ? null : infoResponse.confirmed_balance, + } + putAccount(freshAccountInfo) + return freshAccountInfo +} + export const useAccounts = () => { const contextValue = useContext(accountContext) if (contextValue === undefined) @@ -36,43 +54,66 @@ export const useAccount = (index?: number) => { : contextValue.currAccount } -export const AccountProvider: FC<{ - initialAccounts?: { [key: number]: AccountInfoCache } | undefined - initialAccountIndex?: number -}> = ({ children, initialAccounts, initialAccountIndex }) => { - const [accounts, setAccounts] = useState< - { [key: number]: AccountInfoCache } | undefined - >(initialAccounts) - useEffect(() => { - setAccounts(initialAccounts) - }, [initialAccounts]) +export const useCurrentAccount = () => useAccount() - const [currAccountIndex, setCurrAccountIndex] = useState( - initialAccountIndex ?? 0 +export const AccountProvider: FC = ({ children }) => { + const [accounts, setAccounts] = useState<{ [key: number]: AccountInfoCache }>( + {} ) - useEffect(() => { - setCurrAccountIndex(initialAccountIndex ?? 0) - }, [initialAccountIndex]) const setAccount = useCallback((account: AccountInfoCache) => { setAccounts(prev => ({ ...prev, [account.index]: account })) + // todo handle error + putAccount(account) }, []) + useEffect(() => { + const refreshAccountsFromNetwork = async (accounts: AccountInfoCache[]) => + accounts.forEach(async account => { + const freshAccount = await refreshAccountFromNetwork(account) + setAccount(freshAccount) + }) + const getAccountsFromIdb = async () => { + const accountList = await getAllAccounts() + const accounts: AccountContextValue['accounts'] = {} + for (const account of accountList) { + accounts[account.index] = account + if (account.precomputedWork === null) { + computeWorkAsync(account.frontier ?? account.address, { + send: account.frontier !== null, + }).then(work => { + if (work !== null) { + setAccounts(prev => ({ + ...prev, + [account.index]: { + ...prev[account.index], + precomputedWork: work, + }, + })) + addPrecomputedWork(account.address, work) + } + }) + } + } + setAccounts(accounts) + refreshAccountsFromNetwork(accountList) + } + getAccountsFromIdb() + }, [setAccount]) + + const [currAccountIndex, setCurrAccountIndex] = useState(0) + const currAccount = accounts?.[currAccountIndex] + const removeAccount = useCallback((index: number) => { setAccounts(prev => { const next = { ...prev } delete next[index] return next }) + // todo handle error + removeAccount(index) }, []) - const currAccount = accounts?.[currAccountIndex] - - useEffect(() => { - if (currAccount !== undefined) { - } - }, [currAccount]) - return ( {children} diff --git a/lib/context/memCacheContextProvider.tsx b/lib/context/memCacheContextProvider.tsx new file mode 100644 index 0000000..6822ef6 --- /dev/null +++ b/lib/context/memCacheContextProvider.tsx @@ -0,0 +1,17 @@ +import type { FC } from 'react' + +import useSetupChallenge from '../hooks/useSetupChallenge' +import { AccountProvider } from './accountContext' +import { PreferencesProvider } from './preferencesContext' + +const MemCacheProvider: FC = ({ children }) => { + useSetupChallenge() + + return ( + + {children} + + ) +} + +export default MemCacheProvider diff --git a/lib/context/preferencesContext.tsx b/lib/context/preferencesContext.tsx index dbf2703..5e15879 100644 --- a/lib/context/preferencesContext.tsx +++ b/lib/context/preferencesContext.tsx @@ -7,25 +7,21 @@ import { useState, } from 'react' -import { getPreference, putPreference } from '../db/preferences' -import { - PreferenceName, - PreferenceTypes, - ShowCurrencyPreference, -} from '../db/types' +import { getAllPreferences, putPreference } from '../db/preferences' +import { PreferenceName, PreferenceTypes } from '../db/types' import useDarkMode from '../hooks/useDarkMode' -import isiOS from '../isiOS' -const preferencesContext = createContext< - | { - preferences: PreferenceTypes - setPreference:

( - preference: P, - value: PreferenceTypes[P] - ) => void - } - | undefined ->(undefined) +export interface PreferenceContextValue { + preferences: PreferenceTypes + setPreference:

( + preference: P, + value: PreferenceTypes[P] + ) => void +} + +const preferencesContext = createContext( + undefined +) export const usePreferences = () => { const preferences = useContext(preferencesContext) @@ -36,32 +32,27 @@ export const usePreferences = () => { return preferences } +const initialState: PreferenceContextValue['preferences'] = { + darkMode: undefined, + biometricsAuth: undefined, + leftHanded: undefined, + showCurrencyDash: undefined, +} + export const PreferencesProvider: FC = ({ children }) => { - const [preferences, setPreferences] = useState({ - darkMode: undefined, - biometricsAuth: undefined, - leftHanded: undefined, - showCurrencyDash: undefined, - }) + const [preferences, setPreferences] = useState(initialState) useEffect(() => { - const setPrefs = async () => { - const [darkMode, biometricsAuth, leftHanded, showCurrencyDash] = - await Promise.all([ - getPreference('darkMode'), - getPreference('biometricsAuth'), - getPreference('leftHanded'), - getPreference('showCurrencyDash'), - ]) - setPreferences({ - darkMode: darkMode ?? true, - biometricsAuth: biometricsAuth ?? !isiOS(), - leftHanded: leftHanded ?? false, - showCurrencyDash: showCurrencyDash ?? ShowCurrencyPreference.Xno, - }) + const fetchPreferencesFromIdb = async () => { + const preferenceList = await getAllPreferences() + const preferences: PreferenceContextValue['preferences'] = initialState + for (const preference of preferenceList) { + // @ts-expect-error i should type this better but should be fine for now + preferences[preference.name] = preference.value + } + setPreferences(preferences) } - setPrefs() + fetchPreferencesFromIdb() }, []) - useDarkMode(preferences.darkMode) const setPreference = useCallback(

(name: P, value: PreferenceTypes[P]) => { setPreferences(prev => ({ ...prev, [name]: value })) diff --git a/lib/db/accounts.ts b/lib/db/accounts.ts index 4114155..031f3ac 100644 --- a/lib/db/accounts.ts +++ b/lib/db/accounts.ts @@ -1,65 +1,49 @@ import db from '.' -import computeWorkAsync from '../computeWorkAsync' import { AccountInfoCache } from '../types' -export const addAccount = async (index: number, account: AccountInfoCache) => { - db.accounts.add({ +export const addAccount = async (index: number, account: AccountInfoCache) => + db()!.add('accounts', { ...account, index, precomputedWork: null, }) - const precomputedWork = await computeWorkAsync( - account.frontier ?? account.publicKey - ) - const hasWork = precomputedWork !== null - if (hasWork) - db.accounts.where({ index }).modify(account => { - if (account !== undefined) account.precomputedWork = precomputedWork - }) -} -export const putAccount = (index: number, account: AccountInfoCache) => - db.accounts.update(index, { ...account }) +export const putAccount = (account: AccountInfoCache) => + db()!.put('accounts', account) -export const removeAccount = (index: number) => db.accounts.delete(index) +export const removeAccount = (index: number) => db()!.delete('accounts', index) -export const getAccount = (index: number) => - db.accounts - .where({ index }) - .first() - .then(res => (res === undefined ? undefined : res)) +export const getAccount = (index: number) => db()!.get('accounts', index) + +export const getAllAccounts = () => db()!.getAll('accounts') export const hasAccount = async (index: number) => - (await db.accounts.where({ index }).count()) > 0 + db()! + .count('accounts', index) + .then(count => count === 1) -export const addPrecomputedWork = async ( - address: string, - work?: string | null -) => - db.accounts.where({ address }).modify(async account => { - if (work !== undefined && work !== null) account.precomputedWork = work - else { - const precomputedWork = await computeWorkAsync( - account.frontier ?? account.publicKey - ) - if (precomputedWork !== null) account.precomputedWork = precomputedWork - } - }) +export const addPrecomputedWork = async (address: string, work: string) => { + const tx = db()!.transaction('accounts', 'readwrite') + const account = await tx.store.index('address').get(address) + if (account !== undefined && work !== account.precomputedWork) { + tx.store.put({ ...account, precomputedWork: work }) + } + return tx.done +} export const getPrecomputedWork = async (address: string) => - db.accounts - .where({ address }) - .first() - .then(account => account?.precomputedWork) + db()! + .getFromIndex('accounts', 'address', address) + .then(account => { + if (account === undefined) throw new Error('not_found') + return account.precomputedWork + }) export const consumePrecomputedWork = async (address: string) => { - const account = await db.accounts.where({ address }).first() - if (account === undefined) return undefined - db.accounts.where({ address }).modify(account => { - account.precomputedWork = null - }) - const precomputedWork = await computeWorkAsync( - account.frontier ?? account.publicKey - ) - addPrecomputedWork(address, precomputedWork) + const tx = db()!.transaction('accounts', 'readwrite') + const account = await tx.store.index('address').get(address) + if (account !== undefined && account.precomputedWork !== null) { + tx.store.put({ ...account, precomputedWork: null }) + } + return tx.done } diff --git a/lib/db/cryptoAssets.ts b/lib/db/cryptoAssets.ts index 10cf67d..6d1b008 100644 --- a/lib/db/cryptoAssets.ts +++ b/lib/db/cryptoAssets.ts @@ -2,16 +2,15 @@ import db from '.' import { CryptoAssetId } from './types' export const addCryptoAsset = (id: CryptoAssetId, cryptoAsset: Uint8Array) => - db.cryptoAssets.add({ id, cryptoAsset }) + db()!.add('cryptoAssets', { id, cryptoAsset }) export const removeCryptoAsset = (id: CryptoAssetId) => - db.cryptoAssets.delete(id) + db()!.delete('cryptoAssets', id) export const getCryptoAsset = (id: CryptoAssetId) => - db.cryptoAssets - .where({ id }) - .first() - .then(res => res?.cryptoAsset) + db()!.get('cryptoAssets', id) export const hasCryptoAsset = async (id: CryptoAssetId) => - (await db.cryptoAssets.where({ id }).count()) > 0 + db()! + .count('cryptoAssets', id) + .then(count => count === 1) diff --git a/lib/db/database.ts b/lib/db/database.ts deleted file mode 100644 index 55eb355..0000000 --- a/lib/db/database.ts +++ /dev/null @@ -1,37 +0,0 @@ -import Dexie, { Table } from 'dexie' - -import type { - AccountsKey, - AccountsValue, - CryptoAssetKey, - CryptoAssetValue, - EncryptedSeedKey, - EncryptedSeedValue, - PreferenceKey, - PreferenceValue, -} from './types' -import { - accountsSchema, - cryptoAssetSchema, - encryptedSeedSchema, - preferenceSchema, -} from './types' - -class Database extends Dexie { - public encryptedSeeds!: Table - public cryptoAssets!: Table - public accounts!: Table - public preferences!: Table - - public constructor() { - super('Database') - this.version(1).stores({ - encryptedSeeds: encryptedSeedSchema, - cryptoAssets: cryptoAssetSchema, - preferences: preferenceSchema, - accounts: accountsSchema, - }) - } -} - -export default Database diff --git a/lib/db/encryptedSeeds.ts b/lib/db/encryptedSeeds.ts index c49207c..a746e63 100644 --- a/lib/db/encryptedSeeds.ts +++ b/lib/db/encryptedSeeds.ts @@ -2,16 +2,15 @@ import db from '.' import { EncryptedSeedId } from './types' export const addEncryptedSeed = (id: EncryptedSeedId, encryptedSeed: string) => - db.encryptedSeeds.add({ id, encryptedSeed }) + db()!.add('encryptedSeed', { id, encryptedSeed }) export const removeEncryptedSeed = (id: EncryptedSeedId) => - db.encryptedSeeds.delete(id) + db()!.delete('encryptedSeed', id) export const getEncryptedSeed = (id: EncryptedSeedId) => - db.encryptedSeeds - .where({ id }) - .first() - .then(res => res?.encryptedSeed) + db()!.get('encryptedSeed', id) export const hasEncryptedSeed = async (id: EncryptedSeedId) => - (await db.encryptedSeeds.where({ id }).count()) > 0 + db()! + .count('encryptedSeed', id) + .then(count => count === 1) diff --git a/lib/db/index.ts b/lib/db/index.ts index faa3f2d..0765fb4 100644 --- a/lib/db/index.ts +++ b/lib/db/index.ts @@ -1,5 +1,55 @@ -import Database from './database' +import { DBSchema, IDBPDatabase, openDB } from 'idb' -const db = new Database() +import { consumePrecomputedWork, getAllAccounts } from './accounts' +import type { + AccountsKey, + AccountsValue, + CryptoAssetKey, + CryptoAssetValue, + EncryptedSeedKey, + EncryptedSeedValue, + PreferenceKey, + PreferenceValue, +} from './types' -export default db +interface Schema extends DBSchema { + preferences: { + key: PreferenceKey + value: PreferenceValue + } + cryptoAssets: { + key: CryptoAssetKey + value: CryptoAssetValue + } + accounts: { + key: AccountsKey + value: AccountsValue + indexes: { + address: string + } + } + encryptedSeed: { + key: EncryptedSeedKey + value: EncryptedSeedValue + } +} + +let dbConnection: IDBPDatabase | undefined = undefined + +export const openDb = async (version = 1) => { + if (dbConnection !== undefined) return + else { + dbConnection = await openDB('Database', version, { + upgrade: db => { + db.createObjectStore('accounts').createIndex('address', 'address') + db.createObjectStore('cryptoAssets') + db.createObjectStore('encryptedSeed') + db.createObjectStore('preferences') + }, + }) + } +} + +const getDb = () => dbConnection + +export default getDb diff --git a/lib/db/preferences.ts b/lib/db/preferences.ts index cd5d63c..7170281 100644 --- a/lib/db/preferences.ts +++ b/lib/db/preferences.ts @@ -1,28 +1,39 @@ import db from '.' -import { PreferenceName, PreferenceTypes } from './types' +import { PreferenceName, PreferenceTypes, PreferenceValue } from './types' export const addPreference =

( name: P, value: PreferenceTypes[P] -) => db.preferences.add({ name, value: JSON.stringify(value) }) +) => db()!.add('preferences', { name, value: JSON.stringify(value) }) export const putPreference =

( name: P, value: PreferenceTypes[P] -) => db.preferences.put({ name, value: JSON.stringify(value) }) +) => db()!.put('preferences', { name, value: JSON.stringify(value) }) export const removePreference = (name: PreferenceName) => - db.preferences.delete(name) + db()!.delete('preferences', name) -export const getPreference =

( - name: P -): Promise => - db.preferences - .where({ name }) - .first() +export const getPreference =

(name: P) => + db()! + .get('preferences', name) .then(pref => - pref?.value === undefined ? undefined : JSON.parse(pref.value) + pref?.value === undefined + ? undefined + : (JSON.parse(pref.value) as PreferenceTypes[P]) + ) + +export const getAllPreferences = () => + db()! + .getAll('preferences') + .then(preferenceList => + preferenceList.map(({ name, value }) => ({ + name, + value: JSON.parse(value) as PreferenceTypes[typeof name], + })) ) export const hasPreference = async (name: PreferenceName) => - (await db.preferences.where({ name }).count()) > 0 + db()! + .count('preferences', name) + .then(count => count === 1) diff --git a/lib/fetcher.ts b/lib/fetcher.ts index 9ef9f73..5fef79d 100644 --- a/lib/fetcher.ts +++ b/lib/fetcher.ts @@ -1,4 +1,7 @@ const fetcher = (...args: Parameters) => - fetch(...args).then(res => res.json() as Promise) + fetch(...args).then(res => { + if (!res.ok) throw new Error() // todo improve this error + return res.json() as Promise + }) export default fetcher diff --git a/lib/hooks/useDrawQrCode.ts b/lib/hooks/useDrawQrCode.ts index eb821ec..ffb96d1 100644 --- a/lib/hooks/useDrawQrCode.ts +++ b/lib/hooks/useDrawQrCode.ts @@ -1,9 +1,9 @@ import qr from 'qrcode' -import { RefObject, useEffect, useRef } from 'react' +import { useEffect, useRef } from 'react' import colors from 'tailwindcss/colors' import { usePreferences } from '../context/preferencesContext' -import genTxnUrl from '../nano/getTxnUrl' +import genTxnUrl from '../xno/getTxnUrl' const useDrawQrCode = ({ raw, address }: { raw?: string; address: string }) => { const canvasRef = useRef(null) diff --git a/lib/hooks/useProtectedRoutes.ts b/lib/hooks/useProtectedRoutes.ts index 4eccbf7..29e44f7 100644 --- a/lib/hooks/useProtectedRoutes.ts +++ b/lib/hooks/useProtectedRoutes.ts @@ -4,24 +4,26 @@ import { useEffect, useState } from 'react' import { getCryptoAsset } from '../db/cryptoAssets' import useIsWelcoming from './useIsWelcoming' -const useProtectedRoutes = () => { +const useProtectedRoutes = (skip?: boolean) => { const { replace, pathname } = useRouter() const [validatingCredential, setValidatingCredential] = useState(true) const isWelcoming = useIsWelcoming() useEffect(() => { - if (isWelcoming) { - setValidatingCredential(false) - return - } + if (!skip) { + if (isWelcoming) { + setValidatingCredential(false) + return + } - const checkCredential = async () => { - const hasCredentialId = - (await getCryptoAsset('credentialId')) !== undefined - if (!hasCredentialId) replace('/welcome') - else setValidatingCredential(false) + const checkCredential = async () => { + const hasCredentialId = + (await getCryptoAsset('credentialId')) !== undefined + if (!hasCredentialId) replace('/welcome') + else setValidatingCredential(false) + } + checkCredential() } - checkCredential() - }, [replace, pathname, isWelcoming]) + }, [replace, pathname, isWelcoming, skip]) return validatingCredential } diff --git a/lib/hooks/useReadQrFromVideo.ts b/lib/hooks/useReadQrFromVideo.ts index f9de6a3..9205cc6 100644 --- a/lib/hooks/useReadQrFromVideo.ts +++ b/lib/hooks/useReadQrFromVideo.ts @@ -51,7 +51,7 @@ const useReadQrFromVideo = (onQrCodeRead: (content: string) => void) => { stream?.getTracks().forEach(track => track.stop()) stopTick = true } - }, []) + }, [onQrCodeRead]) return { videoRef, videoLive } } diff --git a/lib/hooks/useReceiveNano.ts b/lib/hooks/useReceiveNano.ts index 67ae348..893afdf 100644 --- a/lib/hooks/useReceiveNano.ts +++ b/lib/hooks/useReceiveNano.ts @@ -1,10 +1,12 @@ import { computeWork, hashBlock } from 'nanocurrency' import { useCallback } from 'react' +import computeWorkAsync from '../computeWorkAsync' import { useAccount } from '../context/accountContext' -import { consumeWork } from '../db/accounts' +import { getPrecomputedWork } from '../db/accounts' import fetcher from '../fetcher' -import receiveNano from '../nano/receiveNano' +import { zeroString } from '../xno/constants' +import receiveNano from '../xno/receiveNano' const useReceiveNano = () => { const account = useAccount() @@ -12,32 +14,24 @@ const useReceiveNano = () => { const receive = useCallback( async (hash: string, amount: string) => { if (account === undefined) return - console.log('signing receive') - const signedBlock = await receiveNano( + let precomputedWork = await getPrecomputedWork(account.address) + if (precomputedWork === null) + precomputedWork = await computeWorkAsync( + account.frontier ?? account.address + ) + if (precomputedWork === null) throw new Error('couldnt_compute_work') + await receiveNano( { transactionHash: hash, walletBalanceRaw: account.balance ?? '0', toAddress: account.address, representativeAddress: account.representative ?? account.address, - frontier: - account.frontier ?? - '0000000000000000000000000000000000000000000000000000000000000000', + frontier: account.frontier ?? zeroString, amountRaw: amount, - work: await consumeWork(account.address), + work: precomputedWork, }, account.index ) - console.log('finished signing') - return fetcher('https://mynano.ninja/api/node', { - method: 'POST', - headers: [['Content-Type', 'application/json']], - body: JSON.stringify({ - action: 'process', - json_block: 'true', - subtype: 'receive', - block: signedBlock, - }), - }) }, [account] ) diff --git a/lib/hooks/useSendNano.ts b/lib/hooks/useSendNano.ts index 06ed34f..650c638 100644 --- a/lib/hooks/useSendNano.ts +++ b/lib/hooks/useSendNano.ts @@ -1,8 +1,14 @@ +import { hashBlock } from 'nanocurrency' import { useCallback } from 'react' +import computeWorkAsync from '../computeWorkAsync' import { useAccount } from '../context/accountContext' -import { consumePrecomputedWork, getPrecomputedWork } from '../db/accounts' -import sendNano from '../nano/sendNano' +import { + addPrecomputedWork, + consumePrecomputedWork, + getPrecomputedWork, +} from '../db/accounts' +import sendNano from '../xno/sendNano' const useSendNano = () => { const account = useAccount() @@ -15,7 +21,7 @@ const useSendNano = () => { account.representative === null || account.frontier === null ) - return + throw new Error('wrong_block_data') // todo improve this error const signedBlock = await sendNano( { walletBalanceRaw: account.balance, @@ -28,24 +34,6 @@ const useSendNano = () => { }, account.index ) - return fetch('https://mynano.ninja/api/node', { - method: 'POST', - headers: [['Content-Type', 'application/json']], - body: JSON.stringify({ - action: 'process', - json_block: 'true', - subtype: 'send', - block: signedBlock, - }), - }) - .then(res => { - if (!res.ok) throw new Error() - return res.json() - }) - .then(data => { - if ('error' in data) throw new Error() - consumePrecomputedWork(account.address) - }) }, [account] ) diff --git a/lib/hooks/useSetupDb.ts b/lib/hooks/useSetupDb.ts new file mode 100644 index 0000000..6e345d9 --- /dev/null +++ b/lib/hooks/useSetupDb.ts @@ -0,0 +1,14 @@ +import { useEffect, useState } from 'react' + +import { openDb } from '../db' + +const useSetupDb = (version = 1) => { + const [ready, setReady] = useState(false) + useEffect(() => { + // todo catch error + openDb(version).then(() => setReady(true)) + }, [version]) + return ready +} + +export default useSetupDb diff --git a/lib/hooks/useSetupSeed.ts b/lib/hooks/useSetupSeed.ts index 0f073f9..0d20e37 100644 --- a/lib/hooks/useSetupSeed.ts +++ b/lib/hooks/useSetupSeed.ts @@ -7,7 +7,8 @@ import { useAccounts } from '../context/accountContext' import { addAccount } from '../db/accounts' import { addEncryptedSeed, hasEncryptedSeed } from '../db/encryptedSeeds' import encryptSeed from '../encryptSeed' -import accountAtIndex from '../nano/accountAtIndex' +import { AccountInfoCache } from '../types' +import accountAtIndex from '../xno/accountAtIndex' import useSetup from './useSetup' const useSetupSeed = (skip?: boolean) => { @@ -31,13 +32,14 @@ const useSetupSeed = (skip?: boolean) => { ]) const { address, publicKey } = accountAtIndex(generatedSeed, 0) - const account = { + const account: AccountInfoCache = { frontier: null, representative: address, balance: '0', index: 0, address, publicKey, + precomputedWork: null, } setSeed({ seed: generatedSeed, mnemonic }) setAccount(account) diff --git a/lib/hooks/useXnoPrice.ts b/lib/hooks/useXnoPrice.ts new file mode 100644 index 0000000..fc34452 --- /dev/null +++ b/lib/hooks/useXnoPrice.ts @@ -0,0 +1,17 @@ +import { useEffect, useState } from 'react' + +import fetcher from '../fetcher' +import { XnoPriceResponse } from '../types' + +const useXnoPrice = () => { + const [xnoPrice, setXnoPrice] = useState() + useEffect(() => { + // todo get url out of here + fetcher('https://nano.to/price?json=true').then(res => + setXnoPrice(res.price) + ) + }, []) + return { xnoPrice, loading: xnoPrice === undefined } +} + +export default useXnoPrice diff --git a/lib/nano/receiveNano.ts b/lib/nano/receiveNano.ts deleted file mode 100644 index 01e9b69..0000000 --- a/lib/nano/receiveNano.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { hashBlock } from 'nanocurrency' -import { block } from 'nanocurrency-web' - -import decryptSeed from '../decryptSeed' -import accountAtIndex from './accountAtIndex' - -const sendNano = async ( - blockData: Parameters[0], - index: number -) => { - const { privateKey } = accountAtIndex( - await decryptSeed('os'), // inline to minimize it's time in memoty (doesn't create a scoped var) - index - ) - - const signedBlock = block.receive(blockData, privateKey) - return signedBlock -} - -export default sendNano diff --git a/lib/nano/sendNano.ts b/lib/nano/sendNano.ts deleted file mode 100644 index 3ed41c4..0000000 --- a/lib/nano/sendNano.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { block } from 'nanocurrency-web' - -import decryptSeed from '../decryptSeed' -import accountAtIndex from './accountAtIndex' - -const sendNano = async ( - blockData: Parameters[0], - index: number -) => { - const { privateKey } = accountAtIndex( - await decryptSeed('os'), // inline to minimize it's time in memoty (doesn't create a scoped var) - index - ) - - const signedBlock = block.send(blockData, privateKey) - return signedBlock -} - -export default sendNano diff --git a/lib/types.ts b/lib/types.ts index 3a69764..57e702b 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -70,4 +70,18 @@ export interface AccountInfoCache { frontier: string | null representative: string | null balance: string | null + precomputedWork: string | null } + +export interface XnoPriceResponse { + symbol: 'XNO' + price: number + currency: 'USD' + timestamp: string +} + +export type ProcessResponse = + | { + hash: string + } + | { error: string } diff --git a/lib/workComputer.ts b/lib/workComputer.ts index 8ca22ce..fac7451 100644 --- a/lib/workComputer.ts +++ b/lib/workComputer.ts @@ -1,8 +1,24 @@ import { computeWork } from 'nanocurrency' +import { receiveDiff, sendDiff } from './xno/constants' + onmessage = async ev => { - console.time(`worker${ev.data.id}`) - const work = await computeWork(ev.data.frontier) - console.timeEnd(`worker${ev.data.id}`) + const { send, id, frontier } = ev.data as { + send: boolean + id: number + frontier: string + } + console.log(`started calculating work`) + console.table({ + workerId: id, + frontier, + send, + difficulty: send ? sendDiff : receiveDiff, + startedAt: new Date(), + }) + const work = await computeWork(frontier, { + workThreshold: send ? sendDiff : receiveDiff, + }) + console.log(`worker ${id} finished computing work: ${work}`) postMessage(work) } diff --git a/lib/nano/accountAtIndex.ts b/lib/xno/accountAtIndex.ts similarity index 100% rename from lib/nano/accountAtIndex.ts rename to lib/xno/accountAtIndex.ts diff --git a/lib/xno/constants.ts b/lib/xno/constants.ts new file mode 100644 index 0000000..218bb2f --- /dev/null +++ b/lib/xno/constants.ts @@ -0,0 +1,6 @@ +export const zeroString = + '0000000000000000000000000000000000000000000000000000000000000000' + +export const receiveDiff = 'fffffe0000000000' + +export const sendDiff = 'fffffff800000000' diff --git a/lib/nano/fetchAccountInfo.ts b/lib/xno/fetchAccountInfo.ts similarity index 100% rename from lib/nano/fetchAccountInfo.ts rename to lib/xno/fetchAccountInfo.ts diff --git a/lib/nano/getTxnUrl.ts b/lib/xno/getTxnUrl.ts similarity index 100% rename from lib/nano/getTxnUrl.ts rename to lib/xno/getTxnUrl.ts diff --git a/lib/nano/isTxnUrl.ts b/lib/xno/isTxnUrl.ts similarity index 100% rename from lib/nano/isTxnUrl.ts rename to lib/xno/isTxnUrl.ts diff --git a/lib/xno/receiveNano.ts b/lib/xno/receiveNano.ts new file mode 100644 index 0000000..39df5de --- /dev/null +++ b/lib/xno/receiveNano.ts @@ -0,0 +1,38 @@ +import { hashBlock } from 'nanocurrency' +import { block } from 'nanocurrency-web' + +import computeWorkAsync from '../computeWorkAsync' +import { addPrecomputedWork, consumePrecomputedWork } from '../db/accounts' +import decryptSeed from '../decryptSeed' +import fetcher from '../fetcher' +import { ProcessResponse } from '../types' +import accountAtIndex from './accountAtIndex' + +const sendNano = async ( + blockData: Parameters[0], + index: number +) => { + const { privateKey } = accountAtIndex( + await decryptSeed('os'), // inline to minimize it's time in memoty (doesn't create a scoped var) + index + ) + + const signedBlock = block.receive(blockData, privateKey) + fetcher('https://mynano.ninja/api/node', { + method: 'POST', + headers: [['Content-Type', 'application/json']], + body: JSON.stringify({ + action: 'process', + json_block: 'true', + subtype: 'receive', + block: signedBlock, + }), + }).then(async data => { + if ('error' in data) throw new Error() + await consumePrecomputedWork(blockData.toAddress) + const work = await computeWorkAsync(hashBlock(signedBlock), { send: false }) + if (work !== null) addPrecomputedWork(blockData.toAddress, work) + }) +} + +export default sendNano diff --git a/lib/xno/sendNano.ts b/lib/xno/sendNano.ts new file mode 100644 index 0000000..e5bd08d --- /dev/null +++ b/lib/xno/sendNano.ts @@ -0,0 +1,38 @@ +import { hashBlock } from 'nanocurrency' +import { block } from 'nanocurrency-web' + +import computeWorkAsync from '../computeWorkAsync' +import { addPrecomputedWork, consumePrecomputedWork } from '../db/accounts' +import decryptSeed from '../decryptSeed' +import fetcher from '../fetcher' +import { ProcessResponse } from '../types' +import accountAtIndex from './accountAtIndex' + +const sendNano = async ( + blockData: Parameters[0], + index: number +) => { + const { privateKey } = accountAtIndex( + await decryptSeed('os'), // inline to minimize it's time in memoty (doesn't create a scoped var) + index + ) + + const signedBlock = block.send(blockData, privateKey) + return fetcher('https://mynano.ninja/api/node', { + method: 'POST', + headers: [['Content-Type', 'application/json']], + body: JSON.stringify({ + action: 'process', + json_block: 'true', + subtype: 'send', + block: signedBlock, + }), + }).then(async data => { + if ('error' in data) throw new Error() + await consumePrecomputedWork(blockData.fromAddress) + const work = await computeWorkAsync(hashBlock(signedBlock), { send: true }) + if (work !== null) addPrecomputedWork(blockData.fromAddress, work) + }) +} + +export default sendNano diff --git a/lib/nano/txnUrlToParts.ts b/lib/xno/txnUrlToParts.ts similarity index 100% rename from lib/nano/txnUrlToParts.ts rename to lib/xno/txnUrlToParts.ts diff --git a/package.json b/package.json index c165f56..23dceb2 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,8 @@ "cbor": "^8.1.0", "clsx": "^1.1.1", "crypto-js": "^4.1.1", - "dexie": "^3.2.0", "dexie-react-hooks": "^1.0.7", + "idb": "^7.0.0", "jsqr": "^1.4.0", "nanocurrency": "^2.5.0", "nanocurrency-web": "^1.3.5", @@ -31,8 +31,7 @@ "prettier": "^2.4.1", "qrcode": "^1.4.4", "react": "^17.0.2", - "react-dom": "^17.0.2", - "swr": "^1.0.1" + "react-dom": "^17.0.2" }, "devDependencies": { "@trivago/prettier-plugin-sort-imports": "^3.1.1", diff --git a/pages/_app.tsx b/pages/_app.tsx index 1afe8cb..3cc296d 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,40 +1,25 @@ import type { AppProps } from 'next/app' -import { FC, useEffect } from 'react' -import { SWRConfig } from 'swr' +import { FC } from 'react' import 'tailwindcss/tailwind.css' import Layout from '../components/Layout' -import { AccountProvider } from '../lib/context/accountContext' -import { PreferencesProvider } from '../lib/context/preferencesContext' -import fetcher from '../lib/fetcher' -import useDarkMode from '../lib/hooks/useDarkMode' +import MemCacheProvider from '../lib/context/memCacheContextProvider' import useProtectedRoutes from '../lib/hooks/useProtectedRoutes' -import useSetupAccounts from '../lib/hooks/useSetupAccounts' -import useSetupChallenge from '../lib/hooks/useSetupChallenge' -import useSetupSw from '../lib/hooks/useSetupSw' +import useSetupDb from '../lib/hooks/useSetupDb' import '../styles/global.css' const MyApp: FC = ({ Component, pageProps }) => { - useSetupChallenge() - useSetupSw() - useDarkMode() - - const { accounts } = useSetupAccounts() - - const validatingCredential = useProtectedRoutes() + const ready = useSetupDb(10) + const validatingCredential = useProtectedRoutes(!ready) if (validatingCredential) return null // todo return ( - new Map() }}> - - - - - - - - + + + + + ) } diff --git a/pages/send/qr.tsx b/pages/send/qr.tsx index 28caf94..032cf66 100644 --- a/pages/send/qr.tsx +++ b/pages/send/qr.tsx @@ -4,8 +4,8 @@ import { useRouter } from 'next/router' import { useCallback } from 'react' import useReadQrFromVideo from '../../lib/hooks/useReadQrFromVideo' -import isTxnUrl from '../../lib/nano/isTxnUrl' -import txnUrlToParts from '../../lib/nano/txnUrlToParts' +import isTxnUrl from '../../lib/xno/isTxnUrl' +import txnUrlToParts from '../../lib/xno/txnUrlToParts' const ReadQrCode: NextPage = () => { const { push } = useRouter() diff --git a/yarn.lock b/yarn.lock index 8960e8a..0734591 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1663,11 +1663,6 @@ depd@~1.1.2: resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= -dequal@2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.2.tgz#85ca22025e3a87e65ef75a7a437b35284a7e319d" - integrity sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug== - des.js@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/des.js/-/des.js-1.0.1.tgz#5382142e1bdc53f85d86d53e5f4aa7deb91e0843" @@ -1690,11 +1685,6 @@ dexie-react-hooks@^1.0.7: resolved "https://registry.yarnpkg.com/dexie-react-hooks/-/dexie-react-hooks-1.0.7.tgz#50316a7829a6dfa013b8471f66b010cc77f00d3e" integrity sha512-hqXGFbfgu1rdfcGHQUPwW2G0iWyupoNWnk3ODvqr+HdZt2ip3y1e/dcWIOsEnlUWWCWk6a3+ok0fvECU05eE2A== -dexie@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/dexie/-/dexie-3.2.0.tgz#a1b0267b111f9422c4126da90d6b121b1deabeab" - integrity sha512-OpS8ss1CLHYAhxRu6hT+/Gt1uLhKCf0O18xHBdRGlemOWXXRiiOZ0ty1/bACIJzGt1DGmvarzrPwYYt9EkRZfw== - didyoumean@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" @@ -2626,6 +2616,11 @@ iconv-lite@^0.6.2: dependencies: safer-buffer ">= 2.1.2 < 3.0.0" +idb@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/idb/-/idb-7.0.0.tgz#f349b418c128f625961147a7d6b0e4b526fd34ed" + integrity sha512-jSx0WOY9Nj+QzP6wX5e7g64jqh8ExtDs/IAuOrOEZCD/h6+0HqyrKsDMfdJc0hqhSvh0LsrwqrkDn+EtjjzSRA== + ieee754@^1.1.13, ieee754@^1.1.4: version "1.2.1" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" @@ -4763,13 +4758,6 @@ swap-case@^1.1.0: lower-case "^1.1.1" upper-case "^1.1.1" -swr@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/swr/-/swr-1.0.1.tgz#15f62846b87ee000e52fa07812bb65eb62d79483" - integrity sha512-EPQAxSjoD4IaM49rpRHK0q+/NzcwoT8c0/Ylu/u3/6mFj/CWnQVjNJ0MV2Iuw/U+EJSd2TX5czdAwKPYZIG0YA== - dependencies: - dequal "2.0.2" - table@^6.0.9, table@^6.7.3: version "6.7.3" resolved "https://registry.yarnpkg.com/table/-/table-6.7.3.tgz#255388439715a738391bd2ee4cbca89a4d05a9b7"