feat: transactions page :)

This commit is contained in:
Filipe Medeiros 2021-11-28 02:34:32 +00:00
parent d9d0f3fcf5
commit 69d83ef95c
10 changed files with 210 additions and 76 deletions

View file

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

View file

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

View file

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

View 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

View 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

View 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

View file

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

View file

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

View file

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

View 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