feat?
This commit is contained in:
parent
1dfef630de
commit
f25791eff8
|
@ -3,7 +3,7 @@ import { tools } from 'nanocurrency-web'
|
|||
import { FC, useMemo } from 'react'
|
||||
import useSWR from 'swr'
|
||||
|
||||
import { useAddress } from '../lib/context/addressContext'
|
||||
import { useAccount, useAccounts } from '../lib/context/accountContext'
|
||||
import { usePreferences } from '../lib/context/preferencesContext'
|
||||
import { ShowCurrencyPreference } from '../lib/db/preferences'
|
||||
import fetcher from '../lib/fetcher'
|
||||
|
@ -26,24 +26,7 @@ const Balance: FC<Props> = ({ className }) => {
|
|||
fetcher,
|
||||
})
|
||||
|
||||
const { address } = useAddress()
|
||||
|
||||
const params = useMemo(
|
||||
() => ({
|
||||
method: 'POST',
|
||||
headers: [['Content-Type', 'application/json']],
|
||||
body: JSON.stringify({
|
||||
action: 'account_balance',
|
||||
account: address,
|
||||
}),
|
||||
}),
|
||||
[address]
|
||||
)
|
||||
|
||||
const { data: account } = useSWR<{
|
||||
balance: string
|
||||
pending: string
|
||||
}>(address !== undefined ? ['https://mynano.ninja/api/node', params] : null)
|
||||
const account = useAccount()
|
||||
|
||||
const {
|
||||
preferences: { showCurrencyDash },
|
||||
|
@ -63,7 +46,7 @@ const Balance: FC<Props> = ({ className }) => {
|
|||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'bg-purple-500 dark:text-gray-900 text-purple-100 py-4 px-7 rounded shadow-lg',
|
||||
'bg-purple-500 dark:text-gray-900 text-purple-50 py-4 px-7 rounded shadow-lg',
|
||||
className
|
||||
)}
|
||||
onClick={() =>
|
||||
|
|
|
@ -15,9 +15,10 @@ import { useRouter } from 'next/router'
|
|||
import { FC, useState } from 'react'
|
||||
|
||||
import { checkBiometrics } from '../lib/biometrics'
|
||||
import { useAddress } from '../lib/context/addressContext'
|
||||
import { useAccount, useAccounts } from '../lib/context/accountContext'
|
||||
import { usePreferences } from '../lib/context/preferencesContext'
|
||||
import { getEncryptedSeed } from '../lib/db/encryptedSeeds'
|
||||
import decryptSeed from '../lib/decryptSeed'
|
||||
import useIsWelcoming from '../lib/hooks/useIsWelcoming'
|
||||
|
||||
export interface Props {
|
||||
|
@ -29,23 +30,23 @@ const BottomMenu: FC<Props> = ({ className }) => {
|
|||
preferences: { leftHanded },
|
||||
} = usePreferences()
|
||||
const { push, pathname } = useRouter()
|
||||
const { address } = useAddress()
|
||||
const account = useAccount()
|
||||
|
||||
const [confirmCopyAddress, setConfirmCopyAddress] = useState(false)
|
||||
const [confirmCopySeed, setConfirmCopySeed] = useState(false)
|
||||
const onCopy = (seed?: string) => {
|
||||
if (address !== undefined || seed !== undefined) {
|
||||
navigator.clipboard.writeText(seed ?? address!)
|
||||
seed !== undefined
|
||||
? setConfirmCopySeed(true)
|
||||
: setConfirmCopyAddress(true)
|
||||
setTimeout(
|
||||
() =>
|
||||
seed !== undefined
|
||||
? setConfirmCopySeed(false)
|
||||
: setConfirmCopyAddress(false),
|
||||
1500
|
||||
)
|
||||
|
||||
const onCopySeed = async () => {
|
||||
const seed = await decryptSeed('os')
|
||||
navigator.clipboard.writeText(seed)
|
||||
setConfirmCopySeed(true)
|
||||
setTimeout(() => setConfirmCopySeed(false), 1500)
|
||||
}
|
||||
|
||||
const onCopyAddress = () => {
|
||||
if (account !== undefined) {
|
||||
navigator.clipboard.writeText(account.address)
|
||||
setConfirmCopyAddress(true)
|
||||
setTimeout(() => setConfirmCopyAddress(false), 1500)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,7 +67,7 @@ const BottomMenu: FC<Props> = ({ className }) => {
|
|||
className="bg-purple-500 p-1 h-12 rounded hover:bg-purple-400 disabled:hover:bg-purple-500 shadow-lg"
|
||||
onClick={() => push('/dashboard')}
|
||||
>
|
||||
<HomeIcon className="h-full text-white dark:text-gray-900" />
|
||||
<HomeIcon className="h-full text-purple-50 dark:text-gray-900" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
|
@ -88,34 +89,22 @@ const BottomMenu: FC<Props> = ({ className }) => {
|
|||
className={clsx(
|
||||
'p-1 h-7 rounded shadow-lg',
|
||||
confirmCopySeed
|
||||
? 'bg-white'
|
||||
? 'bg-purple-50'
|
||||
: 'bg-purple-500 hover:bg-purple-400 disabled:hover:bg-purple-500'
|
||||
)}
|
||||
onClick={async () => {
|
||||
const {
|
||||
// @ts-expect-error
|
||||
response: { signature: sig },
|
||||
} = (await checkBiometrics())!
|
||||
|
||||
const encryptedSeed = (await getEncryptedSeed('os'))!
|
||||
const decryptedSeed = AES.decrypt(
|
||||
encryptedSeed,
|
||||
sig.toString()
|
||||
).toString(enc.Utf8)
|
||||
onCopy(decryptedSeed)
|
||||
}}
|
||||
onClick={onCopySeed}
|
||||
>
|
||||
{confirmCopySeed ? (
|
||||
<CheckIcon className="h-full text-purple-500" />
|
||||
) : (
|
||||
<KeyIcon className="h-full text-white dark:text-gray-900" />
|
||||
<KeyIcon className="h-full text-purple-50 dark:text-gray-900" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
disabled={isWelcoming}
|
||||
className="bg-purple-500 p-1 h-7 rounded hover:bg-purple-400 disabled:hover:bg-purple-500 shadow-lg"
|
||||
>
|
||||
<LibraryIcon className="h-full text-white dark:text-gray-900" />
|
||||
<LibraryIcon className="h-full text-purple-50 dark:text-gray-900" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex flex-col h-16 justify-between">
|
||||
|
@ -124,22 +113,22 @@ const BottomMenu: FC<Props> = ({ className }) => {
|
|||
className={clsx(
|
||||
'p-1 h-7 rounded shadow-lg',
|
||||
confirmCopyAddress
|
||||
? 'bg-white'
|
||||
? 'bg-purple-50'
|
||||
: 'bg-purple-500 hover:bg-purple-400 disabled:hover:bg-purple-500'
|
||||
)}
|
||||
onClick={() => onCopy()}
|
||||
onClick={onCopyAddress}
|
||||
>
|
||||
{confirmCopyAddress ? (
|
||||
<CheckIcon className="h-full text-purple-500" />
|
||||
) : (
|
||||
<DocumentDuplicateIcon className="h-full text-white dark:text-gray-900" />
|
||||
<DocumentDuplicateIcon className="h-full text-purple-50 dark:text-gray-900" />
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
disabled={isWelcoming}
|
||||
className="bg-purple-500 p-1 h-7 rounded hover:bg-purple-400 disabled:hover:bg-purple-500 shadow-lg"
|
||||
>
|
||||
<DownloadIcon className="h-full text-white dark:text-gray-900" />
|
||||
<DownloadIcon className="h-full text-purple-50 dark:text-gray-900" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -155,7 +144,7 @@ const BottomMenu: FC<Props> = ({ className }) => {
|
|||
)}
|
||||
onClick={() => push('/readQrCode')}
|
||||
>
|
||||
<UploadIcon className="h-full text-white dark:text-gray-900 w-full" />
|
||||
<UploadIcon className="h-full text-purple-50 dark:text-gray-900 w-full" />
|
||||
</button>
|
||||
|
||||
<div className="border-purple-500 border-t-2 border-b-2 py-1 px-3 h-16 shadow-lg">
|
||||
|
@ -172,7 +161,7 @@ const BottomMenu: FC<Props> = ({ className }) => {
|
|||
)}
|
||||
onClick={() => push('/myQrCode')}
|
||||
>
|
||||
<DownloadIcon className="h-full text-white dark:text-gray-900 w-full" />
|
||||
<DownloadIcon className="h-full text-purple-50 dark:text-gray-900 w-full" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
@ -181,13 +170,13 @@ const BottomMenu: FC<Props> = ({ className }) => {
|
|||
disabled={isWelcoming}
|
||||
className="bg-purple-500 p-1 h-7 rounded hover:bg-purple-400 disabled:hover:bg-purple-500 shadow-md"
|
||||
>
|
||||
<UploadIcon className="h-full text-white dark:text-gray-900" />
|
||||
<UploadIcon className="h-full text-purple-50 dark:text-gray-900" />
|
||||
</button>
|
||||
<button
|
||||
disabled={isWelcoming}
|
||||
className="bg-purple-500 p-1 h-7 rounded hover:bg-purple-400 disabled:hover:bg-purple-500 shadow-lg"
|
||||
>
|
||||
<RssIcon className="h-full text-white dark:text-gray-900" />
|
||||
<RssIcon className="h-full text-purple-50 dark:text-gray-900" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -21,7 +21,7 @@ const Layout: FC<Props> = ({ children }) => {
|
|||
|
||||
return (
|
||||
<div
|
||||
className="dark:text-white bg-purple-50 dark:bg-gray-900 relative w-screen h-screen pt-4 pb-4 px-5 grid justify-center gap-4"
|
||||
className="dark:text-purple-50 bg-purple-50 dark:bg-gray-900 relative w-screen h-screen pt-4 pb-4 px-5 grid justify-center gap-4"
|
||||
style={{
|
||||
gridTemplate:
|
||||
'"top-menu" auto "balance" auto "main" 1fr "bottom-menu" auto / 1fr',
|
||||
|
@ -50,7 +50,7 @@ const Layout: FC<Props> = ({ children }) => {
|
|||
<Balance />
|
||||
</div>
|
||||
<main
|
||||
className="overflow-auto bg-purple-500 rounded border-t-8 border-b-8 border-purple-500 shadow-md py-6 px-3"
|
||||
className="overflow-auto bg-purple-500 rounded border-t-8 border-b-8 border-purple-500 shadow-md py-4 px-4"
|
||||
style={{ gridArea: 'main' }}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -31,7 +31,7 @@ const PreferencesMenu: FC<Props> = () => {
|
|||
<div className="relative z-20 justify-center" ref={menuRef}>
|
||||
<button
|
||||
className={clsx(
|
||||
'w-10 p-1 rounded bg-purple-500 shadow-md text-white dark:text-gray-900 hover:cursor-pointer hover:bg-purple-400 transition-colors dark text-white:dark:text-gray-900'
|
||||
'w-10 p-1 rounded bg-purple-500 shadow-md text-purple-50 dark:text-gray-900 hover:cursor-pointer hover:bg-purple-400 transition-colors dark text-purple-50:dark:text-gray-900'
|
||||
)}
|
||||
onClick={toggleMenu}
|
||||
>
|
||||
|
@ -52,10 +52,10 @@ const PreferencesMenu: FC<Props> = () => {
|
|||
<button
|
||||
disabled={!showMenu}
|
||||
className={clsx(
|
||||
'p-1 rounded dark:hover:text-purple-300 transition-colors duration-100 w-full text-white',
|
||||
'p-1 rounded transition-colors duration-100 w-full',
|
||||
biometricsAuth
|
||||
? 'dark:text-purple-400 dark:bg-gray-900 dark:hover:text-purple-400 shadow-md hover:bg-purple-400 bg-purple-100 text-purple-400'
|
||||
: 'hover:text-purple-400 bg-purple-400 dark:text-gray-900',
|
||||
? 'dark:text-purple-400 dark:bg-gray-900 dark:hover:text-purple-400 shadow-md hover:bg-purple-400 bg-purple-50 text-purple-400'
|
||||
: 'hover:text-purple-400 bg-purple-400 dark:text-gray-900 text-purple-50 dark:hover:text-purple-300',
|
||||
showMenu ? 'hover:cursor-pointer' : 'cursor-default'
|
||||
)}
|
||||
onClick={() => {
|
||||
|
@ -71,10 +71,10 @@ const PreferencesMenu: FC<Props> = () => {
|
|||
<button
|
||||
disabled={!showMenu}
|
||||
className={clsx(
|
||||
'p-1 rounded dark:hover:text-purple-300 transition-colors duration-100 w-full text-white',
|
||||
'p-1 rounded transition-colors duration-100 w-full',
|
||||
darkMode
|
||||
? 'dark:text-purple-400 dark:bg-gray-900 dark:hover:text-purple-400 shadow-md hover:bg-purple-400 bg-purple-100 text-purple-400'
|
||||
: 'hover:text-purple-400 bg-purple-400 dark:text-gray-900',
|
||||
? 'dark:text-purple-400 dark:bg-gray-900 dark:hover:text-purple-400 shadow-md hover:bg-purple-400 bg-purple-50 text-purple-400'
|
||||
: 'hover:text-purple-400 bg-purple-400 dark:text-gray-900 text-purple-50 dark:hover:text-purple-300',
|
||||
showMenu ? 'hover:cursor-pointer' : 'cursor-default'
|
||||
)}
|
||||
onClick={() => {
|
||||
|
@ -89,10 +89,10 @@ const PreferencesMenu: FC<Props> = () => {
|
|||
<button
|
||||
disabled={!showMenu}
|
||||
className={clsx(
|
||||
'p-1 rounded dark:hover:text-purple-300 transition-colors duration-100 w-full text-white dark:text-gray-900',
|
||||
'p-1 rounded transition-colors duration-100 w-full',
|
||||
leftHanded
|
||||
? 'dark:text-purple-400 dark:bg-gray-900 dark:hover:text-purple-400 shadow-md hover:bg-purple-400 bg-purple-100 text-purple-400'
|
||||
: 'hover:text-purple-400 bg-purple-400 dark:text-gray-900',
|
||||
? 'dark:text-purple-400 dark:bg-gray-900 dark:hover:text-purple-400 shadow-md hover:bg-purple-400 bg-purple-50 text-purple-400'
|
||||
: 'hover:text-purple-400 bg-purple-400 dark:text-gray-900 dark:hover:text-purple-300 text-purple-50',
|
||||
showMenu ? 'hover:cursor-pointer' : 'cursor-default'
|
||||
)}
|
||||
onClick={() => {
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import { DownloadIcon, UploadIcon } from '@heroicons/react/solid'
|
||||
import clsx from 'clsx'
|
||||
import { tools } from 'nanocurrency-web'
|
||||
import { FC, useCallback, useMemo, useState } from 'react'
|
||||
import { FC, useCallback, useMemo } from 'react'
|
||||
import useSwr from 'swr'
|
||||
import useSwrInfinite from 'swr/infinite'
|
||||
|
||||
import { useAddress } from '../lib/context/addressContext'
|
||||
import { useAccount } from '../lib/context/accountContext'
|
||||
import fetcher from '../lib/fetcher'
|
||||
import receiveNano from '../lib/nano/receiveNano'
|
||||
import useReceiveNano from '../lib/hooks/useReceiveNano'
|
||||
import {
|
||||
AccountHistoryResponse,
|
||||
AccountPendingResponse,
|
||||
|
@ -31,25 +31,25 @@ const mockAddressBook: Record<string, { displayName: string }> = {
|
|||
}
|
||||
|
||||
const RecentTransactions: FC<Props> = ({ className }) => {
|
||||
const { address } = useAddress()
|
||||
const account = useAccount()
|
||||
const params = useCallback(
|
||||
(cursor: string | undefined) => ({
|
||||
method: 'POST',
|
||||
headers: [['Content-Type', 'application/json']],
|
||||
body: JSON.stringify({
|
||||
action: 'account_history',
|
||||
account: address,
|
||||
account: account?.address,
|
||||
count: 20,
|
||||
head: cursor,
|
||||
}),
|
||||
}),
|
||||
[address]
|
||||
[account]
|
||||
)
|
||||
|
||||
const { data: historyPages, setSize } =
|
||||
useSwrInfinite<AccountHistoryResponse>(
|
||||
(_, prevHistory) =>
|
||||
address === undefined
|
||||
account === undefined
|
||||
? null
|
||||
: prevHistory === null
|
||||
? 'no-cursor'
|
||||
|
@ -64,13 +64,13 @@ const RecentTransactions: FC<Props> = ({ className }) => {
|
|||
const paramsPending = useMemo(
|
||||
() => ({
|
||||
action: 'accounts_pending',
|
||||
accounts: [address],
|
||||
accounts: [account?.address],
|
||||
count: '20',
|
||||
}),
|
||||
[address]
|
||||
[account]
|
||||
)
|
||||
const { data: pendingTxnHashes } = useSwr<AccountPendingResponse>(
|
||||
address !== undefined ? address : null,
|
||||
account !== undefined ? account.address : null,
|
||||
() =>
|
||||
fetcher<AccountPendingResponse>('https://mynano.ninja/api/node', {
|
||||
method: 'POST',
|
||||
|
@ -107,7 +107,7 @@ const RecentTransactions: FC<Props> = ({ className }) => {
|
|||
const txns = useMemo(
|
||||
() =>
|
||||
historyPages?.flatMap(({ history }) =>
|
||||
(history !== '' ? history : []).map(txn => {
|
||||
(history !== '' ? history ?? [] : []).map(txn => {
|
||||
return {
|
||||
send: txn.type === 'send',
|
||||
account: txn.account,
|
||||
|
@ -117,7 +117,7 @@ const RecentTransactions: FC<Props> = ({ className }) => {
|
|||
receivable: false,
|
||||
}
|
||||
})
|
||||
),
|
||||
) ?? [],
|
||||
[historyPages]
|
||||
)
|
||||
|
||||
|
@ -137,76 +137,137 @@ const RecentTransactions: FC<Props> = ({ className }) => {
|
|||
[pendingTxns]
|
||||
)
|
||||
|
||||
const txnsWithPending = useMemo(
|
||||
() => [...(mappedPendingTxns ?? []), ...(txns ?? [])],
|
||||
[txns, mappedPendingTxns]
|
||||
)
|
||||
const receiveNano = useReceiveNano()
|
||||
|
||||
if (historyPages === undefined || address === undefined) return null
|
||||
if (historyPages === undefined || account === undefined) return null
|
||||
|
||||
const hasTxns = txnsWithPending.length > 0
|
||||
const hasPendingTxns = (mappedPendingTxns ?? []).length > 0
|
||||
const hasTxns = (txns ?? []).length > 0
|
||||
|
||||
return (
|
||||
<div className={clsx('flex flex-col gap-6 w-full items-center', className)}>
|
||||
<h2 className="text-2xl font-semibold text-white">recent transactions</h2>
|
||||
{hasTxns ? (
|
||||
<ol className="flex flex-col gap-3 w-full">
|
||||
{txnsWithPending.map(txn => (
|
||||
<li
|
||||
key={txn.hash}
|
||||
className={clsx(
|
||||
'bg-white shadow rounded px-3 py-3 flex items-center justify-between gap-2 text-black border-r-4',
|
||||
txn.send
|
||||
? 'border-yellow-500'
|
||||
: txn.receivable
|
||||
? 'border-blue-500'
|
||||
: 'border-green-500'
|
||||
)}
|
||||
>
|
||||
<button
|
||||
className="contents"
|
||||
onClick={() => receiveNano(address, txn.hash, txn.amount)}
|
||||
>
|
||||
{txn.send ? (
|
||||
<UploadIcon className="w-6 text-yellow-500 flex-shrink-0" />
|
||||
) : (
|
||||
<DownloadIcon
|
||||
className={clsx(
|
||||
'w-6 flex-shrink-0',
|
||||
txn.receivable ? 'text-blue-500' : 'text-green-500'
|
||||
)}
|
||||
/>
|
||||
<div className={clsx('flex flex-col gap-6 w-full', className)}>
|
||||
{hasPendingTxns && (
|
||||
<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 => (
|
||||
<li
|
||||
key={txn.hash}
|
||||
className={clsx(
|
||||
'bg-purple-50 shadow rounded px-3 py-3 flex items-center justify-between gap-2 text-black border-r-4',
|
||||
txn.send
|
||||
? 'border-yellow-500'
|
||||
: txn.receivable
|
||||
? 'border-blue-500'
|
||||
: 'border-green-500'
|
||||
)}
|
||||
<div className="overflow-hidden overflow-ellipsis text-left flex-1 whitespace-nowrap">
|
||||
{Intl.DateTimeFormat([], {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: '2-digit',
|
||||
}).format(txn.timestamp * 1000)}{' '}
|
||||
-{' '}
|
||||
{mockAddressBook[txn.account]?.displayName ?? (
|
||||
<span className="text-xs">{txn.account}</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="flex-shrink-0 font-medium">
|
||||
Ӿ{' '}
|
||||
{rawToNanoDisplay(txn.amount) === 'small' ? (
|
||||
'<.01'
|
||||
) : rawToNanoDisplay(txn.amount).startsWith('0.') ? (
|
||||
<>
|
||||
<span className="text-sm font-semibold">0</span>
|
||||
{rawToNanoDisplay(txn.amount).substring(1)}
|
||||
</>
|
||||
>
|
||||
<button
|
||||
className="contents"
|
||||
onClick={() => receiveNano(txn.hash, txn.amount)}
|
||||
>
|
||||
{txn.send ? (
|
||||
<UploadIcon className="w-6 text-yellow-500 flex-shrink-0" />
|
||||
) : (
|
||||
rawToNanoDisplay(txn.amount)
|
||||
<DownloadIcon
|
||||
className={clsx(
|
||||
'w-6 flex-shrink-0',
|
||||
txn.receivable ? 'text-blue-500' : 'text-green-500'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
) : (
|
||||
<div className="text-center pt-8">
|
||||
<div className="overflow-hidden overflow-ellipsis text-left flex-1 whitespace-nowrap">
|
||||
{Intl.DateTimeFormat([], {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: '2-digit',
|
||||
}).format(txn.timestamp * 1000)}{' '}
|
||||
-{' '}
|
||||
{mockAddressBook[txn.account]?.displayName ?? (
|
||||
<span className="text-xs">{txn.account}</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="flex-shrink-0 font-medium">
|
||||
Ӿ{' '}
|
||||
{rawToNanoDisplay(txn.amount) === 'small' ? (
|
||||
'<.01'
|
||||
) : rawToNanoDisplay(txn.amount).startsWith('0.') ? (
|
||||
<>
|
||||
<span className="text-sm font-semibold">0</span>
|
||||
{rawToNanoDisplay(txn.amount).substring(1)}
|
||||
</>
|
||||
) : (
|
||||
rawToNanoDisplay(txn.amount)
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
)}
|
||||
{hasPendingTxns && hasTxns && <hr />}
|
||||
{hasTxns && (
|
||||
<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 => (
|
||||
<li
|
||||
key={txn.hash}
|
||||
className={clsx(
|
||||
'bg-purple-50 shadow rounded px-3 py-3 flex items-center justify-between gap-2 text-black border-r-4',
|
||||
txn.send
|
||||
? 'border-yellow-500'
|
||||
: txn.receivable
|
||||
? 'border-blue-500'
|
||||
: 'border-green-500'
|
||||
)}
|
||||
>
|
||||
<button className="contents" onClick={() => {}}>
|
||||
{txn.send ? (
|
||||
<UploadIcon className="w-6 text-yellow-500 flex-shrink-0" />
|
||||
) : (
|
||||
<DownloadIcon
|
||||
className={clsx(
|
||||
'w-6 flex-shrink-0',
|
||||
txn.receivable ? 'text-blue-500' : 'text-green-500'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<div className="overflow-hidden overflow-ellipsis text-left flex-1 whitespace-nowrap">
|
||||
{Intl.DateTimeFormat([], {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: '2-digit',
|
||||
}).format(txn.timestamp * 1000)}{' '}
|
||||
-{' '}
|
||||
{mockAddressBook[txn.account]?.displayName ?? (
|
||||
<span className="text-xs">{txn.account}</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="flex-shrink-0 font-medium">
|
||||
Ӿ{' '}
|
||||
{rawToNanoDisplay(txn.amount) === 'small' ? (
|
||||
'<.01'
|
||||
) : rawToNanoDisplay(txn.amount).startsWith('0.') ? (
|
||||
<>
|
||||
<span className="text-sm font-semibold">0</span>
|
||||
{rawToNanoDisplay(txn.amount).substring(1)}
|
||||
</>
|
||||
) : (
|
||||
rawToNanoDisplay(txn.amount)
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</section>
|
||||
)}
|
||||
{!hasPendingTxns && !hasTxns && (
|
||||
<div className="text-center pt-8 text-purple-50">
|
||||
<p className="pb-4">no transactions yet...</p>
|
||||
<p>
|
||||
get your first nano
|
||||
|
|
81
lib/context/accountContext.tsx
Normal file
81
lib/context/accountContext.tsx
Normal file
|
@ -0,0 +1,81 @@
|
|||
import {
|
||||
FC,
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
|
||||
import { AccountInfoCache } from '../types'
|
||||
|
||||
export interface AddressContextValue {
|
||||
currAccount: AccountInfoCache | undefined
|
||||
accounts: { [key: number]: AccountInfoCache } | undefined
|
||||
setAccount: (info: AccountInfoCache) => void
|
||||
removeAccount: (index: number) => void
|
||||
}
|
||||
|
||||
const addressContext = createContext<AddressContextValue | undefined>(undefined)
|
||||
|
||||
export const useAccounts = () => {
|
||||
const contextValue = useContext(addressContext)
|
||||
if (contextValue === undefined)
|
||||
throw new Error('`useAddress` must be used insisde a context `Provider`')
|
||||
return contextValue
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index index of the account you want, or no index if you want the current account
|
||||
* @returns the requested account, or undefined if no account was found
|
||||
*/
|
||||
export const useAccount = (index?: number) => {
|
||||
const contextValue = useAccounts()
|
||||
return index !== undefined
|
||||
? contextValue.accounts?.[index]
|
||||
: contextValue.currAccount
|
||||
}
|
||||
|
||||
export const AddressProvider: FC<{
|
||||
initialAccounts?: { [key: number]: AccountInfoCache } | undefined
|
||||
initialAccountIndex?: number
|
||||
}> = ({ children, initialAccounts, initialAccountIndex }) => {
|
||||
const [accounts, setAccounts] = useState<
|
||||
{ [key: number]: AccountInfoCache } | undefined
|
||||
>(initialAccounts)
|
||||
useEffect(() => {
|
||||
setAccounts(initialAccounts)
|
||||
}, [initialAccounts])
|
||||
|
||||
const [currAccountIndex, setCurrAccountIndex] = useState<number>(
|
||||
initialAccountIndex ?? 0
|
||||
)
|
||||
useEffect(() => {
|
||||
setCurrAccountIndex(initialAccountIndex ?? 0)
|
||||
}, [initialAccountIndex])
|
||||
|
||||
const setAccount = useCallback((account: AccountInfoCache) => {
|
||||
setAccounts(prev => ({ ...prev, [account.index]: account }))
|
||||
}, [])
|
||||
|
||||
const removeAccount = useCallback((index: number) => {
|
||||
setAccounts(prev => {
|
||||
const next = { ...prev }
|
||||
delete next[index]
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<addressContext.Provider
|
||||
value={{
|
||||
accounts,
|
||||
setAccount,
|
||||
removeAccount,
|
||||
currAccount: accounts?.[currAccountIndex],
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</addressContext.Provider>
|
||||
)
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
import { FC, createContext, useContext, useEffect, useState } from 'react'
|
||||
|
||||
export interface AddressContextValue {
|
||||
address: string | undefined
|
||||
setAddress: (address: string) => void
|
||||
}
|
||||
|
||||
const addressContext = createContext<AddressContextValue | undefined>(undefined)
|
||||
|
||||
export const useAddress = () => {
|
||||
const contextValue = useContext(addressContext)
|
||||
if (contextValue === undefined)
|
||||
throw new Error('`useAddress` must be used insisde a context `Provider`')
|
||||
return contextValue
|
||||
}
|
||||
|
||||
export const AddressProvider: FC<{ address?: string }> = ({
|
||||
children,
|
||||
address: initialAddress,
|
||||
}) => {
|
||||
const [address, setAddress] = useState<string | undefined>(initialAddress)
|
||||
useEffect(() => {
|
||||
setAddress(initialAddress)
|
||||
}, [initialAddress])
|
||||
|
||||
return (
|
||||
<addressContext.Provider value={{ address, setAddress }}>
|
||||
{children}
|
||||
</addressContext.Provider>
|
||||
)
|
||||
}
|
|
@ -1,8 +1,9 @@
|
|||
import Dexie, { Table } from 'dexie'
|
||||
|
||||
interface Account {
|
||||
import { AccountInfoCache } from '../types'
|
||||
|
||||
interface Account extends AccountInfoCache {
|
||||
index: number
|
||||
account: string
|
||||
}
|
||||
|
||||
class Accounts extends Dexie {
|
||||
|
@ -18,8 +19,11 @@ class Accounts extends Dexie {
|
|||
|
||||
const db = new Accounts()
|
||||
|
||||
export const addAccount = (index: number, account: string) =>
|
||||
db.accounts.add({ account, index })
|
||||
export const addAccount = (index: number, account: AccountInfoCache) =>
|
||||
db.accounts.add({ ...account, index })
|
||||
|
||||
export const putAccount = (index: number, account: AccountInfoCache) =>
|
||||
db.accounts.put({ ...account, index })
|
||||
|
||||
export const removeAccount = (index: number) => db.accounts.delete(index)
|
||||
|
||||
|
@ -27,7 +31,7 @@ export const getAccount = (index: number) =>
|
|||
db.accounts
|
||||
.where({ index })
|
||||
.first()
|
||||
.then(res => res?.account)
|
||||
.then(res => (res === undefined ? undefined : res))
|
||||
|
||||
export const hasAccount = async (index: number) =>
|
||||
(await db.accounts.where({ index }).count()) > 0
|
||||
|
|
|
@ -4,13 +4,15 @@ import { checkBiometrics } from './biometrics'
|
|||
import { getEncryptedSeed } from './db/encryptedSeeds'
|
||||
|
||||
const decryptSeed = async (id: 'os' | 'pin') => {
|
||||
const encryptedSeed = (await getEncryptedSeed(id))?.encryptedSeed
|
||||
const encryptedSeed = await getEncryptedSeed(id)
|
||||
const {
|
||||
// @ts-expect-error
|
||||
response: { signature: sig },
|
||||
} = await checkBiometrics()
|
||||
console.log({ sig })
|
||||
return AES.decrypt(encryptedSeed!, sig.toString()).toString(enc.Utf8)
|
||||
const decryptedSeed = AES.decrypt(encryptedSeed!, sig.toString()).toString(
|
||||
enc.Utf8
|
||||
)
|
||||
return decryptedSeed
|
||||
}
|
||||
|
||||
export default decryptSeed
|
||||
|
|
14
lib/encryptSeed.ts
Normal file
14
lib/encryptSeed.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { AES } from 'crypto-js'
|
||||
|
||||
import { checkBiometrics } from './biometrics'
|
||||
|
||||
const encryptSeed = async (seed: string, id: 'os' | 'pin' = 'os') => {
|
||||
const {
|
||||
// @ts-expect-error
|
||||
response: { signature: sig },
|
||||
} = await checkBiometrics()
|
||||
const encryptedSeed = AES.encrypt(seed, sig.toString()).toString()
|
||||
return encryptedSeed
|
||||
}
|
||||
|
||||
export default encryptSeed
|
|
@ -5,7 +5,10 @@ import { getPreference } from '../db/preferences'
|
|||
const useDarkMode = (darkMode?: boolean) => {
|
||||
useEffect(() => {
|
||||
const setDarkModeClass = async () => {
|
||||
const isDark = darkMode ?? (await getPreference('darkMode')) === true
|
||||
const isDark =
|
||||
darkMode ??
|
||||
(await getPreference('darkMode')) ??
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
const htmlClasses = document.querySelector('html')?.classList
|
||||
if (isDark) htmlClasses?.add('dark')
|
||||
else htmlClasses?.remove('dark')
|
||||
|
|
|
@ -4,7 +4,6 @@ import colors from 'tailwindcss/colors'
|
|||
|
||||
import { usePreferences } from '../context/preferencesContext'
|
||||
import genTxnUrl from '../nano/getTxnUrl'
|
||||
import { prefersDarkMode } from '../preferences/darkMode'
|
||||
|
||||
const useDrawQrCode = ({ raw, address }: { raw?: string; address: string }) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { useAddress } from '../context/addressContext'
|
||||
import { useAccounts } from '../context/accountContext'
|
||||
|
||||
const useListenToTxn = () => {
|
||||
const { address } = useAddress()
|
||||
const { accounts } = useAccounts()
|
||||
const [mostRecentTxn, setMostRecentTxn] = useState<any | undefined>(undefined)
|
||||
useEffect(() => {
|
||||
if (address !== undefined) {
|
||||
if (accounts !== undefined) {
|
||||
const ws = new WebSocket('wss://ws.mynano.ninja/')
|
||||
|
||||
ws.onopen = () => {
|
||||
|
@ -16,7 +16,7 @@ const useListenToTxn = () => {
|
|||
action: 'subscribe',
|
||||
topic: 'confirmation',
|
||||
options: {
|
||||
accounts: [address],
|
||||
accounts: [accounts],
|
||||
},
|
||||
})
|
||||
)
|
||||
|
@ -29,7 +29,7 @@ const useListenToTxn = () => {
|
|||
|
||||
return () => ws.close()
|
||||
}
|
||||
}, [address])
|
||||
}, [accounts])
|
||||
return mostRecentTxn
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import jsqr from 'jsqr'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
const useReadQrFromVideo = () => {
|
||||
const useReadQrFromVideo = (onQrCodeRead: (content: string) => void) => {
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
const [videoLive, setVideoLive] = useState(false)
|
||||
useEffect(() => {
|
||||
|
@ -36,7 +36,7 @@ const useReadQrFromVideo = () => {
|
|||
)
|
||||
|
||||
if (qr !== null) {
|
||||
console.log(qr) // todo
|
||||
onQrCodeRead(qr.data)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
47
lib/hooks/useReceiveNano.ts
Normal file
47
lib/hooks/useReceiveNano.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
import { computeWork, hashBlock } from 'nanocurrency'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
import { useAccount } from '../context/accountContext'
|
||||
import fetcher from '../fetcher'
|
||||
import receiveNano from '../nano/receiveNano'
|
||||
|
||||
const useReceiveNano = () => {
|
||||
const account = useAccount()
|
||||
|
||||
const receive = useCallback(
|
||||
async (hash: string, amount: string) => {
|
||||
if (account === undefined) return
|
||||
console.log('signing receive')
|
||||
const signedBlock = await receiveNano(
|
||||
{
|
||||
transactionHash: hash,
|
||||
walletBalanceRaw: account.balance ?? '0',
|
||||
toAddress: account.address,
|
||||
representativeAddress: account.representative ?? account.address,
|
||||
frontier:
|
||||
account.frontier ??
|
||||
'0000000000000000000000000000000000000000000000000000000000000000',
|
||||
amountRaw: amount,
|
||||
work: (await computeWork(account.frontier ?? account.publicKey))!,
|
||||
},
|
||||
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]
|
||||
)
|
||||
|
||||
return receive
|
||||
}
|
||||
|
||||
export default useReceiveNano
|
49
lib/hooks/useSendNano.ts
Normal file
49
lib/hooks/useSendNano.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import { computeWork } from 'nanocurrency'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
import { useAccount } from '../context/accountContext'
|
||||
import fetcher from '../fetcher'
|
||||
import sendNano from '../nano/sendNano'
|
||||
|
||||
const useSendNano = () => {
|
||||
const account = useAccount()
|
||||
|
||||
const send = useCallback(
|
||||
async (to: string, amount: string) => {
|
||||
if (
|
||||
account === undefined ||
|
||||
account.balance === null ||
|
||||
account.representative === null ||
|
||||
account.frontier === null
|
||||
)
|
||||
return
|
||||
const signedBlock = sendNano(
|
||||
{
|
||||
walletBalanceRaw: account.balance,
|
||||
fromAddress: account.address,
|
||||
toAddress: to,
|
||||
representativeAddress: account.representative,
|
||||
frontier: account.frontier,
|
||||
amountRaw: amount,
|
||||
work: (await computeWork(account.frontier))!,
|
||||
},
|
||||
account.index
|
||||
)
|
||||
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,
|
||||
}),
|
||||
})
|
||||
},
|
||||
[account]
|
||||
)
|
||||
|
||||
return send
|
||||
}
|
||||
|
||||
export default useSendNano
|
32
lib/hooks/useSetupAccounts.ts
Normal file
32
lib/hooks/useSetupAccounts.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { useCallback, useState } from 'react'
|
||||
|
||||
import { getAccount, putAccount } from '../db/accounts'
|
||||
import fetchAccountInfo from '../nano/fetchAccountInfo'
|
||||
import { AccountInfoCache } from '../types'
|
||||
import useSetup from './useSetup'
|
||||
|
||||
const useSetupAccounts = (skip?: boolean) => {
|
||||
const [accounts, setAccounts] = useState<
|
||||
{ [key: number]: AccountInfoCache } | undefined
|
||||
>(undefined)
|
||||
const setupAddress = useCallback(async () => {
|
||||
const dbAccount = await getAccount(0)
|
||||
if (dbAccount !== undefined) {
|
||||
setAccounts({ 0: dbAccount })
|
||||
const infoRes = await fetchAccountInfo(dbAccount.address)
|
||||
|
||||
const freshAccountInfo = {
|
||||
...dbAccount,
|
||||
frontier: 'error' in infoRes ? null : infoRes.confirmed_frontier,
|
||||
representative:
|
||||
'error' in infoRes ? null : infoRes.confirmed_representative,
|
||||
}
|
||||
setAccounts({ 0: freshAccountInfo })
|
||||
putAccount(dbAccount.index, freshAccountInfo)
|
||||
}
|
||||
}, [setAccounts])
|
||||
const { settingUp } = useSetup(setupAddress, skip)
|
||||
return { accounts, settingUp }
|
||||
}
|
||||
|
||||
export default useSetupAccounts
|
|
@ -1,16 +0,0 @@
|
|||
import { useCallback, useState } from 'react'
|
||||
|
||||
import { getAccount } from '../db/accounts'
|
||||
import useSetup from './useSetup'
|
||||
|
||||
const useSetupAddress = (skip?: boolean) => {
|
||||
const [address, setAddress] = useState<string | undefined>(undefined)
|
||||
const setupAddress = useCallback(async () => {
|
||||
const dbAddress = await getAccount(0)
|
||||
if (dbAddress !== undefined) setAddress(dbAddress)
|
||||
}, [setAddress])
|
||||
const { settingUp } = useSetup(setupAddress, skip)
|
||||
return { address, settingUp }
|
||||
}
|
||||
|
||||
export default useSetupAddress
|
|
@ -4,14 +4,16 @@ import { useRouter } from 'next/router'
|
|||
import { useCallback, useState } from 'react'
|
||||
|
||||
import { checkBiometrics, registerBiometrics } from '../biometrics'
|
||||
import { useAddress } from '../context/addressContext'
|
||||
import { useAccounts } from '../context/accountContext'
|
||||
import { addAccount } from '../db/accounts'
|
||||
import { addEncryptedSeed, hasEncryptedSeed } from '../db/encryptedSeeds'
|
||||
import addressFromSeed from '../nano/addressFromSeed'
|
||||
import encryptSeed from '../encryptSeed'
|
||||
import accountAtIndex from '../nano/accountAtIndex'
|
||||
import fetchAccountInfo from '../nano/fetchAccountInfo'
|
||||
import useSetup from './useSetup'
|
||||
|
||||
const useSetupSeed = (skip?: boolean) => {
|
||||
const { setAddress } = useAddress()
|
||||
const { setAccount } = useAccounts()
|
||||
const [seed, setSeed] = useState<
|
||||
{ seed: string; mnemonic: string } | undefined
|
||||
>(undefined)
|
||||
|
@ -29,20 +31,28 @@ const useSetupSeed = (skip?: boolean) => {
|
|||
wallet.generate(),
|
||||
registerBiometrics(),
|
||||
])
|
||||
const { address, publicKey } = accountAtIndex(generatedSeed, 0)
|
||||
|
||||
const infoRes = await fetchAccountInfo(address)
|
||||
|
||||
const account = {
|
||||
frontier: 'error' in infoRes ? null : infoRes.confirmed_frontier,
|
||||
representative:
|
||||
'error' in infoRes ? null : infoRes.confirmed_representative,
|
||||
balance: '0',
|
||||
index: 0,
|
||||
address,
|
||||
publicKey,
|
||||
}
|
||||
setSeed({ seed: generatedSeed, mnemonic })
|
||||
setAddress(addressFromSeed(generatedSeed, 0))
|
||||
await addAccount(0, addressFromSeed(generatedSeed, 0))
|
||||
setAccount(account)
|
||||
await addAccount(0, account)
|
||||
} catch {}
|
||||
}, [replace, setAddress])
|
||||
}, [replace, setAccount])
|
||||
|
||||
const storeSeed = useCallback(async () => {
|
||||
if (seed === undefined) return
|
||||
const {
|
||||
// @ts-expect-error
|
||||
response: { signature: sig },
|
||||
} = await checkBiometrics()
|
||||
const encryptedSeed = AES.encrypt(seed.seed, sig.toString()).toString()
|
||||
await addEncryptedSeed('os', encryptedSeed)
|
||||
await addEncryptedSeed('os', await encryptSeed(seed.seed))
|
||||
}, [seed])
|
||||
|
||||
const { settingUp, lazy } = useSetup(setupSeed, skip)
|
||||
|
|
6
lib/nano/accountAtIndex.ts
Normal file
6
lib/nano/accountAtIndex.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { wallet } from 'nanocurrency-web'
|
||||
|
||||
const accountAtIndex = (seed: string, index: number) =>
|
||||
wallet.accounts(seed, index, index)[0]
|
||||
|
||||
export default accountAtIndex
|
|
@ -1,6 +0,0 @@
|
|||
import { wallet } from 'nanocurrency-web'
|
||||
|
||||
const addressFromSeed = (seed: string, index: number) =>
|
||||
wallet.accounts(seed, index, index)[0].address
|
||||
|
||||
export default addressFromSeed
|
16
lib/nano/fetchAccountInfo.ts
Normal file
16
lib/nano/fetchAccountInfo.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import fetcher from '../fetcher'
|
||||
import { AccountInfoResponse } from '../types'
|
||||
|
||||
const fetchAccountInfo = (address: string) =>
|
||||
fetcher('https://mynano.ninja/api/node', {
|
||||
method: 'POST',
|
||||
headers: [['Content-Type', 'application/json']],
|
||||
body: JSON.stringify({
|
||||
action: 'account_info',
|
||||
account: address,
|
||||
representative: 'true',
|
||||
include_confirmed: 'true',
|
||||
}),
|
||||
}) as Promise<AccountInfoResponse>
|
||||
|
||||
export default fetchAccountInfo
|
|
@ -1,56 +1,19 @@
|
|||
import { computeWork, hashBlock } from 'nanocurrency'
|
||||
import { block, wallet } from 'nanocurrency-web'
|
||||
import { block } from 'nanocurrency-web'
|
||||
|
||||
import decryptSeed from '../decryptSeed'
|
||||
import fetcher from '../fetcher'
|
||||
import accountAtIndex from './accountAtIndex'
|
||||
|
||||
const receiveNano = async (myAddress: string, hash: string, amount: string) => {
|
||||
const accountInfo: any = await fetcher('https://mynano.ninja/api/node', {
|
||||
method: 'POST',
|
||||
headers: [['Content-Type', 'application/json']],
|
||||
body: JSON.stringify({
|
||||
action: 'account_info',
|
||||
account: myAddress,
|
||||
}),
|
||||
})
|
||||
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
|
||||
)
|
||||
|
||||
if ('error' in accountInfo) {
|
||||
const seed = await decryptSeed('os')
|
||||
console.log('start work')
|
||||
const work = (await computeWork(wallet.accounts(seed, 0, 0)[0].publicKey, {
|
||||
workThreshold: 'fffffe0000000000',
|
||||
}))!
|
||||
console.log('end work')
|
||||
const data = {
|
||||
walletBalanceRaw: '0',
|
||||
toAddress: myAddress,
|
||||
representativeAddress: myAddress,
|
||||
frontier:
|
||||
'0000000000000000000000000000000000000000000000000000000000000000',
|
||||
amountRaw: amount,
|
||||
transactionHash: hash,
|
||||
work,
|
||||
}
|
||||
console.log(seed)
|
||||
const signedBlock = block.receive(
|
||||
data,
|
||||
wallet.accounts(seed, 0, 0)[0].privateKey
|
||||
)
|
||||
console.log(signedBlock)
|
||||
|
||||
const res: any = await fetcher('https://mynano.ninja/api/node', {
|
||||
method: 'POST',
|
||||
headers: [['Content-Type', 'application/json']],
|
||||
body: JSON.stringify({
|
||||
action: 'process',
|
||||
json_block: 'true',
|
||||
subtype: 'open',
|
||||
block: signedBlock,
|
||||
}),
|
||||
})
|
||||
|
||||
console.log(res)
|
||||
}
|
||||
const signedBlock = block.receive(blockData, privateKey)
|
||||
return signedBlock
|
||||
}
|
||||
|
||||
export default receiveNano
|
||||
export default sendNano
|
||||
|
|
56
lib/nano/receiveNano2.ts
Normal file
56
lib/nano/receiveNano2.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
import { computeWork, hashBlock } from 'nanocurrency'
|
||||
import { block, wallet } from 'nanocurrency-web'
|
||||
|
||||
import decryptSeed from '../decryptSeed'
|
||||
import fetcher from '../fetcher'
|
||||
|
||||
const receiveNano = async (myAddress: string, hash: string, amount: string) => {
|
||||
const accountInfo: any = await fetcher('https://mynano.ninja/api/node', {
|
||||
method: 'POST',
|
||||
headers: [['Content-Type', 'application/json']],
|
||||
body: JSON.stringify({
|
||||
action: 'account_info',
|
||||
account: myAddress,
|
||||
}),
|
||||
})
|
||||
|
||||
if ('error' in accountInfo) {
|
||||
const seed = await decryptSeed('os')
|
||||
console.log('start work')
|
||||
const work = (await computeWork(wallet.accounts(seed, 0, 0)[0].publicKey, {
|
||||
workThreshold: 'fffffe0000000000',
|
||||
}))!
|
||||
console.log('end work')
|
||||
const data = {
|
||||
walletBalanceRaw: '0',
|
||||
toAddress: myAddress,
|
||||
representativeAddress: myAddress,
|
||||
frontier:
|
||||
'0000000000000000000000000000000000000000000000000000000000000000',
|
||||
amountRaw: amount,
|
||||
transactionHash: hash,
|
||||
work,
|
||||
}
|
||||
console.log(seed)
|
||||
const signedBlock = block.receive(
|
||||
data,
|
||||
wallet.accounts(seed, 0, 0)[0].privateKey
|
||||
)
|
||||
console.log(signedBlock)
|
||||
|
||||
const res: any = await fetcher('https://mynano.ninja/api/node', {
|
||||
method: 'POST',
|
||||
headers: [['Content-Type', 'application/json']],
|
||||
body: JSON.stringify({
|
||||
action: 'process',
|
||||
json_block: 'true',
|
||||
subtype: 'open',
|
||||
block: signedBlock,
|
||||
}),
|
||||
})
|
||||
|
||||
console.log(res)
|
||||
}
|
||||
}
|
||||
|
||||
export default receiveNano
|
19
lib/nano/sendNano.ts
Normal file
19
lib/nano/sendNano.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
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
|
28
lib/types.ts
28
lib/types.ts
|
@ -43,3 +43,31 @@ export interface BlocksInfoResponse {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type AccountInfoResponse =
|
||||
| {
|
||||
frontier: string
|
||||
open_block: string
|
||||
representative_block: string
|
||||
balance: string
|
||||
modified_timestamp: string
|
||||
block_count: string
|
||||
account_version: string
|
||||
confirmation_height: string
|
||||
confirmation_height_frontier: string
|
||||
confirmed_balance: '11999999999999999918751838129509869131'
|
||||
confirmed_height: '22966'
|
||||
confirmed_frontier: '80A6745762493FA21A22718ABFA4F635656A707B48B3324198AC7F3938DE6D4F'
|
||||
representative: 'nano_1gyeqc6u5j3oaxbe5qy1hyz3q745a318kh8h9ocnpan7fuxnq85cxqboapu5'
|
||||
confirmed_representative: 'nano_1gyeqc6u5j3oaxbe5qy1hyz3q745a318kh8h9ocnpan7fuxnq85cxqboapu5'
|
||||
}
|
||||
| { error: 'Account not found' }
|
||||
|
||||
export interface AccountInfoCache {
|
||||
address: string
|
||||
index: number
|
||||
publicKey: string
|
||||
frontier: string | null
|
||||
representative: string | null
|
||||
balance: string | null
|
||||
}
|
||||
|
|
|
@ -4,12 +4,12 @@ import { SWRConfig } from 'swr'
|
|||
import 'tailwindcss/tailwind.css'
|
||||
|
||||
import Layout from '../components/Layout'
|
||||
import { AddressProvider } from '../lib/context/addressContext'
|
||||
import { AddressProvider } from '../lib/context/accountContext'
|
||||
import { PreferencesProvider } from '../lib/context/preferencesContext'
|
||||
import fetcher from '../lib/fetcher'
|
||||
import useDarkMode from '../lib/hooks/useDarkMode'
|
||||
import useProtectedRoutes from '../lib/hooks/useProtectedRoutes'
|
||||
import useSetupAddress from '../lib/hooks/useSetupAddress'
|
||||
import useSetupAccounts from '../lib/hooks/useSetupAccounts'
|
||||
import useSetupChallenge from '../lib/hooks/useSetupChallenge'
|
||||
import useSetupSw from '../lib/hooks/useSetupSw'
|
||||
import '../styles/global.css'
|
||||
|
@ -19,7 +19,7 @@ const MyApp: FC<AppProps> = ({ Component, pageProps }) => {
|
|||
useSetupSw()
|
||||
useDarkMode()
|
||||
|
||||
const { address } = useSetupAddress()
|
||||
const { accounts } = useSetupAccounts()
|
||||
|
||||
const validatingCredential = useProtectedRoutes()
|
||||
|
||||
|
@ -27,7 +27,7 @@ const MyApp: FC<AppProps> = ({ Component, pageProps }) => {
|
|||
|
||||
return (
|
||||
<SWRConfig value={{ fetcher, provider: () => new Map() }}>
|
||||
<AddressProvider address={address}>
|
||||
<AddressProvider initialAccounts={accounts} initialAccountIndex={0}>
|
||||
<PreferencesProvider>
|
||||
<Layout>
|
||||
<Component {...pageProps} />
|
||||
|
|
|
@ -13,7 +13,7 @@ class MyDocument extends Document {
|
|||
|
||||
render() {
|
||||
return (
|
||||
<Html>
|
||||
<Html className="dark">
|
||||
<Head>
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link rel="apple-touch-icon" href="/images/icon-small.png" />
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import type { NextPage } from 'next'
|
||||
|
||||
import { useAddress } from '../lib/context/addressContext'
|
||||
import { useAccount } from '../lib/context/accountContext'
|
||||
import useDrawQrCode from '../lib/hooks/useDrawQrCode'
|
||||
|
||||
const MyQrCode: NextPage = () => {
|
||||
const { address } = useAddress()
|
||||
const canvasRef = useDrawQrCode({ address: address ?? '' })
|
||||
const account = useAccount()
|
||||
const canvasRef = useDrawQrCode({ address: account?.address ?? '' })
|
||||
|
||||
return (
|
||||
<div className="grid gap-6 place-content-center h-full w-full">
|
||||
|
|
|
@ -1,14 +1,20 @@
|
|||
import clsx from 'clsx'
|
||||
import type { NextPage } from 'next'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
import useReadQrFromVideo from '../lib/hooks/useReadQrFromVideo'
|
||||
|
||||
const ReadQrCode: NextPage = () => {
|
||||
const { videoLive, videoRef } = useReadQrFromVideo()
|
||||
const onQrCodeRead = useCallback((url: string) => {
|
||||
console.log(url)
|
||||
}, [])
|
||||
const { videoLive, videoRef } = useReadQrFromVideo(onQrCodeRead)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 items-center justify-center w-full h-full">
|
||||
<h1 className="text-3xl font-semibold text-center text-white">scan!</h1>
|
||||
<h1 className="text-3xl font-semibold text-center text-purple-50">
|
||||
scan!
|
||||
</h1>
|
||||
<video
|
||||
className={clsx('rounded shadow-md', { hidden: !videoLive })}
|
||||
ref={videoRef}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { useRouter } from 'next/router'
|
|||
const Welcome: NextPage = () => {
|
||||
const { push } = useRouter()
|
||||
return (
|
||||
<div className="w-full text-center flex flex-col justify-center h-full">
|
||||
<div className="w-full text-center flex flex-col justify-center h-full text-purple-50">
|
||||
<h1 className="text-4xl mb-3 font-bold">hey!</h1>
|
||||
<p className="text-xl font-medium mb-2">
|
||||
do you already have
|
||||
|
@ -22,11 +22,11 @@ const Welcome: NextPage = () => {
|
|||
and will only be decrypted for a few moments to send nano
|
||||
</aside>
|
||||
<div className="flex flex-col gap-3 justify-center w-full mb-6">
|
||||
<button className="dark:bg-gray-900 bg-white dark:hover:bg-gray-800 py-2 px-5 rounded text-lg font-bold shadow-lg hover:shadow-md active:shadow transition-all duration-100">
|
||||
<button className="dark:bg-gray-900 bg-purple-50 dark:hover:bg-gray-800 py-2 px-5 rounded text-lg font-bold shadow-lg hover:shadow-md active:shadow transition-all duration-100 text-gray-900">
|
||||
i have a passphrase/seed
|
||||
</button>
|
||||
<button
|
||||
className="dark:bg-gray-900 bg-white dark:hover:bg-gray-800 py-2 px-5 rounded text-lg font-bold shadow-lg hover:shadow-md active:shadow transition-all duration-100"
|
||||
className="dark:bg-gray-900 bg-purple-50 dark:hover:bg-gray-800 py-2 px-5 rounded text-lg font-bold shadow-lg hover:shadow-md active:shadow transition-all duration-100 text-gray-900"
|
||||
onClick={() => push('/welcome/new')}
|
||||
>
|
||||
what's a passphrase?
|
||||
|
|
|
@ -4,11 +4,12 @@ import { useRouter } from 'next/router'
|
|||
const Done: NextPage = () => {
|
||||
const { push } = useRouter()
|
||||
return (
|
||||
<div className="flex flex-col h-full justify-start items-center text-center px-4 gap-2">
|
||||
<div className="flex flex-col h-full justify-start items-center text-center px-4 gap-2 text-purple-50">
|
||||
<h1 className="text-9xl font-extrabold">3</h1>
|
||||
<p className="text-3xl">you're done!</p>
|
||||
<p className="text-xl">
|
||||
all the buttons are now enabled and you can start using zep and nano!
|
||||
all the buttons are now enabled and you can start using <b>zep</b> and{' '}
|
||||
<b>nano</b>!
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -5,7 +5,7 @@ import { useEffect, useState } from 'react'
|
|||
import useSetupSeed from '../../../lib/hooks/useSetupSeed'
|
||||
|
||||
const New: NextPage = () => {
|
||||
const { lazy, seed, storeSeed } = useSetupSeed()
|
||||
const { lazy, seed, storeSeed } = useSetupSeed(true)
|
||||
const { push } = useRouter()
|
||||
const [storing, setStoring] = useState(false)
|
||||
useEffect(() => {
|
||||
|
@ -18,8 +18,13 @@ const New: NextPage = () => {
|
|||
copyAndGoToStore()
|
||||
}, [seed, push])
|
||||
|
||||
const onStoreClick = async () => {
|
||||
await storeSeed()
|
||||
push('/welcome/new/done')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full justify-start items-center text-center px-4 gap-2">
|
||||
<div className="flex flex-col h-full justify-start items-center text-center px-4 gap-2 text-purple-50">
|
||||
<h1 className="text-9xl font-extrabold">{!storing ? 1 : 2}</h1>
|
||||
{!storing ? (
|
||||
<>
|
||||
|
@ -27,7 +32,7 @@ const New: NextPage = () => {
|
|||
generate a <b>passphrase</b> and copy it to your clipboard
|
||||
</p>
|
||||
<button
|
||||
className="dark:bg-gray-900 dark:text-purple-100 py-2 px-5 rounded text-2xl"
|
||||
className="dark:bg-gray-900 dark:text-purple-100 py-2 px-5 rounded text-xl bg-purple-50 font-bold text-gray-900"
|
||||
onClick={lazy}
|
||||
>
|
||||
generate passphrase
|
||||
|
@ -45,8 +50,8 @@ const New: NextPage = () => {
|
|||
store the <b>passphrase</b> securely in zep
|
||||
</p>
|
||||
<button
|
||||
className="dark:bg-gray-900 dark:text-purple-100 py-2 px-5 rounded text-2xl"
|
||||
onClick={storeSeed}
|
||||
className="dark:bg-gray-900 dark:text-purple-100 py-2 px-5 rounded text-xl bg-purple-50 font-bold text-gray-900"
|
||||
onClick={onStoreClick}
|
||||
>
|
||||
store passphrase
|
||||
</button>
|
||||
|
|
3
seeds.json
Normal file
3
seeds.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
[
|
||||
"expand target gospel goose oppose acquire genius hurdle trade size huge pact square silk canal bar curve shallow pistol push crowd glory slice news"
|
||||
]
|
Reference in a new issue