From 39a80659778a4c173d0a0e5008f03b1b9e61978f Mon Sep 17 00:00:00 2001 From: Lucas Magnus Date: Thu, 8 Feb 2024 14:15:25 -0300 Subject: [PATCH 1/6] feat: create cost center page --- .../src/app/core/pages/cost-center/index.tsx | 55 +++++++ frontend/src/app/core/routes/routes.ts | 6 + frontend/src/components/enums/path-route.ts | 1 + .../src/components/enums/settings-options.ts | 1 + .../organisms/menu-settings/index.tsx | 18 +++ .../templates/cost-center/index.tsx | 135 ++++++++++++++++++ 6 files changed, 216 insertions(+) create mode 100644 frontend/src/app/core/pages/cost-center/index.tsx create mode 100644 frontend/src/components/templates/cost-center/index.tsx diff --git a/frontend/src/app/core/pages/cost-center/index.tsx b/frontend/src/app/core/pages/cost-center/index.tsx new file mode 100644 index 00000000..f9e3bc60 --- /dev/null +++ b/frontend/src/app/core/pages/cost-center/index.tsx @@ -0,0 +1,55 @@ +import { Flex, VStack, useMediaQuery } from '@chakra-ui/react' +import React, { useEffect, useState } from 'react' + +import { useAuth } from 'hooks/useAuth' +import { useTransactions } from 'hooks/useTransactions' +import { GAService } from 'utils/ga' + +import { PathRoute } from 'components/enums/path-route' +import { SettingsOptions } from 'components/enums/settings-options' +import { MenuSettings } from 'components/organisms/menu-settings' +import { Sidebar } from 'components/organisms/sidebar' +import { CostCenterTemplate } from 'components/templates/cost-center' +import { RolesManageTemplate } from 'components/templates/roles-manage-template' + +export const CostCenter: React.FC = () => { + const [isLargerThanMd] = useMediaQuery('(min-width: 768px)') + const { userPermissions, getUserPermissions } = useAuth() + const { getSponsorPK } = useTransactions() + const [loading, setLoading] = useState(true) + + useEffect(() => { + GAService.GAPageView('Coast center') + }, []) + + useEffect(() => { + getUserPermissions() + getSponsorPK().then(sponsor => console.log(sponsor)) + }, [getUserPermissions]) + + return ( + + + + + + + {isLargerThanMd && ( + + + + )} + + + + ) +} diff --git a/frontend/src/app/core/routes/routes.ts b/frontend/src/app/core/routes/routes.ts index 8bf7ecd7..db28161b 100644 --- a/frontend/src/app/core/routes/routes.ts +++ b/frontend/src/app/core/routes/routes.ts @@ -7,6 +7,7 @@ import { ClawbackAsset } from '../pages/clawback-asset' import { Contracts } from '../pages/contracts' import { ContractsCreate } from '../pages/contracts-create' import { ContractsDetail } from '../pages/contracts-detail' +import { CostCenter } from '../pages/cost-center' import { Dashboards } from '../pages/dashboards' import { DistributeAsset } from '../pages/distribute-asset' import { ForgeAsset } from '../pages/forge-asset' @@ -133,4 +134,9 @@ export const coreRoutes: AppRoute[] = [ component: TomlFile, isPrivate: true, }, + { + path: PathRoute.COST_CENTER, + component: CostCenter, + isPrivate: true, + }, ] diff --git a/frontend/src/components/enums/path-route.ts b/frontend/src/components/enums/path-route.ts index 44980974..5b832a19 100644 --- a/frontend/src/components/enums/path-route.ts +++ b/frontend/src/components/enums/path-route.ts @@ -29,4 +29,5 @@ export enum PathRoute { TOKEN_MANAGEMENT = '/token-management', DASHBOARDS = '/dashboards', TOML_FILE = '/.well-known/stellar.toml', + COST_CENTER = '/cost-center', } diff --git a/frontend/src/components/enums/settings-options.ts b/frontend/src/components/enums/settings-options.ts index 6276be52..d6d46e55 100644 --- a/frontend/src/components/enums/settings-options.ts +++ b/frontend/src/components/enums/settings-options.ts @@ -2,4 +2,5 @@ export enum SettingsOptions { TEAM_MEMBERS = 'Team members', ROLE_PERMISSIONS = 'Role permissions', ROLES_MANAGE = 'Roles', + COST_CENTER = 'Cost Center' } diff --git a/frontend/src/components/organisms/menu-settings/index.tsx b/frontend/src/components/organisms/menu-settings/index.tsx index 09e60ac2..5748131c 100644 --- a/frontend/src/components/organisms/menu-settings/index.tsx +++ b/frontend/src/components/organisms/menu-settings/index.tsx @@ -74,6 +74,24 @@ export const MenuSettings: React.FC = ({ option }) => { > Roles + ) diff --git a/frontend/src/components/templates/cost-center/index.tsx b/frontend/src/components/templates/cost-center/index.tsx new file mode 100644 index 00000000..ea274b41 --- /dev/null +++ b/frontend/src/components/templates/cost-center/index.tsx @@ -0,0 +1,135 @@ +import { + Container, + Flex, + Table, + Tbody, + Th, + Thead, + Tr, + Text, + Td, + Button, + Img, + Skeleton, + useMediaQuery, +} from '@chakra-ui/react' +import React from 'react' +import { useNavigate } from 'react-router-dom' + +import { havePermission } from 'utils' +import { getCurrencyIcon } from 'utils/constants/constants' +import { MAX_PAGE_WIDTH } from 'utils/constants/sizes' +import { toCrypto } from 'utils/formatter' + +import { CompoundTime } from '../contracts-create/components/select-compound' +import { PathRoute } from 'components/enums/path-route' +import { Permissions } from 'components/enums/permissions' +import { ArrowRightIcon, NewIcon } from 'components/icons' +import { Empty } from 'components/molecules/empty' + +interface ICostCenterTemplate { + loading: boolean + transactions: Hooks.UseContractsTypes.IContract[] | undefined + userPermissions: Hooks.UseAuthTypes.IUserPermission[] | undefined +} + +export const CostCenterTemplate: React.FC = ({ + loading, + transactions, + userPermissions, +}) => { + const navigate = useNavigate() + const [isLargerThanLg] = useMediaQuery('(min-width: 992px)') + const [isLargerThanMd] = useMediaQuery('(min-width: 768px)') + const [isLargerThanSm] = useMediaQuery('(min-width: 480px)') + + return ( + + + + + Yield-bearing asset + + {havePermission(Permissions.CREATE_CERTIFICATES, userPermissions) && ( + + )} + + + {loading ? ( + + ) : !transactions || transactions.length === 0 ? ( + + ) : ( + + + + + + {isLargerThanSm && } + {isLargerThanMd && } + {isLargerThanLg && } + {isLargerThanLg && } + + + + {transactions.map((contract, index) => ( + { + navigate(`${PathRoute.CONTRACT_DETAIL}/${contract.id}`) + }} + > + + + + {isLargerThanSm && ( + + )} + {isLargerThanMd && ( + + )} + {isLargerThanLg && ( + + )} + {isLargerThanLg && ( + + )} + + + ))} + +
+ AssetVaultYield %TermCompoundMinimum Deposit +
+ {contract.asset.image ? ( + + ) : ( + getCurrencyIcon(contract.asset.code, '2rem') + )} + {contract.asset.code} + {!isLargerThanLg && contract.vault.name.length > 10 + ? `${contract.vault.name.substring(0, 8)}...` + : contract.vault.name} + {`${contract.yield_rate / 100}%`}{`${contract.term / 86400} day(s)`}{`${ + contract.compound === 0 + ? 'Simple interest' + : CompoundTime[contract.compound] + }`}{toCrypto(contract.min_deposit)} + +
+ )} +
+
+
+ ) +} From 067c2a4e3ae3c5fd42a8b861765dae96ba346737 Mon Sep 17 00:00:00 2001 From: Lucas Magnus Date: Thu, 15 Feb 2024 21:25:08 -0300 Subject: [PATCH 2/6] feat: list transactions cost center --- .../src/app/core/pages/cost-center/index.tsx | 80 ++++++- frontend/src/components/icons/fonts/coins.svg | 4 + frontend/src/components/icons/index.tsx | 2 + .../organisms/menu-admin-mobile/index.tsx | 12 +- .../organisms/menu-settings/index.tsx | 4 +- .../components/opex-card/index.tsx | 116 ++++++++++ .../components/transactions-card/index.tsx | 206 ++++++++++++++++++ .../templates/cost-center/index.tsx | 154 ++++--------- frontend/src/hooks/useHorizon/context.tsx | 93 ++++++++ .../hooks/useHorizon/use-horizon-types.d.ts | 49 +++++ frontend/src/utils/formatter/index.tsx | 10 +- 11 files changed, 606 insertions(+), 124 deletions(-) create mode 100644 frontend/src/components/icons/fonts/coins.svg create mode 100644 frontend/src/components/templates/cost-center/components/opex-card/index.tsx create mode 100644 frontend/src/components/templates/cost-center/components/transactions-card/index.tsx diff --git a/frontend/src/app/core/pages/cost-center/index.tsx b/frontend/src/app/core/pages/cost-center/index.tsx index f9e3bc60..987d470d 100644 --- a/frontend/src/app/core/pages/cost-center/index.tsx +++ b/frontend/src/app/core/pages/cost-center/index.tsx @@ -1,8 +1,11 @@ import { Flex, VStack, useMediaQuery } from '@chakra-ui/react' import React, { useEffect, useState } from 'react' +import { useAssets } from 'hooks/useAssets' import { useAuth } from 'hooks/useAuth' +import { useHorizon } from 'hooks/useHorizon' import { useTransactions } from 'hooks/useTransactions' +import { useVaults } from 'hooks/useVaults' import { GAService } from 'utils/ga' import { PathRoute } from 'components/enums/path-route' @@ -10,13 +13,26 @@ import { SettingsOptions } from 'components/enums/settings-options' import { MenuSettings } from 'components/organisms/menu-settings' import { Sidebar } from 'components/organisms/sidebar' import { CostCenterTemplate } from 'components/templates/cost-center' -import { RolesManageTemplate } from 'components/templates/roles-manage-template' export const CostCenter: React.FC = () => { const [isLargerThanMd] = useMediaQuery('(min-width: 768px)') + const [sponsorAccount, setSponsorAccount] = useState() + const [transactions, setTransactions] = + useState() + const [accountData, setAccountData] = + useState() + const [latestFeeCharged, setLatestFeeCharged] = useState() + const [vaults, setVaults] = useState() + const [assets, setAssets] = useState() + const [historyTransactions, setHistoryTransactions] = useState< + Hooks.UseHorizonTypes.ITransactions[] + >([]) + const { userPermissions, getUserPermissions } = useAuth() const { getSponsorPK } = useTransactions() - const [loading, setLoading] = useState(true) + const { getTransactions, getAccount } = useHorizon() + const { getVaults } = useVaults() + const { getAssets } = useAssets() useEffect(() => { GAService.GAPageView('Coast center') @@ -24,8 +40,54 @@ export const CostCenter: React.FC = () => { useEffect(() => { getUserPermissions() - getSponsorPK().then(sponsor => console.log(sponsor)) - }, [getUserPermissions]) + getSponsorPK().then(sponsor => setSponsorAccount(sponsor)) + }, [getUserPermissions, getSponsorPK]) + + useEffect(() => { + getVaults(true).then(vaults => setVaults(vaults)) + }, [getVaults]) + + useEffect(() => { + getAssets(true).then(assets => setAssets(assets)) + }, [getAssets]) + + useEffect(() => { + if (sponsorAccount) { + getTransactions(sponsorAccount).then(transactions => { + setTransactions(transactions) + setLatestFeeCharged( + transactions?._embedded.records.reduce( + (total, transaction) => total + Number(transaction.fee_charged), + 0 + ) + ) + }) + } + }, [sponsorAccount, getTransactions]) + + useEffect(() => { + if (sponsorAccount) { + getAccount(sponsorAccount).then(account => setAccountData(account)) + } + }, [sponsorAccount, getTransactions, getAccount]) + + const getTransactionsByLink = (action: 'prev' | 'next'): void => { + if (action === 'prev') { + const transactionsPrev = + historyTransactions[historyTransactions.length - 1] + setTransactions(transactionsPrev) + setHistoryTransactions(previous => previous.slice(0, -1)) + return + } + + const link = transactions?._links.next.href + if (link) { + setHistoryTransactions(history => [...history, transactions]) + getTransactions(undefined, link).then(transactions => { + setTransactions(transactions) + }) + } + } return ( @@ -38,9 +100,15 @@ export const CostCenter: React.FC = () => { > {isLargerThanMd && ( diff --git a/frontend/src/components/icons/fonts/coins.svg b/frontend/src/components/icons/fonts/coins.svg new file mode 100644 index 00000000..afa03f6d --- /dev/null +++ b/frontend/src/components/icons/fonts/coins.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/icons/index.tsx b/frontend/src/components/icons/index.tsx index 35141e86..b71c5318 100644 --- a/frontend/src/components/icons/index.tsx +++ b/frontend/src/components/icons/index.tsx @@ -56,6 +56,7 @@ import { ReactComponent as TransferIcon } from './fonts/transfer.svg' import { ReactComponent as UsdcIcon } from './fonts/usdc.svg' import { ReactComponent as VaultIcon } from './fonts/vault.svg' import { ReactComponent as WalletIcon } from './fonts/wallet.svg' +import { ReactComponent as CoinsIcon } from './fonts/coins.svg' export { MenuIcon, @@ -116,4 +117,5 @@ export { ArrowForwardIcon, CircleCheckIcon, FileIcon, + CoinsIcon } diff --git a/frontend/src/components/organisms/menu-admin-mobile/index.tsx b/frontend/src/components/organisms/menu-admin-mobile/index.tsx index 2f5b014e..d510a0ce 100644 --- a/frontend/src/components/organisms/menu-admin-mobile/index.tsx +++ b/frontend/src/components/organisms/menu-admin-mobile/index.tsx @@ -5,18 +5,19 @@ import { ItemActionAssetMobile } from 'components/atoms/item-action-asset-mobile import { PathRoute } from 'components/enums/path-route' import { ChevronDownIcon, + CoinsIcon, MembersIcon, PermissionsIcon, RoleIcon, } from 'components/icons' interface IMenuAdminMobile { - selected: 'TEAM_MEMBERS' | 'ROLE_PERMISSIONS' | 'ROLES' + selected: 'TEAM_MEMBERS' | 'ROLE_PERMISSIONS' | 'ROLES' | 'COST_CENTER' } export const MenuAdminMobile: React.FC = ({ selected }) => { const isSelected = ( - page: 'TEAM_MEMBERS' | 'ROLE_PERMISSIONS' | 'ROLES' + page: 'TEAM_MEMBERS' | 'ROLE_PERMISSIONS' | 'ROLES' | 'COST_CENTER' ): boolean => { return selected === page } @@ -61,6 +62,13 @@ export const MenuAdminMobile: React.FC = ({ selected }) => { icon={} path={`${PathRoute.ROLES_MANAGE}`} /> + + } + path={`${PathRoute.COST_CENTER}`} + /> ) diff --git a/frontend/src/components/organisms/menu-settings/index.tsx b/frontend/src/components/organisms/menu-settings/index.tsx index 5748131c..fbcaeaf0 100644 --- a/frontend/src/components/organisms/menu-settings/index.tsx +++ b/frontend/src/components/organisms/menu-settings/index.tsx @@ -4,7 +4,7 @@ import { useNavigate } from 'react-router-dom' import { PathRoute } from 'components/enums/path-route' import { SettingsOptions } from 'components/enums/settings-options' -import { MembersIcon, PermissionsIcon, RoleIcon } from 'components/icons' +import { CoinsIcon, MembersIcon, PermissionsIcon, RoleIcon } from 'components/icons' interface IMenuSettings { option: SettingsOptions @@ -83,7 +83,7 @@ export const MenuSettings: React.FC = ({ option }) => { borderBottomRadius="0.25rem" leftIcon={ - + } onClick={(): void => { diff --git a/frontend/src/components/templates/cost-center/components/opex-card/index.tsx b/frontend/src/components/templates/cost-center/components/opex-card/index.tsx new file mode 100644 index 00000000..028cadda --- /dev/null +++ b/frontend/src/components/templates/cost-center/components/opex-card/index.tsx @@ -0,0 +1,116 @@ +import { Container, Flex, Progress, Text } from '@chakra-ui/react' +import React from 'react' + +import { toCrypto } from 'utils/formatter' + +interface IOpexCard { + accountData: Hooks.UseHorizonTypes.IAccount | undefined + latestFeeCharged: number | undefined +} + +export const OpexCard: React.FC = ({ + accountData, + latestFeeCharged, +}) => { + const BASE_RESERVE = 0.5 + + const getNativeBalance = (): Hooks.UseHorizonTypes.IBalance | undefined => { + return accountData?.balances.find( + balance => balance.asset_type === 'native' + ) + } + + const getReserved = (): number => { + const subentry = + Number(accountData?.subentry_count || 0) + + Number(accountData?.num_sponsoring || 0) + const lockedReserves = subentry * BASE_RESERVE + 1 + return lockedReserves + } + + const getProgress = (): number => { + const totalBalance = Number(getNativeBalance()?.balance || 0) + const reserved = getReserved() + const result = ((totalBalance - reserved) / totalBalance) * 100 + return result + } + + return ( + <> + {!accountData ? ( +
+ ) : ( + + Sandbox Expenses account + + Operating xpenses account + + + {accountData.account_id || '-'} + + + Account balance + + + + + {`${toCrypto( + Number(getNativeBalance()?.balance) - getReserved() + )} XLM avaliable / ${toCrypto(getReserved())} XLM reserved`} + + + + {`Total balance ${toCrypto( + Number(getNativeBalance()?.balance) + )} XLM`} + + + + + + + Total sponsored reserves + + + {accountData.num_sponsoring} + + + + + Average Fee Charged + + (based on the last 10 transactions) + + {`${toCrypto( + (latestFeeCharged || 0) / 10, + undefined, + true + )} XLM`} + + + + + Total covered fees + + (based on the last 10 transactions) + + {`${toCrypto(latestFeeCharged || 0, undefined, true)} XLM`} + + + + + )} + + ) +} diff --git a/frontend/src/components/templates/cost-center/components/transactions-card/index.tsx b/frontend/src/components/templates/cost-center/components/transactions-card/index.tsx new file mode 100644 index 00000000..b8f111dd --- /dev/null +++ b/frontend/src/components/templates/cost-center/components/transactions-card/index.tsx @@ -0,0 +1,206 @@ +import { + Button, + Container, + Flex, + Table, + Tbody, + Td, + Text, + Th, + Thead, + Tr, +} from '@chakra-ui/react' +import React from 'react' + +import { formatDateFull, toCrypto } from 'utils/formatter' + +import { NavLeftIcon, NavRightIcon, SendedIcon } from 'components/icons' + +interface ITransactionsCard { + transactions: Hooks.UseHorizonTypes.ITransactions | undefined + vaults: Hooks.UseVaultsTypes.IVault[] | undefined + assets: Hooks.UseAssetsTypes.IAssetDto[] | undefined + isPrevDisabled: boolean + getTransactionsByLink(link: 'prev' | 'next'): void +} + +interface IHorizonData { + name: string + type: string +} + +export const TransactionsCard: React.FC = ({ + transactions, + vaults, + assets, + isPrevDisabled, + getTransactionsByLink, +}) => { + const formatAccount = (account: string): string => { + return `${account.substring(0, 4)}...${account.substring( + account.length - 4, + account.length + )}` + } + + const walletToName = (publicKey: string): string => { + if ( + assets && + assets.find(asset => asset.distributor.key.publicKey === publicKey) + ) { + return 'Asset issuer' + } + + return ( + vaults?.find(vault => vault.wallet.key.publicKey === publicKey)?.name || + formatAccount(publicKey) + ) + } + + const getTransactionData = ( + transaction: Hooks.UseHorizonTypes.ITransactionItem + ): IHorizonData => { + const accountCreated = transaction.effects?._embedded.records.find( + effect => effect.type === 'account_created' + ) + if (accountCreated) { + return { + name: walletToName(accountCreated.account), + type: 'Account created', + } + } + + const credited = transaction.effects?._embedded.records.find( + effect => effect.type === 'account_credited' + ) + const debited = transaction.effects?._embedded.records.find( + effect => effect.type === 'account_debited' + ) + + if ( + credited && + assets?.find( + asset => asset.distributor.key.publicKey === credited.account + ) && + debited && + debited.asset_issuer === debited.account + ) { + return { + name: credited.asset_code, + type: 'Minted', + } + } + + if ( + debited && + assets?.find( + asset => asset.distributor.key.publicKey === debited.account + ) && + credited && + credited.asset_issuer === credited.account + ) { + return { + name: debited.asset_code, + type: 'Burned', + } + } + + const accountDebited = transaction.effects?._embedded.records.find( + effect => effect.type === 'account_debited' + ) + if (accountDebited) { + const accountCredited = transaction.effects?._embedded.records.find( + effect => effect.type === 'account_credited' + ) + return { + name: `${walletToName(accountDebited.account)} to ${walletToName( + accountCredited?.account || '-' + )}`, + type: 'Payment', + } + } + return { + name: '-', + type: '-', + } + } + + return ( + + Transaction's history + + + + + + + + + + + {transactions?._embedded.records.map(transaction => { + const transactionData = getTransactionData(transaction) + return ( + + + + + + + + ) + })} + +
+ TransactionDateAccountsCovered fee
+ + {transactionData.type} + + {formatDateFull(transaction.created_at)} + + {transactionData.name}{`${toCrypto( + Number(transaction.fee_charged), + undefined, + true + )} XLM`}
+ + + + +
+ ) +} diff --git a/frontend/src/components/templates/cost-center/index.tsx b/frontend/src/components/templates/cost-center/index.tsx index ea274b41..dbc36153 100644 --- a/frontend/src/components/templates/cost-center/index.tsx +++ b/frontend/src/components/templates/cost-center/index.tsx @@ -1,134 +1,62 @@ -import { - Container, - Flex, - Table, - Tbody, - Th, - Thead, - Tr, - Text, - Td, - Button, - Img, - Skeleton, - useMediaQuery, -} from '@chakra-ui/react' +import { Flex, Text, useMediaQuery } from '@chakra-ui/react' import React from 'react' -import { useNavigate } from 'react-router-dom' -import { havePermission } from 'utils' -import { getCurrencyIcon } from 'utils/constants/constants' import { MAX_PAGE_WIDTH } from 'utils/constants/sizes' -import { toCrypto } from 'utils/formatter' -import { CompoundTime } from '../contracts-create/components/select-compound' -import { PathRoute } from 'components/enums/path-route' -import { Permissions } from 'components/enums/permissions' -import { ArrowRightIcon, NewIcon } from 'components/icons' -import { Empty } from 'components/molecules/empty' +import { OpexCard } from './components/opex-card' +import { TransactionsCard } from './components/transactions-card' +import { MenuAdminMobile } from 'components/organisms/menu-admin-mobile' interface ICostCenterTemplate { - loading: boolean - transactions: Hooks.UseContractsTypes.IContract[] | undefined + transactions: Hooks.UseHorizonTypes.ITransactions | undefined userPermissions: Hooks.UseAuthTypes.IUserPermission[] | undefined + accountData: Hooks.UseHorizonTypes.IAccount | undefined + sponsorAccount: string | undefined + latestFeeCharged: number | undefined + vaults: Hooks.UseVaultsTypes.IVault[] | undefined + assets: Hooks.UseAssetsTypes.IAssetDto[] | undefined + isPrevDisabled: boolean + getTransactionsByLink(link: 'prev' | 'next'): void } export const CostCenterTemplate: React.FC = ({ - loading, transactions, - userPermissions, + accountData, + latestFeeCharged, + vaults, + assets, + isPrevDisabled, + getTransactionsByLink, }) => { - const navigate = useNavigate() - const [isLargerThanLg] = useMediaQuery('(min-width: 992px)') - const [isLargerThanMd] = useMediaQuery('(min-width: 768px)') - const [isLargerThanSm] = useMediaQuery('(min-width: 480px)') + const [isSmallerThanMd] = useMediaQuery('(max-width: 768px)') return ( - + - + - Yield-bearing asset + Administration - {havePermission(Permissions.CREATE_CERTIFICATES, userPermissions) && ( - - )} + {isSmallerThanMd && } + + + + - - {loading ? ( - - ) : !transactions || transactions.length === 0 ? ( - - ) : ( - - - - - - {isLargerThanSm && } - {isLargerThanMd && } - {isLargerThanLg && } - {isLargerThanLg && } - - - - {transactions.map((contract, index) => ( - { - navigate(`${PathRoute.CONTRACT_DETAIL}/${contract.id}`) - }} - > - - - - {isLargerThanSm && ( - - )} - {isLargerThanMd && ( - - )} - {isLargerThanLg && ( - - )} - {isLargerThanLg && ( - - )} - - - ))} - -
- AssetVaultYield %TermCompoundMinimum Deposit -
- {contract.asset.image ? ( - - ) : ( - getCurrencyIcon(contract.asset.code, '2rem') - )} - {contract.asset.code} - {!isLargerThanLg && contract.vault.name.length > 10 - ? `${contract.vault.name.substring(0, 8)}...` - : contract.vault.name} - {`${contract.yield_rate / 100}%`}{`${contract.term / 86400} day(s)`}{`${ - contract.compound === 0 - ? 'Simple interest' - : CompoundTime[contract.compound] - }`}{toCrypto(contract.min_deposit)} - -
- )} -
) diff --git a/frontend/src/hooks/useHorizon/context.tsx b/frontend/src/hooks/useHorizon/context.tsx index d143bc7a..56ae0844 100644 --- a/frontend/src/hooks/useHorizon/context.tsx +++ b/frontend/src/hooks/useHorizon/context.tsx @@ -192,6 +192,32 @@ export const HorizonProvider: React.FC = ({ children }) => { [getOperation] ) + const getTransactiontEffects = useCallback( + async ( + transactionId: string + ): Promise => { + setLoadingHorizon(true) + try { + const response = await axios.get( + `${BASE_URL}/transactions/${transactionId}/effects?order=desc` + ) + const data = response.data as Hooks.UseHorizonTypes.IEffects + if (data) { + return data + } + return undefined + } catch (error) { + if (axios.isAxiosError(error) && error?.response?.status === 400) { + throw new Error(error.message) + } + throw new Error(MessagesError.errorOccurred) + } finally { + setLoadingHorizon(false) + } + }, + [] + ) + const getAssetAccounts = useCallback( async ( assetCode: string, @@ -231,6 +257,71 @@ export const HorizonProvider: React.FC = ({ children }) => { } }, []) + const getTransactions = useCallback( + async ( + wallet?: string, + link?: string + ): Promise => { + setLoadingHorizon(true) + try { + const response = await axios.get( + link + ? link + : `${BASE_URL}/accounts/${wallet}/transactions?order=desc + ` + ) + const data = response.data as Hooks.UseHorizonTypes.ITransactions + if (data) { + await Promise.all( + data._embedded.records.map(async record => { + record.effects = await getTransactiontEffects(record.id) + }) + ) + + const resultNext = await axios.get(data._links.next.href) + + data._links.next.results = + resultNext.data?._embedded?.records.length || 0 + + return data + } + return undefined + } catch (error) { + if (axios.isAxiosError(error) && error?.response?.status === 400) { + throw new Error(error.message) + } + throw new Error(MessagesError.errorOccurred) + } finally { + setLoadingHorizon(false) + } + }, + [getTransactiontEffects] + ) + + const getAccount = useCallback( + async ( + wallet: string + ): Promise => { + setLoadingHorizon(true) + try { + const response = await axios.get(`${BASE_URL}/accounts/${wallet}`) + const data = response.data as Hooks.UseHorizonTypes.IAccount + if (data) { + return data + } + return undefined + } catch (error) { + if (axios.isAxiosError(error) && error?.response?.status === 400) { + throw new Error(error.message) + } + throw new Error(MessagesError.errorOccurred) + } finally { + setLoadingHorizon(false) + } + }, + [] + ) + return ( = ({ children }) => { getAccountEffects, getAssetAccounts, getLatestSequenceLedger, + getTransactions, + getAccount, }} > {children} diff --git a/frontend/src/hooks/useHorizon/use-horizon-types.d.ts b/frontend/src/hooks/useHorizon/use-horizon-types.d.ts index 63f12b67..9bfd28e4 100644 --- a/frontend/src/hooks/useHorizon/use-horizon-types.d.ts +++ b/frontend/src/hooks/useHorizon/use-horizon-types.d.ts @@ -207,6 +207,50 @@ declare namespace Hooks { percentage: number } + interface ITransactionItem { + id: string + successful: true + hash: string + ledger: number + created_at: string + source_account: string + fee_account: string + fee_charged: string + max_fee: string + operation_count: number + valid_after: string + valid_before: string + fee_bump_transaction: { + hash: string + signatures: string[] + } + inner_transaction: { + hash: string + signatures: string[] + max_fee: string + } + effects?: IEffects + } + + interface ITransactions { + _embedded: { + records: ITransactionItem[] + } + _links: { + next: { + href: string + results: number + } + prev: { + href: string + results: number + } + self: { + href: string + } + } + } + interface IHorizonContext { loadingHorizon: boolean assetData: IAsset | undefined @@ -229,6 +273,11 @@ declare namespace Hooks { assetIssuer: string ): Promise getLatestSequenceLedger(): Promise + getTransactions( + wallet?: string, + link?: string + ): Promise + getAccount(wallet: string): Promise } } } diff --git a/frontend/src/utils/formatter/index.tsx b/frontend/src/utils/formatter/index.tsx index 8690fd5e..9b562bfc 100644 --- a/frontend/src/utils/formatter/index.tsx +++ b/frontend/src/utils/formatter/index.tsx @@ -3,7 +3,15 @@ export const toCurrency = (value: number): string => { return formatter.format(value) } -export const toCrypto = (value?: number, prefix?: string): string => { +export const toCrypto = ( + value: number | undefined, + prefix?: string, + convertStroops?: boolean +): string => { + if (value && convertStroops) { + value = value / 10000000 + } + const moneyFormatter = new Intl.NumberFormat('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 7, From e0b579632f9c218c39237eb63143c5501bfe892c Mon Sep 17 00:00:00 2001 From: Lucas Magnus Date: Mon, 19 Feb 2024 09:47:31 -0300 Subject: [PATCH 3/6] feat: mobile sizes --- .../src/app/core/pages/cost-center/index.tsx | 162 ++++++++++++- .../components/opex-card/index.tsx | 7 +- .../components/transactions-card/index.tsx | 215 +++++++----------- .../templates/cost-center/index.tsx | 16 +- frontend/src/hooks/useHorizon/context.tsx | 56 ++--- .../hooks/useHorizon/use-horizon-types.d.ts | 23 +- 6 files changed, 304 insertions(+), 175 deletions(-) diff --git a/frontend/src/app/core/pages/cost-center/index.tsx b/frontend/src/app/core/pages/cost-center/index.tsx index 987d470d..29d6560b 100644 --- a/frontend/src/app/core/pages/cost-center/index.tsx +++ b/frontend/src/app/core/pages/cost-center/index.tsx @@ -1,5 +1,5 @@ import { Flex, VStack, useMediaQuery } from '@chakra-ui/react' -import React, { useEffect, useState } from 'react' +import React, { useCallback, useEffect, useState } from 'react' import { useAssets } from 'hooks/useAssets' import { useAuth } from 'hooks/useAuth' @@ -14,6 +14,12 @@ import { MenuSettings } from 'components/organisms/menu-settings' import { Sidebar } from 'components/organisms/sidebar' import { CostCenterTemplate } from 'components/templates/cost-center' +export interface IHorizonData { + name: string + type: string + asset: string +} + export const CostCenter: React.FC = () => { const [isLargerThanMd] = useMediaQuery('(min-width: 768px)') const [sponsorAccount, setSponsorAccount] = useState() @@ -22,6 +28,8 @@ export const CostCenter: React.FC = () => { const [accountData, setAccountData] = useState() const [latestFeeCharged, setLatestFeeCharged] = useState() + const [mostRepeatedType, setMostRepeatedType] = useState() + const [vaults, setVaults] = useState() const [assets, setAssets] = useState() const [historyTransactions, setHistoryTransactions] = useState< @@ -41,7 +49,8 @@ export const CostCenter: React.FC = () => { useEffect(() => { getUserPermissions() getSponsorPK().then(sponsor => setSponsorAccount(sponsor)) - }, [getUserPermissions, getSponsorPK]) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) useEffect(() => { getVaults(true).then(vaults => setVaults(vaults)) @@ -89,6 +98,153 @@ export const CostCenter: React.FC = () => { } } + const formatAccount = (account: string): string => { + return `${account.substring(0, 4)}...${account.substring( + account.length - 4, + account.length + )}` + } + + const walletToName = useCallback((publicKey: string): string => { + if ( + assets && + assets.find(asset => asset.distributor.key.publicKey === publicKey) + ) { + return 'Asset issuer' + } + + return ( + vaults?.find(vault => vault.wallet.key.publicKey === publicKey)?.name || + formatAccount(publicKey) + ) + }, [assets, vaults]) + + const getTransactionData = useCallback( + (transaction: Hooks.UseHorizonTypes.ITransactionItem): IHorizonData => { + const accountCreated = transaction.operations?.find( + operation => operation.type === 'create_account' + ) + if (accountCreated) { + return { + name: walletToName(accountCreated.account || ''), + type: 'Account created', + asset: '-', + } + } + + const payment = transaction.operations?.find( + operation => operation.type === 'payment' + ) + + if ( + payment && + assets?.find(asset => asset.distributor.key.publicKey === payment.to) + ) { + return { + name: '-', + type: 'Minted', + asset: payment.asset_code || '-', + } + } + + if ( + payment && + assets?.find(asset => asset.distributor.key.publicKey === payment.from) + ) { + return { + name: '-', + type: assets?.find(asset => asset.issuer.key.publicKey === payment.to) + ? 'Burned' + : 'Distribute', + asset: payment.asset_code || '-', + } + } + + if (payment) { + return { + name: `${walletToName(payment.from || '')} to ${walletToName( + payment.to || '-' + )}`, + type: 'Payment', + asset: payment.asset_code || '-', + } + } + + const contractInvoke = transaction.operations?.find( + operation => operation.type === 'invoke_host_function' + ) + if (contractInvoke) { + return { + name: walletToName(contractInvoke.source_account || ''), + type: 'Contract invoke', + asset: '-', + } + } + + const trustlineOperation = transaction.operations?.find( + operation => operation.type === 'set_trust_line_flags' + ) + + if (trustlineOperation) { + if (trustlineOperation.set_flags_s?.includes('authorized')) { + return { + name: walletToName(trustlineOperation.trustor || ''), + type: 'Authorized', + asset: trustlineOperation.asset_code || '-', + } + } + + if (trustlineOperation.clear_flags_s?.includes('authorized')) { + return { + name: walletToName(trustlineOperation.trustor || ''), + type: 'Freezed', + asset: trustlineOperation.asset_code || '-', + } + } + } + + const clawback = transaction.operations?.find( + operation => operation.type === 'clawback' + ) + + if (clawback) { + return { + name: walletToName(clawback.from || ''), + type: 'Clawbacked', + asset: clawback.asset_code || '-', + } + } + + return { + name: '-', + type: '-', + asset: '-', + } + }, + [assets, walletToName] + ) + + useEffect(() => { + const counter: Record = {} + + transactions?._embedded.records.forEach(transaction => { + const transactionData = getTransactionData(transaction) + counter[transactionData.type] = (counter[transactionData.type] || 0) + 1 + }) + + let mostRepeatedType: string | undefined + let maxOccurrences = 0 + + for (const type in counter) { + if (counter[type] > maxOccurrences) { + maxOccurrences = counter[type] + mostRepeatedType = type + } + } + + setMostRepeatedType(mostRepeatedType) + }, [transactions, assets, vaults, getTransactionData]) + return ( @@ -108,7 +264,9 @@ export const CostCenter: React.FC = () => { vaults={vaults} assets={assets} isPrevDisabled={historyTransactions.length === 0} + mostRepeatedType={mostRepeatedType} getTransactionsByLink={getTransactionsByLink} + getTransactionData={getTransactionData} /> {isLargerThanMd && ( diff --git a/frontend/src/components/templates/cost-center/components/opex-card/index.tsx b/frontend/src/components/templates/cost-center/components/opex-card/index.tsx index 028cadda..5f88ce33 100644 --- a/frontend/src/components/templates/cost-center/components/opex-card/index.tsx +++ b/frontend/src/components/templates/cost-center/components/opex-card/index.tsx @@ -6,11 +6,13 @@ import { toCrypto } from 'utils/formatter' interface IOpexCard { accountData: Hooks.UseHorizonTypes.IAccount | undefined latestFeeCharged: number | undefined + mostRepeatedType: string | undefined } export const OpexCard: React.FC = ({ accountData, latestFeeCharged, + mostRepeatedType, }) => { const BASE_RESERVE = 0.5 @@ -77,6 +79,7 @@ export const OpexCard: React.FC = ({ w="full" justifyContent="space-between" mt="1rem" + flexDir={{base: 'column', md: 'row'}} > @@ -101,11 +104,11 @@ export const OpexCard: React.FC = ({ - Total covered fees + Main type of transaction (based on the last 10 transactions) - {`${toCrypto(latestFeeCharged || 0, undefined, true)} XLM`} + {mostRepeatedType || '-'}
diff --git a/frontend/src/components/templates/cost-center/components/transactions-card/index.tsx b/frontend/src/components/templates/cost-center/components/transactions-card/index.tsx index b8f111dd..89ee1599 100644 --- a/frontend/src/components/templates/cost-center/components/transactions-card/index.tsx +++ b/frontend/src/components/templates/cost-center/components/transactions-card/index.tsx @@ -9,160 +9,117 @@ import { Th, Thead, Tr, + useMediaQuery, } from '@chakra-ui/react' import React from 'react' -import { formatDateFull, toCrypto } from 'utils/formatter' +import { formatDateFullClean, toCrypto } from 'utils/formatter' import { NavLeftIcon, NavRightIcon, SendedIcon } from 'components/icons' +import { IHorizonData } from 'app/core/pages/cost-center' + interface ITransactionsCard { transactions: Hooks.UseHorizonTypes.ITransactions | undefined - vaults: Hooks.UseVaultsTypes.IVault[] | undefined - assets: Hooks.UseAssetsTypes.IAssetDto[] | undefined isPrevDisabled: boolean getTransactionsByLink(link: 'prev' | 'next'): void -} - -interface IHorizonData { - name: string - type: string + getTransactionData( + transaction: Hooks.UseHorizonTypes.ITransactionItem + ): IHorizonData } export const TransactionsCard: React.FC = ({ transactions, - vaults, - assets, isPrevDisabled, getTransactionsByLink, + getTransactionData, }) => { - const formatAccount = (account: string): string => { - return `${account.substring(0, 4)}...${account.substring( - account.length - 4, - account.length - )}` - } - - const walletToName = (publicKey: string): string => { - if ( - assets && - assets.find(asset => asset.distributor.key.publicKey === publicKey) - ) { - return 'Asset issuer' - } - - return ( - vaults?.find(vault => vault.wallet.key.publicKey === publicKey)?.name || - formatAccount(publicKey) - ) - } - - const getTransactionData = ( - transaction: Hooks.UseHorizonTypes.ITransactionItem - ): IHorizonData => { - const accountCreated = transaction.effects?._embedded.records.find( - effect => effect.type === 'account_created' - ) - if (accountCreated) { - return { - name: walletToName(accountCreated.account), - type: 'Account created', - } - } - - const credited = transaction.effects?._embedded.records.find( - effect => effect.type === 'account_credited' - ) - const debited = transaction.effects?._embedded.records.find( - effect => effect.type === 'account_debited' - ) - - if ( - credited && - assets?.find( - asset => asset.distributor.key.publicKey === credited.account - ) && - debited && - debited.asset_issuer === debited.account - ) { - return { - name: credited.asset_code, - type: 'Minted', - } - } - - if ( - debited && - assets?.find( - asset => asset.distributor.key.publicKey === debited.account - ) && - credited && - credited.asset_issuer === credited.account - ) { - return { - name: debited.asset_code, - type: 'Burned', - } - } - - const accountDebited = transaction.effects?._embedded.records.find( - effect => effect.type === 'account_debited' - ) - if (accountDebited) { - const accountCredited = transaction.effects?._embedded.records.find( - effect => effect.type === 'account_credited' - ) - return { - name: `${walletToName(accountDebited.account)} to ${walletToName( - accountCredited?.account || '-' - )}`, - type: 'Payment', - } - } - return { - name: '-', - type: '-', - } - } + const [isLargerThanSm] = useMediaQuery('(min-width: 480px)') return ( Transaction's history - - - - - - - - - - - {transactions?._embedded.records.map(transaction => { + {isLargerThanSm ? ( +
- TransactionDateAccountsCovered fee
+ + + + + + + + + + + {transactions?._embedded.records.map(transaction => { + const transactionData = getTransactionData(transaction) + return ( + + + + + + + + + ) + })} + +
+ TransactionDateAssetAccountsCovered fee
+ + {transactionData.type} + + {formatDateFullClean(transaction.created_at)} + + {transactionData.asset}{transactionData.name}{`${toCrypto( + Number(transaction.fee_charged), + undefined, + true + )} XLM`}
+ ) : ( + + {transactions?._embedded.records?.map((transaction, index) => { const transactionData = getTransactionData(transaction) return ( - - - - - {transactionData.type} - - - {formatDateFull(transaction.created_at)} - - - {transactionData.name} - {`${toCrypto( - Number(transaction.fee_charged), - undefined, - true - )} XLM`} - + + + {formatDateFullClean(transaction.created_at)} + + + Transaction + {`${transactionData.type}`} + + + Asset + {transactionData.asset} + + + Accounts + {`${transactionData.name}`} + + + Fee charged + {`${toCrypto( + Number(transaction.fee_charged), + undefined, + true + )} XLM`} + + ) })} - - + + )} diff --git a/frontend/src/components/templates/contracts-detail/components/contract-info/index.tsx b/frontend/src/components/templates/contracts-detail/components/contract-info/index.tsx index ece1a830..3e3f88ee 100644 --- a/frontend/src/components/templates/contracts-detail/components/contract-info/index.tsx +++ b/frontend/src/components/templates/contracts-detail/components/contract-info/index.tsx @@ -29,7 +29,7 @@ export const ContractInfo: React.FC = ({ contract }) => { boxShadow="lower" _dark={{ bg: 'black.800' }} > - Yield-bearing asset + Yield-bearing Asset = ({ - Yield-bearing asset + Yield-bearing Asset {havePermission(Permissions.CREATE_CERTIFICATES, userPermissions) && ( )} diff --git a/frontend/src/components/templates/login/components/overview/index.tsx b/frontend/src/components/templates/login/components/overview/index.tsx index f8040a36..e9ffc66e 100644 --- a/frontend/src/components/templates/login/components/overview/index.tsx +++ b/frontend/src/components/templates/login/components/overview/index.tsx @@ -140,7 +140,7 @@ export const Overview: React.FC = ({ - Create Yield-bearing asset + Create Yield-bearing Asset In the Sandbox you can define permissions for each type of role From 4db7d93b4cb81ad2b782b5981064df4c988b20d3 Mon Sep 17 00:00:00 2001 From: Lucas Magnus Date: Mon, 19 Feb 2024 15:19:45 -0300 Subject: [PATCH 5/6] feat: checkbox disabled color --- frontend/src/app/core/pages/contracts-create/index.tsx | 10 +++++++++- .../templates/role-permissions-template/index.tsx | 4 +++- .../templates/roles-manage-template/index.tsx | 4 +++- frontend/src/config/theme/components/atoms/checkbox.ts | 7 +++++++ frontend/src/soroban/custom-account-handler/index.tsx | 2 +- 5 files changed, 23 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/core/pages/contracts-create/index.tsx b/frontend/src/app/core/pages/contracts-create/index.tsx index 7098ae44..fc15d95b 100644 --- a/frontend/src/app/core/pages/contracts-create/index.tsx +++ b/frontend/src/app/core/pages/contracts-create/index.tsx @@ -67,7 +67,15 @@ export const ContractsCreate: React.FC = () => { const codVault = ContractsService.loadAccount(vault.wallet.key.publicKey) const codTxInvocation = ContractsService.getTxInvocation( codVault, - INNER_FEE + INNER_FEE, + /*{ + signers: [opex], + header: { + fee: BUMP_FEE, + source: opex.getPublicKey(), + timeout: 60, + }, + }*/ ) const codClient = new StellarPlus.Contracts.CertificateOfDeposit({ diff --git a/frontend/src/components/templates/role-permissions-template/index.tsx b/frontend/src/components/templates/role-permissions-template/index.tsx index e6333c90..940db641 100644 --- a/frontend/src/components/templates/role-permissions-template/index.tsx +++ b/frontend/src/components/templates/role-permissions-template/index.tsx @@ -18,6 +18,7 @@ import React, { Dispatch, SetStateAction } from 'react' import { MAX_PAGE_WIDTH } from 'utils/constants/sizes' +import { InfoTag } from 'components/atoms/info-tag' import { MenuAdminMobile } from 'components/organisms/menu-admin-mobile' import { IChange } from 'app/core/pages/role-permissions' @@ -108,11 +109,12 @@ export const RolePermissionsTemplate: React.FC = ({ > Permissions +