feat: transactions page :)
This commit is contained in:
parent
d9d0f3fcf5
commit
69d83ef95c
|
@ -1,8 +1,12 @@
|
|||
import { ClockIcon } from '@heroicons/react/outline'
|
||||
import { DownloadIcon, UploadIcon } from '@heroicons/react/solid'
|
||||
import clsx from 'clsx'
|
||||
import { tools } from 'nanocurrency-web'
|
||||
import { FC } from 'react'
|
||||
import type { FC } from 'react'
|
||||
|
||||
import { useCurrentAccount } from '../lib/context/accountContext'
|
||||
import useAccountHistory from '../lib/hooks/useAccountHistory'
|
||||
import useAccountReceivable from '../lib/hooks/useAccountReceivable'
|
||||
import useReceiveNano from '../lib/hooks/useReceiveNano'
|
||||
|
||||
const rawToNanoDisplay = (raw: string) =>
|
||||
|
@ -15,78 +19,71 @@ export interface Props {
|
|||
className?: string
|
||||
}
|
||||
|
||||
const mockAddressBook: Record<string, { displayName: string }> = {
|
||||
nano_3cpz7oh9qr5b7obbcb5867omqf8esix4sdd5w6mh8kkknamjgbnwrimxsaaf: {
|
||||
displayName: 'kraken',
|
||||
},
|
||||
}
|
||||
|
||||
const RecentTransactions: FC<Props> = ({ className }) => {
|
||||
const { receive } = useReceiveNano()
|
||||
|
||||
const {
|
||||
accountReceivable,
|
||||
blocksInfo: receivableBlocksInfo,
|
||||
loading: loadingReceivable,
|
||||
} = useAccountReceivable()
|
||||
const { accountHistory, loading: loadingHistory } = useAccountHistory()
|
||||
|
||||
const account = useCurrentAccount()
|
||||
|
||||
const hasReceivable =
|
||||
!loadingReceivable && Object.keys(accountReceivable.blocks).length > 0
|
||||
const hasHistory = !loadingHistory && accountHistory.history.length > 0
|
||||
|
||||
const receivable = Object.entries(
|
||||
accountReceivable?.blocks[account?.address ?? ''] ?? {}
|
||||
).map(([hash, { amount, source }]) => ({
|
||||
hash,
|
||||
amount,
|
||||
from: source,
|
||||
}))
|
||||
|
||||
return (
|
||||
<div className={clsx('flex flex-col gap-6 w-full', className)}>
|
||||
{false && (
|
||||
{hasReceivable && (
|
||||
<section className="flex flex-col gap-3 w-full items-center">
|
||||
<h2 className="text-2xl font-semibold text-purple-50">pending</h2>
|
||||
<h2 className="text-2xl font-semibold text-purple-50">receivable</h2>
|
||||
<ol className="flex flex-col gap-3 w-full">
|
||||
{[
|
||||
{
|
||||
hash: 'string',
|
||||
send: 'string',
|
||||
receivable: true,
|
||||
amount: '0',
|
||||
account: '',
|
||||
timestamp: '',
|
||||
},
|
||||
].map(txn => (
|
||||
{receivable.map(receivable => (
|
||||
<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'
|
||||
)}
|
||||
key={receivable.hash}
|
||||
className="bg-purple-50 shadow rounded px-3 py-3 flex items-center justify-between gap-2 text-black border-r-4 border-blue-500"
|
||||
>
|
||||
<button
|
||||
className="contents"
|
||||
onClick={() => receive(txn.hash, txn.amount)}
|
||||
onClick={() => receive(receivable.hash, receivable.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'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<ClockIcon className="w-6 flex-shrink-0 text-blue-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(Number(txn.timestamp) * 1000)}{' '}
|
||||
-{' '}
|
||||
{mockAddressBook[txn.account]?.displayName ?? (
|
||||
<span className="text-xs">{txn.account}</span>
|
||||
)}
|
||||
}).format(
|
||||
Number(
|
||||
receivableBlocksInfo!.blocks[receivable.hash]
|
||||
.local_timestamp
|
||||
) * 1000
|
||||
)}{' '}
|
||||
- {<span className="text-xs">{receivable.from}</span>}
|
||||
</div>
|
||||
<span className="flex-shrink-0 font-medium">
|
||||
Ӿ{' '}
|
||||
{rawToNanoDisplay(txn.amount) === 'small' ? (
|
||||
{rawToNanoDisplay(receivable.amount) === 'small' ? (
|
||||
'<.01'
|
||||
) : rawToNanoDisplay(txn.amount).startsWith('0.') ? (
|
||||
) : rawToNanoDisplay(receivable.amount).startsWith('0.') ? (
|
||||
<>
|
||||
<span className="text-sm font-semibold">0</span>
|
||||
{rawToNanoDisplay(txn.amount).substring(1)}
|
||||
{rawToNanoDisplay(receivable.amount).substring(1)}
|
||||
</>
|
||||
) : (
|
||||
rawToNanoDisplay(txn.amount)
|
||||
rawToNanoDisplay(receivable.amount)
|
||||
)}
|
||||
</span>
|
||||
</button>
|
||||
|
@ -95,8 +92,8 @@ const RecentTransactions: FC<Props> = ({ className }) => {
|
|||
</ol>
|
||||
</section>
|
||||
)}
|
||||
{false && false && <hr />}
|
||||
{false && (
|
||||
{hasHistory && hasReceivable && <hr />}
|
||||
{hasHistory && (
|
||||
<section className="flex flex-col gap-3 w-full items-center">
|
||||
<h2 className="text-2xl font-semibold text-purple-50">
|
||||
recent transactions
|
||||
|
@ -140,10 +137,7 @@ const RecentTransactions: FC<Props> = ({ className }) => {
|
|||
month: '2-digit',
|
||||
year: '2-digit',
|
||||
}).format(Number(txn.timestamp) * 1000)}{' '}
|
||||
-{' '}
|
||||
{mockAddressBook[txn.account]?.displayName ?? (
|
||||
<span className="text-xs">{txn.account}</span>
|
||||
)}
|
||||
- {<span className="text-xs">{txn.account}</span>}
|
||||
</div>
|
||||
<span className="flex-shrink-0 font-medium">
|
||||
Ӿ{' '}
|
||||
|
@ -164,7 +158,7 @@ const RecentTransactions: FC<Props> = ({ className }) => {
|
|||
</ol>
|
||||
</section>
|
||||
)}
|
||||
{!false && !false && (
|
||||
{!hasReceivable && !hasHistory && (
|
||||
<div className="text-center pt-8 text-purple-50">
|
||||
<p className="pb-4">no transactions yet...</p>
|
||||
<p>
|
||||
|
|
|
@ -2,15 +2,15 @@ import db from '.'
|
|||
import { EncryptedSeedId } from './types'
|
||||
|
||||
export const addEncryptedSeed = (id: EncryptedSeedId, encryptedSeed: string) =>
|
||||
db()!.add('encryptedSeed', { id, encryptedSeed })
|
||||
db()!.add('encryptedSeeds', { id, encryptedSeed })
|
||||
|
||||
export const removeEncryptedSeed = (id: EncryptedSeedId) =>
|
||||
db()!.delete('encryptedSeed', id)
|
||||
db()!.delete('encryptedSeeds', id)
|
||||
|
||||
export const getEncryptedSeed = (id: EncryptedSeedId) =>
|
||||
db()!.get('encryptedSeed', id)
|
||||
db()!.get('encryptedSeeds', id)
|
||||
|
||||
export const hasEncryptedSeed = async (id: EncryptedSeedId) =>
|
||||
db()!
|
||||
.count('encryptedSeed', id)
|
||||
.count('encryptedSeeds', id)
|
||||
.then(count => count === 1)
|
||||
|
|
|
@ -28,7 +28,7 @@ interface Schema extends DBSchema {
|
|||
address: string
|
||||
}
|
||||
}
|
||||
encryptedSeed: {
|
||||
encryptedSeeds: {
|
||||
key: EncryptedSeedKey
|
||||
value: EncryptedSeedValue
|
||||
}
|
||||
|
@ -49,7 +49,7 @@ export const openDb = async (version = 1) => {
|
|||
keyPath: 'id',
|
||||
autoIncrement: true,
|
||||
})
|
||||
db.createObjectStore('encryptedSeed', {
|
||||
db.createObjectStore('encryptedSeeds', {
|
||||
keyPath: 'id',
|
||||
autoIncrement: true,
|
||||
})
|
||||
|
|
30
lib/hooks/useAccountHistory.ts
Normal file
30
lib/hooks/useAccountHistory.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { useCurrentAccount } from '../context/accountContext'
|
||||
import type { AccountHistoryResponse } from '../types'
|
||||
import fetchAccountHistory from '../xno/fetchAccountHistory'
|
||||
|
||||
type ReturnValue =
|
||||
| {
|
||||
accountHistory: undefined
|
||||
loading: true
|
||||
}
|
||||
| { accountHistory: AccountHistoryResponse; loading: false }
|
||||
|
||||
const useAccountHistory = (): ReturnValue => {
|
||||
const [accountHistory, setAccountHistory] = useState<
|
||||
AccountHistoryResponse | undefined
|
||||
>(undefined)
|
||||
|
||||
const account = useCurrentAccount()
|
||||
|
||||
useEffect(() => {
|
||||
if (account !== undefined)
|
||||
fetchAccountHistory(account.address).then(setAccountHistory)
|
||||
}, [account])
|
||||
|
||||
const loading = accountHistory === undefined
|
||||
return { accountHistory, loading } as ReturnValue
|
||||
}
|
||||
|
||||
export default useAccountHistory
|
30
lib/hooks/useAccountInfo.ts
Normal file
30
lib/hooks/useAccountInfo.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { useCurrentAccount } from '../context/accountContext'
|
||||
import { AccountInfoResponse } from '../types'
|
||||
import fetchAccountInfo from '../xno/fetchAccountInfo'
|
||||
|
||||
type ReturnValue =
|
||||
| {
|
||||
accountInfo: undefined
|
||||
loading: true
|
||||
}
|
||||
| { accountInfo: AccountInfoResponse; loading: false }
|
||||
|
||||
const useAccountInfo = (): ReturnValue => {
|
||||
const [accountInfo, setAccountInfo] = useState<
|
||||
AccountInfoResponse | undefined
|
||||
>(undefined)
|
||||
|
||||
const account = useCurrentAccount()
|
||||
|
||||
useEffect(() => {
|
||||
if (account !== undefined)
|
||||
fetchAccountInfo(account.address).then(setAccountInfo)
|
||||
}, [account])
|
||||
|
||||
const loading = accountInfo === undefined
|
||||
return { accountInfo, loading } as ReturnValue
|
||||
}
|
||||
|
||||
export default useAccountInfo
|
53
lib/hooks/useAccountReceivable.ts
Normal file
53
lib/hooks/useAccountReceivable.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { useCurrentAccount } from '../context/accountContext'
|
||||
import type { AccountReceivableResponse, BlocksInfoResponse } from '../types'
|
||||
import fetchAccountReceivable from '../xno/fetchAccountReceivable'
|
||||
import fetchBlocksInfo from '../xno/fetchBlocksInfo'
|
||||
|
||||
type ReturnValue =
|
||||
| {
|
||||
accountReceivable: undefined
|
||||
blocksInfo: undefined
|
||||
loading: true
|
||||
}
|
||||
| {
|
||||
accountReceivable: AccountReceivableResponse
|
||||
blocksInfo: BlocksInfoResponse
|
||||
loading: false
|
||||
}
|
||||
|
||||
const useAccountReceivable = (): ReturnValue => {
|
||||
const [accountReceivableWithInfo, setAccountReceivableWithInfo] = useState<{
|
||||
accountReceivable: AccountReceivableResponse | undefined
|
||||
blocksInfo: BlocksInfoResponse | undefined
|
||||
}>({ accountReceivable: undefined, blocksInfo: undefined })
|
||||
|
||||
const account = useCurrentAccount()
|
||||
|
||||
useEffect(() => {
|
||||
if (account !== undefined) {
|
||||
const fetchReceivable = async () => {
|
||||
const receivableBlocks = await fetchAccountReceivable(account.address)
|
||||
const blocksInfo = await fetchBlocksInfo(
|
||||
Object.values(receivableBlocks.blocks)
|
||||
.map(blocks => Object.keys(blocks))
|
||||
.flat()
|
||||
)
|
||||
setAccountReceivableWithInfo({
|
||||
accountReceivable: receivableBlocks,
|
||||
blocksInfo,
|
||||
})
|
||||
}
|
||||
|
||||
fetchReceivable()
|
||||
}
|
||||
}, [account])
|
||||
|
||||
const loading =
|
||||
accountReceivableWithInfo.accountReceivable === undefined &&
|
||||
accountReceivableWithInfo.blocksInfo === undefined
|
||||
return { ...accountReceivableWithInfo, loading } as ReturnValue
|
||||
}
|
||||
|
||||
export default useAccountReceivable
|
|
@ -3,7 +3,14 @@ import { useEffect, useState } from 'react'
|
|||
import fetcher from '../fetcher'
|
||||
import { XnoPriceResponse } from '../types'
|
||||
|
||||
const useXnoPrice = () => {
|
||||
type ReturnValue =
|
||||
| {
|
||||
xnoPrice: undefined
|
||||
loading: true
|
||||
}
|
||||
| { xnoPrice: number; loading: false }
|
||||
|
||||
const useXnoPrice = (): ReturnValue => {
|
||||
const [xnoPrice, setXnoPrice] = useState<number | undefined>()
|
||||
useEffect(() => {
|
||||
// todo get url out of here
|
||||
|
@ -11,7 +18,8 @@ const useXnoPrice = () => {
|
|||
setXnoPrice(res.price)
|
||||
)
|
||||
}, [])
|
||||
return { xnoPrice, loading: xnoPrice === undefined }
|
||||
const loading = xnoPrice === undefined
|
||||
return { xnoPrice, loading } as ReturnValue
|
||||
}
|
||||
|
||||
export default useXnoPrice
|
||||
|
|
|
@ -16,9 +16,14 @@ export interface AccountHistoryResponse {
|
|||
previous?: string
|
||||
}
|
||||
|
||||
export interface AccountPendingResponse {
|
||||
export interface AccountReceivableResponse {
|
||||
blocks: {
|
||||
[key: string]: string[]
|
||||
[destinationAddress: string]: {
|
||||
[blockHash: string]: {
|
||||
amount: string
|
||||
source: string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,27 +1,27 @@
|
|||
import fetcher from '../fetcher'
|
||||
import { AccountHistoryResponse } from '../types'
|
||||
import type { AccountReceivableResponse } from '../types'
|
||||
|
||||
const fetchAccountReceivable = (
|
||||
const _fetchAccountReceivable = (
|
||||
address: string,
|
||||
count = 20,
|
||||
head = undefined,
|
||||
version22 = false
|
||||
) =>
|
||||
fetcher('https://mynano.ninja/api/node', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
action: version22 ? 'account_pending' : 'account_receivable',
|
||||
account: address,
|
||||
head,
|
||||
action: version22 ? 'accounts_pending' : 'accounts_receivable',
|
||||
accounts: [address],
|
||||
count,
|
||||
threshold: '0',
|
||||
source: 'true',
|
||||
},
|
||||
}) as Promise<AccountHistoryResponse>
|
||||
}) as Promise<AccountReceivableResponse>
|
||||
|
||||
// most nodes haven't upgraded yet https://docs.nano.org/commands/rpc-protocol/#accounts_pending
|
||||
// this will be the future api for this function
|
||||
const withVersionFallback = (address: string, count = 20, head = undefined) =>
|
||||
fetchAccountReceivable(address, count, head).catch(() =>
|
||||
fetchAccountReceivable(address, count, head, true)
|
||||
const fetchAccountReceivable = async (address: string, count = 20) =>
|
||||
_fetchAccountReceivable(address, count).catch(() =>
|
||||
_fetchAccountReceivable(address, count, true)
|
||||
)
|
||||
|
||||
export default withVersionFallback
|
||||
export default fetchAccountReceivable
|
||||
|
|
14
lib/xno/fetchBlocksInfo.ts
Normal file
14
lib/xno/fetchBlocksInfo.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import fetcher from '../fetcher'
|
||||
import type { BlocksInfoResponse } from '../types'
|
||||
|
||||
const fetchBlocksInfo = (hashes: string[]) =>
|
||||
fetcher('https://mynano.ninja/api/node', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
action: 'blocks_info',
|
||||
json_block: 'true',
|
||||
hashes,
|
||||
},
|
||||
}) as Promise<BlocksInfoResponse>
|
||||
|
||||
export default fetchBlocksInfo
|
Reference in a new issue