From c0ff5393c20b737d8116075861ce4decb4ab7cb3 Mon Sep 17 00:00:00 2001 From: Alex Freska Date: Fri, 9 Aug 2024 07:55:25 -0700 Subject: [PATCH] feat: hostd v2 changes --- .changeset/dirty-jeans-search.md | 6 + .changeset/famous-pears-melt.md | 7 + .changeset/kind-trains-bathe.md | 9 + .changeset/old-hounds-camp.md | 5 + .changeset/pretty-coins-compare.md | 8 + .changeset/thin-shirts-tickle.md | 5 + .changeset/two-olives-whisper.md | 7 + .changeset/wise-dolls-burn.md | 5 + apps/hostd-e2e/src/specs/volumes.spec.ts | 4 +- .../components/CmdRoot/WalletCmdGroup.tsx | 6 +- .../components/Config/AnnounceButton.tsx | 4 +- apps/hostd/components/Config/index.tsx | 219 +++++++--------- .../components/HostdTestnetWarningBanner.tsx | 8 +- apps/hostd/components/Profile/index.tsx | 24 +- apps/hostd/components/Wallet/StateError.tsx | 15 ++ .../components/Wallet/StateNoneMatching.tsx | 15 ++ apps/hostd/components/Wallet/StateNoneYet.tsx | 15 ++ .../components/Wallet/WalletFilterBar.tsx | 13 +- apps/hostd/components/Wallet/index.tsx | 54 ++-- apps/hostd/config/routes.ts | 4 +- apps/hostd/contexts/config/index.tsx | 4 +- apps/hostd/contexts/config/useForm.tsx | 4 +- apps/hostd/contexts/config/useOnValid.tsx | 12 +- apps/hostd/contexts/transactions/columns.tsx | 175 +++++++++++++ apps/hostd/contexts/transactions/index.tsx | 185 +++++++++----- apps/hostd/contexts/transactions/types.ts | 53 ++++ apps/hostd/hooks/useHostOSPathSeparator.ts | 4 +- apps/hostd/hooks/useSiascanUrl.ts | 6 +- apps/hostd/hooks/useSyncStatus.ts | 38 +-- apps/renterd/contexts/transactions/index.tsx | 3 +- .../RenterdTransactionDetailsDialog.tsx | 7 +- apps/walletd/contexts/events/columns.tsx | 14 +- apps/walletd/contexts/events/index.tsx | 17 +- apps/walletd/contexts/events/types.ts | 6 +- apps/walletd/contexts/events/utils.ts | 46 ---- .../src/app/TransactionDetailsDialog.tsx | 3 +- libs/design-system/src/app/TxPoolList.tsx | 24 -- libs/design-system/src/app/WalletBalance.tsx | 1 + .../src/app/WalletBalanceTip.tsx | 21 ++ .../src/app/WalletLayoutActions.tsx | 1 + .../src/components/BlockList.tsx | 3 +- .../src/components/DatumCard.tsx | 2 +- .../src/components/EntityAvatar.tsx | 2 +- .../src/components/EntityListItem.tsx | 2 +- .../src/components/EntityListItemLayout.tsx | 2 +- .../src/components/ValueCopyable.tsx | 2 +- .../src/components/ValueMenu.tsx | 2 +- libs/design-system/src/index.ts | 2 - libs/design-system/src/lib/entityTypes.ts | 233 ------------------ libs/hostd-js/src/api.ts | 70 ++++-- libs/hostd-react/src/api.ts | 78 ++++-- libs/hostd-types/src/api.ts | 99 ++++++-- libs/types/src/events.ts | 91 +++++++ libs/types/src/index.ts | 1 + libs/units/package.json | 3 +- libs/units/src/entityTypes.ts | 109 ++++++++ libs/units/src/events.ts | 49 ++++ libs/units/src/index.ts | 4 + libs/units/src/transactionTypes.ts | 104 ++++++++ .../units/src}/transactionValue.spec.ts | 16 +- .../units/src}/transactionValue.ts | 2 +- libs/walletd-types/src/api.ts | 9 +- libs/walletd-types/src/types.ts | 116 +-------- 63 files changed, 1265 insertions(+), 793 deletions(-) create mode 100644 .changeset/dirty-jeans-search.md create mode 100644 .changeset/famous-pears-melt.md create mode 100644 .changeset/kind-trains-bathe.md create mode 100644 .changeset/old-hounds-camp.md create mode 100644 .changeset/pretty-coins-compare.md create mode 100644 .changeset/thin-shirts-tickle.md create mode 100644 .changeset/two-olives-whisper.md create mode 100644 .changeset/wise-dolls-burn.md create mode 100644 apps/hostd/components/Wallet/StateError.tsx create mode 100644 apps/hostd/components/Wallet/StateNoneMatching.tsx create mode 100644 apps/hostd/components/Wallet/StateNoneYet.tsx create mode 100644 apps/hostd/contexts/transactions/columns.tsx create mode 100644 apps/hostd/contexts/transactions/types.ts delete mode 100644 apps/walletd/contexts/events/utils.ts delete mode 100644 libs/design-system/src/app/TxPoolList.tsx delete mode 100644 libs/design-system/src/lib/entityTypes.ts create mode 100644 libs/types/src/events.ts create mode 100644 libs/units/src/entityTypes.ts create mode 100644 libs/units/src/events.ts create mode 100644 libs/units/src/transactionTypes.ts rename {apps/walletd/contexts/events => libs/units/src}/transactionValue.spec.ts (96%) rename {apps/walletd/contexts/events => libs/units/src}/transactionValue.ts (98%) diff --git a/.changeset/dirty-jeans-search.md b/.changeset/dirty-jeans-search.md new file mode 100644 index 000000000..75df62cab --- /dev/null +++ b/.changeset/dirty-jeans-search.md @@ -0,0 +1,6 @@ +--- +'hostd': minor +--- + +Sync status is now determined by whether the last block's timestamp is within +the last 12 hours. diff --git a/.changeset/famous-pears-melt.md b/.changeset/famous-pears-melt.md new file mode 100644 index 000000000..d37910885 --- /dev/null +++ b/.changeset/famous-pears-melt.md @@ -0,0 +1,7 @@ +--- +'@siafoundation/hostd-js': minor +'@siafoundation/hostd-react': minor +'@siafoundation/hostd-types': minor +--- + +Updated with v2 endpoints and data types. Closes https://github.com/SiaFoundation/hostd/issues/440 diff --git a/.changeset/kind-trains-bathe.md b/.changeset/kind-trains-bathe.md new file mode 100644 index 000000000..ca2e0d58a --- /dev/null +++ b/.changeset/kind-trains-bathe.md @@ -0,0 +1,9 @@ +--- +'@siafoundation/hostd-js': minor +'@siafoundation/hostd-react': minor +'@siafoundation/hostd-types': minor +'@siafoundation/types': minor +'@siafoundation/walletd-types': minor +--- + +Core Event types have been moved to the core types library. Closes https://github.com/SiaFoundation/hostd/issues/440 diff --git a/.changeset/old-hounds-camp.md b/.changeset/old-hounds-camp.md new file mode 100644 index 000000000..516bc4c9b --- /dev/null +++ b/.changeset/old-hounds-camp.md @@ -0,0 +1,5 @@ +--- +'hostd': minor +--- + +The app has been updated to use the new v2 endpoints and data types. Closes https://github.com/SiaFoundation/hostd/issues/440 diff --git a/.changeset/pretty-coins-compare.md b/.changeset/pretty-coins-compare.md new file mode 100644 index 000000000..76e4b43b6 --- /dev/null +++ b/.changeset/pretty-coins-compare.md @@ -0,0 +1,8 @@ +--- +'hostd': minor +'walletd': minor +'@siafoundation/design-system': minor +'@siafoundation/units': minor +--- + +Transaction types have been refined to include new v2 derived transaction types. diff --git a/.changeset/thin-shirts-tickle.md b/.changeset/thin-shirts-tickle.md new file mode 100644 index 000000000..53c348fac --- /dev/null +++ b/.changeset/thin-shirts-tickle.md @@ -0,0 +1,5 @@ +--- +'hostd': minor +--- + +The wallet balance tip now includes an immature balance. diff --git a/.changeset/two-olives-whisper.md b/.changeset/two-olives-whisper.md new file mode 100644 index 000000000..12f15a39e --- /dev/null +++ b/.changeset/two-olives-whisper.md @@ -0,0 +1,7 @@ +--- +'walletd': minor +'@siafoundation/units': minor +'@siafoundation/design-system': minor +--- + +Event and transaction utility methods have been moved to the units library. diff --git a/.changeset/wise-dolls-burn.md b/.changeset/wise-dolls-burn.md new file mode 100644 index 000000000..57723aafb --- /dev/null +++ b/.changeset/wise-dolls-burn.md @@ -0,0 +1,5 @@ +--- +'@siafoundation/design-system': minor +--- + +WalletBalance now supports an optional immature sc value. diff --git a/apps/hostd-e2e/src/specs/volumes.spec.ts b/apps/hostd-e2e/src/specs/volumes.spec.ts index a9f95a37b..05155aeb6 100644 --- a/apps/hostd-e2e/src/specs/volumes.spec.ts +++ b/apps/hostd-e2e/src/specs/volumes.spec.ts @@ -8,8 +8,8 @@ import { } from '../fixtures/volumes' test('can create and delete a volume', async ({ page }) => { - const name = 'my-new-bucket' - const path = '/tmp' + const name = 'my-new-volume' + const path = '/data' await login({ page }) await navigateToVolumes({ page }) await deleteVolumeIfExists(page, name, path) diff --git a/apps/hostd/components/CmdRoot/WalletCmdGroup.tsx b/apps/hostd/components/CmdRoot/WalletCmdGroup.tsx index cdf41cd8b..ea3decf25 100644 --- a/apps/hostd/components/CmdRoot/WalletCmdGroup.tsx +++ b/apps/hostd/components/CmdRoot/WalletCmdGroup.tsx @@ -3,7 +3,7 @@ import { routes } from '../../config/routes' import { useRouter } from 'next/router' import { useDialog } from '../../contexts/dialog' import { CommandGroup, CommandItemNav, CommandItemSearch } from './Item' -import { useStateHost } from '@siafoundation/hostd-react' +import { useWallet } from '@siafoundation/hostd-react' import { Page } from './types' const commandPage = { @@ -20,7 +20,7 @@ type Props = { export function WalletCmdGroup({ currentPage, parentPage, pushPage }: Props) { const { openDialog, closeDialog } = useDialog() const router = useRouter() - const state = useStateHost({ + const wallet = useWallet({ config: { swr: { revalidateOnFocus: false, @@ -81,7 +81,7 @@ export function WalletCmdGroup({ currentPage, parentPage, pushPage }: Props) { currentPage={currentPage} commandPage={commandPage} onSelect={() => { - copyToClipboard(state.data?.walletAddress, 'wallet address') + copyToClipboard(wallet.data?.address, 'wallet address') closeDialog() }} > diff --git a/apps/hostd/components/Config/AnnounceButton.tsx b/apps/hostd/components/Config/AnnounceButton.tsx index 847eaefdc..1b7db4420 100644 --- a/apps/hostd/components/Config/AnnounceButton.tsx +++ b/apps/hostd/components/Config/AnnounceButton.tsx @@ -10,7 +10,7 @@ import { useDialog } from '../../contexts/dialog' import { useSettings, useSettingsAnnounce, - useStateHost, + useHostState, useTxPoolFee, } from '@siafoundation/hostd-react' import { humanSiacoin } from '@siafoundation/units' @@ -21,7 +21,7 @@ export function AnnounceButton() { const { openConfirmDialog } = useDialog() const txpoolFee = useTxPoolFee() const settingsAnnounce = useSettingsAnnounce() - const host = useStateHost() + const host = useHostState() const settings = useSettings({ config: { swr: { diff --git a/apps/hostd/components/Config/index.tsx b/apps/hostd/components/Config/index.tsx index 2a995ed57..f3f3900b4 100644 --- a/apps/hostd/components/Config/index.tsx +++ b/apps/hostd/components/Config/index.tsx @@ -3,7 +3,6 @@ import { ConfigurationPanel, PanelMenuSection, PanelMenuSetting, - Separator, FieldSwitch, ConfigurationPanelSetting, shouldShowField, @@ -73,26 +72,18 @@ export function Config() { form={form} /> - {shouldShowField({ form, fields, name: 'pinnedCurrency' }) && ( - <> - - - - )} - {shouldShowField({ form, fields, name: 'pinnedThreshold' }) && ( - <> - - - - )} + + } /> - } /> - } /> - - - - {shouldShowField({ - form, - fields, - name: 'shouldPinMaxCollateral', - }) && ( - -
- - Pin - - -
-
- )} - {shouldShowField({ - form, - fields, - name: 'shouldPinMaxCollateral', - }) && shouldPinMaxCollateral ? ( - - ) : ( - - )} - - } - /> - - )} - {shouldShowField({ - form, - fields, - name: 'contractPrice', - }) && ( - <> - - - - )} - {shouldShowField({ - form, - fields, - name: 'baseRPCPrice', - }) && ( - <> - - - - )} - {shouldShowField({ - form, - fields, - name: 'sectorAccessPrice', - }) && ( - <> - - - - )} - {shouldShowField({ - form, - fields, - name: 'priceTableValidity', - }) && ( - <> - - - + + {shouldShowField({ + form, + fields, + name: 'shouldPinMaxCollateral', + }) && ( + +
+ + Pin + + +
+
+ )} + {shouldShowField({ + form, + fields, + name: 'shouldPinMaxCollateral', + }) && shouldPinMaxCollateral ? ( + + ) : ( + + )} + + } + /> )} + + + +
+ return } diff --git a/apps/hostd/components/Profile/index.tsx b/apps/hostd/components/Profile/index.tsx index 0cc2fe218..643273d68 100644 --- a/apps/hostd/components/Profile/index.tsx +++ b/apps/hostd/components/Profile/index.tsx @@ -7,8 +7,10 @@ import { } from '@siafoundation/design-system' import { useSettings, - useStateHost, + useHostState, useSyncerPeers, + useWallet, + useConsensusNetwork, } from '@siafoundation/hostd-react' import { useSyncStatus } from '../../hooks/useSyncStatus' import { useDialog } from '../../contexts/dialog' @@ -17,7 +19,21 @@ import { humanTime } from '@siafoundation/units' export function Profile() { const { openDialog } = useDialog() - const state = useStateHost({ + const state = useHostState({ + config: { + swr: { + revalidateOnFocus: false, + }, + }, + }) + const wallet = useWallet({ + config: { + swr: { + revalidateOnFocus: false, + }, + }, + }) + const network = useConsensusNetwork({ config: { swr: { revalidateOnFocus: false, @@ -94,7 +110,7 @@ export function Profile() { @@ -114,7 +130,7 @@ export function Profile() { Network
- {state.data?.network} + {network.data?.name}
diff --git a/apps/hostd/components/Wallet/StateError.tsx b/apps/hostd/components/Wallet/StateError.tsx new file mode 100644 index 000000000..e7e6d84fc --- /dev/null +++ b/apps/hostd/components/Wallet/StateError.tsx @@ -0,0 +1,15 @@ +import { Text } from '@siafoundation/design-system' +import { MisuseOutline32 } from '@siafoundation/react-icons' + +export function StateError() { + return ( +
+ + + + + Error fetching transactions. + +
+ ) +} diff --git a/apps/hostd/components/Wallet/StateNoneMatching.tsx b/apps/hostd/components/Wallet/StateNoneMatching.tsx new file mode 100644 index 000000000..db7256e7d --- /dev/null +++ b/apps/hostd/components/Wallet/StateNoneMatching.tsx @@ -0,0 +1,15 @@ +import { Text } from '@siafoundation/design-system' +import { Filter32 } from '@siafoundation/react-icons' + +export function StateNoneMatching() { + return ( +
+ + + + + No transactions matching filters. + +
+ ) +} diff --git a/apps/hostd/components/Wallet/StateNoneYet.tsx b/apps/hostd/components/Wallet/StateNoneYet.tsx new file mode 100644 index 000000000..3608574d9 --- /dev/null +++ b/apps/hostd/components/Wallet/StateNoneYet.tsx @@ -0,0 +1,15 @@ +import { Text } from '@siafoundation/design-system' +import { Money32 } from '@siafoundation/react-icons' + +export function StateNoneYet() { + return ( +
+ + + + + The wallet has no transactions yet. + +
+ ) +} diff --git a/apps/hostd/components/Wallet/WalletFilterBar.tsx b/apps/hostd/components/Wallet/WalletFilterBar.tsx index 8a2b6f3c2..bef137535 100644 --- a/apps/hostd/components/Wallet/WalletFilterBar.tsx +++ b/apps/hostd/components/Wallet/WalletFilterBar.tsx @@ -1,9 +1,14 @@ -import { WalletSyncWarning } from '@siafoundation/design-system' +import { + PaginatorUnknownTotal, + WalletSyncWarning, +} from '@siafoundation/design-system' import { useSyncStatus } from '../../hooks/useSyncStatus' +import { useTransactions } from '../../contexts/transactions' export function WalletFilterBar() { const { isSynced, syncPercent, isWalletSynced, walletScanPercent } = useSyncStatus() + const { offset, limit, pageCount, dataState } = useTransactions() return (
+
) } diff --git a/apps/hostd/components/Wallet/index.tsx b/apps/hostd/components/Wallet/index.tsx index 63e5cea4d..2808d1b06 100644 --- a/apps/hostd/components/Wallet/index.tsx +++ b/apps/hostd/components/Wallet/index.tsx @@ -1,8 +1,7 @@ import { - EntityList, WalletLayoutActions, BalanceEvolution, - PaginatorUnknownTotal, + Table, } from '@siafoundation/design-system' import { useWallet } from '@siafoundation/hostd-react' import { useDialog } from '../../contexts/dialog' @@ -11,9 +10,11 @@ import BigNumber from 'bignumber.js' import { HostdSidenav } from '../HostdSidenav' import { HostdAuthedLayout } from '../HostdAuthedLayout' import { useSyncStatus } from '../../hooks/useSyncStatus' -import { EmptyState } from './EmptyState' import { useTransactions } from '../../contexts/transactions' import { WalletFilterBar } from './WalletFilterBar' +import { StateNoneMatching } from './StateNoneMatching' +import { StateNoneYet } from './StateNoneYet' +import { StateError } from './StateError' export function Wallet() { const { openDialog } = useDialog() @@ -21,8 +22,19 @@ export function Wallet() { const { isSynced, isWalletSynced, syncPercent, walletScanPercent } = useSyncStatus() - const { dataset, balances, metrics, offset, limit, dataState, pageCount } = - useTransactions() + const { + balances, + metrics, + dataset, + dataState, + columns, + cellContext, + sortableColumns, + sortDirection, + sortField, + toggleSort, + defaultPageSize, + } = useTransactions() return ( } > -
+
{balances?.length && balances.find((b) => b.sc) ? ( ) : null} - } - actions={ - + emptyState={ + dataState === 'noneMatchingFilters' ? ( + + ) : dataState === 'noneYet' ? ( + + ) : dataState === 'error' ? ( + + ) : null } + pageSize={defaultPageSize} + data={dataset} + context={cellContext} + columns={columns} + sortableColumns={sortableColumns} + sortDirection={sortDirection} + sortField={sortField} + toggleSort={toggleSort} />
diff --git a/apps/hostd/config/routes.ts b/apps/hostd/config/routes.ts index e24d5eeb9..21b329a71 100644 --- a/apps/hostd/config/routes.ts +++ b/apps/hostd/config/routes.ts @@ -1,4 +1,4 @@ -import { stateHostRoute } from '@siafoundation/hostd-types' +import { hostStateRoute } from '@siafoundation/hostd-types' export const routes = { home: '/', @@ -28,4 +28,4 @@ export const routes = { login: '/login', } -export const connectivityRoute = stateHostRoute +export const connectivityRoute = hostStateRoute diff --git a/apps/hostd/contexts/config/index.tsx b/apps/hostd/contexts/config/index.tsx index bb5aebf05..db297921e 100644 --- a/apps/hostd/contexts/config/index.tsx +++ b/apps/hostd/contexts/config/index.tsx @@ -17,7 +17,7 @@ import { checkIfAnyResourcesErrored, } from './resources' import { useOnValid } from './useOnValid' -import { useStateHost } from '@siafoundation/hostd-react' +import { useHostState } from '@siafoundation/hostd-react' export function useConfigMain() { const { settings, settingsPinned, dynDNSCheck } = useResources() @@ -54,7 +54,7 @@ export function useConfigMain() { [resources] ) - const state = useStateHost() + const state = useHostState() const pinningEnabled = state.data?.explorer.enabled const revalidateAndResetForm = useCallback(async () => { const _settings = await settings.mutate() diff --git a/apps/hostd/contexts/config/useForm.tsx b/apps/hostd/contexts/config/useForm.tsx index 34454f476..36dd80139 100644 --- a/apps/hostd/contexts/config/useForm.tsx +++ b/apps/hostd/contexts/config/useForm.tsx @@ -4,7 +4,7 @@ import { useEffect, useMemo, useRef } from 'react' import { getFields } from './fields' import useLocalStorageState from 'use-local-storage-state' import { useSiaCentralExchangeRates } from '@siafoundation/sia-central-react' -import { useStateHost } from '@siafoundation/hostd-react' +import { useHostState } from '@siafoundation/hostd-react' import { useAutoCalculatedFields } from './useAutoCalculatedFields' export function useForm() { @@ -25,7 +25,7 @@ export function useForm() { }) const rates = useSiaCentralExchangeRates() - const state = useStateHost() + const state = useHostState() const pinningEnabled = state.data?.explorer.enabled // Field validation is only re-applied on re-mount, // so we pass a ref with latest data that can be used interally. diff --git a/apps/hostd/contexts/config/useOnValid.tsx b/apps/hostd/contexts/config/useOnValid.tsx index 6250f8e29..e7fbf9360 100644 --- a/apps/hostd/contexts/config/useOnValid.tsx +++ b/apps/hostd/contexts/config/useOnValid.tsx @@ -8,9 +8,9 @@ import { SettingsData } from './types' import { transformUpSettings, transformUpSettingsPinned } from './transform' import { Resources } from './resources' import { + useHostState, useSettingsPinnedUpdate, useSettingsUpdate, - useStateHost, } from '@siafoundation/hostd-react' export function useOnValid({ @@ -20,16 +20,15 @@ export function useOnValid({ resources: Resources revalidateAndResetForm: () => Promise }) { - const state = useStateHost() - const settingsUpdate = useSettingsUpdate() - const settingsPinnedUpdate = useSettingsPinnedUpdate() - const host = useStateHost({ + const state = useHostState({ config: { swr: { refreshInterval: minutesInMilliseconds(1), }, }, }) + const settingsUpdate = useSettingsUpdate() + const settingsPinnedUpdate = useSettingsPinnedUpdate() const onValid = useCallback( async (values: SettingsData) => { if (!resources) { @@ -60,7 +59,7 @@ export function useOnValid({ } const needsToAnnounce = - host.data?.lastAnnouncement?.address !== values.netAddress + state.data?.lastAnnouncement?.address !== values.netAddress if (needsToAnnounce) { triggerSuccessToast({ title: 'Settings have been saved', @@ -86,7 +85,6 @@ export function useOnValid({ settingsUpdate, settingsPinnedUpdate, revalidateAndResetForm, - host.data, state.data, ] ) diff --git a/apps/hostd/contexts/transactions/columns.tsx b/apps/hostd/contexts/transactions/columns.tsx new file mode 100644 index 000000000..8724064c5 --- /dev/null +++ b/apps/hostd/contexts/transactions/columns.tsx @@ -0,0 +1,175 @@ +import { + Text, + TableColumn, + ValueCopyable, + LoadingDots, + ValueScFiat, + ValueSf, + Tooltip, + Badge, +} from '@siafoundation/design-system' +import { humanDate, getTxTypeLabel } from '@siafoundation/units' +import { CellContext, EventData, TableColumnId } from './types' +import { Locked16, Unlocked16 } from '@siafoundation/react-icons' + +type EventsTableColumn = TableColumn & { + fixed?: boolean + category?: string +} + +export const columns: EventsTableColumn[] = [ + { + id: 'transactionId', + label: 'transaction ID', + category: 'general', + render: ({ data: { id }, context }) => { + if (!id) { + return null + } + return ( + + ) + }, + }, + { + id: 'type', + label: 'type', + category: 'general', + fixed: true, + render: ({ data: { txType } }) => { + return {getTxTypeLabel(txType)} + }, + }, + { + id: 'height', + label: 'height', + category: 'general', + contentClassName: 'justify-end', + render: ({ data: { height, pending, maturityHeight, isMature } }) => { + if (pending) { + return ( + + + + ) + } + if (!height) { + return null + } + if (height && maturityHeight && maturityHeight > height) { + return ( + +
+
+ + {isMature ? : } + {maturityHeight.toLocaleString()} + +
+
+
+
+
+ + {height.toLocaleString()} + +
+
+ + ) + } + return ( + + {height.toLocaleString()} + + ) + }, + }, + { + id: 'timestamp', + label: 'timestamp', + category: 'general', + contentClassName: 'justify-end', + render: ({ data: { timestamp, pending } }) => { + if (pending) { + return ( + + + + ) + } + return ( + + {humanDate(timestamp, { timeStyle: 'short' })} + + ) + }, + }, + { + id: 'amount', + label: 'amount', + category: 'general', + contentClassName: 'w-[120px] justify-end', + render: ({ data: { amountSc, amountSf } }) => { + if (!amountSc) { + return null + } + return ( +
+ {!amountSc.isZero() && ( + + )} + {!!amountSf && } +
+ ) + }, + }, + { + id: 'fee', + label: 'fee', + category: 'general', + contentClassName: 'w-[120px] justify-end', + render: ({ data: { fee } }) => { + if (!fee) { + return null + } + return + }, + }, + { + id: 'contractId', + label: 'contract ID', + category: 'general', + render: ({ data: { contractId }, context }) => { + if (!contractId) { + return null + } + return ( + + ) + }, + }, +] diff --git a/apps/hostd/contexts/transactions/index.tsx b/apps/hostd/contexts/transactions/index.tsx index 6e17e556b..38cfdbf6b 100644 --- a/apps/hostd/contexts/transactions/index.tsx +++ b/apps/hostd/contexts/transactions/index.tsx @@ -1,24 +1,38 @@ import { - TxType, daysInMilliseconds, - getTransactionType, useDatasetEmptyState, + useServerFilters, + useTableState, } from '@siafoundation/design-system' import { useMetricsPeriod, + useWalletEvents, useWalletPending, - useWalletTransactions, } from '@siafoundation/hostd-react' import { createContext, useContext, useMemo } from 'react' -import { useDialog } from '../dialog' import BigNumber from 'bignumber.js' import { useRouter } from 'next/router' import { useSiascanUrl } from '../../hooks/useSiascanUrl' import { Transaction } from '@siafoundation/types' import { defaultDatasetRefreshInterval } from '../../config/swr' +import { useSyncStatus } from '../../hooks/useSyncStatus' +import { columns } from './columns' +import { + TxType, + calculateScValue, + getEventContractId, + getEventFee, + getEventTxType, +} from '@siafoundation/units' +import { + CellContext, + EventData, + columnsDefaultVisible, + defaultSortField, + sortOptions, +} from './types' -const defaultLimit = 50 -const filters = [] +const defaultPageSize = 50 export type TransactionData = { id: string @@ -37,9 +51,9 @@ export type TransactionData = { function useTransactionsMain() { const router = useRouter() - const limit = Number(router.query.limit || defaultLimit) + const limit = Number(router.query.limit || defaultPageSize) const offset = Number(router.query.offset || 0) - const transactions = useWalletTransactions({ + const events = useWalletEvents({ params: { limit, offset, @@ -58,52 +72,92 @@ function useTransactionsMain() { }, }) - const { openDialog } = useDialog() - const siascanUrl = useSiascanUrl() + const { filters, setFilter, removeFilter, removeLastFilter, resetFilters } = + useServerFilters() - const dataset: TransactionData[] | null = useMemo(() => { - if (!pending.data || !transactions.data) { + const syncStatus = useSyncStatus() + const dataset = useMemo(() => { + if (!events.data || !pending.data) { return null } - return [ - ...(pending.data || []).map((t): TransactionData => { - const notRealTxn = t.source !== 'transaction' - return { - id: t.id, - type: 'transaction', - unconfirmed: true, - txType: getTransactionType(t.transaction, t.source), - hash: t.id, - inflow: t.inflow, - outflow: t.outflow, - sc: new BigNumber(t.inflow).minus(t.outflow), - siascanUrl: notRealTxn ? undefined : siascanUrl, - timestamp: new Date(t.timestamp).getTime(), - onClick: () => openDialog('transactionDetails', t.id), - raw: t.transaction, - } - }), - ...(transactions.data || []) - .map((t): TransactionData => { - const notRealTxn = t.source !== 'transaction' - return { - id: t.id, - type: 'transaction', - unconfirmed: false, - txType: getTransactionType(t.transaction, t.source), - hash: t.id, - inflow: t.inflow, - outflow: t.outflow, - sc: new BigNumber(t.inflow).minus(t.outflow), - siascanUrl: notRealTxn ? undefined : siascanUrl, - timestamp: new Date(t.timestamp).getTime(), - onClick: () => openDialog('transactionDetails', t.id), - raw: t.transaction, - } - }) - .sort((a, b) => (a.timestamp < b.timestamp ? 1 : -1)), - ] - }, [pending, transactions, openDialog, siascanUrl]) + const dataPending: EventData[] = pending.data.map((e) => { + const amountSc = calculateScValue(e) + const fee = getEventFee(e) + const event: EventData = { + id: e.id, + timestamp: 0, + pending: true, + type: e.type, + txType: getEventTxType(e), + isMature: false, + amountSc, + fee, + } + return event + }) + const dataEvents: EventData[] = events.data.map((e) => { + const amountSc = calculateScValue(e) + const fee = getEventFee(e) + const contractId = getEventContractId(e) + const isMature = e.maturityHeight <= syncStatus.nodeBlockHeight + const res: EventData = { + id: e.id, + type: e.type, + txType: getEventTxType(e), + timestamp: new Date(e.timestamp).getTime(), + maturityHeight: e.maturityHeight, + isMature, + height: e.index.height, + pending: false, + amountSc, + fee, + contractId, + } + return res + }) + return [...dataPending.reverse(), ...dataEvents] + }, [events.data, pending.data, syncStatus.nodeBlockHeight]) + + const { + configurableColumns, + enabledColumns, + sortableColumns, + toggleColumnVisibility, + setColumnsVisible, + setColumnsHidden, + toggleSort, + setSortDirection, + setSortField, + sortField, + sortDirection, + resetDefaultColumnVisibility, + } = useTableState('walletd/v0/events', { + columns, + columnsDefaultVisible, + sortOptions, + defaultSortField, + }) + + const filteredTableColumns = useMemo( + () => + columns.filter( + (column) => column.fixed || enabledColumns.includes(column.id) + ), + [enabledColumns] + ) + + const isValidating = events.isValidating || pending.isValidating + const error = events.error || pending.error + + const dataState = useDatasetEmptyState(dataset, isValidating, error, filters) + + const siascanUrl = useSiascanUrl() + const cellContext = useMemo( + () => ({ + siascanUrl, + }), + [siascanUrl] + ) const dayPeriods = 30 const start = useMemo(() => { @@ -132,14 +186,6 @@ function useTransactionsMain() { [metrics.data] ) - const error = transactions.error - const dataState = useDatasetEmptyState( - dataset, - transactions.isValidating, - error, - filters - ) - return { balances, metrics, @@ -149,6 +195,27 @@ function useTransactionsMain() { offset, limit, pageCount: dataset?.length || 0, + defaultPageSize, + cellContext, + configurableColumns, + enabledColumns, + sortableColumns, + toggleColumnVisibility, + setColumnsVisible, + setColumnsHidden, + toggleSort, + setSortDirection, + setSortField, + sortField, + sortDirection, + resetDefaultColumnVisibility, + filters, + setFilter, + removeFilter, + removeLastFilter, + resetFilters, + filteredTableColumns, + columns, } } diff --git a/apps/hostd/contexts/transactions/types.ts b/apps/hostd/contexts/transactions/types.ts new file mode 100644 index 000000000..034f0da3b --- /dev/null +++ b/apps/hostd/contexts/transactions/types.ts @@ -0,0 +1,53 @@ +import { TxType } from '@siafoundation/units' +import { WalletEventType } from '@siafoundation/types' +import BigNumber from 'bignumber.js' + +export type CellContext = { + siascanUrl: string +} + +export type EventData = { + id: string + transactionId?: string + timestamp: number + height?: number + maturityHeight?: number + isMature?: boolean + pending: boolean + type: WalletEventType + txType?: TxType + fee?: BigNumber + amountSc?: BigNumber + amountSf?: number + contractId?: string + className?: string +} + +export type TableColumnId = + | 'transactionId' + | 'type' + | 'height' + | 'timestamp' + | 'amount' + | 'fee' + | 'transactionId' + | 'contractId' + +export const columnsDefaultVisible: TableColumnId[] = [ + 'transactionId', + 'type', + 'height', + 'timestamp', + 'amount', + 'fee', +] + +export type SortField = 'id' + +export const defaultSortField: SortField = 'id' + +export const sortOptions: { + id: SortField + label: string + category: string +}[] = [] diff --git a/apps/hostd/hooks/useHostOSPathSeparator.ts b/apps/hostd/hooks/useHostOSPathSeparator.ts index 89e69acbc..ed5de51ae 100644 --- a/apps/hostd/hooks/useHostOSPathSeparator.ts +++ b/apps/hostd/hooks/useHostOSPathSeparator.ts @@ -1,7 +1,7 @@ -import { useStateHost } from '@siafoundation/hostd-react' +import { useHostState } from '@siafoundation/hostd-react' export function useHostOSPathSeparator() { - const state = useStateHost({ + const state = useHostState({ config: { swr: { revalidateOnFocus: false, diff --git a/apps/hostd/hooks/useSiascanUrl.ts b/apps/hostd/hooks/useSiascanUrl.ts index 92ce44e0c..059255883 100644 --- a/apps/hostd/hooks/useSiascanUrl.ts +++ b/apps/hostd/hooks/useSiascanUrl.ts @@ -1,9 +1,9 @@ import { webLinks } from '@siafoundation/design-system' -import { useStateHost } from '@siafoundation/hostd-react' +import { useConsensusNetwork } from '@siafoundation/hostd-react' export function useSiascanUrl() { - const state = useStateHost() - return state.data?.network === 'Zen Testnet' + const state = useConsensusNetwork() + return state.data?.name === 'zen' ? webLinks.explore.testnetZen : webLinks.explore.mainnet } diff --git a/apps/hostd/hooks/useSyncStatus.ts b/apps/hostd/hooks/useSyncStatus.ts index 8b18be4d6..e576cf0f9 100644 --- a/apps/hostd/hooks/useSyncStatus.ts +++ b/apps/hostd/hooks/useSyncStatus.ts @@ -1,26 +1,29 @@ import { useAppSettings } from '@siafoundation/react-core' import { useEstimatedNetworkBlockHeight, - useStateConsensus, - useWallet, + useConsensusTipState, + useIndexTip, } from '@siafoundation/hostd-react' +import { hoursInMilliseconds } from '@siafoundation/design-system' export function useSyncStatus() { const { isUnlockedAndAuthedRoute } = useAppSettings() - const state = useStateConsensus({ + + const state = useConsensusTipState({ config: { swr: { - refreshInterval: (data) => (data?.synced ? 60_000 : 10_000), + refreshInterval: (data) => (getIsSynced(data) ? 60_000 : 10_000), }, }, }) + const isSynced = getIsSynced(state.data) const estimatedBlockHeight = useEstimatedNetworkBlockHeight() - const nodeBlockHeight = state.data ? state.data?.chainIndex.height : 0 - const wallet = useWallet({ + const nodeBlockHeight = state.data ? state.data?.index.height : 0 + const scan = useIndexTip({ config: { swr: { refreshInterval: (data) => - data?.scanHeight >= nodeBlockHeight ? 60_000 : 10_000, + data?.height >= nodeBlockHeight ? 60_000 : 10_000, }, }, }) @@ -33,11 +36,11 @@ export function useSyncStatus() { : 0 const walletScanPercent = - isUnlockedAndAuthedRoute && nodeBlockHeight && wallet.data + isUnlockedAndAuthedRoute && nodeBlockHeight && scan.data ? Number( - ( - Math.min(wallet.data.scanHeight / estimatedBlockHeight, 1) * 100 - ).toFixed(1) + (Math.min(scan.data.height / estimatedBlockHeight, 1) * 100).toFixed( + 1 + ) ) : 0 @@ -52,9 +55,8 @@ export function useSyncStatus() { : false return { - isSynced: state.data?.synced, - isWalletSynced: - state.data?.synced && wallet.data?.scanHeight >= nodeBlockHeight - 1, + isSynced, + isWalletSynced: isSynced && scan.data?.height >= nodeBlockHeight - 1, nodeBlockHeight, estimatedBlockHeight, syncPercent, @@ -63,3 +65,11 @@ export function useSyncStatus() { firstTimeSyncing, } } + +function getIsSynced(data?: { prevTimestamps: string[] }) { + // Last block is greater than 12 hours ago + return data?.prevTimestamps[0] + ? new Date(data?.prevTimestamps[0]).getTime() > + Date.now() - hoursInMilliseconds(12) + : false +} diff --git a/apps/renterd/contexts/transactions/index.tsx b/apps/renterd/contexts/transactions/index.tsx index 588bd3d04..99189745a 100644 --- a/apps/renterd/contexts/transactions/index.tsx +++ b/apps/renterd/contexts/transactions/index.tsx @@ -1,7 +1,5 @@ import { - TxType, daysInMilliseconds, - getTransactionType, stripPrefix, useDatasetEmptyState, } from '@siafoundation/design-system' @@ -17,6 +15,7 @@ import { useRouter } from 'next/router' import { useSiascanUrl } from '../../hooks/useSiascanUrl' import { Transaction } from '@siafoundation/types' import { defaultDatasetRefreshInterval } from '../../config/swr' +import { TxType, getTransactionType } from '@siafoundation/units' const defaultLimit = 50 const filters = [] diff --git a/apps/renterd/dialogs/RenterdTransactionDetailsDialog.tsx b/apps/renterd/dialogs/RenterdTransactionDetailsDialog.tsx index 917887560..256a2f611 100644 --- a/apps/renterd/dialogs/RenterdTransactionDetailsDialog.tsx +++ b/apps/renterd/dialogs/RenterdTransactionDetailsDialog.tsx @@ -1,10 +1,7 @@ import { useMemo } from 'react' import { TransactionDetailsDialog } from '@siafoundation/design-system' import { useDialog } from '../contexts/dialog' -import { - TransactionDataConfirmed, - useTransactions, -} from '../contexts/transactions' +import { useTransactions } from '../contexts/transactions' export function RenterdTransactionDetailsDialog() { const { id, dialog, onOpenChange } = useDialog() @@ -18,7 +15,7 @@ export function RenterdTransactionDetailsDialog() { return ( diff --git a/apps/walletd/contexts/events/columns.tsx b/apps/walletd/contexts/events/columns.tsx index 179231228..05a0c7c5d 100644 --- a/apps/walletd/contexts/events/columns.tsx +++ b/apps/walletd/contexts/events/columns.tsx @@ -8,10 +8,9 @@ import { Tooltip, Badge, } from '@siafoundation/design-system' -import { humanDate } from '@siafoundation/units' +import { getTxTypeLabel, humanDate } from '@siafoundation/units' import { CellContext, EventData, TableColumnId } from './types' import { Locked16, Unlocked16 } from '@siafoundation/react-icons' -import { eventTypeToLabel } from './utils' type EventsTableColumn = TableColumn & { fixed?: boolean @@ -19,13 +18,6 @@ type EventsTableColumn = TableColumn & { } export const columns: EventsTableColumn[] = [ - // { - // id: 'actions', - // label: '', - // fixed: true, - // cellClassName: 'w-[50px] !pl-2 !pr-4 [&+*]:!pl-0', - // render: ({ data: { name } }) => null, - // }, { id: 'transactionId', label: 'transaction ID', @@ -50,8 +42,8 @@ export const columns: EventsTableColumn[] = [ label: 'type', category: 'general', fixed: true, - render: ({ data: { type } }) => { - return {eventTypeToLabel(type)} + render: ({ data: { txType } }) => { + return {getTxTypeLabel(txType)} }, }, { diff --git a/apps/walletd/contexts/events/index.tsx b/apps/walletd/contexts/events/index.tsx index fb36de4ac..64d5df722 100644 --- a/apps/walletd/contexts/events/index.tsx +++ b/apps/walletd/contexts/events/index.tsx @@ -7,6 +7,13 @@ import { useWalletEvents, useWalletEventsUnconfirmed, } from '@siafoundation/walletd-react' +import { + calculateScValue, + calculateSfValue, + getEventContractId, + getEventFee, + getEventTxType, +} from '@siafoundation/units' import { createContext, useContext, useMemo } from 'react' import { CellContext, @@ -20,8 +27,6 @@ import { useRouter } from 'next/router' import { useSiascanUrl } from '../../hooks/useSiascanUrl' import { defaultDatasetRefreshInterval } from '../../config/swr' import { useSyncStatus } from '../../hooks/useSyncStatus' -import { getContractId, getFee } from './utils' -import { calculateScValue, calculateSfValue } from './transactionValue' const defaultLimit = 100 @@ -65,12 +70,13 @@ export function useEventsMain() { const dataTxPool: EventData[] = responseTxPool.data.map((e) => { const amountSc = calculateScValue(e) const amountSf = calculateSfValue(e) - const fee = getFee(e) + const fee = getEventFee(e) const event: EventData = { id: e.id, timestamp: 0, pending: true, type: e.type, + txType: getEventTxType(e), isMature: false, amountSc, amountSf, @@ -81,12 +87,13 @@ export function useEventsMain() { const dataEvents: EventData[] = responseEvents.data.map((e) => { const amountSc = calculateScValue(e) const amountSf = calculateSfValue(e) - const fee = getFee(e) - const contractId = getContractId(e) + const fee = getEventFee(e) + const contractId = getEventContractId(e) const isMature = e.maturityHeight <= syncStatus.nodeBlockHeight const res: EventData = { id: e.id, type: e.type, + txType: getEventTxType(e), timestamp: new Date(e.timestamp).getTime(), maturityHeight: e.maturityHeight, isMature, diff --git a/apps/walletd/contexts/events/types.ts b/apps/walletd/contexts/events/types.ts index 0c9b9abb9..2c3196574 100644 --- a/apps/walletd/contexts/events/types.ts +++ b/apps/walletd/contexts/events/types.ts @@ -1,4 +1,5 @@ -import { WalletEvent } from '@siafoundation/walletd-types' +import { WalletEventType } from '@siafoundation/types' +import { TxType } from '@siafoundation/units' import BigNumber from 'bignumber.js' export type CellContext = { @@ -13,7 +14,8 @@ export type EventData = { maturityHeight?: number isMature?: boolean pending: boolean - type: WalletEvent['type'] + type: WalletEventType + txType: TxType fee?: BigNumber amountSc?: BigNumber amountSf?: number diff --git a/apps/walletd/contexts/events/utils.ts b/apps/walletd/contexts/events/utils.ts deleted file mode 100644 index 52dd12c49..000000000 --- a/apps/walletd/contexts/events/utils.ts +++ /dev/null @@ -1,46 +0,0 @@ -import BigNumber from 'bignumber.js' -import { WalletEvent } from '@siafoundation/walletd-types' - -export function getFee(e: WalletEvent) { - if (e.type === 'v2Transaction') { - return new BigNumber(e.data.minerFee) - } - return 'transaction' in e.data && e.data.transaction.minerFees?.length - ? new BigNumber(e.data.transaction.minerFees[0]) - : undefined -} - -export function getContractId(e: WalletEvent) { - if (e.type === 'v1ContractResolution') { - return e.data.parent.id - } - if (e.type === 'v2ContractResolution') { - return e.data.parent.id - } - return undefined -} - -export function eventTypeToLabel(type: WalletEvent['type']) { - if (type === 'v1Transaction') { - return 'v1 transaction' - } - if (type === 'v2Transaction') { - return 'v2 transaction' - } - if (type === 'v1ContractResolution') { - return 'v1 contract resolution' - } - if (type === 'v2ContractResolution') { - return 'v2 contract resolution' - } - if (type === 'miner') { - return 'miner payout' - } - if (type === 'siafundClaim') { - return 'siafund claim' - } - if (type === 'foundation') { - return 'foundation subsidy' - } - return 'unknown' -} diff --git a/libs/design-system/src/app/TransactionDetailsDialog.tsx b/libs/design-system/src/app/TransactionDetailsDialog.tsx index 14b65303e..f89f1bac4 100644 --- a/libs/design-system/src/app/TransactionDetailsDialog.tsx +++ b/libs/design-system/src/app/TransactionDetailsDialog.tsx @@ -1,12 +1,11 @@ import { Codeblock } from '../core/Codeblock' import { Text } from '../core/Text' import { ValueSc } from '../components/ValueSc' -import { humanDate } from '@siafoundation/units' +import { humanDate, getTxTypeLabel, TxType } from '@siafoundation/units' import BigNumber from 'bignumber.js' import { Dialog } from '../core/Dialog' import { getTitleId } from '../lib/utils' import { Transaction } from '@siafoundation/types' -import { getTxTypeLabel, TxType } from '../lib/entityTypes' import { upperFirst } from '@technically/lodash' type Props = { diff --git a/libs/design-system/src/app/TxPoolList.tsx b/libs/design-system/src/app/TxPoolList.tsx deleted file mode 100644 index dea432413..000000000 --- a/libs/design-system/src/app/TxPoolList.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { EntityList } from '../components/EntityList' -import { getTransactionTotals, getTransactionType } from '../lib/entityTypes' -import { Transaction } from '@siafoundation/types' - -type Props = { - isLoading?: boolean - transactions?: Transaction[] -} - -export function TxPoolList({ transactions, isLoading }: Props) { - return ( - ({ - type: 'transaction', - txType: getTransactionType(t), - sc: getTransactionTotals(t).sc, - sf: getTransactionTotals(t).sf, - }))} - /> - ) -} diff --git a/libs/design-system/src/app/WalletBalance.tsx b/libs/design-system/src/app/WalletBalance.tsx index 468b01956..4d2fd012b 100644 --- a/libs/design-system/src/app/WalletBalance.tsx +++ b/libs/design-system/src/app/WalletBalance.tsx @@ -15,6 +15,7 @@ export function WalletBalance({ unconfirmed: BigNumber confirmed: BigNumber spendable: BigNumber + immature?: BigNumber } isSynced: boolean syncingMessage?: string diff --git a/libs/design-system/src/app/WalletBalanceTip.tsx b/libs/design-system/src/app/WalletBalanceTip.tsx index b889fad52..e6dd6c76d 100644 --- a/libs/design-system/src/app/WalletBalanceTip.tsx +++ b/libs/design-system/src/app/WalletBalanceTip.tsx @@ -14,6 +14,7 @@ export function WalletBalanceTip({ unconfirmed: BigNumber confirmed: BigNumber spendable: BigNumber + immature?: BigNumber } children: React.ReactNode }) { @@ -39,6 +40,26 @@ export function WalletBalanceTip({ />
+ {balanceSc.immature && ( + <> + +
+
+ immature + + All confirmed but still locked outputs. + +
+
+ +
+
+ + )}
diff --git a/libs/design-system/src/app/WalletLayoutActions.tsx b/libs/design-system/src/app/WalletLayoutActions.tsx index d28902a01..83adeffd5 100644 --- a/libs/design-system/src/app/WalletLayoutActions.tsx +++ b/libs/design-system/src/app/WalletLayoutActions.tsx @@ -12,6 +12,7 @@ type Props = { unconfirmed: BigNumber confirmed: BigNumber spendable: BigNumber + immature?: BigNumber } receiveSiacoin?: () => void sendSiacoin: () => void diff --git a/libs/design-system/src/components/BlockList.tsx b/libs/design-system/src/components/BlockList.tsx index 2a8c8326c..8a78b0829 100644 --- a/libs/design-system/src/components/BlockList.tsx +++ b/libs/design-system/src/components/BlockList.tsx @@ -2,8 +2,7 @@ import { Panel } from '../core/Panel' import { Heading } from '../core/Heading' import { Link } from '../core/Link' import { Text } from '../core/Text' -import { getEntityTypeLabel } from '../lib/entityTypes' -import { humanNumber } from '@siafoundation/units' +import { humanNumber, getEntityTypeLabel } from '@siafoundation/units' import { formatDistance } from 'date-fns' import { EntityAvatar } from './EntityAvatar' import { cx } from 'class-variance-authority' diff --git a/libs/design-system/src/components/DatumCard.tsx b/libs/design-system/src/components/DatumCard.tsx index 79bfb1d7e..e4f7e1f3b 100644 --- a/libs/design-system/src/components/DatumCard.tsx +++ b/libs/design-system/src/components/DatumCard.tsx @@ -5,7 +5,7 @@ import { Tooltip } from '../core/Tooltip' import BigNumber from 'bignumber.js' import { upperFirst } from '@technically/lodash' import { Panel } from '../core/Panel' -import { EntityType, getEntityTypeLabel } from '../lib/entityTypes' +import { EntityType, getEntityTypeLabel } from '@siafoundation/units' import { cx } from 'class-variance-authority' import { Skeleton } from '../core/Skeleton' import { ValueScFiat } from './ValueScFiat' diff --git a/libs/design-system/src/components/EntityAvatar.tsx b/libs/design-system/src/components/EntityAvatar.tsx index 702959d3b..c5b7489fe 100644 --- a/libs/design-system/src/components/EntityAvatar.tsx +++ b/libs/design-system/src/components/EntityAvatar.tsx @@ -3,7 +3,7 @@ import { Avatar } from '../core/Avatar' import { Link } from '../core/Link' import { Tooltip } from '../core/Tooltip' -import { EntityType, getEntityTypeLabel } from '../lib/entityTypes' +import { EntityType, getEntityTypeLabel } from '@siafoundation/units' type Props = { initials?: string diff --git a/libs/design-system/src/components/EntityListItem.tsx b/libs/design-system/src/components/EntityListItem.tsx index 8df484f8f..3c701a05e 100644 --- a/libs/design-system/src/components/EntityListItem.tsx +++ b/libs/design-system/src/components/EntityListItem.tsx @@ -9,7 +9,7 @@ import { getEntityTypeLabel, getTxTypeLabel, TxType, -} from '../lib/entityTypes' +} from '@siafoundation/units' import { humanNumber } from '@siafoundation/units' import { formatDistance } from 'date-fns' import { upperFirst } from '@technically/lodash' diff --git a/libs/design-system/src/components/EntityListItemLayout.tsx b/libs/design-system/src/components/EntityListItemLayout.tsx index 4aaa9ba4f..de5b5d468 100644 --- a/libs/design-system/src/components/EntityListItemLayout.tsx +++ b/libs/design-system/src/components/EntityListItemLayout.tsx @@ -1,4 +1,4 @@ -import { EntityType } from '../lib/entityTypes' +import { EntityType } from '@siafoundation/units' import { EntityAvatar } from './EntityAvatar' import { cx } from 'class-variance-authority' diff --git a/libs/design-system/src/components/ValueCopyable.tsx b/libs/design-system/src/components/ValueCopyable.tsx index 5f8705f7a..db5da2715 100644 --- a/libs/design-system/src/components/ValueCopyable.tsx +++ b/libs/design-system/src/components/ValueCopyable.tsx @@ -14,7 +14,7 @@ import { getEntityTypeCopyLabel, formatEntityValue, defaultFormatValue, -} from '../lib/entityTypes' +} from '@siafoundation/units' import { cx } from 'class-variance-authority' import { DropdownMenu, diff --git a/libs/design-system/src/components/ValueMenu.tsx b/libs/design-system/src/components/ValueMenu.tsx index d61f000e2..fdada8fc0 100644 --- a/libs/design-system/src/components/ValueMenu.tsx +++ b/libs/design-system/src/components/ValueMenu.tsx @@ -1,7 +1,7 @@ import { Text } from '../core/Text' import { Link } from '../core/Link' import { stripPrefix } from '../lib/utils' -import { EntityType, getEntityDisplayLength } from '../lib/entityTypes' +import { EntityType, getEntityDisplayLength } from '@siafoundation/units' import { cx } from 'class-variance-authority' type Props = { diff --git a/libs/design-system/src/index.ts b/libs/design-system/src/index.ts index 78828cb39..22d7a665c 100644 --- a/libs/design-system/src/index.ts +++ b/libs/design-system/src/index.ts @@ -96,7 +96,6 @@ export * from './app/WalletAddressCode' export * from './app/WalletLayoutActions' export * from './app/WalletBalanceEvolution' export * from './app/WalletBalance' -export * from './app/TxPoolList' export * from './app/PeerList' export * from './app/DataLabel' export * from './app/DataPanel' @@ -175,7 +174,6 @@ export * from './lib/toast' export * from './lib/clipboard' export * from './lib/image' export * from './lib/links' -export * from './lib/entityTypes' export * from './lib/utils' export * from './lib/time' export * from './lib/numbers' diff --git a/libs/design-system/src/lib/entityTypes.ts b/libs/design-system/src/lib/entityTypes.ts deleted file mode 100644 index d86613825..000000000 --- a/libs/design-system/src/lib/entityTypes.ts +++ /dev/null @@ -1,233 +0,0 @@ -import { Transaction } from '@siafoundation/types' -import BigNumber from 'bignumber.js' - -export type EntityType = - | 'contract' - | 'transaction' - | 'block' - | 'output' - | 'address' - | 'ip' - | 'hostIp' - | 'hostPublicKey' - | 'contract' - | 'blockHash' - -export type TxType = - | 'siacoin' - | 'siafund' - | 'storageProof' - | 'contractFormation' - | 'contractRevision' - | 'contractRenewal' - | 'contractPayout' - | 'minerPayout' - | 'siafundClaim' - | 'foundationSubsidy' - | 'hostAnnouncement' -// | 'block' -// | 'defrag' -// | 'setup' - -export function getTransactionTotals(txn: Transaction) { - const sc = (txn.siacoinOutputs || []).reduce( - (acc, i) => acc.plus(i.value), - new BigNumber(0) - ) - const sf = (txn.siafundOutputs || []).reduce( - (acc, i) => acc.plus(i.value), - new BigNumber(0) - ) - - return { - sc, - sf: sf.toNumber(), - } -} - -export function getTransactionType( - txn: Transaction, - source?: string -): TxType | undefined { - if (source === 'miner') { - return 'minerPayout' - } - if (source === 'siafundClaim') { - return 'siafundClaim' - } - if (source === 'contract') { - return 'contractPayout' - } - if (source === 'foundation') { - return 'foundationSubsidy' - } - if (txn.storageProofs && txn.storageProofs.length > 0) { - return 'storageProof' - } - if ( - txn.fileContracts && - txn.fileContracts.length > 0 && - txn.fileContractRevisions && - txn.fileContractRevisions.length > 0 - ) { - return 'contractRenewal' - } - if (txn.fileContractRevisions && txn.fileContractRevisions.length > 0) { - return 'contractRevision' - } - if (txn.fileContracts && txn.fileContracts.length > 0) { - return 'contractFormation' - } - if ( - txn.arbitraryData && - txn.arbitraryData.length > 0 && - atob(txn.arbitraryData[0]).indexOf('HostAnnouncement') === 0 - ) { - return 'hostAnnouncement' - } - if (txn.siafundOutputs && txn.siafundOutputs.length > 0) { - return 'siafund' - } - if (txn.siacoinOutputs && txn.siacoinOutputs.length > 0) { - return 'siacoin' - } - - return undefined - - // if ( - // txn.siacoinOutputs && - // txn.siacoinInputs?.length === 0 && - // txn.siafundInputs?.length === 0 - // ) { - // return 'block' - // } - // if ( - // (txn.siacoinInputs?.length || 0) >= 20 && - // txn.siacoinOutputs?.length === 1 - // ) { - // return 'defrag' - // } - - // return undefined -} - -const entityLabels: Record = { - transaction: 'transaction', - contract: 'contract', - block: 'block', - output: 'output', - address: 'address', - hostIp: 'host', - hostPublicKey: 'host', - ip: 'IP', - blockHash: 'block hash', -} - -const entityCopyLabels: Record = { - transaction: 'transaction ID', - contract: 'contract ID', - block: 'block', - output: 'output ID', - address: 'address', - hostIp: 'host address', - hostPublicKey: 'host public key', - ip: 'IP', - blockHash: 'block hash', -} - -const txTypeMap: Record = { - siacoin: 'siacoin transfer', - siafund: 'siafund transfer', - contractFormation: 'contract formation', - contractRenewal: 'contract renewal', - contractRevision: 'contract revision', - contractPayout: 'contract payout', - storageProof: 'storage proof', - minerPayout: 'miner payout', - siafundClaim: 'siafund claim', - foundationSubsidy: 'foundation subsidy', - hostAnnouncement: 'host announcement', - // block: 'block', - // defrag: 'defrag', - // setup: 'setup', -} - -export function getEntityTypeLabel(type?: EntityType): string | undefined { - return type ? entityLabels[type] : undefined -} - -export function getEntityTypeCopyLabel(type?: EntityType): string | undefined { - return type ? entityCopyLabels[type] : undefined -} - -export function getTxTypeLabel(type?: TxType): string | undefined { - return type ? txTypeMap[type] : undefined -} - -export function getEntityDisplayLength(type?: EntityType): number { - const longList: EntityType[] = ['ip', 'hostIp'] - return type && longList.includes(type) ? 20 : 12 -} - -export function doesEntityHaveSiascanUrl(type?: EntityType) { - const includeList: EntityType[] = [ - 'hostIp', - 'hostPublicKey', - 'contract', - 'address', - 'transaction', - 'block', - ] - return type && includeList.includes(type) -} - -export function getEntitySiascanUrl( - baseUrl: string, - type: EntityType, - value: string -) { - switch (type) { - case 'hostIp': - return `${baseUrl}/host/${value}` - case 'hostPublicKey': - return `${baseUrl}/host/${value}` - case 'contract': - return `${baseUrl}/contract/${value}` - case 'transaction': - return `${baseUrl}/tx/${value}` - case 'address': - return `${baseUrl}/address/${value}` - case 'block': - return `${baseUrl}/block/${value}` - default: - return '' - } -} - -export function defaultFormatValue(text: string, maxLength: number) { - return `${text?.slice(0, maxLength)}${ - (text?.length || 0) > maxLength ? '...' : '' - }` -} - -export function formatEntityValue( - type: EntityType, - text: string, - maxLength: number -) { - switch (type) { - case 'blockHash': { - const halfMax = maxLength / 2 - // Floor and ceil here to handle odd maxLengths. - // .slice() will round down on floats. - const firstHalf = text.slice(0, Math.floor(halfMax)) - const lastHalf = text.slice(text.length - Math.ceil(halfMax)) - return firstHalf + '...' + lastHalf - } - default: { - // We could also return null here, forcing the issue of defining - // formats for the missing cases in the switch above. - return defaultFormatValue(text, maxLength) - } - } -} diff --git a/libs/hostd-js/src/api.ts b/libs/hostd-js/src/api.ts index 47cae24ee..e8b97b364 100644 --- a/libs/hostd-js/src/api.ts +++ b/libs/hostd-js/src/api.ts @@ -5,12 +5,21 @@ import { AlertsParams, AlertsPayload, AlertsResponse, + ConsensusTipParams, + ConsensusTipPayload, + ConsensusTipResponse, + ConsensusTipStateParams, + ConsensusTipStatePayload, + ConsensusTipStateResponse, ContractsIntegrityCheckParams, ContractsIntegrityCheckPayload, ContractsIntegrityCheckResponse, ContractsParams, ContractsPayload, ContractsResponse, + IndexTipParams, + IndexTipPayload, + IndexTipResponse, LogsSearchParams, LogsSearchPayload, LogsSearchResponse, @@ -32,12 +41,9 @@ import { SettingsUpdateParams, SettingsUpdatePayload, SettingsUpdateResponse, - StateConsensusParams, - StateConsensusPayload, - StateConsensusResponse, - StateHostParams, - StateHostPayload, - StateHostResponse, + HostStateParams, + HostStatePayload, + HostStateResponse, SyncerConnectParams, SyncerConnectPayload, SyncerConnectResponse, @@ -74,6 +80,9 @@ import { VolumesParams, VolumesPayload, VolumesResponse, + WalletEventsParams, + WalletEventsPayload, + WalletEventsResponse, WalletParams, WalletPayload, WalletPendingParams, @@ -83,21 +92,20 @@ import { WalletSendParams, WalletSendPayload, WalletSendResponse, - WalletTransactionsParams, - WalletTransactionsPayload, - WalletTransactionsResponse, alertsDismissRoute, alertsRoute, + consensusTipRoute, + consensusTipStateRoute, contractsIdIntegrityRoute, contractsRoute, + indexTipRoute, logEntriesRoute, metricsIntervalRoute, metricsRoute, settingsAnnounceRoute, settingsDdnsUpdateRoute, settingsRoute, - stateConsensusRoute, - stateHostRoute, + hostStateRoute, syncerPeersRoute, systemDirRoute, tpoolFeeRoute, @@ -105,10 +113,10 @@ import { volumesIdResizeRoute, volumesIdRoute, volumesRoute, + walletEventsRoute, walletPendingRoute, walletRoute, walletSendRoute, - walletTransactionsRoute, } from '@siafoundation/hostd-types' import { buildRequestHandler, initAxios } from '@siafoundation/request' @@ -117,15 +125,25 @@ export function Hostd({ api, password }: { api: string; password?: string }) { return { axios, stateHost: buildRequestHandler< - StateHostParams, - StateHostPayload, - StateHostResponse - >(axios, 'get', stateHostRoute), - stateConsensus: buildRequestHandler< - StateConsensusParams, - StateConsensusPayload, - StateConsensusResponse - >(axios, 'get', stateConsensusRoute), + HostStateParams, + HostStatePayload, + HostStateResponse + >(axios, 'get', hostStateRoute), + consensusTip: buildRequestHandler< + ConsensusTipParams, + ConsensusTipPayload, + ConsensusTipResponse + >(axios, 'get', consensusTipRoute), + consensusTipState: buildRequestHandler< + ConsensusTipStateParams, + ConsensusTipStatePayload, + ConsensusTipStateResponse + >(axios, 'get', consensusTipStateRoute), + indexTip: buildRequestHandler< + IndexTipParams, + IndexTipPayload, + IndexTipResponse + >(axios, 'get', indexTipRoute), syncerPeers: buildRequestHandler< SyncerPeersParams, SyncerPeersPayload, @@ -141,16 +159,16 @@ export function Hostd({ api, password }: { api: string; password?: string }) { 'get', walletRoute ), - walletTransactions: buildRequestHandler< - WalletTransactionsParams, - WalletTransactionsPayload, - WalletTransactionsResponse - >(axios, 'get', walletTransactionsRoute), walletPending: buildRequestHandler< WalletPendingParams, WalletPendingPayload, WalletPendingResponse >(axios, 'get', walletPendingRoute), + walletEvents: buildRequestHandler< + WalletEventsParams, + WalletEventsPayload, + WalletEventsResponse + >(axios, 'get', walletEventsRoute), walletSend: buildRequestHandler< WalletSendParams, WalletSendPayload, diff --git a/libs/hostd-react/src/api.ts b/libs/hostd-react/src/api.ts index 300d51239..d454e3003 100644 --- a/libs/hostd-react/src/api.ts +++ b/libs/hostd-react/src/api.ts @@ -48,10 +48,8 @@ import { SettingsUpdateParams, SettingsUpdatePayload, SettingsUpdateResponse, - StateConsensusParams, - StateConsensusResponse, - StateHostParams, - StateHostResponse, + HostStateParams, + HostStateResponse, SyncerConnectParams, SyncerConnectPayload, SyncerConnectResponse, @@ -90,8 +88,6 @@ import { WalletSendParams, WalletSendPayload, WalletSendResponse, - WalletTransactionsParams, - WalletTransactionsResponse, alertsDismissRoute, alertsRoute, contractsIdIntegrityRoute, @@ -102,8 +98,7 @@ import { settingsAnnounceRoute, settingsDdnsUpdateRoute, settingsRoute, - stateConsensusRoute, - stateHostRoute, + hostStateRoute, syncerPeersRoute, systemDirRoute, tpoolFeeRoute, @@ -114,31 +109,72 @@ import { walletPendingRoute, walletRoute, walletSendRoute, - walletTransactionsRoute, + ConsensusNetworkParams, + ConsensusNetworkResponse, + consensusNetworkRoute, + IndexTipParams, + IndexTipResponse, + indexTipRoute, + ConsensusTipParams, + ConsensusTipResponse, + ConsensusTipStateResponse, + ConsensusTipStateParams, + consensusTipStateRoute, + consensusTipRoute, + WalletEventsParams, + WalletEventsResponse, + walletEventsRoute, } from '@siafoundation/hostd-types' // state -export function useStateHost( - args?: HookArgsSwr +export function useHostState( + args?: HookArgsSwr ) { return useGetSwr({ ...args, - route: stateHostRoute, + route: hostStateRoute, }) } -export function useStateConsensus( - args?: HookArgsSwr +export function useConsensusTip( + args?: HookArgsSwr ) { return useGetSwr({ ...args, - route: stateConsensusRoute, + route: consensusTipRoute, + }) +} + +export function useConsensusTipState( + args?: HookArgsSwr +) { + return useGetSwr({ + ...args, + route: consensusTipStateRoute, + }) +} + +export function useConsensusNetwork( + args?: HookArgsSwr +) { + return useGetSwr({ + ...args, + route: consensusNetworkRoute, + }) +} + +export function useIndexTip( + args?: HookArgsSwr +) { + return useGetSwr({ + ...args, + route: indexTipRoute, }) } export function useEstimatedNetworkBlockHeight(): number { - const state = useStateHost({ + const state = useConsensusNetwork({ config: { swr: { revalidateOnFocus: false, @@ -148,7 +184,7 @@ export function useEstimatedNetworkBlockHeight(): number { const res = useSWR( state, () => { - if (state.data?.network === 'Zen Testnet') { + if (state.data?.name === 'zen') { return getTestnetZenBlockHeight() } return getMainnetBlockHeight() @@ -199,12 +235,12 @@ export function useWallet(args?: HookArgsSwr) { }) } -export function useWalletTransactions( - args?: HookArgsSwr +export function useWalletEvents( + args?: HookArgsSwr ) { return useGetSwr({ ...args, - route: walletTransactionsRoute, + route: walletEventsRoute, }) } @@ -309,7 +345,7 @@ export function useSettingsUpdate( ) { return usePatchFunc({ ...args, route: settingsRoute }, async (mutate) => { await mutate((key) => { - return key.startsWith(settingsRoute) || key.startsWith(stateHostRoute) + return key.startsWith(settingsRoute) || key.startsWith(hostStateRoute) }) }) } diff --git a/libs/hostd-types/src/api.ts b/libs/hostd-types/src/api.ts index 0fd443fb0..7917d4ec3 100644 --- a/libs/hostd-types/src/api.ts +++ b/libs/hostd-types/src/api.ts @@ -4,14 +4,18 @@ import { Currency, TransactionID, ChainIndex, + WalletEvent, } from '@siafoundation/types' import { Contract, ContractStatus, WalletTransaction } from './types' -export const stateHostRoute = '/state/host' -export const stateConsensusRoute = '/state/consensus' +export const hostStateRoute = '/state' +export const consensusNetworkRoute = '/consensus/network' +export const consensusTipRoute = '/consensus/tip' +export const consensusTipStateRoute = '/consensus/tipstate' +export const indexTipRoute = '/index/tip' export const syncerPeersRoute = '/syncer/peers' export const walletRoute = '/wallet' -export const walletTransactionsRoute = '/wallet/transactions' +export const walletEventsRoute = '/wallet/events' export const walletPendingRoute = '/wallet/pending' export const walletSendRoute = '/wallet/send' export const tpoolFeeRoute = '/tpool/fee' @@ -33,13 +37,11 @@ export const alertsDismissRoute = '/alerts/dismiss' // state -export type StateHostParams = void -export type StateHostPayload = void -export type StateHostResponse = { +export type HostStateParams = void +export type HostStatePayload = void +export type HostStateResponse = { name?: string publicKey: string - walletAddress: string - network: 'Mainnet' | 'Zen Testnet' version: string commit: string os: string @@ -56,16 +58,75 @@ export type StateHostResponse = { } } -export type StateConsensusParams = void -export type StateConsensusPayload = void -export type StateConsensusResponse = { - chainIndex: { +export type ConsensusNetworkParams = void +export type ConsensusNetworkPayload = void +export type ConsensusNetworkResponse = { + name: 'mainnet' | 'zen' + initialCoinbase: string + minimumCoinbase: string + initialTarget: string + hardforkDevAddr: { height: number - id: string + oldAddress: string + newAddress: string } - synced: boolean + hardforkTax: { + height: number + } + hardforkStorageProof: { + height: number + } + hardforkOak: { + height: number + fixHeight: number + genesisTimestamp: string + } + hardforkASIC: { + height: number + oakTime: number + oakTarget: string + } + hardforkFoundation: { + height: number + primaryAddress: string + failsafeAddress: string + } + hardforkV2: { + allowHeight: number + requireHeight: number + } +} + +export type ConsensusTipParams = void +export type ConsensusTipPayload = void +export type ConsensusTipResponse = ChainIndex + +export type ConsensusTipStateParams = void +export type ConsensusTipStatePayload = void +export type ConsensusTipStateResponse = { + index: ChainIndex + prevTimestamps: string[] + depth: string + childTarget: string + siafundPool: string + oakTime: number + oakTarget: string + foundationPrimaryAddress: string + foundationFailsafeAddress: string + totalWork: string + difficulty: string + oakWork: string + elements: { + numLeaves: number + trees: string[] + } + attestations: number } +export type IndexTipParams = void +export type IndexTipPayload = void +export type IndexTipResponse = ChainIndex + // syncer type Peer = { @@ -86,20 +147,24 @@ export type SyncerConnectResponse = never export type WalletParams = void export type WalletPayload = void export type WalletResponse = { - scanHeight: number - address: string spendable: string confirmed: string unconfirmed: string + immature: string + address: string } export type WalletTransactionsParams = { limit?: number; offset?: number } export type WalletTransactionsPayload = void export type WalletTransactionsResponse = WalletTransaction[] +export type WalletEventsParams = { limit?: number; offset?: number } +export type WalletEventsPayload = void +export type WalletEventsResponse = WalletEvent[] + export type WalletPendingParams = void export type WalletPendingPayload = void -export type WalletPendingResponse = WalletTransaction[] +export type WalletPendingResponse = WalletEvent[] export type WalletSendParams = void export type WalletSendPayload = { diff --git a/libs/types/src/events.ts b/libs/types/src/events.ts new file mode 100644 index 000000000..d2787a474 --- /dev/null +++ b/libs/types/src/events.ts @@ -0,0 +1,91 @@ +import { + ChainIndex, + Transaction, + SiacoinElement, + SiafundElement, + FileContractElement, + Hash256, + Address, +} from './core' +import { + V2Transaction, + V2FileContractResolutionType, + V2FileContractElement, +} from './v2' + +export type UnconfirmedChainIndex = { + height: number +} + +export type WalletEventBase = { + id: Hash256 + timestamp: string + index: ChainIndex | UnconfirmedChainIndex + maturityHeight: number + relevant: Address[] +} + +export type WalletEventTransactionV1 = WalletEventBase & { + type: 'v1Transaction' + data: { + transaction: Transaction + spentSiacoinElements?: SiacoinElement[] + spentSiafundElements?: SiafundElement[] + } +} + +export type WalletEventTransactionV2 = WalletEventBase & { + type: 'v2Transaction' + data: V2Transaction +} + +export type WalletEventContractResolutionV1 = WalletEventBase & { + type: 'v1ContractResolution' + data: { + parent: FileContractElement + siacoinElement: SiacoinElement + missed: boolean + } +} + +export type WalletEventContractResolutionV2 = WalletEventBase & { + type: 'v2ContractResolution' + data: { + parent: V2FileContractElement + resolution: V2FileContractResolutionType + siacoinElement: SiacoinElement + missed: boolean + } +} + +export type WalletEventMinerPayout = WalletEventBase & { + type: 'miner' + data: { + siacoinElement: SiacoinElement + } +} + +export type WalletEventSiafundClaim = WalletEventBase & { + type: 'siafundClaim' + data: { + siacoinElement: SiacoinElement + } +} + +export type WalletEventFoundationSubsidy = WalletEventBase & { + type: 'foundation' + data: { + siacoinElement: SiacoinElement + } +} + +export type WalletEvent = + | WalletEventTransactionV1 + | WalletEventTransactionV2 + | WalletEventContractResolutionV1 + | WalletEventContractResolutionV2 + | WalletEventMinerPayout + | WalletEventFoundationSubsidy + | WalletEventSiafundClaim + +export type WalletEventType = WalletEvent['type'] diff --git a/libs/types/src/index.ts b/libs/types/src/index.ts index afc97d4a9..0957f1243 100644 --- a/libs/types/src/index.ts +++ b/libs/types/src/index.ts @@ -1,2 +1,3 @@ export * from './core' +export * from './events' export * from './v2' diff --git a/libs/units/package.json b/libs/units/package.json index c1b4cfb9e..df3cdb440 100644 --- a/libs/units/package.json +++ b/libs/units/package.json @@ -8,7 +8,8 @@ "bignumber.js": "^9.0.2", "@technically/lodash": "^4.17.0", "blakejs": "^1.2.1", - "@siafoundation/sia-central-types": "0.1.1" + "@siafoundation/sia-central-types": "0.1.1", + "@siafoundation/types": "0.4.0" }, "types": "./src/index.d.ts" } diff --git a/libs/units/src/entityTypes.ts b/libs/units/src/entityTypes.ts new file mode 100644 index 000000000..27fad01dc --- /dev/null +++ b/libs/units/src/entityTypes.ts @@ -0,0 +1,109 @@ +export type EntityType = + | 'contract' + | 'transaction' + | 'block' + | 'output' + | 'address' + | 'ip' + | 'hostIp' + | 'hostPublicKey' + | 'contract' + | 'blockHash' + +const entityLabels: Record = { + transaction: 'transaction', + contract: 'contract', + block: 'block', + output: 'output', + address: 'address', + hostIp: 'host', + hostPublicKey: 'host', + ip: 'IP', + blockHash: 'block hash', +} +export function getEntityTypeLabel(type?: EntityType): string | undefined { + return type ? entityLabels[type] : undefined +} + +const entityCopyLabels: Record = { + transaction: 'transaction ID', + contract: 'contract ID', + block: 'block', + output: 'output ID', + address: 'address', + hostIp: 'host address', + hostPublicKey: 'host public key', + ip: 'IP', + blockHash: 'block hash', +} +export function getEntityTypeCopyLabel(type?: EntityType): string | undefined { + return type ? entityCopyLabels[type] : undefined +} + +export function getEntityDisplayLength(type?: EntityType): number { + const longList: EntityType[] = ['ip', 'hostIp'] + return type && longList.includes(type) ? 20 : 12 +} + +export function doesEntityHaveSiascanUrl(type?: EntityType) { + const includeList: EntityType[] = [ + 'hostIp', + 'hostPublicKey', + 'contract', + 'address', + 'transaction', + 'block', + ] + return type && includeList.includes(type) +} + +export function getEntitySiascanUrl( + baseUrl: string, + type: EntityType, + value: string +) { + switch (type) { + case 'hostIp': + return `${baseUrl}/host/${value}` + case 'hostPublicKey': + return `${baseUrl}/host/${value}` + case 'contract': + return `${baseUrl}/contract/${value}` + case 'transaction': + return `${baseUrl}/tx/${value}` + case 'address': + return `${baseUrl}/address/${value}` + case 'block': + return `${baseUrl}/block/${value}` + default: + return '' + } +} + +export function defaultFormatValue(text: string, maxLength: number) { + return `${text?.slice(0, maxLength)}${ + (text?.length || 0) > maxLength ? '...' : '' + }` +} + +export function formatEntityValue( + type: EntityType, + text: string, + maxLength: number +) { + switch (type) { + case 'blockHash': { + const halfMax = maxLength / 2 + // Floor and ceil here to handle odd maxLengths. + // .slice() will round down on floats. + const firstHalf = text.slice(0, Math.floor(halfMax)) + const lastHalf = text.slice(text.length - Math.ceil(halfMax)) + return firstHalf + '...' + lastHalf + } + default: { + // We could also return null here, forcing the issue of defining + // formats for the missing cases in the switch above. + return defaultFormatValue(text, maxLength) + } + } +} diff --git a/libs/units/src/events.ts b/libs/units/src/events.ts new file mode 100644 index 000000000..c35b194b2 --- /dev/null +++ b/libs/units/src/events.ts @@ -0,0 +1,49 @@ +import BigNumber from 'bignumber.js' +import { WalletEvent } from '@siafoundation/types' +import { + TxType, + getTransactionType, + getV2TransactionType, +} from './transactionTypes' + +export function getEventFee(e: WalletEvent) { + if (e.type === 'v2Transaction') { + return new BigNumber(e.data.minerFee) + } + return 'transaction' in e.data && e.data.transaction.minerFees?.length + ? new BigNumber(e.data.transaction.minerFees[0]) + : undefined +} + +export function getEventContractId(e: WalletEvent) { + if (e.type === 'v1ContractResolution') { + return e.data.parent.id + } + if (e.type === 'v2ContractResolution') { + return e.data.parent.id + } + return undefined +} + +export function getEventTxType(e: WalletEvent): TxType | undefined { + const eventType = e.type + if (eventType === 'v1Transaction') { + return getTransactionType(e.data.transaction) + } + if (eventType === 'v2Transaction') { + return getV2TransactionType(e.data) + } + if (eventType === 'v1ContractResolution') { + return 'contractPayout' + } + if (eventType === 'v2ContractResolution') { + return 'contractPayout' + } + if (eventType === 'miner') { + return 'minerPayout' + } + if (eventType === 'foundation') { + return 'foundationSubsidy' + } + return eventType +} diff --git a/libs/units/src/index.ts b/libs/units/src/index.ts index 138440db2..14e6622ea 100644 --- a/libs/units/src/index.ts +++ b/libs/units/src/index.ts @@ -5,3 +5,7 @@ export * from './currency' export * from './blockTime' export * from './bytes' export * from './valuePer' +export * from './events' +export * from './transactionValue' +export * from './transactionTypes' +export * from './entityTypes' diff --git a/libs/units/src/transactionTypes.ts b/libs/units/src/transactionTypes.ts new file mode 100644 index 000000000..294f4252b --- /dev/null +++ b/libs/units/src/transactionTypes.ts @@ -0,0 +1,104 @@ +import { + Transaction, + V2FileContractResolutionType, + V2Transaction, +} from '@siafoundation/types' + +export type TxType = + | 'siacoin' + | 'siafund' + | 'storageProof' + | 'contractFormation' + | 'contractRevision' + | 'contractRenewal' + | 'contractExpiration' + | 'contractFinalization' + | 'contractPayout' + | 'minerPayout' + | 'siafundClaim' + | 'foundationSubsidy' + | 'hostAnnouncement' + +export function getTransactionType(txn: Transaction): TxType | undefined { + if (txn.storageProofs && txn.storageProofs.length > 0) { + return 'storageProof' + } + if ( + txn.fileContracts && + txn.fileContracts.length > 0 && + txn.fileContractRevisions && + txn.fileContractRevisions.length > 0 + ) { + return 'contractRenewal' + } + if (txn.fileContractRevisions && txn.fileContractRevisions.length > 0) { + return 'contractRevision' + } + if (txn.fileContracts && txn.fileContracts.length > 0) { + return 'contractFormation' + } + if ( + txn.arbitraryData && + txn.arbitraryData.length > 0 && + atob(txn.arbitraryData[0]).indexOf('HostAnnouncement') === 0 + ) { + return 'hostAnnouncement' + } + if (txn.siafundOutputs && txn.siafundOutputs.length > 0) { + return 'siafund' + } + if (txn.siacoinOutputs && txn.siacoinOutputs.length > 0) { + return 'siacoin' + } + + return undefined +} + +export function getV2TransactionType(txn: V2Transaction): TxType | undefined { + if (txn.fileContractResolutions && txn.fileContractResolutions.length > 0) { + const mapping: Record = { + expiration: 'contractExpiration', + finalization: 'contractFinalization', + renewal: 'contractRenewal', + 'storage proof': 'storageProof', + } + return mapping[txn.fileContractResolutions[0].resolution.type] + } + if (txn.fileContractRevisions && txn.fileContractRevisions.length > 0) { + return 'contractRevision' + } + if (txn.fileContracts && txn.fileContracts.length > 0) { + return 'contractFormation' + } + const announcement = txn.attestations?.filter( + (at) => at.key === 'HostAnnouncement' + ) + if (announcement && announcement.length > 0) return 'hostAnnouncement' + if (txn.siafundOutputs && txn.siafundOutputs.length > 0) { + return 'siafund' + } + if (txn.siacoinOutputs && txn.siacoinOutputs.length > 0) { + return 'siacoin' + } + + return undefined +} + +const txTypeMap: Record = { + siacoin: 'siacoin transfer', + siafund: 'siafund transfer', + contractFormation: 'contract formation', + contractRenewal: 'contract renewal', + contractRevision: 'contract revision', + contractExpiration: 'contract expiration', + contractFinalization: 'contract finalization', + contractPayout: 'contract payout', + storageProof: 'storage proof', + minerPayout: 'miner payout', + siafundClaim: 'siafund claim', + foundationSubsidy: 'foundation subsidy', + hostAnnouncement: 'host announcement', +} +export function getTxTypeLabel(type?: TxType): string | undefined { + return type ? txTypeMap[type] : undefined +} diff --git a/apps/walletd/contexts/events/transactionValue.spec.ts b/libs/units/src/transactionValue.spec.ts similarity index 96% rename from apps/walletd/contexts/events/transactionValue.spec.ts rename to libs/units/src/transactionValue.spec.ts index b02028475..b3e5ff0ef 100644 --- a/apps/walletd/contexts/events/transactionValue.spec.ts +++ b/libs/units/src/transactionValue.spec.ts @@ -1,6 +1,6 @@ -import { WalletEvent } from '@siafoundation/walletd-types' import { calculateScValue, calculateSfValue } from './transactionValue' -import { toHastings } from '@siafoundation/units' +import { toHastings } from './currency' +import { WalletEvent } from '@siafoundation/types' test('v1TxnCalculateScValue', () => { const e: WalletEvent = { @@ -62,7 +62,7 @@ test('v1TxnCalculateScValue', () => { }, }, ], - spentSiafundElements: null, + spentSiafundElements: undefined, }, relevant: ['addr:1', 'addr:2'], } @@ -95,8 +95,8 @@ test('v1TxnCalculateScValue when no relevant / spentSiacoinElements is null', () minerFees: ['1000000000000000000000000'], signatures: [], }, - spentSiacoinElements: null, - spentSiafundElements: null, + spentSiacoinElements: undefined, + spentSiafundElements: undefined, }, relevant: ['addr:1'], } @@ -193,7 +193,7 @@ test('v1TxnCalculateSfValue', () => { minerFees: ['1000000000000000000000000'], signatures: [], }, - spentSiacoinElements: null, + spentSiacoinElements: undefined, spentSiafundElements: [ { id: 'id-1', @@ -258,8 +258,8 @@ test('v1TxnCalculateSfValue when no relevant / spentSiafundElements is null', () minerFees: ['1000000000000000000000000'], signatures: [], }, - spentSiacoinElements: null, - spentSiafundElements: null, + spentSiacoinElements: undefined, + spentSiafundElements: undefined, }, relevant: ['addr:1'], } diff --git a/apps/walletd/contexts/events/transactionValue.ts b/libs/units/src/transactionValue.ts similarity index 98% rename from apps/walletd/contexts/events/transactionValue.ts rename to libs/units/src/transactionValue.ts index 0d9e7ca81..27d1a6add 100644 --- a/apps/walletd/contexts/events/transactionValue.ts +++ b/libs/units/src/transactionValue.ts @@ -3,7 +3,7 @@ import { WalletEvent, WalletEventTransactionV1, WalletEventTransactionV2, -} from '@siafoundation/walletd-types' +} from '@siafoundation/types' export function calculateScValue(e: WalletEvent) { if (e.type === 'v2Transaction') { diff --git a/libs/walletd-types/src/api.ts b/libs/walletd-types/src/api.ts index bff9cb54c..108b01560 100644 --- a/libs/walletd-types/src/api.ts +++ b/libs/walletd-types/src/api.ts @@ -10,14 +10,9 @@ import { SiafundElement, Transaction, V2Transaction, -} from '@siafoundation/types' -import { WalletEvent, - GatewayPeer, - Wallet, - WalletAddress, - WalletMetadata, -} from './types' +} from '@siafoundation/types' +import { GatewayPeer, Wallet, WalletAddress, WalletMetadata } from './types' export const stateRoute = '/state' export const consensusTipRoute = '/consensus/tip' diff --git a/libs/walletd-types/src/types.ts b/libs/walletd-types/src/types.ts index 911b2c0bd..200666e8b 100644 --- a/libs/walletd-types/src/types.ts +++ b/libs/walletd-types/src/types.ts @@ -1,129 +1,15 @@ -import { - ChainIndex, - Transaction, - SiacoinElement, - SiafundElement, - FileContractElement, - Hash256, - FileContract, - PublicKey, - SpendPolicy, - UnlockConditions, - V2Transaction, - V2FileContractResolutionType, - Address, - V2FileContractElement, -} from '@siafoundation/types' +import { SpendPolicy, UnlockConditions } from '@siafoundation/types' export type GatewayPeer = { addr: string inbound: boolean version: string - firstSeen?: string connectedSince?: string syncedBlocks?: number syncDuration?: number } -export type UnconfirmedChainIndex = { - height: number -} - -export type WalletEventBase = { - id: Hash256 - timestamp: string - index: ChainIndex | UnconfirmedChainIndex - maturityHeight: number - relevant: Address[] -} - -export type WalletSiafundInput = { - siafundElement: SiafundElement - claimElement: SiacoinElement -} - -export type WalletFileContract = { - fileContract: FileContractElement - revision?: FileContract - validOutputs?: SiacoinElement[] -} - -export type WalletV2FileContract = { - fileContract: FileContractElement - revision?: FileContract - resolution?: V2FileContractResolutionType - outputs?: SiacoinElement[] -} - -export type WalletHostAnnouncement = { - publicKey: PublicKey - netAddress: string -} - -export type WalletEventTransactionV1 = WalletEventBase & { - type: 'v1Transaction' - data: { - transaction: Transaction - spentSiacoinElements?: SiacoinElement[] - spentSiafundElements?: SiafundElement[] - } -} - -export type WalletEventTransactionV2 = WalletEventBase & { - type: 'v2Transaction' - data: V2Transaction -} - -export type WalletEventContractResolutionV1 = WalletEventBase & { - type: 'v1ContractResolution' - data: { - parent: FileContractElement - siacoinElement: SiacoinElement - missed: boolean - } -} - -export type WalletEventContractResolutionV2 = WalletEventBase & { - type: 'v2ContractResolution' - data: { - parent: V2FileContractElement - resolution: V2FileContractResolutionType - siacoinElement: SiacoinElement - missed: boolean - } -} - -export type WalletEventMinerPayout = WalletEventBase & { - type: 'miner' - data: { - siacoinElement: SiacoinElement - } -} - -export type WalletEventSiafundClaim = WalletEventBase & { - type: 'siafundClaim' - data: { - siacoinElement: SiacoinElement - } -} - -export type WalletEventFoundationSubsidy = WalletEventBase & { - type: 'foundation' - data: { - siacoinElement: SiacoinElement - } -} - -export type WalletEvent = - | WalletEventTransactionV1 - | WalletEventTransactionV2 - | WalletEventContractResolutionV1 - | WalletEventContractResolutionV2 - | WalletEventMinerPayout - | WalletEventFoundationSubsidy - | WalletEventSiafundClaim - export type Metadata = Record export type WalletType = 'seed' | 'ledger' | 'watch'