This commit is contained in:
Filipe Medeiros 2021-11-27 18:04:30 +00:00
parent a92cafff92
commit 23b2959d9b
38 changed files with 499 additions and 465 deletions

View file

@ -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<Props> = ({ 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<Props> = ({ className }) => {
</h3>
{showFiatBalance && (
<h3 className="text-xl text-center">
$ {(Number(xnoBalance) * xnoPrice.price).toFixed(2)}
$ {(Number(xnoBalance) * xnoPrice).toFixed(2)}
</h3>
)}
</div>

View file

@ -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'

View file

@ -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<string, { displayName: string }> = {
const RecentTransactions: FC<Props> = ({ 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<AccountHistoryResponse>(
(_, prevHistory) =>
account === undefined
? null
: prevHistory === null
? 'no-cursor'
: prevHistory.previous ?? null,
(cursor: 'no-cursor' | string) =>
fetcher<AccountHistoryResponse>(
'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<AccountPendingResponse>(
account !== undefined ? account.address : null,
() =>
fetcher<AccountPendingResponse>('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<BlocksInfoResponse>(
[formattedPendingHashes.length > 0 ? formattedPendingHashes : null],
hashes =>
fetcher<BlocksInfoResponse>('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 (
<div className={clsx('flex flex-col gap-6 w-full', className)}>
{hasPendingTxns && (
{false && (
<section className="flex flex-col gap-3 w-full items-center">
<h2 className="text-2xl font-semibold text-purple-50">pending</h2>
<ol className="flex flex-col gap-3 w-full">
{mappedPendingTxns.map(txn => (
{[
{
hash: 'string',
send: 'string',
receivable: true,
amount: '0',
account: '',
timestamp: '',
},
].map(txn => (
<li
key={txn.hash}
className={clsx(
@ -181,7 +79,7 @@ const RecentTransactions: FC<Props> = ({ className }) => {
day: '2-digit',
month: '2-digit',
year: '2-digit',
}).format(txn.timestamp * 1000)}{' '}
}).format(Number(txn.timestamp) * 1000)}{' '}
-{' '}
{mockAddressBook[txn.account]?.displayName ?? (
<span className="text-xs">{txn.account}</span>
@ -206,14 +104,23 @@ const RecentTransactions: FC<Props> = ({ className }) => {
</ol>
</section>
)}
{hasPendingTxns && hasTxns && <hr />}
{hasTxns && (
{false && false && <hr />}
{false && (
<section className="flex flex-col gap-3 w-full items-center">
<h2 className="text-2xl font-semibold text-purple-50">
recent transactions
</h2>
<ol className="flex flex-col gap-3 w-full">
{txns.map(txn => (
{[
{
hash: 'string',
send: 'string',
receivable: true,
amount: '0',
account: '',
timestamp: '',
},
].map(txn => (
<li
key={txn.hash}
className={clsx(
@ -241,7 +148,7 @@ const RecentTransactions: FC<Props> = ({ className }) => {
day: '2-digit',
month: '2-digit',
year: '2-digit',
}).format(txn.timestamp * 1000)}{' '}
}).format(Number(txn.timestamp) * 1000)}{' '}
-{' '}
{mockAddressBook[txn.account]?.displayName ?? (
<span className="text-xs">{txn.account}</span>
@ -266,7 +173,7 @@ const RecentTransactions: FC<Props> = ({ className }) => {
</ol>
</section>
)}
{!hasPendingTxns && !hasTxns && (
{!false && !false && (
<div className="text-center pt-8 text-purple-50">
<p className="pb-4">no transactions yet...</p>
<p>
@ -276,10 +183,10 @@ const RecentTransactions: FC<Props> = ({ className }) => {
</p>
</div>
)}
{hasMoreTxns && (
{false && (
<button
className="bg-purple-200 py-2 px-4 rounded dark:text-gray-900 font-bold shadow"
onClick={() => setSize(prev => prev + 1)}
onClick={() => {}}
>
load more
</button>

View file

@ -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<string | null>((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
}

View file

@ -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<AccountContextValue | undefined>(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<number>(
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<number>(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 (
<accountContext.Provider
value={{
@ -80,6 +121,7 @@ export const AccountProvider: FC<{
setAccount,
removeAccount,
currAccount,
setCurrAccountIndex,
}}
>
{children}

View file

@ -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 (
<AccountProvider>
<PreferencesProvider>{children}</PreferencesProvider>
</AccountProvider>
)
}
export default MemCacheProvider

View file

@ -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: <P extends PreferenceName>(
preference: P,
value: PreferenceTypes[P]
) => void
}
| undefined
>(undefined)
export interface PreferenceContextValue {
preferences: PreferenceTypes
setPreference: <P extends PreferenceName>(
preference: P,
value: PreferenceTypes[P]
) => void
}
const preferencesContext = createContext<PreferenceContextValue | undefined>(
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<PreferenceTypes>({
darkMode: undefined,
biometricsAuth: undefined,
leftHanded: undefined,
showCurrencyDash: undefined,
})
const [preferences, setPreferences] = useState<PreferenceTypes>(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(
<P extends PreferenceName>(name: P, value: PreferenceTypes[P]) => {
setPreferences(prev => ({ ...prev, [name]: value }))

View file

@ -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
}

View file

@ -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)

View file

@ -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<EncryptedSeedValue, EncryptedSeedKey>
public cryptoAssets!: Table<CryptoAssetValue, CryptoAssetKey>
public accounts!: Table<AccountsValue, AccountsKey>
public preferences!: Table<PreferenceValue, PreferenceKey>
public constructor() {
super('Database')
this.version(1).stores({
encryptedSeeds: encryptedSeedSchema,
cryptoAssets: cryptoAssetSchema,
preferences: preferenceSchema,
accounts: accountsSchema,
})
}
}
export default Database

View file

@ -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)

View file

@ -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<Schema> | undefined = undefined
export const openDb = async (version = 1) => {
if (dbConnection !== undefined) return
else {
dbConnection = await openDB<Schema>('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

View file

@ -1,28 +1,39 @@
import db from '.'
import { PreferenceName, PreferenceTypes } from './types'
import { PreferenceName, PreferenceTypes, PreferenceValue } from './types'
export const addPreference = <P extends PreferenceName>(
name: P,
value: PreferenceTypes[P]
) => db.preferences.add({ name, value: JSON.stringify(value) })
) => db()!.add('preferences', { name, value: JSON.stringify(value) })
export const putPreference = <P extends PreferenceName>(
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 = <P extends PreferenceName>(
name: P
): Promise<PreferenceTypes[P]> =>
db.preferences
.where({ name })
.first()
export const getPreference = <P extends PreferenceName>(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)

View file

@ -1,4 +1,7 @@
const fetcher = <T>(...args: Parameters<typeof fetch>) =>
fetch(...args).then(res => res.json() as Promise<T>)
fetch(...args).then(res => {
if (!res.ok) throw new Error() // todo improve this error
return res.json() as Promise<T>
})
export default fetcher

View file

@ -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<HTMLCanvasElement>(null)

View file

@ -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
}

View file

@ -51,7 +51,7 @@ const useReadQrFromVideo = (onQrCodeRead: (content: string) => void) => {
stream?.getTracks().forEach(track => track.stop())
stopTick = true
}
}, [])
}, [onQrCodeRead])
return { videoRef, videoLive }
}

View file

@ -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]
)

View file

@ -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]
)

14
lib/hooks/useSetupDb.ts Normal file
View file

@ -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

View file

@ -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)

17
lib/hooks/useXnoPrice.ts Normal file
View file

@ -0,0 +1,17 @@
import { useEffect, useState } from 'react'
import fetcher from '../fetcher'
import { XnoPriceResponse } from '../types'
const useXnoPrice = () => {
const [xnoPrice, setXnoPrice] = useState<number | undefined>()
useEffect(() => {
// todo get url out of here
fetcher<XnoPriceResponse>('https://nano.to/price?json=true').then(res =>
setXnoPrice(res.price)
)
}, [])
return { xnoPrice, loading: xnoPrice === undefined }
}
export default useXnoPrice

View file

@ -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<typeof block['receive']>[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

View file

@ -1,19 +0,0 @@
import { block } from 'nanocurrency-web'
import decryptSeed from '../decryptSeed'
import accountAtIndex from './accountAtIndex'
const sendNano = async (
blockData: Parameters<typeof block['send']>[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

View file

@ -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 }

View file

@ -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)
}

6
lib/xno/constants.ts Normal file
View file

@ -0,0 +1,6 @@
export const zeroString =
'0000000000000000000000000000000000000000000000000000000000000000'
export const receiveDiff = 'fffffe0000000000'
export const sendDiff = 'fffffff800000000'

38
lib/xno/receiveNano.ts Normal file
View file

@ -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<typeof block['receive']>[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<ProcessResponse>('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

38
lib/xno/sendNano.ts Normal file
View file

@ -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<typeof block['send']>[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<ProcessResponse>('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

View file

@ -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",

View file

@ -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<AppProps> = ({ 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 (
<SWRConfig value={{ fetcher, provider: () => new Map() }}>
<AccountProvider initialAccounts={accounts} initialAccountIndex={0}>
<PreferencesProvider>
<Layout>
<Component {...pageProps} />
</Layout>
</PreferencesProvider>
</AccountProvider>
</SWRConfig>
<MemCacheProvider>
<Layout>
<Component {...pageProps} />
</Layout>
</MemCacheProvider>
)
}

View file

@ -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()

View file

@ -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"