diff --git a/.env.example b/.env.example
index 9dc7eb7..a1d86ab 100644
--- a/.env.example
+++ b/.env.example
@@ -1,2 +1,3 @@
+VITE_ENABLE_PROXY=
VITE_QLI_API_URL=
VITE_ARCHIVER_API_URL=
diff --git a/README.md b/README.md
index 0f3cc75..e4a693b 100644
--- a/README.md
+++ b/README.md
@@ -16,8 +16,7 @@ cd
### Step 2: Configure Environment Variables
-Before running the project, you must configure environment-specific variables for development and
-production environments.
+Before running the project, you must configure environment-specific variables for development environment.
- **Development Environment**:
@@ -30,25 +29,11 @@ production environments.
Open this file and add the development API URL:
```
+ VITE_ENABLE_PROXY=true
VITE_QLI_API_URL=/dev-proxy-qli-api
VITE_ARCHIVER_API_URL=/dev-proxy-archiver-api
```
-- **Production Environment**:
-
- Copy the `.env.example` file, renaming it to `.env.production.local`:
-
- ```
- cp .env.production.local.example .env.production.local
- ```
-
- Then, set the production API URL:
-
- ```
- VITE_QLI_API_URL=https://api.qubic.li
- VITE_ARCHIVER_API_URL=https://rpc.qubic.org/v1
- ```
-
Ensure these files are not committed to the repository to protect sensitive information.
### Step 3: Install Dependencies
diff --git a/dev-proxy.config.ts b/dev-proxy.config.ts
index 988b1f4..4a79f8c 100644
--- a/dev-proxy.config.ts
+++ b/dev-proxy.config.ts
@@ -1,69 +1,51 @@
import type { HttpProxy, ProxyOptions } from 'vite'
-export const qliApiProxy: ProxyOptions = {
- target: 'https://api.qubic.li',
- changeOrigin: true,
- rewrite: (path: string) => path.replace(/^\/dev-proxy-qli-api/, ''),
- configure: (proxy: HttpProxy.Server, options: ProxyOptions) => {
- proxy.on('proxyReq', (proxyReq, req, res) => {
- res.setHeader('Access-Control-Allow-Origin', '*')
- res.setHeader('Access-Control-Allow-Methods', 'GET, PUT, PATCH, POST, DELETE')
- res.setHeader(
- 'Access-Control-Allow-Headers',
- req.headers['access-control-request-headers'] || ''
- )
-
- if (req.method === 'OPTIONS') {
- res.writeHead(200)
- res.end()
- return
- }
-
- // eslint-disable-next-line no-console
- console.log(`[QLI-API-DEV-PROXY] - API CALL - [${req.method}] ${options.target}${req.url}`)
- proxyReq.setHeader('Authorization', req.headers.authorization || '')
- })
-
- proxy.on('error', (err, _req, res) => {
- // eslint-disable-next-line no-console
- console.error(`Proxy error: ${err.message}`)
- res.writeHead(500, { 'Content-Type': 'application/json' })
- res.end(JSON.stringify({ error: 'Proxy error', details: err.message }))
- })
+export const createProxyConfig = (
+ target: string,
+ rewritePath: string,
+ label = 'PROXY'
+): ProxyOptions => {
+ return {
+ target,
+ changeOrigin: true,
+ rewrite: (path: string) => path.replace(rewritePath, ''),
+ configure: (proxy: HttpProxy.Server, options: ProxyOptions) => {
+ proxy.on('proxyReq', (proxyReq, req, res) => {
+ res.setHeader('Access-Control-Allow-Origin', '*')
+ res.setHeader('Access-Control-Allow-Methods', 'GET, PUT, PATCH, POST, DELETE')
+ res.setHeader(
+ 'Access-Control-Allow-Headers',
+ req.headers['access-control-request-headers'] || ''
+ )
+
+ if (req.method === 'OPTIONS') {
+ res.writeHead(200)
+ res.end()
+ return
+ }
+
+ // eslint-disable-next-line no-console
+ console.log(`[${label}] - API CALL - [${req.method}] ${options.target}${req.url}`)
+ proxyReq.setHeader('Authorization', req.headers.authorization || '')
+ })
+
+ proxy.on('error', (err, _req, res) => {
+ // eslint-disable-next-line no-console
+ console.error(`Proxy error: ${err.message}`)
+ res.writeHead(500, { 'Content-Type': 'application/json' })
+ res.end(JSON.stringify({ error: 'Proxy error', details: err.message }))
+ })
+ }
}
}
-export const archiverApiProxy: ProxyOptions = {
- target: 'https://rpc.qubic.org/v1',
- changeOrigin: true,
- rewrite: (path: string) => path.replace(/^\/dev-proxy-archiver-api/, ''),
- configure: (proxy: HttpProxy.Server, options: ProxyOptions) => {
- proxy.on('proxyReq', (proxyReq, req, res) => {
- res.setHeader('Access-Control-Allow-Origin', '*')
- res.setHeader('Access-Control-Allow-Methods', 'GET, PUT, PATCH, POST, DELETE')
- res.setHeader(
- 'Access-Control-Allow-Headers',
- req.headers['access-control-request-headers'] || ''
- )
-
- if (req.method === 'OPTIONS') {
- res.writeHead(200)
- res.end()
- return
- }
-
- // eslint-disable-next-line no-console
- console.log(
- `[ARCHIVER-API-DEV-PROXY] - API CALL - [${req.method}] ${options.target}${req.url}`
- )
- proxyReq.setHeader('Authorization', req.headers.authorization || '')
- })
-
- proxy.on('error', (err, _req, res) => {
- // eslint-disable-next-line no-console
- console.error(`Proxy error: ${err.message}`)
- res.writeHead(500, { 'Content-Type': 'application/json' })
- res.end(JSON.stringify({ error: 'Proxy error', details: err.message }))
- })
- }
-}
+export const qliApiProxy = createProxyConfig(
+ 'https://api.qubic.li',
+ '/dev-proxy-qli-api',
+ 'QLI-API-DEV-PROXY'
+)
+export const archiverApiProxy = createProxyConfig(
+ 'https://rpc.qubic.org',
+ '/dev-proxy-archiver-api',
+ 'ARCHIVER-API-DEV-PROXY'
+)
diff --git a/public/locales/ar/network-page.json b/public/locales/ar/network-page.json
index bca729d..75b5a53 100644
--- a/public/locales/ar/network-page.json
+++ b/public/locales/ar/network-page.json
@@ -53,5 +53,6 @@
"rank": "الترتيب",
"addressID": "معرّف العنوان",
"richListLoadFailed": "خطأ: فشل تحميل قائمة الأثرياء. يرجى تحديث الصفحة أو المحاولة مرة أخرى لاحقًا.",
- "richListWarning": "يتم تحديث بيانات قائمة الأثرياء في بداية كل فترة"
+ "richListWarning": "يتم تحديث بيانات قائمة الأثرياء في بداية كل فترة",
+ "timestamp": "الطابع الزمني"
}
diff --git a/public/locales/de/network-page.json b/public/locales/de/network-page.json
index 4456ada..60e3328 100644
--- a/public/locales/de/network-page.json
+++ b/public/locales/de/network-page.json
@@ -53,5 +53,6 @@
"rank": "Rang",
"addressID": "Adress-ID",
"richListLoadFailed": "Fehler: Reichenliste konnte nicht geladen werden. Bitte aktualisieren Sie die Seite oder versuchen Sie es später erneut.",
- "richListWarning": "Die Daten der Reichenliste werden zu Beginn jeder Epoche aktualisiert"
+ "richListWarning": "Die Daten der Reichenliste werden zu Beginn jeder Epoche aktualisiert",
+ "timestamp": "Zeitstempel"
}
diff --git a/public/locales/en/network-page.json b/public/locales/en/network-page.json
index 4fa14ed..01e43d9 100644
--- a/public/locales/en/network-page.json
+++ b/public/locales/en/network-page.json
@@ -53,5 +53,6 @@
"rank": "Rank",
"addressID": "Address ID",
"richListLoadFailed": "Error: Failed to load rich list. Please refresh the page or try again later.",
- "richListWarning": "Rich list data is updated at the beginning of each epoch"
+ "richListWarning": "Rich list data is updated at the beginning of each epoch",
+ "timestamp": "Timestamp"
}
diff --git a/public/locales/es/network-page.json b/public/locales/es/network-page.json
index d7dcf4b..f3118cd 100644
--- a/public/locales/es/network-page.json
+++ b/public/locales/es/network-page.json
@@ -53,5 +53,6 @@
"rank": "Rango",
"addressID": "ID de Dirección",
"richListLoadFailed": "Error: No se pudo cargar la lista de ricos. Por favor, actualice la página o intente nuevamente más tarde.",
- "richListWarning": "Los datos de la lista de ricos se actualizan al comienzo de cada época"
+ "richListWarning": "Los datos de la lista de ricos se actualizan al comienzo de cada época",
+ "timestamp": "Fecha"
}
diff --git a/public/locales/fr/network-page.json b/public/locales/fr/network-page.json
index 6a12071..6d4fe76 100644
--- a/public/locales/fr/network-page.json
+++ b/public/locales/fr/network-page.json
@@ -53,5 +53,6 @@
"rank": "Rang",
"addressID": "ID d'adresse",
"richListLoadFailed": "Erreur : Impossible de charger la liste des riches. Veuillez actualiser la page ou réessayer plus tard.",
- "richListWarning": "Les données de la liste des riches sont mises à jour au début de chaque époque"
+ "richListWarning": "Les données de la liste des riches sont mises à jour au début de chaque époque",
+ "timestamp": "Horodatage"
}
diff --git a/public/locales/ja/network-page.json b/public/locales/ja/network-page.json
index 1cb3dd9..dc1a32f 100644
--- a/public/locales/ja/network-page.json
+++ b/public/locales/ja/network-page.json
@@ -53,5 +53,6 @@
"rank": "ランク",
"addressID": "アドレスID",
"richListLoadFailed": "エラー:リッチリストの読み込みに失敗しました。ページを更新するか、後でもう一度お試しください。",
- "richListWarning": "リッチリストのデータは各エポックの開始時に更新されます"
+ "richListWarning": "リッチリストのデータは各エポックの開始時に更新されます",
+ "timestamp": "タイムスタンプ"
}
diff --git a/public/locales/nl/network-page.json b/public/locales/nl/network-page.json
index 6e7afca..5e088cd 100644
--- a/public/locales/nl/network-page.json
+++ b/public/locales/nl/network-page.json
@@ -53,5 +53,6 @@
"rank": "Rang",
"addressID": "Adres-ID",
"richListLoadFailed": "Fout: Het laden van de rijkelijst is mislukt. Ververs de pagina of probeer het later opnieuw.",
- "richListWarning": "Rijkelijstgegevens worden aan het begin van elke epoche bijgewerkt"
+ "richListWarning": "Rijkelijstgegevens worden aan het begin van elke epoche bijgewerkt",
+ "timestamp": "Tijdstempel"
}
diff --git a/public/locales/pt/network-page.json b/public/locales/pt/network-page.json
index 12aa391..d6814c7 100644
--- a/public/locales/pt/network-page.json
+++ b/public/locales/pt/network-page.json
@@ -53,5 +53,6 @@
"rank": "Classificação",
"addressID": "ID do Endereço",
"richListLoadFailed": "Erro: Falha ao carregar a lista de ricos. Por favor, atualize a página ou tente novamente mais tarde.",
- "richListWarning": "Os dados da lista de ricos são atualizados no início de cada época"
+ "richListWarning": "Os dados da lista de ricos são atualizados no início de cada época",
+ "timestamp": "Carimbo de tempo"
}
diff --git a/public/locales/ru/network-page.json b/public/locales/ru/network-page.json
index 912ff45..8fca17d 100644
--- a/public/locales/ru/network-page.json
+++ b/public/locales/ru/network-page.json
@@ -53,5 +53,6 @@
"rank": "Ранг",
"addressID": "ID адреса",
"richListLoadFailed": "Ошибка: Не удалось загрузить список богатых. Пожалуйста, обновите страницу или попробуйте позже.",
- "richListWarning": "Данные списка богатых обновляются в начале каждой эпохи"
+ "richListWarning": "Данные списка богатых обновляются в начале каждой эпохи",
+ "timestamp": "Метка времени"
}
diff --git a/public/locales/tr/network-page.json b/public/locales/tr/network-page.json
index e84cfcd..5c631c4 100644
--- a/public/locales/tr/network-page.json
+++ b/public/locales/tr/network-page.json
@@ -53,5 +53,6 @@
"rank": "Rütbe",
"addressID": "Adres Kimliği",
"richListLoadFailed": "Hata: Zenginler listesi yüklenemedi. Lütfen sayfayı yenileyin veya daha sonra tekrar deneyin.",
- "richListWarning": "Zenginler listesi verileri her dönemin başında güncellenir"
+ "richListWarning": "Zenginler listesi verileri her dönemin başında güncellenir",
+ "timestamp": "Zaman damgası"
}
diff --git a/public/locales/zh/network-page.json b/public/locales/zh/network-page.json
index 72117fb..f95533b 100644
--- a/public/locales/zh/network-page.json
+++ b/public/locales/zh/network-page.json
@@ -53,5 +53,6 @@
"rank": "排名",
"addressID": "地址标识",
"richListLoadFailed": "错误:无法加载富豪榜。请刷新页面或稍后再试。",
- "richListWarning": "富豪榜数据会在每个纪元开始时更新"
+ "richListWarning": "富豪榜数据会在每个纪元开始时更新",
+ "timestamp": "时间戳"
}
diff --git a/src/components/ui/Breadcrumbs.tsx b/src/components/ui/Breadcrumbs.tsx
index 6b5844c..970760d 100644
--- a/src/components/ui/Breadcrumbs.tsx
+++ b/src/components/ui/Breadcrumbs.tsx
@@ -1,10 +1,10 @@
-import { Children, Fragment } from 'react'
+import { Children, Fragment, memo } from 'react'
type Props = {
children?: React.ReactNode | React.ReactNode[]
}
-export default function Breadcrumbs({ children }: Props) {
+function Breadcrumbs({ children }: Props) {
return (
)
}
+
+const MemoizedBreadcrumbs = memo(Breadcrumbs)
+
+export default MemoizedBreadcrumbs
diff --git a/src/components/ui/InfiniteScroll.tsx b/src/components/ui/InfiniteScroll.tsx
index 9666165..b6914ef 100644
--- a/src/components/ui/InfiniteScroll.tsx
+++ b/src/components/ui/InfiniteScroll.tsx
@@ -3,8 +3,8 @@ import { useCallback, useEffect, useRef, useState } from 'react'
import { ArrowUpIcon } from '@app/assets/icons'
import { Alert } from '@app/components/ui'
+import { DotsLoader } from '@app/components/ui/loaders'
import { clsxTwMerge } from '@app/utils'
-import { DotsLoader } from './loaders'
interface InfiniteScrollProps {
items: T[] // Array of items to display
@@ -36,8 +36,8 @@ export default function InfiniteScroll({
const [internalError, setInternalError] = useState(null)
const [showScrollToTop, setShowScrollToTop] = useState(false)
- const isLoading = externalIsLoading !== undefined ? externalIsLoading : internalIsLoading
- const error = externalError !== undefined ? externalError : internalError
+ const isLoading = externalIsLoading ?? internalIsLoading
+ const error = externalError ?? internalError
const handleLoadMore = useCallback(async () => {
try {
@@ -69,22 +69,29 @@ export default function InfiniteScroll({
[handleLoadMore, hasMore, isLoading, threshold]
)
- const handleScrollToTop = () => {
+ const handleScrollToTop = useCallback(() => {
window.scrollTo({ top: 0, behavior: 'smooth' })
- }
+ }, [])
- useEffect(() => {
- const handleScroll = () => {
- if (window.scrollY > window.innerHeight) {
- setShowScrollToTop(true)
- } else {
- setShowScrollToTop(false)
- }
+ const handleScroll = useCallback(() => {
+ if (window.scrollY > window.innerHeight) {
+ setShowScrollToTop(true)
+ } else {
+ setShowScrollToTop(false)
}
+ }, [])
+ useEffect(() => {
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
- }, [])
+ }, [handleScroll])
+
+ const renderStatus = useCallback(() => {
+ if (error) return {error}
+ if (isLoading) return loader
+ if (!hasMore && endMessage) return endMessage
+ return null
+ }, [error, isLoading, loader, hasMore, endMessage])
return (
@@ -98,20 +105,15 @@ export default function InfiniteScroll
({
))}
- {isLoading && loader}
- {!hasMore && !isLoading && endMessage}
- {error && (
-
- {error}
-
- )}
+
+ {renderStatus()}
{showScrollToTop && (
diff --git a/src/components/ui/SearchBar/SearchBar.tsx b/src/components/ui/SearchBar/SearchBar.tsx
index 5273230..e690111 100644
--- a/src/components/ui/SearchBar/SearchBar.tsx
+++ b/src/components/ui/SearchBar/SearchBar.tsx
@@ -90,7 +90,7 @@ export default function SearchBar() {
closeOnOutsideClick
onClose={handleCloseCallback}
>
-
+
{isLoading && (
diff --git a/src/components/ui/layouts/Footer.tsx b/src/components/ui/layouts/Footer.tsx
index 3a870f6..53ddfd3 100644
--- a/src/components/ui/layouts/Footer.tsx
+++ b/src/components/ui/layouts/Footer.tsx
@@ -51,7 +51,7 @@ function Footer() {
))}
-
Version 1.5
+
Version 1.5.1
)
}
diff --git a/src/pages/network/OverviewPage.tsx b/src/pages/network/OverviewPage.tsx
index 8d7016d..d0d36b9 100644
--- a/src/pages/network/OverviewPage.tsx
+++ b/src/pages/network/OverviewPage.tsx
@@ -13,7 +13,7 @@ import {
StarsIcon,
WalletIcon
} from '@app/assets/icons'
-import { PaginationBar } from '@app/components/ui'
+import { PaginationBar, Tooltip } from '@app/components/ui'
import { LinearProgress } from '@app/components/ui/loaders'
import { useAppDispatch, useAppSelector } from '@app/hooks/redux'
import { getOverview, selectOverview } from '@app/store/network/overviewSlice'
@@ -105,9 +105,11 @@ export default function OverviewPage() {
id: 'empty-ticks',
icon: EmptyTicksIcon,
label: (
-
+
{t('empty')}
-
+
+
+
),
value: formatString(overview?.numberOfEmptyTicks)
diff --git a/src/pages/network/TickPage.tsx b/src/pages/network/TickPage.tsx
index d99a2e7..07ac543 100644
--- a/src/pages/network/TickPage.tsx
+++ b/src/pages/network/TickPage.tsx
@@ -94,7 +94,7 @@ export default function TickPage() {
-
-
{t('transactions')}
-
-
- {t('latest')}
- {t('historical')}
-
-
-
-
-
-
-
-
-
-
+ {detailsOpen &&
}
+
)
}
diff --git a/src/pages/network/address/components/AddressDetails.tsx b/src/pages/network/address/components/AddressDetails.tsx
new file mode 100644
index 0000000..bda8645
--- /dev/null
+++ b/src/pages/network/address/components/AddressDetails.tsx
@@ -0,0 +1,53 @@
+import type { Address } from '@app/store/network/addressSlice'
+import { formatString } from '@app/utils'
+import { useTranslation } from 'react-i18next'
+import { CardItem, TickLink } from '../../components'
+
+type Props = {
+ address: Address
+}
+
+export default function AddressDetails({ address }: Props) {
+ const { t } = useTranslation('network-page')
+
+ return (
+
+ {(Object.entries(address.reportedValues) || []).map(([ip, details]) => (
+
+
+
+
{t('value')}
+
+ {formatString(details.incomingAmount - details.outgoingAmount)}{' '}
+ QUBIC
+
+
+
{ip}
+
+
+
+ {t('incoming')}:
+ {details.numberOfIncomingTransfers} (
+ {t('latest')}:{' '}
+
+ )
+
+
+ {t('outgoing')}:
+ {details.numberOfOutgoingTransfers} (
+ {t('latest')}:{' '}
+
+ )
+
+
+
+ ))}
+
+ )
+}
diff --git a/src/pages/network/address/components/Transactions.tsx b/src/pages/network/address/components/Transactions.tsx
deleted file mode 100644
index b15d0cc..0000000
--- a/src/pages/network/address/components/Transactions.tsx
+++ /dev/null
@@ -1,101 +0,0 @@
-import { useCallback, useEffect, useState } from 'react'
-import { useTranslation } from 'react-i18next'
-
-import { InfiniteScroll } from '@app/components/ui'
-import { DotsLoader } from '@app/components/ui/loaders'
-import { useAppDispatch, useAppSelector } from '@app/hooks/redux'
-import type { Address, TransactionWithMoneyFlew } from '@app/store/network/addressSlice'
-import { getTransferTxs, selectTransferTxs } from '@app/store/network/addressSlice'
-import { TxItem } from '../../components'
-
-type Props = {
- addressId: string
- address: Address
-}
-
-export const BATCH_SIZE = 50
-
-export const TICK_SIZE = 100_000
-
-export default function Transactions({ addressId, address }: Props) {
- const { t } = useTranslation('network-page')
- const dispatch = useAppDispatch()
- const {
- data: transferTxs,
- isLoading,
- error,
- hasMore,
- lastStartTick,
- lastEndTick
- } = useAppSelector(selectTransferTxs)
- const [displayTransferTxs, setDisplayTransferTxs] = useState([])
-
- const loadMore = useCallback(() => {
- const remainingTxs = transferTxs.slice(
- displayTransferTxs.length,
- displayTransferTxs.length + BATCH_SIZE
- )
- if (remainingTxs.length >= BATCH_SIZE) {
- setDisplayTransferTxs((prev) => [...prev, ...remainingTxs])
- } else if (!isLoading && hasMore) {
- const newEndTick = Math.max(0, lastEndTick - 1 - TICK_SIZE)
- const newStartTick = Math.max(0, lastStartTick - 1 - TICK_SIZE)
- dispatch(getTransferTxs({ addressId, startTick: newStartTick, endTick: newEndTick }))
- }
- }, [
- transferTxs,
- displayTransferTxs.length,
- isLoading,
- hasMore,
- lastEndTick,
- lastStartTick,
- dispatch,
- addressId
- ])
-
- useEffect(() => {
- if (!transferTxs.length && address.endTick) {
- dispatch(
- getTransferTxs({
- addressId,
- startTick: address.endTick - TICK_SIZE,
- endTick: address.endTick
- })
- )
- }
- }, [address.endTick, addressId, dispatch, transferTxs.length])
-
- useEffect(() => {
- if (transferTxs.length > 0) {
- setDisplayTransferTxs((prev) => [
- ...prev,
- ...transferTxs.slice(prev.length, prev.length + BATCH_SIZE)
- ])
- }
- }, [transferTxs])
-
- return (
- }
- error={error && t('loadingTransactionsError')}
- endMessage={
-
- {displayTransferTxs.length === 0 ? t('noTransactions') : t('allTransactionsLoaded')}
-
- }
- renderItem={(tx: TransactionWithMoneyFlew) => (
-
- )}
- />
- )
-}
diff --git a/src/pages/network/address/components/HistoricalTxs.tsx b/src/pages/network/address/components/TransactionsOverview/HistoricalTxs.tsx
similarity index 79%
rename from src/pages/network/address/components/HistoricalTxs.tsx
rename to src/pages/network/address/components/TransactionsOverview/HistoricalTxs.tsx
index 92fd6d1..ec48c26 100644
--- a/src/pages/network/address/components/HistoricalTxs.tsx
+++ b/src/pages/network/address/components/TransactionsOverview/HistoricalTxs.tsx
@@ -3,9 +3,10 @@ import { InfiniteScroll } from '@app/components/ui'
import { DotsLoader } from '@app/components/ui/loaders'
import { useAppDispatch, useAppSelector } from '@app/hooks/redux'
import { getHistoricalTxs, selectHistoricalTxs } from '@app/store/network/addressSlice'
+import type { TransactionWithStatus } from '@app/types'
import { useCallback, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
-import { TxItem } from '../../components'
+import { TxItem } from '../../../components'
type Props = {
addressId: string
@@ -20,6 +21,20 @@ export default function HistoricalTxs({ addressId }: Props) {
dispatch(getHistoricalTxs(addressId))
}, [dispatch, addressId])
+ const renderTxItem = useCallback(
+ ({ tx, status }: TransactionWithStatus) => (
+
+ ),
+ [addressId]
+ )
+
useEffect(() => {
if (historicalTxs.length === 0) {
loadMoreTxs()
@@ -47,16 +62,7 @@ export default function HistoricalTxs({ addressId }: Props) {
{historicalTxs.length === 0 ? t('noTransactions') : t('allTransactionsLoaded')}
}
- renderItem={({ tx, status }) => (
-
- )}
+ renderItem={renderTxItem}
/>
)
diff --git a/src/pages/network/address/components/TransactionsOverview/LatestTransactions.tsx b/src/pages/network/address/components/TransactionsOverview/LatestTransactions.tsx
new file mode 100644
index 0000000..ec776a3
--- /dev/null
+++ b/src/pages/network/address/components/TransactionsOverview/LatestTransactions.tsx
@@ -0,0 +1,58 @@
+import { useTranslation } from 'react-i18next'
+
+import { InfiniteScroll } from '@app/components/ui'
+import { DotsLoader } from '@app/components/ui/loaders'
+import type { TransactionV2 } from '@app/store/apis/archiver-v2.types'
+import { useCallback } from 'react'
+import { TxItem } from '../../../components'
+
+type Props = {
+ addressId: string
+ transactions: TransactionV2[]
+ loadMore: () => Promise
+ hasMore: boolean
+ isLoading: boolean
+ error: string | null
+}
+
+export default function LatestTransactions({
+ addressId,
+ transactions,
+ loadMore,
+ hasMore,
+ isLoading,
+ error
+}: Props) {
+ const { t } = useTranslation('network-page')
+
+ const renderTxItem = useCallback(
+ ({ transaction, moneyFlew, timestamp }: TransactionV2) => (
+
+ ),
+ [addressId]
+ )
+
+ return (
+ }
+ error={error && t('loadingTransactionsError')}
+ endMessage={
+
+ {transactions.length === 0 ? t('noTransactions') : t('allTransactionsLoaded')}
+
+ }
+ renderItem={renderTxItem}
+ />
+ )
+}
diff --git a/src/pages/network/address/components/TransactionsOverview/TransactionsOverview.tsx b/src/pages/network/address/components/TransactionsOverview/TransactionsOverview.tsx
new file mode 100644
index 0000000..fde35d6
--- /dev/null
+++ b/src/pages/network/address/components/TransactionsOverview/TransactionsOverview.tsx
@@ -0,0 +1,51 @@
+import { useTranslation } from 'react-i18next'
+
+import { Tabs } from '@app/components/ui'
+import type { Address } from '@app/store/network/addressSlice'
+import { memo } from 'react'
+import { useLatestTransactions } from '../../hooks'
+import HistoricalTxs from './HistoricalTxs'
+import LatestTransactions from './LatestTransactions'
+
+type Props = {
+ address: Address
+ addressId: string
+}
+
+function TransactionsOverview({ address, addressId }: Props) {
+ const { t } = useTranslation('network-page')
+ const { transactions, loadMoreTransactions, hasMore, isLoading, error } = useLatestTransactions(
+ addressId,
+ address.endTick
+ )
+
+ return (
+
+
{t('transactions')}
+
+
+ {t('latest')}
+ {t('historical')}
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+const MemoizedTransactionsOverview = memo(TransactionsOverview)
+export default MemoizedTransactionsOverview
diff --git a/src/pages/network/address/components/index.ts b/src/pages/network/address/components/index.ts
index a1ef3bd..4411351 100644
--- a/src/pages/network/address/components/index.ts
+++ b/src/pages/network/address/components/index.ts
@@ -1,2 +1,2 @@
-export { default as HistoricalTxs } from './HistoricalTxs'
-export { default as Transactions } from './Transactions'
+export { default as AddressDetails } from './AddressDetails'
+export { default as TransactionsOverview } from './TransactionsOverview/TransactionsOverview'
diff --git a/src/pages/network/address/hooks/index.ts b/src/pages/network/address/hooks/index.ts
new file mode 100644
index 0000000..7a2af43
--- /dev/null
+++ b/src/pages/network/address/hooks/index.ts
@@ -0,0 +1,2 @@
+// eslint-disable-next-line
+export { default as useLatestTransactions } from './useLatestTransactions'
diff --git a/src/pages/network/address/hooks/useLatestTransactions.ts b/src/pages/network/address/hooks/useLatestTransactions.ts
new file mode 100644
index 0000000..d0f5fb3
--- /dev/null
+++ b/src/pages/network/address/hooks/useLatestTransactions.ts
@@ -0,0 +1,129 @@
+import { useLazyGetIndentityTransfersQuery } from '@app/store/apis/archiver-v2.api'
+import type { TransactionV2 } from '@app/store/apis/archiver-v2.types'
+import type { Address } from '@app/store/network/addressSlice'
+import { useCallback, useEffect, useState } from 'react'
+
+const BATCH_SIZE = 50
+const TICK_SIZE = 200_000
+
+export interface UseLatestTransactionsResult {
+ transactions: TransactionV2[]
+ loadMoreTransactions: () => Promise
+ hasMore: boolean
+ isLoading: boolean
+ error: string | null
+}
+
+export default function useLatestTransactions(
+ addressId: string,
+ addressEndTick: Address['endTick']
+): UseLatestTransactionsResult {
+ const [startTick, setStartTick] = useState(Math.max(0, addressEndTick - TICK_SIZE))
+ const [transactions, setTransactions] = useState([])
+ const [txsList, setTxsList] = useState([])
+ const [isLoading, setIsLoading] = useState(false)
+ const [getIdentityTransfersQuery, { isFetching, error }] = useLazyGetIndentityTransfersQuery({})
+
+ const hasMore = startTick > 0
+
+ const fetchTransfers = useCallback(
+ async (start: number, end: number) => {
+ const result = await getIdentityTransfersQuery({
+ addressId,
+ startTick: start,
+ endTick: end
+ }).unwrap()
+
+ return result || []
+ },
+ [getIdentityTransfersQuery, addressId]
+ )
+
+ const fetchRecursive = useCallback(
+ async (start: number, end: number, accumulatedData: TransactionV2[] = []) => {
+ const newTxs = await fetchTransfers(start, end)
+ const combinedData = [...new Set(accumulatedData.concat(newTxs))]
+
+ if (combinedData.length < BATCH_SIZE && start > 0) {
+ const newEndTick = Math.max(0, start - 1)
+ const newStartTick = Math.max(0, start - 1 - TICK_SIZE)
+ return fetchRecursive(newStartTick, newEndTick, combinedData)
+ }
+
+ return {
+ newTxs: combinedData.sort((a, b) => b.transaction.tickNumber - a.transaction.tickNumber),
+ lastStartTick: start
+ }
+ },
+ [fetchTransfers]
+ )
+
+ const loadMoreTransactions = useCallback(async () => {
+ if (isLoading || isFetching || !hasMore) return
+
+ setIsLoading(true)
+ try {
+ if (txsList.length < BATCH_SIZE) {
+ const newStartTick = Math.max(0, startTick - 1 - TICK_SIZE)
+ const newEndTick = Math.max(0, startTick - 1)
+ const { newTxs, lastStartTick } = await fetchRecursive(newStartTick, newEndTick)
+ // Since there could be some txs in txsList already, we need to merge them and then slice it
+ const updatedTxList = [...txsList, ...newTxs]
+ // Adding the new transactions to the list to be displayed
+ setTransactions((prev) => [...prev, ...updatedTxList.slice(0, BATCH_SIZE)])
+ // Updating the list of remaining transactions
+ setTxsList(updatedTxList.slice(BATCH_SIZE, updatedTxList.length))
+ // Updating the start and end tick
+ setStartTick(lastStartTick)
+ } else {
+ setTransactions((prev) => [...prev, ...txsList.slice(0, BATCH_SIZE)])
+ setTxsList((prevTxsList) => prevTxsList.slice(BATCH_SIZE, prevTxsList.length))
+ }
+ } finally {
+ setIsLoading(false)
+ }
+ }, [startTick, fetchRecursive, isLoading, isFetching, hasMore, txsList])
+
+ useEffect(() => {
+ let isMounted = true
+
+ const initialFetch = async () => {
+ setIsLoading(true)
+ const initialStartTick = Math.max(0, addressEndTick - TICK_SIZE)
+ const { newTxs, lastStartTick } = await fetchRecursive(initialStartTick, addressEndTick)
+
+ if (isMounted) {
+ setTransactions(newTxs.slice(0, BATCH_SIZE))
+ setTxsList(newTxs.slice(BATCH_SIZE, newTxs.length))
+ setStartTick(lastStartTick)
+ setIsLoading(false)
+ }
+ }
+
+ if (transactions.length === 0 && addressEndTick) {
+ initialFetch()
+ }
+
+ return () => {
+ isMounted = false
+ }
+ }, [fetchRecursive, transactions.length, addressEndTick])
+
+ useEffect(() => {
+ return () => {
+ if (addressId) {
+ setTransactions([])
+ setTxsList([])
+ setStartTick(0)
+ }
+ }
+ }, [addressId])
+
+ return {
+ transactions,
+ loadMoreTransactions,
+ hasMore,
+ isLoading,
+ error: error ? String(error) : null
+ }
+}
diff --git a/src/pages/network/components/OverviewCardItem.tsx b/src/pages/network/components/OverviewCardItem.tsx
index 96d21cb..42aa7c8 100644
--- a/src/pages/network/components/OverviewCardItem.tsx
+++ b/src/pages/network/components/OverviewCardItem.tsx
@@ -14,18 +14,20 @@ export default function OverviewCardItem({
value: string
variant?: 'normal' | 'small'
}) {
+ const LabelTag = typeof label === 'string' ? 'p' : 'div'
+
return (
-
+
-
{label}
-
{value}
+
{label}
+
{value}
diff --git a/src/pages/network/components/TxItem/TransactionDetails.tsx b/src/pages/network/components/TxItem/TransactionDetails.tsx
index 93816d2..6829aa3 100644
--- a/src/pages/network/components/TxItem/TransactionDetails.tsx
+++ b/src/pages/network/components/TxItem/TransactionDetails.tsx
@@ -1,8 +1,9 @@
import { useTranslation } from 'react-i18next'
import type { Transaction } from '@app/services/archiver'
-import { formatString } from '@app/utils'
+import { formatDate, formatString } from '@app/utils'
import type { Transfer } from '@app/utils/qubic-ts'
+import { useMemo } from 'react'
import AddressLink from '../AddressLink'
import SubCardItem from '../SubCardItem'
import TickLink from '../TickLink'
@@ -15,6 +16,7 @@ type Props = {
entries: Transfer[]
isHistoricalTx?: boolean
variant?: TxItemVariant
+ timestamp?: string
}
function TransactionDetailsWrapper({
@@ -37,11 +39,13 @@ export default function TransactionDetails({
txDetails: { txId, sourceId, tickNumber, destId, inputType, amount },
entries,
isHistoricalTx = false,
+ timestamp,
variant = 'primary'
}: Props) {
const { t } = useTranslation('network-page')
const isSecondaryVariant = variant === 'secondary'
+ const { date, time } = useMemo(() => formatDate(timestamp, { split: true }), [timestamp])
return (
@@ -105,6 +109,19 @@ export default function TransactionDetails({
/>
)}
+ {timestamp && (
+
+ {date}{' '}
+ {time}
+
+ }
+ />
+ )}
+
)
diff --git a/src/pages/network/components/TxItem/TxItem.tsx b/src/pages/network/components/TxItem/TxItem.tsx
index 44f37cb..8ee33b7 100644
--- a/src/pages/network/components/TxItem/TxItem.tsx
+++ b/src/pages/network/components/TxItem/TxItem.tsx
@@ -15,18 +15,20 @@ import type { TxItemVariant } from './TxItem.types'
type Props = {
tx: Omit
- identify?: string
+ identity?: string
nonExecutedTxIds: string[]
variant?: TxItemVariant
isHistoricalTx?: boolean
+ timestamp?: string
}
function TxItem({
tx: { txId, sourceId, tickNumber, destId, inputType, amount, inputHex },
- identify,
+ identity,
nonExecutedTxIds,
variant = 'primary',
- isHistoricalTx = false
+ isHistoricalTx = false,
+ timestamp
}: Props) {
const [entries, setEntries] = useState([])
const [detailsOpen, setDetailsOpen] = useState(false)
@@ -69,6 +71,7 @@ function TxItem({
isHistoricalTx={isHistoricalTx}
variant={variant}
entries={entries}
+ timestamp={timestamp}
/>
>
)
@@ -82,16 +85,16 @@ function TxItem({
isTransferTx={isTransferTransaction}
/>
- {identify ? (
+ {identity ? (
- {identify === sourceId ? (
+ {identity === sourceId ? (
) : (
)}
@@ -121,6 +124,7 @@ function TxItem({
isHistoricalTx={isHistoricalTx}
variant={variant}
entries={entries}
+ timestamp={timestamp}
/>
)}
diff --git a/src/services/archiver/endpoints.ts b/src/services/archiver/endpoints.ts
index 93874cc..2fb0db8 100644
--- a/src/services/archiver/endpoints.ts
+++ b/src/services/archiver/endpoints.ts
@@ -1,6 +1,6 @@
import { envConfig } from '@app/configs'
-const BASE_URL = envConfig.ARCHIVER_API_URL
+const BASE_URL = `${envConfig.ARCHIVER_API_URL}/v1`
const formatTick = (tick: string) => parseInt(tick.replace(/,/g, ''), 10)
diff --git a/src/store/apis/archiver-v2.api.ts b/src/store/apis/archiver-v2.api.ts
new file mode 100644
index 0000000..462fe27
--- /dev/null
+++ b/src/store/apis/archiver-v2.api.ts
@@ -0,0 +1,34 @@
+import { envConfig } from '@app/configs'
+import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
+import type {
+ GetIdentityTransfersArgs,
+ GetIdentityTransfersResponse,
+ GetTransactionResponse
+} from './archiver-v2.types'
+
+const BASE_URL = `${envConfig.ARCHIVER_API_URL}/v2`
+
+export const archiverV2Api = createApi({
+ reducerPath: 'archiverV2Api',
+ baseQuery: fetchBaseQuery({ baseUrl: BASE_URL }),
+ endpoints: (builder) => ({
+ getTransaction: builder.query
({
+ query: (txId) => `transactions/${txId}`
+ }),
+ getIndentityTransfers: builder.query<
+ GetIdentityTransfersResponse['transactions'][0]['transactions'],
+ GetIdentityTransfersArgs
+ >({
+ query: ({ addressId, startTick, endTick }) =>
+ `identities/${addressId}/transfers?startTick=${startTick}&endTick=${endTick}`,
+ transformResponse: (response: GetIdentityTransfersResponse) =>
+ response.transactions.flatMap(({ transactions }) => transactions)
+ })
+ })
+})
+
+export const {
+ useGetTransactionQuery,
+ useGetIndentityTransfersQuery,
+ useLazyGetIndentityTransfersQuery
+} = archiverV2Api
diff --git a/src/store/apis/archiver-v2.types.ts b/src/store/apis/archiver-v2.types.ts
new file mode 100644
index 0000000..816e311
--- /dev/null
+++ b/src/store/apis/archiver-v2.types.ts
@@ -0,0 +1,23 @@
+import type { Transaction } from '@app/services/archiver'
+
+export interface TransactionV2 {
+ transaction: Transaction
+ timestamp: string
+ moneyFlew: boolean
+}
+
+export type GetTransactionResponse = TransactionV2
+
+export interface GetIdentityTransfersArgs {
+ addressId: string
+ startTick: number
+ endTick: number
+}
+
+export interface GetIdentityTransfersResponse {
+ transactions: {
+ identity: string
+ tickNumber: number
+ transactions: TransactionV2[]
+ }[]
+}
diff --git a/src/store/index.ts b/src/store/index.ts
index 46f1562..6f01fa2 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -1,4 +1,5 @@
import { configureStore } from '@reduxjs/toolkit'
+import { archiverV2Api } from './apis/archiver-v2.api'
import localeReducer from './localeSlice'
import { networkReducer } from './network/networkReducer'
import searchReducer from './searchSlice'
@@ -7,8 +8,11 @@ export const store = configureStore({
reducer: {
locale: localeReducer,
search: searchReducer,
- network: networkReducer
- }
+ network: networkReducer,
+ [archiverV2Api.reducerPath]: archiverV2Api.reducer
+ },
+
+ middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(archiverV2Api.middleware)
})
export type RootState = ReturnType
diff --git a/src/store/network/adapters/convertHistoricalTxToTxWithStatus.ts b/src/store/network/adapters/convertHistoricalTxToTxWithStatus.ts
index 3e0aff9..cd47019 100644
--- a/src/store/network/adapters/convertHistoricalTxToTxWithStatus.ts
+++ b/src/store/network/adapters/convertHistoricalTxToTxWithStatus.ts
@@ -1,7 +1,6 @@
import type { HistoricalTx } from '@app/services/qli'
-import { TxTypeEnum } from '@app/types'
-import { isTransferTx } from '@app/utils/qubic-ts'
-import type { TransactionWithStatus } from '../txSlice'
+import { type TransactionWithStatus } from '@app/types'
+import { getTxType } from '@app/utils'
import convertHistoricalTxToLatestTx from './convertHistoricalTxToLatestTx'
export default function convertHistoricalTxToTxWithStatus(
@@ -12,9 +11,7 @@ export default function convertHistoricalTxToTxWithStatus(
status: {
txId: historicalTx.id,
moneyFlew: historicalTx.moneyFlew,
- txType: isTransferTx(historicalTx.sourceId, historicalTx.destId, historicalTx.amount)
- ? TxTypeEnum.TRANSFER
- : TxTypeEnum.PROTOCOL
+ txType: getTxType(historicalTx)
}
}
}
diff --git a/src/store/network/adapters/convertTxV2ToTxWithStatus.ts b/src/store/network/adapters/convertTxV2ToTxWithStatus.ts
new file mode 100644
index 0000000..5106d4e
--- /dev/null
+++ b/src/store/network/adapters/convertTxV2ToTxWithStatus.ts
@@ -0,0 +1,15 @@
+import type { TransactionV2 } from '@app/store/apis/archiver-v2.types'
+import type { TransactionWithStatus } from '@app/types'
+import { getTxType } from '@app/utils'
+
+export default function convertTxV2ToTxWithStatus(tx: TransactionV2): TransactionWithStatus {
+ return {
+ tx: tx.transaction,
+ status: {
+ txId: tx.transaction.txId,
+ moneyFlew: tx.moneyFlew,
+ txType: getTxType(tx.transaction)
+ },
+ timestamp: tx.timestamp
+ }
+}
diff --git a/src/store/network/adapters/index.ts b/src/store/network/adapters/index.ts
index 7c588ce..88df7cb 100644
--- a/src/store/network/adapters/index.ts
+++ b/src/store/network/adapters/index.ts
@@ -1,2 +1,3 @@
export { default as convertHistoricalTxToLatestTx } from './convertHistoricalTxToLatestTx'
export { default as convertHistoricalTxToTxWithStatus } from './convertHistoricalTxToTxWithStatus'
+export { default as convertTxV2ToTxWithStatus } from './convertTxV2ToTxWithStatus'
diff --git a/src/store/network/addressSlice.ts b/src/store/network/addressSlice.ts
index 408a1e7..8948b65 100644
--- a/src/store/network/addressSlice.ts
+++ b/src/store/network/addressSlice.ts
@@ -1,21 +1,13 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
-import { BATCH_SIZE, TICK_SIZE } from '@app/pages/network/address/components/Transactions'
-import type { Balance, Transaction } from '@app/services/archiver'
+import type { Balance } from '@app/services/archiver'
import { archiverApiService } from '@app/services/archiver'
import type { ReportedValues } from '@app/services/qli'
import { qliApiService } from '@app/services/qli'
import type { RootState } from '@app/store'
-import { TxTypeEnum } from '@app/types'
+import type { TransactionWithStatus } from '@app/types'
import { handleThunkError } from '@app/utils/error-handlers'
-import { isTransferTx } from '@app/utils/qubic-ts'
import { convertHistoricalTxToTxWithStatus } from './adapters'
-import type { TransactionWithStatus } from './txSlice'
-
-export type TransactionWithMoneyFlew = Transaction & {
- moneyFlew: boolean | null
- txType: TxTypeEnum
-}
export const getAddress = createAsyncThunk(
'network/address',
@@ -27,6 +19,7 @@ export const getAddress = createAsyncThunk(
archiverApiService.getBalance(addressId)
])
return {
+ addressId,
reportedValues,
endTick: lastProcessedTick.tickNumber,
balance
@@ -37,76 +30,6 @@ export const getAddress = createAsyncThunk(
}
)
-export const getTransferTxs = createAsyncThunk<
- {
- data: TransactionWithMoneyFlew[]
- lastStartTick: number
- lastEndTick: number
- },
- { addressId: string; startTick: number; endTick: number },
- {
- state: RootState
- }
->(
- 'network/getTransferTxs',
- async ({ addressId, startTick, endTick }, { rejectWithValue }) => {
- try {
- let data: Transaction[] = []
- let lastStartTick = startTick
- let lastEndTick = endTick
-
- const getTransfers = async (start: number, end: number) => {
- const { transferTransactionsPerTick } =
- await archiverApiService.getAddressTransferTransactions(addressId, start, end)
- return transferTransactionsPerTick.flatMap(({ transactions }) => transactions) || []
- }
- const fetchRecursive = async (start: number, end: number) => {
- const transfers = await getTransfers(start, end)
- data = [...new Set(data.concat(transfers))]
-
- if (start === 0 && transfers.length === 0) {
- return { data: data.sort((a, b) => b.tickNumber - a.tickNumber) }
- }
-
- if (data.length < BATCH_SIZE) {
- lastEndTick = Math.max(0, start - 1)
- lastStartTick = Math.max(0, lastEndTick - TICK_SIZE)
-
- return fetchRecursive(lastStartTick, lastEndTick)
- }
- return { data: data.sort((a, b) => b.tickNumber - a.tickNumber) }
- }
-
- const finalResult = await fetchRecursive(startTick, endTick)
-
- const txsWithMoneyFlew = await Promise.all(
- finalResult.data.map(async (tx) => {
- if (!isTransferTx(tx.sourceId, tx.destId, tx.amount)) {
- return { ...tx, moneyFlew: true, txType: TxTypeEnum.PROTOCOL }
- }
- try {
- const { transactionStatus } = await archiverApiService.getTransactionStatus(tx.txId)
- return { ...tx, moneyFlew: transactionStatus.moneyFlew, txType: TxTypeEnum.TRANSFER }
- } catch (error) {
- return { ...tx, moneyFlew: null, txType: TxTypeEnum.TRANSFER }
- }
- })
- )
-
- return { data: txsWithMoneyFlew, lastStartTick, lastEndTick }
- } catch (error) {
- return rejectWithValue(handleThunkError(error))
- }
- },
- // Conditionally fetch historical transactions to prevent issues from React.StrictMode - https://redux.js.org/tutorials/essentials/part-5-async-logic#avoiding-duplicate-fetches
- {
- condition: (_, { getState }) => {
- const { isLoading, hasMore, error } = getState().network.address.transferTxs
- return !isLoading && hasMore && !error
- }
- }
-)
-
export const getHistoricalTxs = createAsyncThunk<
TransactionWithStatus[],
string,
@@ -134,6 +57,7 @@ export const getHistoricalTxs = createAsyncThunk<
)
export type Address = {
+ addressId: string
reportedValues: ReportedValues
endTick: number
balance: Balance
@@ -143,14 +67,6 @@ export interface AddressState {
address: Address | null
isLoading: boolean
error: string | null
- transferTxs: {
- data: TransactionWithMoneyFlew[]
- isLoading: boolean
- error: string | null
- hasMore: boolean
- lastStartTick: number
- lastEndTick: number
- }
historicalTxs: {
data: TransactionWithStatus[]
isLoading: boolean
@@ -164,14 +80,6 @@ const initialState: AddressState = {
address: null,
isLoading: false,
error: null,
- transferTxs: {
- data: [],
- isLoading: false,
- error: null,
- hasMore: true,
- lastStartTick: 0,
- lastEndTick: 0
- },
historicalTxs: {
data: [],
isLoading: false,
@@ -204,26 +112,6 @@ const addressSlice = createSlice({
state.isLoading = false
state.error = action.error.message ?? 'Unknown error'
})
- // getTransferTxs
- .addCase(getTransferTxs.pending, (state) => {
- state.transferTxs.isLoading = true
- state.transferTxs.error = null
- })
- .addCase(getTransferTxs.fulfilled, (state, action) => {
- state.transferTxs.isLoading = false
- const newTxs = action.payload
- if (newTxs.data.length === 0) {
- state.transferTxs.hasMore = false
- } else {
- state.transferTxs.data.push(...newTxs.data)
- }
- state.transferTxs.lastStartTick = newTxs.lastStartTick
- state.transferTxs.lastEndTick = newTxs.lastEndTick
- })
- .addCase(getTransferTxs.rejected, (state, action) => {
- state.transferTxs.isLoading = false
- state.transferTxs.error = action.error.message ?? 'Unknown error'
- })
// getHistoricalTxs
.addCase(getHistoricalTxs.pending, (state) => {
state.historicalTxs.isLoading = true
@@ -249,7 +137,6 @@ const addressSlice = createSlice({
// Selectors
export const selectAddress = (state: RootState) => state.network.address
-export const selectTransferTxs = (state: RootState) => state.network.address.transferTxs
export const selectHistoricalTxs = (state: RootState) => state.network.address.historicalTxs
// actions
diff --git a/src/store/network/txSlice.ts b/src/store/network/txSlice.ts
index c690dd5..d11d868 100644
--- a/src/store/network/txSlice.ts
+++ b/src/store/network/txSlice.ts
@@ -1,18 +1,10 @@
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
-import type { Transaction, TransactionStatus } from '@app/services/archiver'
-import { archiverApiService } from '@app/services/archiver'
import { qliApiService } from '@app/services/qli'
import type { RootState } from '@app/store'
-import { TxTypeEnum, type TxEra, type TxType } from '@app/types'
-import { isTransferTx } from '@app/utils/qubic-ts'
+import type { TransactionWithStatus, TxEra } from '@app/types'
import { convertHistoricalTxToTxWithStatus } from './adapters'
-export type TransactionWithStatus = {
- tx: Transaction
- status: TransactionStatus & { txType: TxType }
-}
-
export interface TxState {
txWithStatus: TransactionWithStatus | null
isLoading: boolean
@@ -31,7 +23,7 @@ type GetTxArgs = {
}
export const getTx = createAsyncThunk<
- TransactionWithStatus,
+ TransactionWithStatus | null,
GetTxArgs,
{
state: RootState
@@ -54,19 +46,7 @@ export const getTx = createAsyncThunk<
return historicalTx
}
- const { transaction } = await archiverApiService.getTransaction(txId)
- const { transactionStatus } = isTransferTx(
- transaction.sourceId,
- transaction.destId,
- transaction.amount
- )
- ? await archiverApiService.getTransactionStatus(txId)
- : { transactionStatus: { txId, moneyFlew: false, txType: TxTypeEnum.PROTOCOL } }
-
- return {
- tx: transaction,
- status: { ...transactionStatus, txType: TxTypeEnum.TRANSFER }
- }
+ return null
})
const txSlice = createSlice({
diff --git a/src/types/index.ts b/src/types/index.ts
index a19692f..e10aa1c 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -1,3 +1,5 @@
+import type { Transaction, TransactionStatus } from '@app/services/archiver'
+
export type Language = {
id: string
label: string
@@ -11,3 +13,9 @@ export enum TxTypeEnum {
}
export type TxType = keyof typeof TxTypeEnum
+
+export type TransactionWithStatus = {
+ tx: Transaction
+ status: TransactionStatus & { txType: TxType }
+ timestamp?: string
+}
diff --git a/src/utils/date.ts b/src/utils/date.ts
new file mode 100644
index 0000000..c12daa7
--- /dev/null
+++ b/src/utils/date.ts
@@ -0,0 +1,37 @@
+// eslint-disable-next-line import/prefer-default-export -- Remove this comment when adding more functions
+export function formatDate(
+ dateString: string | undefined,
+ options?: { split?: T }
+): T extends true ? { date: string; time: string } : string {
+ const defaultResult = (options?.split ? { date: '', time: '' } : '') as T extends true
+ ? { date: string; time: string }
+ : string
+ const formatDateTime = (date: Date, dateTimeFormatOptions: Intl.DateTimeFormatOptions) =>
+ new Intl.DateTimeFormat('en-US', dateTimeFormatOptions).format(date)
+
+ if (!dateString) return defaultResult
+
+ const dateOptions: Intl.DateTimeFormatOptions = {
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric'
+ }
+
+ const timeOptions: Intl.DateTimeFormatOptions = {
+ hour: 'numeric',
+ minute: 'numeric',
+ second: 'numeric',
+ timeZoneName: 'short',
+ hour12: true
+ }
+
+ const date = new Date(dateString.includes('T') ? dateString : parseInt(dateString, 10))
+
+ if (Number.isNaN(date.getTime())) return defaultResult
+
+ return (
+ options?.split
+ ? { date: formatDateTime(date, dateOptions), time: formatDateTime(date, timeOptions) }
+ : formatDateTime(date, { ...dateOptions, ...timeOptions })
+ ) as T extends true ? { date: string; time: string } : string
+}
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 92b2693..a7289fc 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -9,31 +9,6 @@ const formatString = (string: string | number | undefined | null) => {
return String(string)
}
-const formatDate = (dateString: string | undefined) => {
- const options = {
- year: 'numeric',
- month: 'long',
- day: 'numeric',
- hour: 'numeric',
- minute: 'numeric',
- second: 'numeric',
- timeZoneName: 'short',
- hour12: true
- } as const
-
- if (dateString) {
- let date
- if (dateString.includes('T')) {
- date = new Date(dateString)
- } else {
- const timestamp = parseInt(dateString, 10) // Include the radix parameter
- date = new Date(timestamp)
- }
- return new Intl.DateTimeFormat('en-US', options).format(date) // Format date
- }
- return ''
-}
-
function formatEllipsis(str = '') {
if (str.length > 10) {
return `${str.slice(0, 5)}...${str.slice(-5)}`
@@ -67,5 +42,7 @@ function copyText(textToCopy: string) {
}
}
+export * from './date'
export * from './styles'
-export { copyText, formatBase64, formatDate, formatEllipsis, formatString }
+export * from './transactions'
+export { copyText, formatBase64, formatEllipsis, formatString }
diff --git a/src/utils/transactions.ts b/src/utils/transactions.ts
new file mode 100644
index 0000000..50c0ec1
--- /dev/null
+++ b/src/utils/transactions.ts
@@ -0,0 +1,11 @@
+import type { TxType } from '@app/types'
+import { TxTypeEnum } from '@app/types'
+import { isTransferTx } from './qubic-ts'
+
+// eslint-disable-next-line import/prefer-default-export -- Remove this comment when adding more functions
+export const getTxType = (tx: {
+ sourceId: string
+ destId: string
+ amount: string | number
+}): TxType =>
+ isTransferTx(tx.sourceId, tx.destId, tx.amount) ? TxTypeEnum.TRANSFER : TxTypeEnum.PROTOCOL
diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts
index 228cb6e..43902ea 100644
--- a/src/vite-env.d.ts
+++ b/src/vite-env.d.ts
@@ -2,6 +2,7 @@
///
interface ImportMetaEnv {
+ readonly VITE_ENABLE_PROXY: string
readonly VITE_QLI_API_URL: string
readonly VITE_ARCHIVER_API_URL: string
}