feat: :/
This commit is contained in:
parent
a92cafff92
commit
23b2959d9b
|
@ -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>
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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}
|
||||
|
|
17
lib/context/memCacheContextProvider.tsx
Normal file
17
lib/context/memCacheContextProvider.tsx
Normal 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
|
|
@ -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 }))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@ const useReadQrFromVideo = (onQrCodeRead: (content: string) => void) => {
|
|||
stream?.getTracks().forEach(track => track.stop())
|
||||
stopTick = true
|
||||
}
|
||||
}, [])
|
||||
}, [onQrCodeRead])
|
||||
return { videoRef, videoLive }
|
||||
}
|
||||
|
||||
|
|
|
@ -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]
|
||||
)
|
||||
|
|
|
@ -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
14
lib/hooks/useSetupDb.ts
Normal 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
|
|
@ -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
17
lib/hooks/useXnoPrice.ts
Normal 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
|
|
@ -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
|
|
@ -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
|
14
lib/types.ts
14
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 }
|
||||
|
|
|
@ -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
6
lib/xno/constants.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export const zeroString =
|
||||
'0000000000000000000000000000000000000000000000000000000000000000'
|
||||
|
||||
export const receiveDiff = 'fffffe0000000000'
|
||||
|
||||
export const sendDiff = 'fffffff800000000'
|
38
lib/xno/receiveNano.ts
Normal file
38
lib/xno/receiveNano.ts
Normal 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
38
lib/xno/sendNano.ts
Normal 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
|
|
@ -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",
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
22
yarn.lock
22
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"
|
||||
|
|
Reference in a new issue