From 20e08049688b2700405851e43f35bc597de2869f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20C=2E=20Morency?= <1102868+fmorency@users.noreply.github.com> Date: Mon, 2 Dec 2024 15:24:37 -0500 Subject: [PATCH] feat: better history (#98) --- .../components/__tests__/historyBox.test.tsx | 23 +- components/bank/components/historyBox.tsx | 121 ++++++++-- components/bank/modals/txInfo.tsx | 41 +++- components/factory/modals/denomInfo.tsx | 23 +- components/react/endpointSelector.tsx | 118 +++++++++- hooks/useQueries.ts | 211 +++++++++++------- pages/bank.tsx | 10 +- store/endpointStore.ts | 12 +- tests/mock.ts | 4 +- 9 files changed, 422 insertions(+), 141 deletions(-) diff --git a/components/bank/components/__tests__/historyBox.test.tsx b/components/bank/components/__tests__/historyBox.test.tsx index 0bbbd209..32683f2d 100644 --- a/components/bank/components/__tests__/historyBox.test.tsx +++ b/components/bank/components/__tests__/historyBox.test.tsx @@ -20,7 +20,7 @@ mock.module('@/hooks', () => ({ ], }, }), - useSendTxIncludingAddressQuery: () => ({ + useGetFilteredTxAndSuccessfulProposals: () => ({ sendTxs: mockTransactions, totalPages: 1, isLoading: false, @@ -31,19 +31,16 @@ mock.module('@/hooks', () => ({ describe('HistoryBox', () => { afterEach(() => { cleanup(); + mock.restore(); }); test('renders correctly', () => { - renderWithChainProvider( - - ); + renderWithChainProvider(); expect(screen.getByText('Transaction History')).toBeTruthy(); }); test('displays transactions', () => { - renderWithChainProvider( - - ); + renderWithChainProvider(); const sentText = screen.getByText('Sent'); const receivedText = screen.getByText('Received'); @@ -53,9 +50,7 @@ describe('HistoryBox', () => { }); test('opens modal when clicking on a transaction', () => { - renderWithChainProvider( - - ); + renderWithChainProvider(); const transactionElement = screen.getByText('Sent').closest('div[role="button"]'); @@ -66,9 +61,7 @@ describe('HistoryBox', () => { }); test('formats amount correctly', () => { - renderWithChainProvider( - - ); + renderWithChainProvider(); const sentAmount = screen.queryByText('-1 TOKEN'); const receivedAmount = screen.queryByText('+2 TOKEN'); @@ -78,9 +71,7 @@ describe('HistoryBox', () => { }); test('displays both sent and received transactions', () => { - renderWithChainProvider( - - ); + renderWithChainProvider(); const sentText = screen.getByText('Sent'); const receivedText = screen.getByText('Received'); diff --git a/components/bank/components/historyBox.tsx b/components/bank/components/historyBox.tsx index 53a364a9..42f0dae7 100644 --- a/components/bank/components/historyBox.tsx +++ b/components/bank/components/historyBox.tsx @@ -1,15 +1,18 @@ -import React, { useState, useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import { TruncatedAddressWithCopy } from '@/components/react/addressCopy'; import TxInfoModal from '../modals/txInfo'; import { shiftDigits, truncateString } from '@/utils'; -import { formatDenom } from '@/components'; -import { useTokenFactoryDenomsMetadata } from '@/hooks'; -import { SendIcon, ReceiveIcon } from '@/components/icons'; - -import { DenomImage } from '@/components'; -import { useSendTxIncludingAddressQuery } from '@/hooks'; +import { BurnIcon, DenomImage, formatDenom, MintIcon } from '@/components'; +import { + HistoryTxType, + useGetFilteredTxAndSuccessfulProposals, + useTokenFactoryDenomsMetadata, +} from '@/hooks'; +import { ReceiveIcon, SendIcon } from '@/components/icons'; +import { useEndpointStore } from '@/store/endpointStore'; interface Transaction { + tx_type: HistoryTxType; from_address: string; to_address: string; amount: Array<{ amount: string; denom: string }>; @@ -45,23 +48,24 @@ function formatLargeNumber(num: number): string { export function HistoryBox({ isLoading: initialLoading, - send, address, }: { isLoading: boolean; - send: TransactionGroup[]; address: string; }) { const [selectedTx, setSelectedTx] = useState(null); const [currentPage, setCurrentPage] = useState(1); const pageSize = 10; + const { selectedEndpoint } = useEndpointStore(); + const indexerUrl = selectedEndpoint?.indexer || ''; + const { sendTxs, totalPages, isLoading: txLoading, isError, - } = useSendTxIncludingAddressQuery(address, undefined, currentPage, pageSize); + } = useGetFilteredTxAndSuccessfulProposals(indexerUrl, address, currentPage, pageSize); const isLoading = initialLoading || txLoading; @@ -91,6 +95,71 @@ export function HistoryBox({ return groups; }, [sendTxs]); + function getTransactionIcon(tx: TransactionGroup, address: string) { + if (tx.data.tx_type === HistoryTxType.SEND) { + return tx.data.from_address === address ? : ; + } else if (tx.data.tx_type === HistoryTxType.MINT || tx.data.tx_type === HistoryTxType.PAYOUT) { + return ( + + ); + } else if ( + tx.data.tx_type === HistoryTxType.BURN || + tx.data.tx_type === HistoryTxType.BURN_HELD_BALANCE + ) { + return ( + + ); + } + return null; + } + + // Get the history message based on the transaction type + function getTransactionMessage(tx: TransactionGroup, address: string) { + if (tx.data.tx_type === HistoryTxType.SEND) { + return tx.data.from_address === address ? 'Sent' : 'Received'; + } else if (tx.data.tx_type === HistoryTxType.MINT || tx.data.tx_type === HistoryTxType.PAYOUT) { + return 'Minted'; + } else if ( + tx.data.tx_type === HistoryTxType.BURN || + tx.data.tx_type === HistoryTxType.BURN_HELD_BALANCE + ) { + return 'Burned'; + } + return 'Unsupported'; + } + + // Get the transaction direction based on the transaction type + function getTransactionPlusMinus(tx: TransactionGroup, address: string) { + if (tx.data.tx_type === HistoryTxType.SEND) { + return tx.data.from_address === address ? '-' : '+'; + } else if (tx.data.tx_type === HistoryTxType.MINT || tx.data.tx_type === HistoryTxType.PAYOUT) { + return '+'; + } else if ( + tx.data.tx_type === HistoryTxType.BURN || + tx.data.tx_type === HistoryTxType.BURN_HELD_BALANCE + ) { + return '-'; + } + return '!!'; + } + + // Get the transaction color based on the transaction type and direction + function getTransactionColor(tx: TransactionGroup, address: string) { + if (tx.data.tx_type === HistoryTxType.SEND) { + return tx.data.from_address === address ? 'text-red-500' : 'text-green-500'; + } else if (tx.data.tx_type === HistoryTxType.MINT || tx.data.tx_type === HistoryTxType.PAYOUT) { + return 'text-green-500'; + } else if ( + tx.data.tx_type === HistoryTxType.BURN || + tx.data.tx_type === HistoryTxType.BURN_HELD_BALANCE + ) { + return 'text-red-500'; + } + return null; + } + return (
@@ -206,7 +275,7 @@ export function HistoryBox({ >
- {tx.data.from_address === address ? : } + {getTransactionIcon(tx, address)}
{tx.data.amount.map((amt, index) => { @@ -217,7 +286,7 @@ export function HistoryBox({

- {tx.data.from_address === address ? 'Sent' : 'Received'} + {getTransactionMessage(tx, address)}

{tx.data.amount.map((amt, index) => { @@ -234,22 +303,26 @@ export function HistoryBox({

e.stopPropagation()}> - + {tx.data.from_address.startsWith('manifest1') ? ( + + ) : ( +
+ {tx.data.from_address} +
+ )}
-

- {tx.data.from_address === address ? '-' : '+'} +

+ {getTransactionPlusMinus(tx, address)} {tx.data.amount .map(amt => { const metadata = metadatas?.metadatas.find( diff --git a/components/bank/modals/txInfo.tsx b/components/bank/modals/txInfo.tsx index 16931956..c6bd6c8f 100644 --- a/components/bank/modals/txInfo.tsx +++ b/components/bank/modals/txInfo.tsx @@ -3,6 +3,7 @@ import { TruncatedAddressWithCopy } from '@/components/react/addressCopy'; import { formatDenom, TransactionGroup } from '@/components'; import { FaExternalLinkAlt } from 'react-icons/fa'; import { shiftDigits } from '@/utils'; +import { useEndpointStore } from '@/store/endpointStore'; interface TxInfoModalProps { tx: TransactionGroup; @@ -11,6 +12,9 @@ interface TxInfoModalProps { } export default function TxInfoModal({ tx, modalId }: TxInfoModalProps) { + const { selectedEndpoint } = useEndpointStore(); + const explorerUrl = selectedEndpoint?.explorer || ''; + function formatDate(dateString: string): string { const date = new Date(dateString); return date.toLocaleString('en-US', { @@ -39,13 +43,36 @@ export default function TxInfoModal({ tx, modalId }: TxInfoModalProps) {

- - - + + +
- - + +

VALUE @@ -74,10 +101,12 @@ export default function TxInfoModal({ tx, modalId }: TxInfoModalProps) { function InfoItem({ label, value, + explorerUrl, isAddress = false, }: { label: string; value: string; + explorerUrl: string; isAddress?: boolean; }) { return ( @@ -88,7 +117,7 @@ function InfoItem({

@@ -70,11 +85,13 @@ export const DenomInfoModal: React.FC<{ function InfoItem({ label, value, + explorerUrl, isAddress = false, className = '', }: { label: string; value: string; + explorerUrl: string; isAddress?: boolean; className?: string; }) { @@ -86,7 +103,7 @@ function InfoItem({
{ } }; +const validateIndexerEndpoint = async (url: string) => { + console.log('Validating Indexer endpoint:', url); + if (!url) return false; + try { + const endpoint = url.startsWith('http') ? url : `https://${url}`; + const baseUrl = endpoint.endsWith('/') ? endpoint.slice(0, -1) : endpoint; + const indexerUrl = `${baseUrl}`; + + console.log('Making Indexer request to:', indexerUrl); + + const response = await fetch(indexerUrl); + console.log('Indexer Response status:', response.status); + + if (!response.ok) { + console.log('Indexer Response not ok'); + return false; + } + + return response.ok; + } catch (error) { + console.error('Indexer Validation error:', error); + return false; + } +}; + +const validateExplorerEndpoint = async (url: string) => { + console.log('Validating Explorer endpoint:', url); + if (!url) return false; + try { + // const endpoint = url.startsWith('http') ? url : `https://${url}`; + // const baseUrl = endpoint.endsWith('/') ? endpoint.slice(0, -1) : endpoint; + // const explorerUrl = `${baseUrl}`; + + // console.log('Making Explorer request to:', explorerUrl); + // + // const response = await fetch(explorerUrl); + // console.log('Explorer Response status:', response.status); + // + // if (!response.ok) { + // console.log('Explorer Response not ok'); + // return false; + // } + // + // return response.ok; + return true; + } catch (error) { + console.error('Explorer Validation error:', error); + return false; + } +}; + const EndpointSchema = Yup.object().shape({ rpc: Yup.string().required('RPC endpoint is required').test({ name: 'rpc-validation', @@ -89,6 +140,16 @@ const EndpointSchema = Yup.object().shape({ message: 'API endpoint is not responding', test: validateAPIEndpoint, }), + indexer: Yup.string().required('Indexer endpoint is required').test({ + name: 'indexer-validation', + message: 'Indexer endpoint is not responding', + test: validateIndexerEndpoint, + }), + explorer: Yup.string().required('Explorer endpoint is required').test({ + name: 'explorer-validation', + message: 'Explorer endpoint is not responding', + test: validateExplorerEndpoint, + }), }); function SSREndpointSelector() { @@ -119,9 +180,20 @@ function SSREndpointSelector() { enabled: true, }); - const handleCustomEndpointSubmit = async (values: { rpc: string; api: string }) => { + const handleCustomEndpointSubmit = async (values: { + rpc: string; + api: string; + indexer: string; + explorer: string; + }) => { const rpcUrl = values.rpc.startsWith('http') ? values.rpc : `https://${values.rpc}`; const apiUrl = values.api.startsWith('http') ? values.api : `https://${values.api}`; + const indexerUrl = values.indexer.startsWith('http') + ? values.indexer + : `https://${values.indexer}`; + const explorerUrl = values.explorer.startsWith('http') + ? values.explorer + : `https://${values.explorer}`; try { const [isRPCValid, isAPIValid] = await Promise.all([ @@ -133,7 +205,7 @@ function SSREndpointSelector() { throw new Error('Endpoint validation failed'); } - await addEndpoint(rpcUrl, apiUrl); + await addEndpoint(rpcUrl, apiUrl, indexerUrl, explorerUrl); setToastMessage({ type: 'alert-success', title: 'Custom endpoint added', @@ -146,7 +218,7 @@ function SSREndpointSelector() { if (error instanceof Error) { if (error.message.includes('Invalid URL')) { - errorMessage = 'Invalid URL format. Please check both RPC and API URLs.'; + errorMessage = 'Invalid URL format. Please check all URLs.'; } else if (error.message.includes('Network error')) { errorMessage = 'Network error. Please check your internet connection and try again.'; } else if (error.message.includes('Timeout')) { @@ -305,7 +377,7 @@ function SSREndpointSelector() {
{ try { @@ -359,6 +431,44 @@ function SSREndpointSelector() {
+
+ +
+ +
+
+ +
+ +
+ +
+
+